Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 33 additions & 1 deletion modules/express/src/typedRoutes/api/v1/fanoutUnspents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
};

/**
Expand Down
82 changes: 82 additions & 0 deletions modules/express/test/unit/typedRoutes/fanoutUnspents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -710,13 +710,79 @@ 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,
walletPassphrase: 'mySecurePassphrase',
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);
Expand All @@ -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 () {
Expand Down