Skip to content

Commit

Permalink
refactor: remove multiple recipients components
Browse files Browse the repository at this point in the history
  • Loading branch information
alter-eggo committed May 3, 2024
1 parent 9caef96 commit 8f83bcc
Show file tree
Hide file tree
Showing 28 changed files with 272 additions and 729 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { BTC_P2WPKH_DUST_AMOUNT } from '@shared/constants';
import { createMoney } from '@shared/models/money.model';

import { sumNumbers } from '@app/common/math/helpers';
import { createNullArrayOfLength } from '@app/common/utils';
Expand All @@ -24,9 +25,8 @@ const demoUtxos = [
function generate10kSpendWithDummyUtxoSet(recipient: string) {
return determineUtxosForSpend({
utxos: demoUtxos as any,
amount: 10_000,
feeRate: 20,
recipient,
recipients: [{ address: recipient, amount: createMoney(10_000, 'BTC') }],
});
}

Expand All @@ -35,8 +35,12 @@ describe(determineUtxosForSpend.name, () => {
test('that Native Segwit, 1 input 2 outputs weighs 140 vBytes', () => {
const estimation = determineUtxosForSpend({
utxos: [{ value: 50_000 }] as any[],
amount: 40_000,
recipient: 'tb1qt28eagxcl9gvhq2rpj5slg7dwgxae2dn2hk93m',
recipients: [
{
address: 'tb1qt28eagxcl9gvhq2rpj5slg7dwgxae2dn2hk93m',
amount: createMoney(40_000, 'BTC'),
},
],
feeRate: 20,
});
console.log(estimation);
Expand All @@ -47,8 +51,12 @@ describe(determineUtxosForSpend.name, () => {
test('that Native Segwit, 2 input 2 outputs weighs 200vBytes', () => {
const estimation = determineUtxosForSpend({
utxos: [{ value: 50_000 }, { value: 50_000 }] as any[],
amount: 60_000,
recipient: 'tb1qt28eagxcl9gvhq2rpj5slg7dwgxae2dn2hk93m',
recipients: [
{
address: 'tb1qt28eagxcl9gvhq2rpj5slg7dwgxae2dn2hk93m',
amount: createMoney(60_000, 'BTC'),
},
],
feeRate: 20,
});
console.log(estimation);
Expand All @@ -70,8 +78,12 @@ describe(determineUtxosForSpend.name, () => {
{ value: 10_000 },
{ value: 10_000 },
] as any[],
amount: 100_000,
recipient: 'tb1qt28eagxcl9gvhq2rpj5slg7dwgxae2dn2hk93m',
recipients: [
{
address: 'tb1qt28eagxcl9gvhq2rpj5slg7dwgxae2dn2hk93m',
amount: createMoney(100_000, 'BTC'),
},
],
feeRate: 20,
});
expect(estimation.txVBytes).toBeGreaterThan(750);
Expand All @@ -82,7 +94,6 @@ describe(determineUtxosForSpend.name, () => {
describe('sorting algorithm', () => {
test('that it filters out dust utxos', () => {
const result = generate10kSpendWithDummyUtxoSet('tb1qt28eagxcl9gvhq2rpj5slg7dwgxae2dn2hk93m');
console.log(result);
const hasDust = result.filteredUtxos.some(utxo => utxo.value <= BTC_P2WPKH_DUST_AMOUNT);
expect(hasDust).toBeFalsy();
});
Expand Down Expand Up @@ -143,8 +154,12 @@ describe(determineUtxosForSpend.name, () => {
const amount = 29123n;
const result = determineUtxosForSpend({
utxos: testData as any,
amount: Number(amount),
recipient: 'tb1qt28eagxcl9gvhq2rpj5slg7dwgxae2dn2hk93m',
recipients: [
{
address: 'tb1qt28eagxcl9gvhq2rpj5slg7dwgxae2dn2hk93m',
amount: createMoney(Number(amount), 'BTC'),
},
],
feeRate: 3,
});
expect(result.outputs[0].value).toEqual(29123n);
Expand Down
192 changes: 46 additions & 146 deletions src/app/common/transactions/bitcoin/coinselect/local-coin-selection.ts
Original file line number Diff line number Diff line change
@@ -1,152 +1,42 @@
import BigNumber from 'bignumber.js';
import { validate } from 'bitcoin-address-validation';

import type { RpcSendTransferRecipient } from '@shared/rpc/methods/send-transfer';
import type { TransferRecipient } from '@shared/models/form.model';

import { sumNumbers } from '@app/common/math/helpers';
import { sumMoney } from '@app/common/money/calculate-money';
import { UtxoResponseItem } from '@app/query/bitcoin/bitcoin-client';

import {
filterUneconomicalUtxos,
filterUneconomicalUtxosMultipleRecipients,
getBitcoinTxSizeEstimation,
getSizeInfoMultipleRecipients,
} from '../utils';

export interface DetermineUtxosForSpendArgs {
amount: number;
feeRate: number;
recipient: string;
utxos: UtxoResponseItem[];
}
import { filterUneconomicalUtxos, getSizeInfo } from '../utils';

export class InsufficientFundsError extends Error {
constructor() {
super('Insufficient funds');
}
}

export function determineUtxosForSpendAll({
amount,
feeRate,
recipient,
utxos,
}: DetermineUtxosForSpendArgs) {
if (!validate(recipient)) throw new Error('Cannot calculate spend of invalid address type');
const filteredUtxos = filterUneconomicalUtxos({ utxos, feeRate, address: recipient });

const sizeInfo = getBitcoinTxSizeEstimation({
inputCount: filteredUtxos.length,
outputCount: 1,
recipient,
});

// Fee has already been deducted from the amount with send all
const outputs = [{ value: BigInt(amount), address: recipient }];

const fee = Math.ceil(sizeInfo.txVBytes * feeRate);

return {
inputs: filteredUtxos,
outputs,
size: sizeInfo.txVBytes,
fee,
};
}

function getUtxoTotal(utxos: UtxoResponseItem[]) {
return sumNumbers(utxos.map(utxo => utxo.value));
}

export function determineUtxosForSpend({
amount,
feeRate,
recipient,
utxos,
}: DetermineUtxosForSpendArgs) {
if (!validate(recipient)) throw new Error('Cannot calculate spend of invalid address type');

const filteredUtxos: UtxoResponseItem[] = filterUneconomicalUtxos({
utxos: utxos.sort((a, b) => b.value - a.value),
feeRate,
address: recipient,
});

if (!filteredUtxos.length) throw new InsufficientFundsError();

// Prepopulate with first UTXO, at least one is needed
const neededUtxos: UtxoResponseItem[] = [filteredUtxos[0]];

function estimateTransactionSize() {
return getBitcoinTxSizeEstimation({
inputCount: neededUtxos.length,
outputCount: 2,
recipient,
});
}

function hasSufficientUtxosForTx() {
const txEstimation = estimateTransactionSize();
const neededAmount = new BigNumber(txEstimation.txVBytes * feeRate).plus(amount);
return getUtxoTotal(neededUtxos).isGreaterThanOrEqualTo(neededAmount);
}

function getRemainingUnspentUtxos() {
return filteredUtxos.filter(utxo => !neededUtxos.includes(utxo));
}

while (!hasSufficientUtxosForTx()) {
const [nextUtxo] = getRemainingUnspentUtxos();
if (!nextUtxo) throw new InsufficientFundsError();
neededUtxos.push(nextUtxo);
}

const fee = Math.ceil(
new BigNumber(estimateTransactionSize().txVBytes).multipliedBy(feeRate).toNumber()
);

const outputs = [
// outputs[0] = the desired amount going to recipient
{ value: BigInt(amount), address: recipient },
// outputs[1] = the remainder to be returned to a change address
{ value: BigInt(getUtxoTotal(neededUtxos).toString()) - BigInt(amount) - BigInt(fee) },
];

return {
filteredUtxos,
inputs: neededUtxos,
outputs,
size: estimateTransactionSize().txVBytes,
fee,
...estimateTransactionSize(),
};
}

export interface DetermineUtxosForSpendArgsMultipleRecipients {
amount: number;
export interface DetermineUtxosForSpendArgs {
feeRate: number;
recipients: RpcSendTransferRecipient[];
recipients: TransferRecipient[];
utxos: UtxoResponseItem[];
}

interface DetermineUtxosForSpendAllArgsMultipleRecipients {
feeRate: number;
recipients: RpcSendTransferRecipient[];
utxos: UtxoResponseItem[];
function getUtxoTotal(utxos: UtxoResponseItem[]) {
return sumNumbers(utxos.map(utxo => utxo.value));
}

export function determineUtxosForSpendAllMultipleRecipients({
export function determineUtxosForSpendAll({
feeRate,
recipients,
utxos,
}: DetermineUtxosForSpendAllArgsMultipleRecipients) {
}: DetermineUtxosForSpendArgs) {
recipients.forEach(recipient => {
if (!validate(recipient.address))
throw new Error('Cannot calculate spend of invalid address type');
});
const filteredUtxos = filterUneconomicalUtxosMultipleRecipients({ utxos, feeRate, recipients });
const filteredUtxos = filterUneconomicalUtxos({ utxos, feeRate, recipients });

const sizeInfo = getSizeInfoMultipleRecipients({
const sizeInfo = getSizeInfo({
inputLength: filteredUtxos.length,
isSendMax: true,
recipients,
Expand All @@ -168,62 +58,72 @@ export function determineUtxosForSpendAllMultipleRecipients({
};
}

export function determineUtxosForSpendMultipleRecipients({
amount,
feeRate,
recipients,
utxos,
}: DetermineUtxosForSpendArgsMultipleRecipients) {
export function determineUtxosForSpend({ feeRate, recipients, utxos }: DetermineUtxosForSpendArgs) {
recipients.forEach(recipient => {
if (!validate(recipient.address))
throw new Error('Cannot calculate spend of invalid address type');
});

const orderedUtxos = utxos.sort((a, b) => b.value - a.value);

const filteredUtxos = filterUneconomicalUtxosMultipleRecipients({
utxos: orderedUtxos,
const filteredUtxos = filterUneconomicalUtxos({
utxos: utxos.sort((a, b) => b.value - a.value),
feeRate,
recipients,
});
if (!filteredUtxos.length) throw new InsufficientFundsError();

const neededUtxos = [];
let sum = 0n;
let sizeInfo = null;
const amount = sumMoney(recipients.map(recipient => recipient.amount));

// Prepopulate with first UTXO, at least one is needed
const neededUtxos: UtxoResponseItem[] = [filteredUtxos[0]];

for (const utxo of filteredUtxos) {
sizeInfo = getSizeInfoMultipleRecipients({
function estimateTransactionSize() {
return getSizeInfo({
inputLength: neededUtxos.length,
recipients,
});
if (sum >= BigInt(amount) + BigInt(Math.ceil(sizeInfo.txVBytes * feeRate))) break;
}

function hasSufficientUtxosForTx() {
const txEstimation = estimateTransactionSize();
const neededAmount = new BigNumber(txEstimation.txVBytes * feeRate).plus(amount.amount);
return getUtxoTotal(neededUtxos).isGreaterThanOrEqualTo(neededAmount);
}

sum += BigInt(utxo.value);
neededUtxos.push(utxo);
function getRemainingUnspentUtxos() {
return filteredUtxos.filter(utxo => !neededUtxos.includes(utxo));
}

if (!sizeInfo) throw new InsufficientFundsError();
while (!hasSufficientUtxosForTx()) {
const [nextUtxo] = getRemainingUnspentUtxos();
if (!nextUtxo) throw new InsufficientFundsError();
neededUtxos.push(nextUtxo);
}

const fee = Math.ceil(sizeInfo.txVBytes * feeRate);
const fee = Math.ceil(
new BigNumber(estimateTransactionSize().txVBytes).multipliedBy(feeRate).toNumber()
);

const outputs: {
value: bigint;
address?: string;
}[] = [
// outputs[0] = the desired amount going to recipient
...recipients.map(({ address, amount }) => ({
value: BigInt(amount.amount.toNumber()),
address,
})),
// outputs[recipients.length] = the remainder to be returned to a change address
{ value: sum - BigInt(amount) - BigInt(fee) },
{
value:
BigInt(getUtxoTotal(neededUtxos).toString()) -
BigInt(amount.amount.toNumber()) -
BigInt(fee),
},
];

return {
filteredUtxos,
inputs: neededUtxos,
outputs,
size: sizeInfo.txVBytes,
size: estimateTransactionSize().txVBytes,
fee,
...estimateTransactionSize(),
};
}

0 comments on commit 8f83bcc

Please sign in to comment.