From aa72bb2a4302ff79fc03d2ce5443dc1db6039baa Mon Sep 17 00:00:00 2001 From: Lokesh Chandra Date: Wed, 12 Nov 2025 10:11:24 +0530 Subject: [PATCH] fix(express): fanoutUnspentsV1 type codec Ticket: WP-6717 --- .../src/typedRoutes/api/v1/fanoutUnspents.ts | 34 +++++++- .../test/unit/typedRoutes/fanoutUnspents.ts | 82 +++++++++++++++++++ 2 files changed, 115 insertions(+), 1 deletion(-) diff --git a/modules/express/src/typedRoutes/api/v1/fanoutUnspents.ts b/modules/express/src/typedRoutes/api/v1/fanoutUnspents.ts index 904c401e3e..43da7d9613 100644 --- a/modules/express/src/typedRoutes/api/v1/fanoutUnspents.ts +++ b/modules/express/src/typedRoutes/api/v1/fanoutUnspents.ts @@ -20,10 +20,42 @@ export const FanoutUnspentsRequestBody = { xprv: optional(t.string), /** Whether to validate addresses (defaults to true) */ validate: optional(t.boolean), - /** Target number of unspents to create (must be at least 2 and less than 300) */ + /** Target number of unspents to create (must be at least 2 and less than 300) - REQUIRED */ target: t.number, /** Minimum number of confirmations needed for an unspent to be included (defaults to 1) */ minConfirms: optional(t.number), + /** Whether to use SegWit change addresses */ + segwitChange: optional(t.boolean), + /** Message or note for the transaction */ + message: optional(t.string), + /** One-time password for 2FA verification */ + otp: optional(t.string), + /** Exact fee amount in satoshis (use either fee, feeRate, or numBlocks, not multiple) */ + fee: optional(t.number), + /** Fee rate in satoshis per kilobyte (use either fee, feeRate, or numBlocks, not multiple) */ + feeRate: optional(t.number), + /** Whether this is an instant transaction */ + instant: optional(t.boolean), + /** Custom sequence ID for the transaction */ + sequenceId: optional(t.string), + /** Target number of blocks for fee estimation (use either fee, feeRate, or numBlocks, not multiple) */ + numBlocks: optional(t.number), + /** Whether minConfirms also applies to change outputs */ + enforceMinConfirmsForChange: optional(t.boolean), + /** Target number of unspents to maintain in the wallet */ + targetWalletUnspents: optional(t.number), + /** Minimum value of unspents to use (in base units) */ + minValue: optional(t.number), + /** Maximum value of unspents to use (in base units) */ + maxValue: optional(t.number), + /** Disable automatic change splitting for unspent management */ + noSplitChange: optional(t.boolean), + /** Comment for the transaction */ + comment: optional(t.string), + /** Dynamic fee confirmation target (number of blocks) */ + dynamicFeeConfirmTarget: optional(t.number), + /** WIF private key for paying fees from a single-key address */ + feeSingleKeyWIF: optional(t.string), }; /** diff --git a/modules/express/test/unit/typedRoutes/fanoutUnspents.ts b/modules/express/test/unit/typedRoutes/fanoutUnspents.ts index 77b85a1520..2139e7fd6d 100644 --- a/modules/express/test/unit/typedRoutes/fanoutUnspents.ts +++ b/modules/express/test/unit/typedRoutes/fanoutUnspents.ts @@ -710,6 +710,56 @@ describe('FanoutUnspents codec tests', function () { assert.strictEqual(decoded.minConfirms, validBody.minConfirms); }); + it('should validate body with fee', function () { + const validBody = { + target: 10, + fee: 10000, + }; + + const decoded = assertDecode(t.type(FanoutUnspentsRequestBody), validBody); + assert.strictEqual(decoded.fee, validBody.fee); + }); + + it('should validate body with feeRate', function () { + const validBody = { + target: 10, + feeRate: 20000, + }; + + const decoded = assertDecode(t.type(FanoutUnspentsRequestBody), validBody); + assert.strictEqual(decoded.feeRate, validBody.feeRate); + }); + + it('should validate body with message', function () { + const validBody = { + target: 10, + message: 'Test message', + }; + + const decoded = assertDecode(t.type(FanoutUnspentsRequestBody), validBody); + assert.strictEqual(decoded.message, validBody.message); + }); + + it('should validate body with otp', function () { + const validBody = { + target: 10, + otp: '123456', + }; + + const decoded = assertDecode(t.type(FanoutUnspentsRequestBody), validBody); + assert.strictEqual(decoded.otp, validBody.otp); + }); + + it('should validate body with instant', function () { + const validBody = { + target: 10, + instant: true, + }; + + const decoded = assertDecode(t.type(FanoutUnspentsRequestBody), validBody); + assert.strictEqual(decoded.instant, validBody.instant); + }); + it('should validate body with all fields', function () { const validBody = { target: 10, @@ -717,6 +767,22 @@ describe('FanoutUnspents codec tests', function () { xprv: 'xprv9s21ZrQH143K3D8TXfvAJgHVfTEeQNW5Ys9wZtnUZkqPzFzSjbEJrWC1vZ4GnXCvR7rQL2UFX3RSuYeU9MrERm1XBvACow7c36vnz5iYyj2', validate: true, minConfirms: 2, + segwitChange: true, + message: 'Test transaction', + otp: '123456', + fee: 10000, + feeRate: 20000, + instant: true, + sequenceId: 'seq-12345', + numBlocks: 6, + enforceMinConfirmsForChange: true, + targetWalletUnspents: 50, + minValue: 1000, + maxValue: 100000, + noSplitChange: false, + comment: 'Fanout transaction', + dynamicFeeConfirmTarget: 3, + feeSingleKeyWIF: 'L1aW4aubDFB7yfras2S1mN3bqg9nwySY8nkoLmJebSLD5BWv3ENZ', }; const decoded = assertDecode(t.type(FanoutUnspentsRequestBody), validBody); @@ -725,6 +791,22 @@ describe('FanoutUnspents codec tests', function () { assert.strictEqual(decoded.xprv, validBody.xprv); assert.strictEqual(decoded.validate, validBody.validate); assert.strictEqual(decoded.minConfirms, validBody.minConfirms); + assert.strictEqual(decoded.segwitChange, validBody.segwitChange); + assert.strictEqual(decoded.message, validBody.message); + assert.strictEqual(decoded.otp, validBody.otp); + assert.strictEqual(decoded.fee, validBody.fee); + assert.strictEqual(decoded.feeRate, validBody.feeRate); + assert.strictEqual(decoded.instant, validBody.instant); + assert.strictEqual(decoded.sequenceId, validBody.sequenceId); + assert.strictEqual(decoded.numBlocks, validBody.numBlocks); + assert.strictEqual(decoded.enforceMinConfirmsForChange, validBody.enforceMinConfirmsForChange); + assert.strictEqual(decoded.targetWalletUnspents, validBody.targetWalletUnspents); + assert.strictEqual(decoded.minValue, validBody.minValue); + assert.strictEqual(decoded.maxValue, validBody.maxValue); + assert.strictEqual(decoded.noSplitChange, validBody.noSplitChange); + assert.strictEqual(decoded.comment, validBody.comment); + assert.strictEqual(decoded.dynamicFeeConfirmTarget, validBody.dynamicFeeConfirmTarget); + assert.strictEqual(decoded.feeSingleKeyWIF, validBody.feeSingleKeyWIF); }); it('should reject body with non-number target', function () {