Skip to content

Commit

Permalink
fix: bouncer btc client mutex for vault swaps (#5436)
Browse files Browse the repository at this point in the history
* fix: bouncer lock utxos after selection

* refactor: use mutex instead of locking

* feat: use bitcoin node's input selection.

* chore: set correct timeouts for LP-API bouncer test

* fix: additional btc tx options:

- lockUnspents: true to ensure we don't try to double-spend
- changePosition: 2 on vault swaps to ensure a valid transaction

---------

Co-authored-by: Daniel <daniel@chainflip.io>
  • Loading branch information
2 people authored and nmammeri committed Nov 27, 2024
1 parent 08c7b7c commit 8fae6cc
Show file tree
Hide file tree
Showing 4 changed files with 64 additions and 65 deletions.
22 changes: 10 additions & 12 deletions bouncer/commands/create_raw_btc_tx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,24 @@
// Constructs a very simple Raw BTC transaction. Can be used for manual testing a raw broadcast for example.
// Usage: ./commands/create_raw_btc_tx.ts <bitcoin_address> <btc_amount>

import { BTC_ENDPOINT, btcClient, selectInputs } from '../shared/send_btc';
import { BTC_ENDPOINT, btcClient } from '../shared/send_btc';

console.log(`Btc endpoint is set to '${BTC_ENDPOINT}'`);

const createRawTransaction = async (toAddress: string, amountInBtc: number | string) => {
try {
// Inputs and outputs
const feeInBtc = 0.00001;
const { inputs, change } = await selectInputs(Number(amountInBtc) + feeInBtc);
const changeAddress = await btcClient.getNewAddress();
const outputs = {
[toAddress]: amountInBtc,
[changeAddress]: change,
};

// Create the raw transaction
const rawTx = await btcClient.createRawTransaction(inputs, outputs);
const rawTx = await btcClient.createRawTransaction([], {
[toAddress]: amountInBtc,
});
const fundedTx = (await btcClient.fundRawTransaction(rawTx, {
changeAddress: await btcClient.getNewAddress(),
feeRate: 0.00001,
lockUnspents: true,
})) as { hex: string };

// Sign the raw transaction
const signedTx = await btcClient.signRawTransactionWithWallet(rawTx);
const signedTx = await btcClient.signRawTransactionWithWallet(fundedTx);

// Here's your raw signed transaction
console.log('Raw signed transaction:', signedTx.hex);
Expand Down
56 changes: 39 additions & 17 deletions bouncer/shared/send_btc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,26 +11,48 @@ export const btcClient = new Client({
wallet: 'whale',
});

export async function selectInputs(amount: number) {
// List unspent UTXOs
const utxos = await btcClient.listUnspent();

// Find a UTXO with enough funds
const utxo = utxos.find((u) => u.amount >= amount);
if (!utxo) throw new Error('Insufficient funds');
// TODO: be able to select more than one UTXO
export async function fundAndSendTransaction(
outputs: object[],
changeAddress: string,
feeRate?: number,
): Promise<string> {
return btcClientMutex.runExclusive(async () => {
const rawTx = await btcClient.createRawTransaction([], outputs);
const fundedTx = (await btcClient.fundRawTransaction(rawTx, {
changeAddress,
feeRate: feeRate ?? 0.00001,
lockUnspents: true,
changePosition: 2,
})) as { hex: string };
const signedTx = await btcClient.signRawTransactionWithWallet(fundedTx.hex);
const txId = (await btcClient.sendRawTransaction(signedTx.hex)) as string | undefined;

const change = utxo.amount - amount;
if (!txId) {
throw new Error('Broadcast failed');
}

// Prepare the transaction inputs and outputs
const inputs = [
{
txid: utxo.txid,
vout: utxo.vout,
},
];
return txId;
});
}

return { inputs, change };
export async function sendVaultTransaction(
nulldataUtxo: string,
amountBtc: number,
depositAddress: string,
refundAddress: string,
): Promise<string> {
return fundAndSendTransaction(
[
{
[depositAddress]: amountBtc,
},
{
// The `createRawTransaction` function will add the op codes, so we have to remove them here.
data: nulldataUtxo.replace('0x', '').substring(4),
},
],
refundAddress,
);
}

export async function waitForBtcTransaction(txid: string, confirmations = 1) {
Expand Down
35 changes: 7 additions & 28 deletions bouncer/tests/btc_vault_swap.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import assert from 'assert';
import { ExecutableTest } from '../shared/executable_test';
import { BTC_ENDPOINT, selectInputs, waitForBtcTransaction, btcClient } from '../shared/send_btc';
import { BTC_ENDPOINT, waitForBtcTransaction, sendVaultTransaction } from '../shared/send_btc';
import {
amountToFineAmount,
Asset,
assetDecimals,
btcClientMutex,
createStateChainKeypair,
newAddress,
observeBalanceIncrease,
Expand Down Expand Up @@ -51,9 +50,6 @@ async function buildAndSendBtcVaultSwap(
affiliates.push({ account: affiliateAddress, bps: commissionBps });
}

const feeBtc = 0.00001;
const { inputs, change } = await selectInputs(Number(depositAmountBtc) + feeBtc);

const vaultSwapDetails = (await chainflip.rpc(
`cf_get_vault_swap_details`,
broker.address,
Expand All @@ -71,32 +67,15 @@ async function buildAndSendBtcVaultSwap(
testBtcVaultSwap.debugLog('nulldata_utxo:', vaultSwapDetails.nulldata_utxo);
testBtcVaultSwap.debugLog('deposit_address:', vaultSwapDetails.deposit_address);

// The `createRawTransaction` function will add the op codes, so we have to remove them here.
const nullDataWithoutOpCodes = vaultSwapDetails.nulldata_utxo.replace('0x', '').substring(4);

const outputs = [
{
[vaultSwapDetails.deposit_address]: depositAmountBtc,
},
{
data: nullDataWithoutOpCodes,
},
{
[refundAddress]: change,
},
];

const rawTx = await btcClient.createRawTransaction(inputs, outputs, 0, false);
const signedTx = await btcClient.signRawTransactionWithWallet(rawTx);
const txid = await btcClientMutex.runExclusive(async () =>
btcClient.sendRawTransaction(signedTx.hex),
const txid = await sendVaultTransaction(
vaultSwapDetails.nulldata_utxo,
depositAmountBtc,
vaultSwapDetails.deposit_address,
refundAddress,
);
if (!txid) {
throw new Error('Broadcast failed');
}
testBtcVaultSwap.log('Broadcast successful, txid:', txid);

await waitForBtcTransaction(txid as string);
await waitForBtcTransaction(txid);
testBtcVaultSwap.debugLog('Transaction confirmed');
}

Expand Down
16 changes: 8 additions & 8 deletions bouncer/tests/lp_api_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -397,27 +397,27 @@ async function main() {
const registerLiquidityRefundAddress = new ExecutableTest(
'LP-API::Register-Liquidity-Refund-Address',
testRegisterLiquidityRefundAddress,
500,
30,
);
const LiquidityDeposit = new ExecutableTest(
'LP-API::Liquidity-Deposit',
testLiquidityDeposit,
500,
60,
);
const WithdrawAsset = new ExecutableTest('LP-API::Withdraw-Asset', testWithdrawAsset, 500);
const WithdrawAsset = new ExecutableTest('LP-API::Withdraw-Asset', testWithdrawAsset, 60);
const RegisterWithExistingLpAccount = new ExecutableTest(
'LP-API::testRegisterWithExistingLpAccount',
testRegisterWithExistingLpAccount,
500,
15,
);
const RangeOrder = new ExecutableTest('LP-API::Range-Order', testRangeOrder, 500);
const LimitOrder = new ExecutableTest('LP-API::Limit-Order', testLimitOrder, 500);
const RangeOrder = new ExecutableTest('LP-API::Range-Order', testRangeOrder, 60);
const LimitOrder = new ExecutableTest('LP-API::Limit-Order', testLimitOrder, 120);
const GetOpenSwapChannels = new ExecutableTest(
'LP-API::Get-Open-Swap-Channels',
testGetOpenSwapChannels,
500,
15,
);
const TransferAsset = new ExecutableTest('LP-API::TransferAsset', testTransferAsset, 500);
const TransferAsset = new ExecutableTest('LP-API::TransferAsset', testTransferAsset, 30);

// Provide the amount of liquidity needed for the tests
await provideLiquidityAndTestAssetBalances();
Expand Down

0 comments on commit 8fae6cc

Please sign in to comment.