Skip to content

Commit

Permalink
feat: add getVaultOutputValue function (#65)
Browse files Browse the repository at this point in the history
* feat: add getVaultOutputValue function, export related functions, modify getFundingAddress function

* feat: modify getVaultFundingBitcoinAddress function filtering logic
  • Loading branch information
Polybius93 authored Feb 11, 2025
1 parent c183427 commit e9b6fb5
Show file tree
Hide file tree
Showing 5 changed files with 123 additions and 68 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.16",
"version": "2.5.17",
"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
109 changes: 65 additions & 44 deletions src/functions/bitcoin/bitcoin-functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ 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 { equals, filter, find, pathOr, pipe, pluck, uniq } from 'ramda';
import * as ellipticCurveCryptography from 'tiny-secp256k1';

import { DUST_LIMIT } from '../../constants/dlc-handler.constants.js';
Expand All @@ -29,7 +29,6 @@ import {
PaymentTypes,
UTXO,
} from '../../models/bitcoin-models.js';
import { RawVault } from '../../models/ethereum-models.js';
import {
compareUint8Arrays,
createRangeFromLength,
Expand Down Expand Up @@ -73,68 +72,90 @@ export function getDerivedUnspendablePublicKeyCommittedToUUID(
/**
* 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.
* @param vaultPayment - The Vault's P2TR payment information containing the multisig address
* @param bitcoinTransaction - The Bitcoin transaction to analyze
* @param feeRecipientAddress - The address that receives transaction fees
* @returns A promise that resolves to the funding Bitcoin address
* @throws {Error} If the funding address cannot be uniquely determined
*/
export async function getVaultFundingBitcoinAddress(
vault: RawVault,
export function getVaultFundingBitcoinAddress(
vaultPayment: P2TROut,
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);

feeRecipientAddress: string
): string {
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.
// Therefore, the funding address is the non-fee and non-vault 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 => !equals(output.scriptpubkey_address, feeRecipientAddress))
.map(output => output.scriptpubkey_address)
: inputAddresses.filter(address => !equals(address, multisigAddress));
const isWithdraw =
equals(inputAddresses.length, 1) && equals(inputAddresses.at(0), vaultPayment.address);

const isNotFeeRecipient = (address: string) => !equals(address, feeRecipientAddress);
const isNotVaultAddress = (address: string) => !equals(address, vaultPayment.address);

const isFundingAddress = (address: string) =>
isNotFeeRecipient(address) && isNotVaultAddress(address);

const addresses = isWithdraw
? pipe(
filter<BitcoinTransactionVectorOutput>(output =>
isFundingAddress(output.scriptpubkey_address)
),
pluck('scriptpubkey_address')
)(bitcoinTransaction.vout)
: filter(isNotVaultAddress)(inputAddresses);

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

return addresses.at(0)!;
}

export async function getVaultOutputValueFromTransaction(
vault: RawVault,
bitcoinTransaction: BitcoinTransaction,
/**
* Calculates the value of a Vault's output in a Bitcoin transaction by finding
* the output that matches the Vault's multisig address.
*
* @param vaultPayment - The vault's P2TR payment information containing the multisig address
* @param bitcoinTransaction - The Bitcoin transaction to analyze
* @returns A promise that resolves to the value of the matching output in satoshis, or 0 if no match is found
*/
export function getVaultOutputValueFromTransaction(
vaultPayment: P2TROut,
bitcoinTransaction: BitcoinTransaction
): number {
return pipe(
find<BitcoinTransactionVectorOutput>(output =>
equals(output.scriptpubkey_address, vaultPayment.address)
),
pathOr(0, ['value'])
)(bitcoinTransaction.vout);
}

/**
* Creates a Pay-to-Taproot (P2TR) multisig payment configuration for a Vault
* using the vault UUID, user's public key, and the Attestor group's extended public key.
*
* @param vaultUUID - Unique identifier of the vault
* @param derivedUserPublicKey - The user's derived Taproot public key in hex format
* @param extendedAttestorGroupPublicKey - The extended public key of the attestor group
* @param bitcoinNetwork - The Bitcoin network configuration to use (mainnet or testnet)
* @returns A promise that resolves to a P2TR payment output configuration containing the multisig address
*/
export function getVaultPayment(
vaultUUID: string,
derivedUserPublicKey: string,
extendedAttestorGroupPublicKey: string,
bitcoinNetwork: Network
): Promise<number> {
const multisigAddress = createTaprootMultisigPayment(
getDerivedUnspendablePublicKeyCommittedToUUID(vault.uuid, bitcoinNetwork),
): P2TROut {
return createTaprootMultisigPayment(
getDerivedUnspendablePublicKeyCommittedToUUID(vaultUUID, bitcoinNetwork),
deriveUnhardenedPublicKey(extendedAttestorGroupPublicKey, bitcoinNetwork),
Buffer.from(vault.taprootPubKey, 'hex'),
Buffer.from(derivedUserPublicKey, 'hex'),
bitcoinNetwork
).address;

return (
bitcoinTransaction.vout.find(output => equals(output.scriptpubkey_address, multisigAddress))
?.value ?? 0
);
}

Expand Down
4 changes: 4 additions & 0 deletions src/functions/bitcoin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import {
getFeeRecipientAddress,
getInputIndicesByScript,
getVaultFundingBitcoinAddress,
getVaultOutputValueFromTransaction,
getVaultPayment,
isBitcoinAddress,
} from '../bitcoin/bitcoin-functions.js';
import {
Expand Down Expand Up @@ -36,4 +38,6 @@ export {
getFeeRecipientAddress,
getInputIndicesByScript,
getBitcoinAddressFromExtendedPublicKey,
getVaultOutputValueFromTransaction,
getVaultPayment,
};
8 changes: 8 additions & 0 deletions tests/mocks/bitcoin-transaction.test.constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1367,6 +1367,14 @@ export const TEST_TESTNET_FUNDING_TRANSACTION_5: BitcoinTransaction = {
scriptpubkey_address: 'tb1prykktsems67p98tqdsf0qxp4d82zwvk4njknhusg4x5l6wcnsfyqar32mq',
value: 61490226,
},
{
scriptpubkey: '5120192d65c33b86bc129d606c12f0183569d42732d59cad3bf208a9a9fd3b138248',
scriptpubkey_asm:
'OP_PUSHNUM_1 OP_PUSHBYTES_32 192d65c33b86bc129d606c12f0183569d42732d59cad3bf208a9a9fd3b138248',
scriptpubkey_type: 'v1_p2tr',
scriptpubkey_address: 'tb1pd4l9qxw8jhg9l57ls9cnq6d28gcfayf2v9244vlt6mj80apvracqgdt090',
value: 61490226,
},
],
size: 236,
weight: 740,
Expand Down
68 changes: 45 additions & 23 deletions tests/unit/bitcoin-functions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
getUnspendableKeyCommittedToUUID,
getVaultFundingBitcoinAddress,
getVaultOutputValueFromTransaction,
getVaultPayment,
removeDustOutputs,
} from '../../src/functions/bitcoin/bitcoin-functions';
import {
Expand Down Expand Up @@ -303,61 +304,82 @@ describe('Bitcoin Functions', () => {
});
describe('getVaultFundingBitcoinAddress', () => {
const expectedFundingAddress = 'tb1prykktsems67p98tqdsf0qxp4d82zwvk4njknhusg4x5l6wcnsfyqar32mq';
const feeRecipientAddress = getFeeRecipientAddress(TEST_VAULT_2.btcFeeRecipient, testnet);

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,
it('should return input address when single non-multisig input exists', () => {
const vaultPayment = getVaultPayment(
TEST_VAULT_2.uuid,
TEST_VAULT_2.taprootPubKey,
TEST_TESTNET_ATTESTOR_EXTENDED_GROUP_PUBLIC_KEY_1,
testnet
);
const result = getVaultFundingBitcoinAddress(
vaultPayment,
TEST_TESTNET_FUNDING_TRANSACTION_1,
feeRecipientAddress
);

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,
it('should return non-multisig address when transaction has multiple inputs', () => {
const vaultPayment = getVaultPayment(
TEST_VAULT_2.uuid,
TEST_VAULT_2.taprootPubKey,
TEST_TESTNET_ATTESTOR_EXTENDED_GROUP_PUBLIC_KEY_1,
testnet
);
const result = getVaultFundingBitcoinAddress(
vaultPayment,
TEST_TESTNET_FUNDING_TRANSACTION_4,
feeRecipientAddress
);

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,
it('should return non-fee-recipient and non-vault output address when input is from vault address', () => {
const vaultPayment = getVaultPayment(
TEST_VAULT_2.uuid,
TEST_VAULT_2.taprootPubKey,
TEST_TESTNET_ATTESTOR_EXTENDED_GROUP_PUBLIC_KEY_1,
testnet
);
const result = getVaultFundingBitcoinAddress(
vaultPayment,
TEST_TESTNET_FUNDING_TRANSACTION_5,
feeRecipientAddress
);

expect(result).toBe(expectedFundingAddress);
});
});
describe('getVaultOutputValueFromTransaction', () => {
it('should return valid output value when multisig output exists', async () => {
const result = await getVaultOutputValueFromTransaction(
TEST_VAULT_2,
TEST_TESTNET_FUNDING_TRANSACTION_1,
it('should return valid output value when multisig output exists', () => {
const vaultPayment = getVaultPayment(
TEST_VAULT_2.uuid,
TEST_VAULT_2.taprootPubKey,
TEST_TESTNET_ATTESTOR_EXTENDED_GROUP_PUBLIC_KEY_1,
testnet
);
const result = getVaultOutputValueFromTransaction(
vaultPayment,
TEST_TESTNET_FUNDING_TRANSACTION_1
);

expect(result).toBe(10000000);
});
it('should return 0 if multisig output does not exist', async () => {
const result = await getVaultOutputValueFromTransaction(
TEST_VAULT_2,
TEST_TESTNET_FUNDING_TRANSACTION_6,
it('should return 0 if multisig output does not exist', () => {
const vaultPayment = getVaultPayment(
TEST_VAULT_2.uuid,
TEST_VAULT_2.taprootPubKey,
TEST_TESTNET_ATTESTOR_EXTENDED_GROUP_PUBLIC_KEY_1,
testnet
);
const result = getVaultOutputValueFromTransaction(
vaultPayment,
TEST_TESTNET_FUNDING_TRANSACTION_6
);

expect(result).toBe(0);
});
Expand Down

0 comments on commit e9b6fb5

Please sign in to comment.