Skip to content

Commit

Permalink
chore: merge branch 'main' into refactor/xrpl-imports
Browse files Browse the repository at this point in the history
  • Loading branch information
Polybius93 committed Feb 3, 2025
2 parents aa3d6c1 + 3737c15 commit 00f3d17
Show file tree
Hide file tree
Showing 6 changed files with 234 additions and 2 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"type": "module",
"name": "dlc-btc-lib",
"version": "2.5.10",
"version": "2.5.11",
"description": "This library provides a comprehensive set of interfaces and functions for minting dlcBTC tokens on supported blockchains.",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down
61 changes: 61 additions & 0 deletions src/functions/bitcoin/bitcoin-functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import { BIP32Factory, BIP32Interface } from 'bip32';
import { Network, address, initEccLib } from 'bitcoinjs-lib';
import { bitcoin, regtest, testnet } from 'bitcoinjs-lib/src/networks.js';
import { Decimal } from 'decimal.js';
import { equals, uniq } from 'ramda';
import { RawVault } from 'src/models/ethereum-models.js';
import * as ellipticCurveCryptography from 'tiny-secp256k1';

import { DUST_LIMIT } from '../../constants/dlc-handler.constants.js';
Expand Down Expand Up @@ -58,6 +60,65 @@ export function removeDustOutputs(
}
}

export function getDerivedUnspendablePublicKeyCommittedToUUID(
vaultUUID: string,
bitcoinNetwork: Network
): Buffer {
return deriveUnhardenedPublicKey(
getUnspendableKeyCommittedToUUID(vaultUUID, bitcoinNetwork),
bitcoinNetwork
);
}

/**
* This function retrieves the Bitcoin address used to fund a Vault by analyzing the inputs and outputs of the Funding Transaction.
*
* @param vault - The Vault object containing the Funding Transaction ID and the User's Public Key.
* @param bitcoinTransaction - The Bitcoin Transaction from which the Funding Address should be retrieved.
* @param feeRecipient - The Fee Recipient's Public Key or Address.
* @param extendedAttestorGroupPublicKey - The Extended Public Key of the Attestor Group.
* @param bitcoinNetwork - The Bitcoin Network to use.
* @param bitcoinBlockchainAPIURL - The Bitcoin Blockchain URL used to fetch the Funding Transaction.
* @returns A promise that resolves to the Funding Bitcoin address.
* @throws An error if the Vault Funding Address cannot be determined.
*/
export async function getVaultFundingBitcoinAddress(
vault: RawVault,
bitcoinTransaction: BitcoinTransaction,
feeRecipient: string,
extendedAttestorGroupPublicKey: string,
bitcoinNetwork: Network
): Promise<string> {
const multisigAddress = createTaprootMultisigPayment(
getDerivedUnspendablePublicKeyCommittedToUUID(vault.uuid, bitcoinNetwork),
deriveUnhardenedPublicKey(extendedAttestorGroupPublicKey, bitcoinNetwork),
Buffer.from(vault.taprootPubKey, 'hex'),
bitcoinNetwork
).address;

const feeRecipientAddress = getFeeRecipientAddress(feeRecipient, bitcoinNetwork);

const inputAddresses = uniq(
bitcoinTransaction.vin.map(input => input.prevout.scriptpubkey_address)
);

// If the only input is the MultiSig address, it is a withdrawal transaction.
// Therefore, the funding address is the non-fee recipient output address.
// If there is a single non-MultiSig input that is not from the MultiSig address, or if there are multiple inputs, it is a funding/deposit transaction.
// Therefore, the funding address is the non-MultiSig input address.
const addresses =
equals(inputAddresses.length, 1) && equals(inputAddresses.at(0), multisigAddress)
? bitcoinTransaction.vout
.filter(output => output.scriptpubkey_address !== feeRecipientAddress)
.map(output => output.scriptpubkey_address)
: inputAddresses.filter(address => !equals(address, multisigAddress));

if (!equals(addresses.length, 1))
throw new Error('Could not determine the Vault Funding Address');

return addresses.at(0)!;
}

/**
* 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
4 changes: 4 additions & 0 deletions src/functions/bitcoin/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import {
finalizeUserInputs,
getBitcoinAddressFromExtendedPublicKey,
getDerivedUnspendablePublicKeyCommittedToUUID,
getFeeAmount,
getFeeRecipientAddress,
getInputIndicesByScript,
getVaultFundingBitcoinAddress,
isBitcoinAddress,
} from '../bitcoin/bitcoin-functions.js';
import {
Expand All @@ -21,6 +23,8 @@ import {
export {
isBitcoinAddress,
createFundingTransaction,
getDerivedUnspendablePublicKeyCommittedToUUID,
getVaultFundingBitcoinAddress,
createDepositTransaction,
createWithdrawTransaction,
broadcastTransaction,
Expand Down
125 changes: 125 additions & 0 deletions tests/mocks/bitcoin-transaction.test.constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1254,6 +1254,131 @@ export const TEST_MAINNET_FUNDING_TRANSACTION_1: BitcoinTransaction = {
},
};

export const TEST_TESTNET_FUNDING_TRANSACTION_4: BitcoinTransaction = {
txid: '4cf5c2954c84bf5225d98ef014aa97bbfa0f05d56b5749782fcd8af8b9d505a5',
version: 2,
locktime: 0,
vin: [
{
txid: 'cefbeafc3e50618a59646ba6e7b3bba8f15b3e2551570af98182f4234586d085',
vout: 2,
prevout: {
scriptpubkey: '5120192d65c33b86bc129d606c12f0183569d42732d59cad3bf208a9a9fd3b138248',
scriptpubkey_asm:
'OP_PUSHNUM_1 OP_PUSHBYTES_32 192d65c33b86bc129d606c12f0183569d42732d59cad3bf208a9a9fd3b138248',
scriptpubkey_type: 'v1_p2tr',
scriptpubkey_address: 'tb1prykktsems67p98tqdsf0qxp4d82zwvk4njknhusg4x5l6wcnsfyqar32mq',
value: 71607616,
},
scriptsig: '',
scriptsig_asm: '',
witness: [
'd4ad3523fdc9ec709e8bf2ecadd56c9266f9c57bccb5d165cd57dc815a88de34957764482a6fab3897ce7be2677168f69be93d799021b502899b556436c3f6bb',
],
is_coinbase: false,
sequence: 4294967280,
},
{
txid: 'cefbeafc3e50618a59646ba6e7b3bba8f15b3e2551570af98182f4234586d085',
vout: 2,
prevout: {
scriptpubkey: '5120192d65c33b86bc129d606c12f0183569d42732d59cad3bf208a9a9fd3b138248',
scriptpubkey_asm:
'OP_PUSHNUM_1 OP_PUSHBYTES_32 192d65c33b86bc129d606c12f0183569d42732d59cad3bf208a9a9fd3b138248',
scriptpubkey_type: 'v1_p2tr',
scriptpubkey_address: 'tb1pd4l9qxw8jhg9l57ls9cnq6d28gcfayf2v9244vlt6mj80apvracqgdt090',
value: 71607616,
},
scriptsig: '',
scriptsig_asm: '',
witness: [
'd4ad3523fdc9ec709e8bf2ecadd56c9266f9c57bccb5d165cd57dc815a88de34957764482a6fab3897ce7be2677168f69be93d799021b502899b556436c3f6bb',
],
is_coinbase: false,
sequence: 4294967280,
},
],
vout: [
{
scriptpubkey: '0014f28ec1a3e3df0240b98582ca7754e6948e9bf930',
scriptpubkey_asm: 'OP_0 OP_PUSHBYTES_20 f28ec1a3e3df0240b98582ca7754e6948e9bf930',
scriptpubkey_type: 'v0_p2wpkh',
scriptpubkey_address: 'tb1q728vrglrmupypwv9st98w48xjj8fh7fs8mrdre',
value: 100000,
},
{
scriptpubkey: '5120192d65c33b86bc129d606c12f0183569d42732d59cad3bf208a9a9fd3b138248',
scriptpubkey_asm:
'OP_PUSHNUM_1 OP_PUSHBYTES_32 192d65c33b86bc129d606c12f0183569d42732d59cad3bf208a9a9fd3b138248',
scriptpubkey_type: 'v1_p2tr',
scriptpubkey_address: 'tb1prykktsems67p98tqdsf0qxp4d82zwvk4njknhusg4x5l6wcnsfyqar32mq',
value: 61490226,
},
],
size: 236,
weight: 740,
fee: 17390,
status: {
confirmed: true,
block_height: 2867279,
block_hash: '000000000000001ee12e0297ff36e8c8041aefb65af0c1033a1af4fdb8146f0d',
block_time: 1720620175,
},
};

export const TEST_TESTNET_FUNDING_TRANSACTION_5: BitcoinTransaction = {
txid: '4cf5c2954c84bf5225d98ef014aa97bbfa0f05d56b5749782fcd8af8b9d505a5',
version: 2,
locktime: 0,
vin: [
{
txid: 'cefbeafc3e50618a59646ba6e7b3bba8f15b3e2551570af98182f4234586d085',
vout: 2,
prevout: {
scriptpubkey: '5120192d65c33b86bc129d606c12f0183569d42732d59cad3bf208a9a9fd3b138248',
scriptpubkey_asm:
'OP_PUSHNUM_1 OP_PUSHBYTES_32 192d65c33b86bc129d606c12f0183569d42732d59cad3bf208a9a9fd3b138248',
scriptpubkey_type: 'v1_p2tr',
scriptpubkey_address: 'tb1pd4l9qxw8jhg9l57ls9cnq6d28gcfayf2v9244vlt6mj80apvracqgdt090',
value: 71607616,
},
scriptsig: '',
scriptsig_asm: '',
witness: [
'd4ad3523fdc9ec709e8bf2ecadd56c9266f9c57bccb5d165cd57dc815a88de34957764482a6fab3897ce7be2677168f69be93d799021b502899b556436c3f6bb',
],
is_coinbase: false,
sequence: 4294967280,
},
],
vout: [
{
scriptpubkey: '0014f28ec1a3e3df0240b98582ca7754e6948e9bf930',
scriptpubkey_asm: 'OP_0 OP_PUSHBYTES_20 f28ec1a3e3df0240b98582ca7754e6948e9bf930',
scriptpubkey_type: 'v0_p2wpkh',
scriptpubkey_address: 'tb1q728vrglrmupypwv9st98w48xjj8fh7fs8mrdre',
value: 100000,
},
{
scriptpubkey: '5120192d65c33b86bc129d606c12f0183569d42732d59cad3bf208a9a9fd3b138248',
scriptpubkey_asm:
'OP_PUSHNUM_1 OP_PUSHBYTES_32 192d65c33b86bc129d606c12f0183569d42732d59cad3bf208a9a9fd3b138248',
scriptpubkey_type: 'v1_p2tr',
scriptpubkey_address: 'tb1prykktsems67p98tqdsf0qxp4d82zwvk4njknhusg4x5l6wcnsfyqar32mq',
value: 61490226,
},
],
size: 236,
weight: 740,
fee: 17390,
status: {
confirmed: true,
block_height: 2867279,
block_hash: '000000000000001ee12e0297ff36e8c8041aefb65af0c1033a1af4fdb8146f0d',
block_time: 1720620175,
},
};

export const TEST_ALICE_UTXOS_REGTEST_1 = [
{
type: 'wpkh',
Expand Down
2 changes: 1 addition & 1 deletion tests/mocks/ethereum-vault.test.constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export const TEST_VAULT_2: RawVault = {
fundingTxId: '4cf5c2954c84bf5225d98ef014aa97bbfa0f05d56b5749782fcd8af8b9d505a5',
closingTxId: '',
wdTxId: '032392b61a5c3b0098774465ad61e429fd892615ff2890f849f8eb237a8a59f3ba',
btcFeeRecipient: '',
btcFeeRecipient: '032392b61a5c3b0098774465ad61e429fd892615ff2890f849f8eb237a8a59f3ba',
btcMintFeeBasisPoints: BigNumber.from('0x64'),
btcRedeemFeeBasisPoints: BigNumber.from('0x64'),
taprootPubKey: 'dc544c17af0887dfc8ca9936755c9fdef0c79bbc8866cd69bf120c71509742d2',
Expand Down
42 changes: 42 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,
getVaultFundingBitcoinAddress,
removeDustOutputs,
} from '../../src/functions/bitcoin/bitcoin-functions';
import {
Expand All @@ -24,6 +25,8 @@ import {
TEST_DEPOSIT_PSBT_PARTIALLY_SIGNED_DEPOSIT_PSBT_3,
TEST_TESTNET_FUNDING_TRANSACTION_1,
TEST_TESTNET_FUNDING_TRANSACTION_2,
TEST_TESTNET_FUNDING_TRANSACTION_4,
TEST_TESTNET_FUNDING_TRANSACTION_5,
TEST_WITHDRAW_PSBT_PARTIALLY_SIGNED_WITHDRAW_PSBT_1,
} from '../mocks/bitcoin-transaction.test.constants';
import {
Expand All @@ -38,6 +41,7 @@ import {
TEST_UNHARDENED_DERIVED_UNSPENDABLE_KEY_COMMITED_TO_UUID_1,
TEST_UNSPENDABLE_KEY_COMMITED_TO_UUID_1,
} from '../mocks/bitcoin.test.constants';
import { TEST_VAULT_2 } from '../mocks/ethereum-vault.test.constants.js';
import { TEST_VAULT_UUID_1 } from '../mocks/ethereum.test.constants';

describe('Bitcoin Functions', () => {
Expand Down Expand Up @@ -295,7 +299,45 @@ describe('Bitcoin Functions', () => {
expect(result).toBeUndefined();
});
});
describe('getVaultFundingBitcoinAddress', () => {
const expectedFundingAddress = 'tb1prykktsems67p98tqdsf0qxp4d82zwvk4njknhusg4x5l6wcnsfyqar32mq';

it('should return input address when single non-multisig input exists', async () => {
const result = await getVaultFundingBitcoinAddress(
TEST_VAULT_2,
TEST_TESTNET_FUNDING_TRANSACTION_1,
TEST_VAULT_2.btcFeeRecipient,
TEST_TESTNET_ATTESTOR_EXTENDED_GROUP_PUBLIC_KEY_1,
testnet
);

expect(result).toBe(expectedFundingAddress);
});

it('should return non-multisig address when transaction has multiple inputs', async () => {
const result = await getVaultFundingBitcoinAddress(
TEST_VAULT_2,
TEST_TESTNET_FUNDING_TRANSACTION_4,
TEST_VAULT_2.btcFeeRecipient,
TEST_TESTNET_ATTESTOR_EXTENDED_GROUP_PUBLIC_KEY_1,
testnet
);

expect(result).toBe(expectedFundingAddress);
});

it('should return non-fee-recipient output address when input is from multisig address', async () => {
const result = await getVaultFundingBitcoinAddress(
TEST_VAULT_2,
TEST_TESTNET_FUNDING_TRANSACTION_5,
TEST_VAULT_2.btcFeeRecipient,
TEST_TESTNET_ATTESTOR_EXTENDED_GROUP_PUBLIC_KEY_1,
testnet
);

expect(result).toBe(expectedFundingAddress);
});
});
describe('getFeeAmount', () => {
test('calculates correct fee for whole numbers', () => {
expect(getFeeAmount(1000000, 50)).toBe(5000);
Expand Down

0 comments on commit 00f3d17

Please sign in to comment.