Skip to content

Commit

Permalink
feat: add dust output removal function, add tests
Browse files Browse the repository at this point in the history
  • Loading branch information
Polybius93 committed Jan 1, 2025
1 parent cf3e113 commit ff9ff5d
Show file tree
Hide file tree
Showing 6 changed files with 88 additions and 7 deletions.
2 changes: 2 additions & 0 deletions src/constants/dlc-handler.constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ export const DLCHandlers = {
LEDGER: 'ledger',
DFNS: 'dfns',
} as const;

export const DUST_LIMIT = 546n;
12 changes: 12 additions & 0 deletions src/functions/bitcoin/bitcoin-functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { bitcoin, regtest, testnet } from 'bitcoinjs-lib/src/networks.js';
import { Decimal } from 'decimal.js';
import * as ellipticCurveCryptography from 'tiny-secp256k1';

import { DUST_LIMIT } from '../../constants/dlc-handler.constants.js';
import {
BitcoinInputSigningConfig,
BitcoinTransaction,
Expand Down Expand Up @@ -46,6 +47,17 @@ export function getFeeAmount(bitcoinAmount: number, feeBasisPoints: number): num
return new Decimal(bitcoinAmount).times(feePercentage).trunc().toNumber();
}

export function removeDustOutputs(
outputs: { address: string; amount: bigint }[],
dustLimit = DUST_LIMIT
): void {
for (let i = outputs.length - 1; i >= 0; i--) {
if (outputs[i].amount < dustLimit) {
outputs.splice(i, 1);
}
}
}

/**
* Derives the Public Key at the Unhardened Path (0/0) from a given Extended Public Key.
* @param extendedPublicKey - The base58-encoded Extended Public Key.
Expand Down
16 changes: 10 additions & 6 deletions src/functions/bitcoin/psbt-functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,18 @@ import { P2Ret, P2TROut } from '@scure/btc-signer/payment';
import { Network, Psbt } from 'bitcoinjs-lib';
import { PartialSignature } from 'ledger-bitcoin/build/main/lib/appClient.js';

import { DUST_LIMIT } from '../../constants/dlc-handler.constants.js';
import { BitcoinInputSigningConfig, PaymentTypes } from '../../models/bitcoin-models.js';
import { reverseBytes } from '../../utilities/index.js';
import {
ecdsaPublicKeyToSchnorr,
getFeeAmount,
getFeeRecipientAddress,
getUTXOs,
removeDustOutputs,
} from '../bitcoin/bitcoin-functions.js';
import { fetchBitcoinTransaction } from './bitcoin-request-functions.js';

const DUST_LIMIT = 546n;

/**
* Creates a Funding Transaction to deposit Bitcoin into an empty Vault.
* Uses the UTXOs of the User to create the Funding Transaction.
Expand Down Expand Up @@ -61,7 +61,9 @@ export async function createFundingTransaction(
const psbtOutputs = [
{ address: multisigAddress, amount: depositAmount },
{ address: feeAddress, amount: BigInt(feeAmount) },
].filter(output => output.amount >= DUST_LIMIT);
];

removeDustOutputs(psbtOutputs);

const selected = selectUTXO(userUTXOs, psbtOutputs, 'default', {
changeAddress: depositAddress,
Expand Down Expand Up @@ -206,7 +208,9 @@ export async function createDepositTransaction(
address: multisigAddress,
amount: BigInt(depositAmount) + BigInt(vaultTransactionOutputValue),
},
].filter(output => output.amount >= DUST_LIMIT);
];

removeDustOutputs(depositOutputs);

const depositSelected = selectUTXO(depositInputs, depositOutputs, 'all', {
changeAddress: depositAddress,
Expand Down Expand Up @@ -326,9 +330,9 @@ export async function createWithdrawTransaction(
});
}

const filteredOutputs = outputs.filter(output => output.amount >= DUST_LIMIT);
removeDustOutputs(outputs);

const selected = selectUTXO(inputs, filteredOutputs, 'default', {
const selected = selectUTXO(inputs, outputs, 'default', {
changeAddress: withdrawAddress,
feePerByte: feeRate,
bip69: false,
Expand Down
7 changes: 7 additions & 0 deletions tests/mocks/bitcoin.test.constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,10 @@ export const TEST_FEE_RECIPIENT_PUBLIC_KEY_1 =
export const TEST_FEE_DEPOSIT_BASIS_POINTS_1 = 100n;

export const TEST_FEE_RATE_1 = 6n;

export const TEST_OUTPUTS = [
{ address: 'addr1', amount: 1000n },
{ address: 'addr2', amount: 545n },
{ address: 'addr3', amount: 546n },
{ address: 'addr4', amount: 100n },
];
22 changes: 22 additions & 0 deletions tests/unit/bitcoin-functions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
getInputIndicesByScript,
getScriptMatchingOutputFromTransaction,
getUnspendableKeyCommittedToUUID,
removeDustOutputs,
} from '../../src/functions/bitcoin/bitcoin-functions';
import {
TEST_TESTNET_ATTESTOR_EXTENDED_GROUP_PUBLIC_KEY_1,
Expand All @@ -31,6 +32,7 @@ import {
TEST_ALICE_NATIVE_SEGWIT_PUBLIC_KEY_2,
TEST_ALICE_TAPROOT_PUBLIC_KEY_1,
TEST_ALICE_TAPROOT_PUBLIC_KEY_2,
TEST_OUTPUTS,
TEST_TAPROOT_MULTISIG_PAYMENT_SCRIPT_1,
TEST_TAPROOT_UNHARDENED_DERIVED_PUBLIC_KEY_1,
TEST_UNHARDENED_DERIVED_UNSPENDABLE_KEY_COMMITED_TO_UUID_1,
Expand Down Expand Up @@ -68,6 +70,26 @@ describe('Bitcoin Functions', () => {
});
});

describe('removeDustOutputs', () => {
it('removes single dust output', () => {
const outputs = [TEST_OUTPUTS[0], TEST_OUTPUTS[1]];
removeDustOutputs(outputs);
expect(outputs).toEqual([TEST_OUTPUTS[0]]);
});

it('removes multiple dust outputs', () => {
const outputs = [...TEST_OUTPUTS];
removeDustOutputs(outputs);
expect(outputs).toEqual([TEST_OUTPUTS[0], TEST_OUTPUTS[2]]);
});

it('keeps all outputs if none are dust', () => {
const outputs = [TEST_OUTPUTS[0], TEST_OUTPUTS[2]];
removeDustOutputs(outputs);
expect(outputs).toEqual(outputs);
});
});

describe('getFeeRecipientAddress', () => {
describe('mainnet', () => {
const network = bitcoin;
Expand Down
36 changes: 35 additions & 1 deletion tests/unit/psbt-functions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ describe('PSBT Functions', () => {
jest.clearAllMocks();
});
describe('createFundingTransaction', () => {
it('should successfully create a valid funding transaction', async () => {
it('should successfully create a valid funding transaction and exclude change output under dust limit', async () => {
jest
.spyOn(bitcoinFunctions, 'getUTXOs')
.mockImplementationOnce(async () => TEST_BITCOIN_REGTEST_NATIVE_SEGWIT_UTXOS_1);
Expand Down Expand Up @@ -83,5 +83,39 @@ describe('PSBT Functions', () => {
)
).not.toThrow();
});
it('should successfully create a valid funding transaction and exclude fee output under dust limit', async () => {
jest
.spyOn(bitcoinFunctions, 'getUTXOs')
.mockImplementationOnce(async () => TEST_BITCOIN_REGTEST_NATIVE_SEGWIT_UTXOS_1);

const depositTransaction = await createFundingTransaction(
TEST_REGTEST_BITCOIN_BLOCKCHAIN_API,
regtest,
9900n,
multisigPayment,
depositPayment,
TEST_FEE_RATE_1,
TEST_FEE_RECIPIENT_PUBLIC_KEY_1,
TEST_FEE_DEPOSIT_BASIS_POINTS_1
);

expect(depositTransaction).toBeDefined();
expect(depositTransaction.inputsLength).toBe(1);
expect(depositTransaction.outputsLength).toBe(2);
expect(depositTransaction.getOutput(0).amount?.toString()).toBe('9900');
expect(depositTransaction.getOutput(0).script).toStrictEqual(multisigPayment.script);
expect(depositTransaction.getOutput(1).amount?.toString()).toBe('99989182');
expect(depositTransaction.getOutput(1).script).toStrictEqual(depositPayment.script);
expect(() =>
depositTransaction.sign(
deriveUnhardenedKeyPairFromRootPrivateKey(
TEST_BITCOIN_REGTEST_NATIVE_SEGWIT_XPRIV_1,
regtest,
'p2wpkh',
0
).privateKey!
)
).not.toThrow();
});
});
});

0 comments on commit ff9ff5d

Please sign in to comment.