-
Notifications
You must be signed in to change notification settings - Fork 19
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
…xo-selection-logic-and-fee-rate-calc feat: add logic for getting best UTXO combo for txn
- Loading branch information
Showing
6 changed files
with
1,078 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
export const utxo10k = { | ||
txid: 'bb01711d83a22efcb10a8f025d17e61a09a53fafb22c4faf831df0cbdf104b40', | ||
vout: 0, | ||
status: { | ||
confirmed: true, | ||
block_height: 790416, | ||
block_hash: '0000000000000000000353903f91b9280da1bfa205469d8fee2d9e79af9a1878', | ||
block_time: 1684480442, | ||
}, | ||
value: 10000, | ||
address: 'bc1pwl57h24ecsurje63h9mlyema6gk264yrgca5y6klyya8qtqqjt2ql2j2g8', | ||
}; | ||
|
||
export const utxo3k = { | ||
txid: '758debef5a1930c93ea0a99d3110c78db0a7d26dc5765ae33ab160ebc68f1b39', | ||
vout: 0, | ||
status: { | ||
confirmed: true, | ||
block_height: 786775, | ||
block_hash: '00000000000000000002104d6d7828d2fbd215e67c443c5da59a311f1ff99321', | ||
block_time: 1682311497, | ||
}, | ||
value: 3000, | ||
address: 'bc1pwl57h24ecsurje63h9mlyema6gk264yrgca5y6klyya8qtqqjt2ql2j2g8', | ||
}; | ||
|
||
export const utxo384k = { | ||
txid: '61afc3bed964c4809fd8461c51331993776254752d03f8f51431f27bb4b4a6dc', | ||
vout: 0, | ||
status: { | ||
confirmed: true, | ||
block_height: 795007, | ||
block_hash: '000000000000000000002ed5bfd475a681dbbfb330b8fdcf1a85e3228ab0370b', | ||
block_time: 1687154049, | ||
}, | ||
value: 384000, | ||
address: 'bc1pwl57h24ecsurje63h9mlyema6gk264yrgca5y6klyya8qtqqjt2ql2j2g8', | ||
}; | ||
|
||
export const utxo792k = { | ||
txid: '0c3d764f62b815803a83c4b6df499d4cd02cebe95bb7f1921f241d1626d40b54', | ||
vout: 2, | ||
status: { | ||
confirmed: true, | ||
block_height: 794726, | ||
block_hash: '00000000000000000002a7b8cf55cf0d82a503ce64f5596ac82df4e7788cd50a', | ||
block_time: 1686984560, | ||
}, | ||
value: 792000, | ||
address: 'bc1pwl57h24ecsurje63h9mlyema6gk264yrgca5y6klyya8qtqqjt2ql2j2g8', | ||
}; | ||
|
||
export const utxos = [ | ||
utxo10k, | ||
utxo3k, | ||
utxo384k, | ||
utxo792k, | ||
// below are dust UTXOs which should never be selected | ||
{ | ||
txid: 'c0979647f08b98dd5e863459bd3b66e7545a87d6dfa377f6437c668fc718861c', | ||
vout: 0, | ||
status: { | ||
confirmed: true, | ||
block_height: 786680, | ||
block_hash: '00000000000000000000f51440ad69222f84657ffcd454804319af24d6cdc5e3', | ||
block_time: 1682256800, | ||
}, | ||
value: 546, | ||
address: 'bc1pwl57h24ecsurje63h9mlyema6gk264yrgca5y6klyya8qtqqjt2ql2j2g8', | ||
}, | ||
{ | ||
txid: 'b11f8946882d156fa0a4c495209bdcc8bbe9aa8aae06ba5954c6f1502eb8b11d', | ||
vout: 0, | ||
status: { | ||
confirmed: true, | ||
block_height: 786583, | ||
block_hash: '000000000000000000024256567d65bc178eaf22d78938ad5a762602fd003281', | ||
block_time: 1682195537, | ||
}, | ||
value: 546, | ||
address: 'bc1pwl57h24ecsurje63h9mlyema6gk264yrgca5y6klyya8qtqqjt2ql2j2g8', | ||
}, | ||
{ | ||
txid: 'aed7a41d0e6f404aa91db1b5bff1b1a75830324c62b28914a30ca8baf895bc84', | ||
vout: 0, | ||
status: { | ||
confirmed: true, | ||
block_height: 786814, | ||
block_hash: '00000000000000000001ba2c991ae6a5107bbc1aade24d9f3724aac929491e2e', | ||
block_time: 1682339651, | ||
}, | ||
value: 546, | ||
address: 'bc1pwl57h24ecsurje63h9mlyema6gk264yrgca5y6klyya8qtqqjt2ql2j2g8', | ||
}, | ||
{ | ||
txid: '6f317fabea61f40a675c8da25cd7907a578e4230ec2d92734eca39c94c4af7e7', | ||
vout: 0, | ||
status: { | ||
confirmed: true, | ||
block_height: 786814, | ||
block_hash: '00000000000000000001ba2c991ae6a5107bbc1aade24d9f3724aac929491e2e', | ||
block_time: 1682339651, | ||
}, | ||
value: 546, | ||
address: 'bc1pwl57h24ecsurje63h9mlyema6gk264yrgca5y6klyya8qtqqjt2ql2j2g8', | ||
}, | ||
]; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,193 @@ | ||
import BigNumber from 'bignumber.js'; | ||
import { UTXO } from 'types'; | ||
import { Recipient } from '../../transactions/btc'; | ||
|
||
import { utxo10k, utxo384k, utxo3k, utxo792k } from './btc.data'; | ||
|
||
type FixtureName = string; | ||
type FeeRate = number; | ||
|
||
type SelectUtxoSendSuccessExpected = { | ||
selectedUtxos: UTXO[]; | ||
change: number; | ||
fee: number; | ||
}; | ||
|
||
type FixtureInput = { | ||
recipients: Recipient[]; | ||
feeRate: FeeRate; | ||
expected: SelectUtxoSendSuccessExpected; | ||
}; | ||
|
||
type SelectUtxoSendSuccessFixture = [FixtureName, FixtureInput]; | ||
|
||
export const recipientAddress1 = 'bc1pgkwmp9u9nel8c36a2t7jwkpq0hmlhmm8gm00kpdxdy864ew2l6zqw2l6vh'; | ||
export const recipientAddress2 = 'bc1pyzfhlkq29sylwlv72ve52w8mn7hclefzhyay3dxh32r0322yx6uqajvr3y'; | ||
|
||
export const selectUtxosForSendSuccessFixtures: SelectUtxoSendSuccessFixture[] = [ | ||
[ | ||
'Large output, prefer largest UTXO with largest change', | ||
{ | ||
recipients: [ | ||
{ | ||
address: recipientAddress1, | ||
amountSats: new BigNumber(60000), | ||
}, | ||
], | ||
feeRate: 10, | ||
expected: { | ||
selectedUtxos: [utxo792k], | ||
change: 730120, | ||
fee: 1880, | ||
}, | ||
}, | ||
], | ||
[ | ||
'Small output, prefer largest UTXO with largest change', | ||
{ | ||
recipients: [ | ||
{ | ||
address: recipientAddress1, | ||
amountSats: new BigNumber(5000), | ||
}, | ||
], | ||
feeRate: 10, | ||
expected: { | ||
selectedUtxos: [utxo792k], | ||
change: 785120, | ||
fee: 1880, | ||
}, | ||
}, | ||
], | ||
[ | ||
'Exact change output, prefer smaller UTXO with no change', | ||
{ | ||
recipients: [ | ||
{ | ||
address: recipientAddress1, | ||
amountSats: new BigNumber(8170), | ||
}, | ||
], | ||
feeRate: 10, | ||
expected: { | ||
selectedUtxos: [utxo10k], | ||
change: 0, | ||
fee: 1830, | ||
}, | ||
}, | ||
], | ||
[ | ||
'Multiple inputs, prefer large change', | ||
{ | ||
recipients: [ | ||
{ | ||
address: recipientAddress1, | ||
amountSats: new BigNumber(792000), | ||
}, | ||
], | ||
feeRate: 10, | ||
expected: { | ||
selectedUtxos: [utxo792k, utxo384k], | ||
change: 381220, | ||
fee: 2780, | ||
}, | ||
}, | ||
], | ||
[ | ||
'Exact change inputs, multiple UTXOs, prefer no change', | ||
{ | ||
recipients: [ | ||
{ | ||
address: recipientAddress1, | ||
amountSats: new BigNumber(799570), | ||
}, | ||
], | ||
feeRate: 10, | ||
expected: { | ||
selectedUtxos: [utxo792k, utxo10k], | ||
change: 0, | ||
fee: 2430, | ||
}, | ||
}, | ||
], | ||
[ | ||
'All inputs, exclude dust', | ||
{ | ||
recipients: [ | ||
{ | ||
address: recipientAddress1, | ||
amountSats: new BigNumber(1184000), | ||
}, | ||
], | ||
feeRate: 10, | ||
expected: { | ||
selectedUtxos: [utxo792k, utxo384k, utxo10k, utxo3k], | ||
change: 0, | ||
fee: 5000, | ||
}, | ||
}, | ||
], | ||
[ | ||
'Multiple recipients, prefer large change', | ||
{ | ||
recipients: [ | ||
{ | ||
address: recipientAddress1, | ||
amountSats: new BigNumber(20000), | ||
}, | ||
{ | ||
address: recipientAddress2, | ||
amountSats: new BigNumber(30000), | ||
}, | ||
], | ||
feeRate: 10, | ||
expected: { | ||
selectedUtxos: [utxo792k], | ||
change: 739690, | ||
fee: 2310, | ||
}, | ||
}, | ||
], | ||
[ | ||
'Multiple recipients, low value, prefer high change', | ||
{ | ||
recipients: [ | ||
{ | ||
address: recipientAddress1, | ||
amountSats: new BigNumber(5000), | ||
}, | ||
{ | ||
address: recipientAddress2, | ||
amountSats: new BigNumber(1200), | ||
}, | ||
], | ||
feeRate: 10, | ||
expected: { | ||
selectedUtxos: [utxo792k], | ||
change: 783490, | ||
fee: 2310, | ||
}, | ||
}, | ||
], | ||
[ | ||
'Multiple recipients, exact value, prefer no change', | ||
{ | ||
recipients: [ | ||
{ | ||
address: recipientAddress1, | ||
amountSats: new BigNumber(5000), | ||
}, | ||
{ | ||
address: recipientAddress2, | ||
amountSats: new BigNumber(2900), | ||
}, | ||
], | ||
feeRate: 10, | ||
expected: { | ||
selectedUtxos: [utxo10k], | ||
change: 0, | ||
fee: 2100, | ||
}, | ||
}, | ||
], | ||
]; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
import { describe, expect, it } from 'vitest'; | ||
|
||
import BigNumber from 'bignumber.js'; | ||
import { selectUtxosForSend } from '../../transactions/btc'; | ||
import { utxo3k, utxo792k, utxos } from './btc.data'; | ||
import { recipientAddress1, selectUtxosForSendSuccessFixtures } from './btc.fixtures'; | ||
|
||
const dummyChangeAddress = 'bc1pzsm9pu47e7npkvxh9dcd0dc2qwqshxt2a9tt7aq3xe9krpl8e82sx6phdj'; | ||
|
||
describe('selectUtxosForSend', () => { | ||
it.each(selectUtxosForSendSuccessFixtures)( | ||
'should select utxos for send: %s', | ||
(_testName, { recipients, feeRate, expected }) => { | ||
const selectedUtxoData = selectUtxosForSend({ | ||
changeAddress: dummyChangeAddress, | ||
recipients, | ||
availableUtxos: utxos, | ||
feeRate, | ||
}); | ||
|
||
expect(selectedUtxoData).toBeDefined(); | ||
|
||
const { feeRate: actualFeeRate, ...selectedWithoutFeeRate } = selectedUtxoData || {}; | ||
expect(selectedWithoutFeeRate).toEqual(expected); | ||
expect(actualFeeRate).toBeGreaterThanOrEqual(feeRate); | ||
}, | ||
); | ||
|
||
it('should force select pinned UTXOs', () => { | ||
const selectedUtxoData = selectUtxosForSend({ | ||
changeAddress: dummyChangeAddress, | ||
recipients: [{ address: recipientAddress1, amountSats: new BigNumber(50000) }], | ||
availableUtxos: utxos, | ||
feeRate: 10, | ||
pinnedUtxos: [utxo3k], | ||
}); | ||
|
||
expect(selectedUtxoData).toEqual({ | ||
selectedUtxos: [utxo3k, utxo792k], | ||
change: 742210, | ||
fee: 2790, | ||
feeRate: 10, | ||
}); | ||
}); | ||
|
||
it('should return undefined if no utxos', () => { | ||
const selectedUtxoData = selectUtxosForSend({ | ||
changeAddress: dummyChangeAddress, | ||
recipients: [{ address: recipientAddress1, amountSats: new BigNumber(1000) }], | ||
availableUtxos: [], | ||
feeRate: 10, | ||
}); | ||
|
||
expect(selectedUtxoData).toBeUndefined(); | ||
}); | ||
|
||
it('should return undefined if not enough value in utxos', () => { | ||
const selectedUtxoData = selectUtxosForSend({ | ||
changeAddress: dummyChangeAddress, | ||
recipients: [{ address: recipientAddress1, amountSats: new BigNumber(10000000) }], | ||
availableUtxos: utxos, | ||
feeRate: 10, | ||
}); | ||
|
||
expect(selectedUtxoData).toBeUndefined(); | ||
}); | ||
|
||
it('should throw if no recipients', () => { | ||
expect(() => | ||
selectUtxosForSend({ changeAddress: dummyChangeAddress, recipients: [], availableUtxos: utxos, feeRate: 10 }), | ||
).toThrow('Must have at least one recipient'); | ||
}); | ||
|
||
it('should throw if fee rate not a positive number', () => { | ||
const recipients = [{ address: recipientAddress1, amountSats: new BigNumber(10000000) }]; | ||
expect(() => | ||
selectUtxosForSend({ | ||
changeAddress: dummyChangeAddress, | ||
recipients: recipients, | ||
availableUtxos: utxos, | ||
feeRate: 0, | ||
}), | ||
).toThrow('Fee rate must be a positive number'); | ||
expect(() => | ||
selectUtxosForSend({ | ||
changeAddress: dummyChangeAddress, | ||
recipients: recipients, | ||
availableUtxos: utxos, | ||
feeRate: -1, | ||
}), | ||
).toThrow('Fee rate must be a positive number'); | ||
}); | ||
}); |
Oops, something went wrong.