diff --git a/package.json b/package.json index a22f76a..f387287 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/coinselect", - "version": "3.1.16", + "version": "3.1.17", "description": "A transaction input selection module for bitcoin.", "keywords": [ "coinselect", diff --git a/test/fixtures/witness.js b/test/fixtures/witness.js index 0f9d083..103dbc7 100644 --- a/test/fixtures/witness.js +++ b/test/fixtures/witness.js @@ -508,6 +508,290 @@ const fixtures = [ outputsPermutation: [0, 1] }, shouldThrow: false + }, + { + description: 'sortingStrategy none - preserves original input/output order', + feeRate: 10, + sortingStrategy: 'none', + inputs: [ + { + txId: 'bbbb_second_in_bip69', + vout: 0, + value: 5000, + amount: '5000', + confirmations: 200, + own: true, + coinbase: false, + address: 'tb1qul5mzh5phe7xqyqek0nl42hflfrn7ugxck59jd', + path: "m/84'/1'/0'/0/0", + required: true + }, + { + txId: 'aaaa_first_in_bip69', + vout: 0, + value: 3000, + amount: '3000', + confirmations: 300, + own: true, + coinbase: false, + address: 'tb1qul5mzh5phe7xqyqek0nl42hflfrn7ugxck59jd', + path: "m/84'/1'/0'/0/1", + required: true + } + ], + outputs: [ + { + type: 'payment', + address: 'tb1quawu6eyfuechu3qhdeejnrzne9y7shr08u8zzt', + value: 1000, + amount: '1000' + } + ], + network: testnet, + changeAddress: { + address: 'tb1qul5mzh5phe7xqyqek0nl42hflfrn7ugxck59jd', + path: "m/84'/1'/0'/0/0" + }, + txType: 'p2wpkh', + expected: { + type: 'final', + fee: '2090', + feePerByte: '10', + bytes: 209, + max: undefined, + totalSpent: '3090', + inputs: [ + { + txId: 'bbbb_second_in_bip69', + vout: 0, + value: 5000, + confirmations: 200, + own: true, + address: 'tb1qul5mzh5phe7xqyqek0nl42hflfrn7ugxck59jd', + path: "m/84'/1'/0'/0/0", + coinbase: false, + amount: '5000', + required: true + }, + { + txId: 'aaaa_first_in_bip69', + vout: 0, + value: 3000, + confirmations: 300, + own: true, + address: 'tb1qul5mzh5phe7xqyqek0nl42hflfrn7ugxck59jd', + path: "m/84'/1'/0'/0/1", + coinbase: false, + amount: '3000', + required: true + } + ], + outputs: [ + { + type: 'payment', + address: 'tb1quawu6eyfuechu3qhdeejnrzne9y7shr08u8zzt', + value: 1000, + amount: '1000' + }, + { + type: 'change', + address: 'tb1qul5mzh5phe7xqyqek0nl42hflfrn7ugxck59jd', + path: "m/84'/1'/0'/0/0", + amount: '4910' + } + ], + outputsPermutation: [0, 1] + }, + shouldThrow: false + }, + { + description: 'sortingStrategy bip69 - sorts inputs by txid and outputs by value', + feeRate: 10, + sortingStrategy: 'bip69', + inputs: [ + { + txid: 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', + vout: 0, + value: 5000, + amount: '5000', + confirmations: 200, + own: true, + coinbase: false, + address: 'tb1qul5mzh5phe7xqyqek0nl42hflfrn7ugxck59jd', + path: "m/84'/1'/0'/0/0", + required: true + }, + { + txid: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + vout: 0, + value: 3000, + amount: '3000', + confirmations: 300, + own: true, + coinbase: false, + address: 'tb1qul5mzh5phe7xqyqek0nl42hflfrn7ugxck59jd', + path: "m/84'/1'/0'/0/1", + required: true + } + ], + outputs: [ + { + type: 'payment', + address: 'tb1quawu6eyfuechu3qhdeejnrzne9y7shr08u8zzt', + value: 1000, + amount: '1000' + } + ], + network: testnet, + changeAddress: { + address: 'tb1qul5mzh5phe7xqyqek0nl42hflfrn7ugxck59jd', + path: "m/84'/1'/0'/0/0" + }, + txType: 'p2wpkh', + expected: { + type: 'final', + fee: '2090', + feePerByte: '10', + bytes: 209, + max: undefined, + totalSpent: '3090', + inputs: [ + { + txid: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + vout: 0, + value: 3000, + confirmations: 300, + own: true, + address: 'tb1qul5mzh5phe7xqyqek0nl42hflfrn7ugxck59jd', + path: "m/84'/1'/0'/0/1", + coinbase: false, + amount: '3000', + required: true + }, + { + txid: 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', + vout: 0, + value: 5000, + confirmations: 200, + own: true, + address: 'tb1qul5mzh5phe7xqyqek0nl42hflfrn7ugxck59jd', + path: "m/84'/1'/0'/0/0", + coinbase: false, + amount: '5000', + required: true + } + ], + outputs: [ + { + type: 'payment', + address: 'tb1quawu6eyfuechu3qhdeejnrzne9y7shr08u8zzt', + value: 1000, + amount: '1000' + }, + { + type: 'change', + address: 'tb1qul5mzh5phe7xqyqek0nl42hflfrn7ugxck59jd', + path: "m/84'/1'/0'/0/0", + amount: '4910' + } + ], + outputsPermutation: [0, 1] + }, + shouldThrow: false + }, + { + description: 'sortingStrategy random (default) - produces valid result with shuffled order', + feeRate: 10, + inputs: [ + { + txId: 'bbbb_second_in_bip69', + vout: 0, + value: 5000, + amount: '5000', + confirmations: 200, + own: true, + coinbase: false, + address: 'tb1qul5mzh5phe7xqyqek0nl42hflfrn7ugxck59jd', + path: "m/84'/1'/0'/0/0", + required: true + }, + { + txId: 'aaaa_first_in_bip69', + vout: 0, + value: 3000, + amount: '3000', + confirmations: 300, + own: true, + coinbase: false, + address: 'tb1qul5mzh5phe7xqyqek0nl42hflfrn7ugxck59jd', + path: "m/84'/1'/0'/0/1", + required: true + } + ], + outputs: [ + { + type: 'payment', + address: 'tb1quawu6eyfuechu3qhdeejnrzne9y7shr08u8zzt', + value: 1000, + amount: '1000' + } + ], + network: testnet, + changeAddress: { + address: 'tb1qul5mzh5phe7xqyqek0nl42hflfrn7ugxck59jd', + path: "m/84'/1'/0'/0/0" + }, + txType: 'p2wpkh', + expected: { + type: 'final', + fee: '2090', + feePerByte: '10', + bytes: 209, + max: undefined, + totalSpent: '3090', + inputs: [ + { + txId: 'bbbb_second_in_bip69', + vout: 0, + value: 5000, + confirmations: 200, + own: true, + address: 'tb1qul5mzh5phe7xqyqek0nl42hflfrn7ugxck59jd', + path: "m/84'/1'/0'/0/0", + coinbase: false, + amount: '5000', + required: true + }, + { + txId: 'aaaa_first_in_bip69', + vout: 0, + value: 3000, + confirmations: 300, + own: true, + address: 'tb1qul5mzh5phe7xqyqek0nl42hflfrn7ugxck59jd', + path: "m/84'/1'/0'/0/1", + coinbase: false, + amount: '3000', + required: true + } + ], + outputs: [ + { + type: 'payment', + address: 'tb1quawu6eyfuechu3qhdeejnrzne9y7shr08u8zzt', + value: 1000, + amount: '1000' + }, + { + type: 'change', + address: 'tb1qul5mzh5phe7xqyqek0nl42hflfrn7ugxck59jd', + path: "m/84'/1'/0'/0/0", + amount: '4910' + } + ], + outputsPermutation: [0, 1] + }, + shouldThrow: false } ] diff --git a/test/witness.js b/test/witness.js index 859fcf1..1c79cc0 100644 --- a/test/witness.js +++ b/test/witness.js @@ -25,7 +25,8 @@ fixtures.forEach(function (f) { feeRate: f.feeRate, network: f.network, changeAddress: f.changeAddress, - txType: f.txType + txType: f.txType, + sortingStrategy: f.sortingStrategy }) const compareOutputs = (actual, expected) => { @@ -63,6 +64,19 @@ fixtures.forEach(function (f) { t.same(actual.totalSpent, f.expected.totalSpent) t.ok(compareOutputs(actual.inputs, f.expected.inputs), 'inputs are the same') t.ok(compareOutputs(actual.outputs, f.expected.outputs), 'outputs are the same') + + // For deterministic strategies, also verify ordering + if (f.sortingStrategy === 'none' || f.sortingStrategy === 'bip69') { + var txIdKey = f.sortingStrategy === 'bip69' ? 'txid' : 'txId' + t.same( + actual.inputs.map(function (i) { return i[txIdKey] }), + f.expected.inputs.map(function (i) { return i[txIdKey] }), + 'input order matches expected for ' + f.sortingStrategy + ' strategy' + ) + t.same(actual.outputsPermutation, f.expected.outputsPermutation, + 'outputsPermutation matches for ' + f.sortingStrategy + ' strategy') + } + t.end() } }) diff --git a/witness.d.ts b/witness.d.ts index 32e0a1f..d31227d 100644 --- a/witness.d.ts +++ b/witness.d.ts @@ -51,6 +51,7 @@ export interface ICoinSelectParams { txType: IPaymentType baseFee?: number; dustThreshold?: number; + sortingStrategy?: 'bip69' | 'none' | 'random'; } export interface ICoinSelectResult { diff --git a/witness.js b/witness.js index 52d622d..c330502 100644 --- a/witness.js +++ b/witness.js @@ -8,13 +8,14 @@ module.exports = function coinSelect ({ network, txType, baseFee = 0, - dustThreshold = 546 + dustThreshold = 546, + sortingStrategy = 'random' }) { const result = composeTx({ utxos, outputs, feeRate, - sortingStrategy: 'random', + sortingStrategy, txType, dustThreshold, changeAddress,