Skip to content

Commit

Permalink
Merge pull request #195 from secretkeylabs/victor/eng-2497-improve-ut…
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
victorkirov authored Aug 7, 2023
2 parents f991c58 + e3e123c commit a313525
Show file tree
Hide file tree
Showing 6 changed files with 1,078 additions and 1 deletion.
107 changes: 107 additions & 0 deletions tests/transactions/btc.data.ts
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',
},
];
193 changes: 193 additions & 0 deletions tests/transactions/btc.fixtures.ts
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,
},
},
],
];
93 changes: 93 additions & 0 deletions tests/transactions/btc.test.ts
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');
});
});
Loading

0 comments on commit a313525

Please sign in to comment.