From 6f90f08ab415f5d1526e02758760e9038591e785 Mon Sep 17 00:00:00 2001 From: Polybius93 <99192647+Polybius93@users.noreply.github.com> Date: Thu, 21 Nov 2024 11:40:43 +0100 Subject: [PATCH] feat: modify fee recipient address retrieving function (#39) --- package.json | 2 +- src/functions/bitcoin/bitcoin-functions.ts | 31 +++++++-- src/functions/bitcoin/index.ts | 4 +- src/functions/bitcoin/psbt-functions.ts | 20 +++--- tests/unit/bitcoin-functions.test.ts | 79 +++++++++++++++++++++- 5 files changed, 115 insertions(+), 21 deletions(-) diff --git a/package.json b/package.json index 477c7a9..f8d0446 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "type": "module", "name": "dlc-btc-lib", - "version": "2.4.14", + "version": "2.4.16", "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", diff --git a/src/functions/bitcoin/bitcoin-functions.ts b/src/functions/bitcoin/bitcoin-functions.ts index fc466fe..381af8e 100644 --- a/src/functions/bitcoin/bitcoin-functions.ts +++ b/src/functions/bitcoin/bitcoin-functions.ts @@ -12,7 +12,7 @@ import { import { P2Ret, P2TROut } from '@scure/btc-signer/payment'; import { TransactionInput } from '@scure/btc-signer/psbt'; import { BIP32Factory, BIP32Interface } from 'bip32'; -import { Network } from 'bitcoinjs-lib'; +import { Network, address } from 'bitcoinjs-lib'; import { bitcoin, regtest, testnet } from 'bitcoinjs-lib/src/networks.js'; import { Decimal } from 'decimal.js'; import * as ellipticCurveCryptography from 'tiny-secp256k1'; @@ -242,18 +242,35 @@ export async function getBalance( } /** - * Gets the Fee Recipient's Address from the Rcipient's Public Key. - * @param feePublicKey - The Fee Recipient's Public Key. + * Validates a Bitcoin Address. + * @param bitcoinAddress + * @param bitcoinNetwork + * @returns A boolean indicating if the Bitcoin Address is valid. + */ +export function isBitcoinAddress(bitcoinAddress: string, bitcoinNetwork: Network): boolean { + try { + return !!address.toOutputScript(bitcoinAddress, bitcoinNetwork); + } catch { + return false; + } +} + +/** + * Gets the Fee Recipient's Address from the Recipient's Public Key or Address. + * @param bitcoinFeeRecipient - The Fee Recipient's Public Key or Address. * @param bitcoinNetwork - The Bitcoin Network to use. * @returns The Fee Recipient's Address. */ -export function getFeeRecipientAddressFromPublicKey( - feePublicKey: string, +export function getFeeRecipientAddress( + bitcoinFeeRecipient: string, bitcoinNetwork: Network ): string { - const feePublicKeyBuffer = Buffer.from(feePublicKey, 'hex'); - const { address } = p2wpkh(feePublicKeyBuffer, bitcoinNetwork); + if (isBitcoinAddress(bitcoinFeeRecipient, bitcoinNetwork)) return bitcoinFeeRecipient; + + const { address } = p2wpkh(Buffer.from(bitcoinFeeRecipient, 'hex'), bitcoinNetwork); + if (!address) throw new Error('Could not create Fee Address from Public Key'); + return address; } diff --git a/src/functions/bitcoin/index.ts b/src/functions/bitcoin/index.ts index 80c546a..8a27458 100644 --- a/src/functions/bitcoin/index.ts +++ b/src/functions/bitcoin/index.ts @@ -2,7 +2,7 @@ import { finalizeUserInputs, getBitcoinAddressFromExtendedPublicKey, getFeeAmount, - getFeeRecipientAddressFromPublicKey, + getFeeRecipientAddress, getInputIndicesByScript, } from '../bitcoin/bitcoin-functions.js'; import { @@ -27,7 +27,7 @@ export { finalizeUserInputs, getFeeAmount, getBalance, - getFeeRecipientAddressFromPublicKey, + getFeeRecipientAddress, getInputIndicesByScript, getBitcoinAddressFromExtendedPublicKey, }; diff --git a/src/functions/bitcoin/psbt-functions.ts b/src/functions/bitcoin/psbt-functions.ts index 5cf6c67..35a78bd 100644 --- a/src/functions/bitcoin/psbt-functions.ts +++ b/src/functions/bitcoin/psbt-functions.ts @@ -9,7 +9,7 @@ import { reverseBytes } from '../../utilities/index.js'; import { ecdsaPublicKeyToSchnorr, getFeeAmount, - getFeeRecipientAddressFromPublicKey, + getFeeRecipientAddress, getUTXOs, } from '../bitcoin/bitcoin-functions.js'; import { fetchBitcoinTransaction } from './bitcoin-request-functions.js'; @@ -25,7 +25,7 @@ import { fetchBitcoinTransaction } from './bitcoin-request-functions.js'; * @param multisigPayment - The Multisig Payment object created from the User's Taproot Public Key, the Attestor's Public Key, and the Unspendable Public Key committed to the Vault's UUID. * @param depositPayment - The User's Payment object which will be used to fund the Deposit Transaction. * @param feeRate - The Fee Rate to use for the Transaction. - * @param feePublicKey - The Fee Recipient's Public Key. + * @param feeRecipient - The Fee Recipient's Public Key or Address. * @param feeBasisPoints - The Fee Basis Points. * @returns A Funding Transaction. */ @@ -36,7 +36,7 @@ export async function createFundingTransaction( multisigPayment: P2TROut, depositPayment: P2Ret | P2TROut, feeRate: bigint, - feePublicKey: string, + feeRecipient: string, feeBasisPoints: bigint ): Promise { const multisigAddress = multisigPayment.address; @@ -51,7 +51,7 @@ export async function createFundingTransaction( throw new Error('Deposit Payment is missing Address'); } - const feeAddress = getFeeRecipientAddressFromPublicKey(feePublicKey, bitcoinNetwork); + const feeAddress = getFeeRecipientAddress(feeRecipient, bitcoinNetwork); const feeAmount = getFeeAmount(Number(depositAmount), Number(feeBasisPoints)); const userUTXOs = await getUTXOs(depositPayment, bitcoinBlockchainAPIURL); @@ -103,7 +103,7 @@ export async function createFundingTransaction( * @param multisigPayment - The Multisig Payment object created from the User's Taproot Public Key, the Attestor's Public Key, and the Unspendable Public Key committed to the Vault's UUID. * @param depositPayment - The User's Payment object which will be used to fund the Deposit Transaction. * @param feeRate - The Fee Rate to use for the Transaction. - * @param feePublicKey - The Fee Recipient's Public Key. + * @param feeRecipient - The Fee Recipient's Public Key or Address. * @param feeBasisPoints - The Fee Basis Points. * @returns A Deposit Transaction. */ @@ -115,7 +115,7 @@ export async function createDepositTransaction( multisigPayment: P2TROut, depositPayment: P2TROut | P2Ret, feeRate: bigint, - feePublicKey: string, + feeRecipient: string, feeBasisPoints: bigint ): Promise { const multisigAddress = multisigPayment.address; @@ -130,7 +130,7 @@ export async function createDepositTransaction( throw new Error('Deposit Payment is missing Address'); } - const feeAddress = getFeeRecipientAddressFromPublicKey(feePublicKey, bitcoinNetwork); + const feeAddress = getFeeRecipientAddress(feeRecipient, bitcoinNetwork); const feeAmount = getFeeAmount(Number(depositAmount), Number(feeBasisPoints)); const vaultTransaction = await fetchBitcoinTransaction(vaultTransactionID, bitcoinBlockchainURL); @@ -248,7 +248,7 @@ export async function createDepositTransaction( * @param multisigPayment - The Multisig Payment object created from the User's Taproot Public Key, the Attestor's Public Key, and the Unspendable Public Key committed to the Vault's UUID. * @param withdrawPayment - The User's Payment object which will be used to receive the withdrawn Bitcoin. * @param feeRate - The Fee Rate to use for the Transaction. - * @param feePublicKey - The Fee Recipient's Public Key. + * @param feeRecipient - The Fee Recipient's Public Key or Address. * @param feeBasisPoints - The Fee Basis Points. * @returns A Withdraw Transaction. */ @@ -260,7 +260,7 @@ export async function createWithdrawTransaction( multisigPayment: P2TROut, withdrawPayment: P2Ret | P2TROut, feeRate: bigint, - feePublicKey: string, + feeRecipient: string, feeBasisPoints: bigint ): Promise { const multisigAddress = multisigPayment.address; @@ -298,7 +298,7 @@ export async function createWithdrawTransaction( const remainingAmount = BigInt(fundingTransaction.vout[fundingTransactionOutputIndex].value) - BigInt(withdrawAmount); - const feeAddress = getFeeRecipientAddressFromPublicKey(feePublicKey, bitcoinNetwork); + const feeAddress = getFeeRecipientAddress(feeRecipient, bitcoinNetwork); const feeAmount = getFeeAmount(Number(withdrawAmount), Number(feeBasisPoints)); const inputs = [ diff --git a/tests/unit/bitcoin-functions.test.ts b/tests/unit/bitcoin-functions.test.ts index ae709ab..82cf91e 100644 --- a/tests/unit/bitcoin-functions.test.ts +++ b/tests/unit/bitcoin-functions.test.ts @@ -1,12 +1,13 @@ import { bytesToHex, hexToBytes } from '@noble/hashes/utils'; import { Transaction, p2tr, p2wpkh } from '@scure/btc-signer'; -import { regtest, testnet } from 'bitcoinjs-lib/src/networks'; +import { bitcoin, regtest, testnet } from 'bitcoinjs-lib/src/networks'; import { createTaprootMultisigPayment, deriveUnhardenedPublicKey, ecdsaPublicKeyToSchnorr, finalizeUserInputs, + getFeeRecipientAddress, getInputIndicesByScript, getScriptMatchingOutputFromTransaction, getUnspendableKeyCommittedToUUID, @@ -66,6 +67,82 @@ describe('Bitcoin Functions', () => { }); }); + describe('getFeeRecipientAddress', () => { + describe('mainnet', () => { + const network = bitcoin; + + it('accepts native segwit (p2wpkh) address', () => { + const address = 'bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4'; + expect(getFeeRecipientAddress(address, network)).toBe(address); + }); + + it('accepts taproot (p2tr) address', () => { + const address = 'bc1qw02rsw9afgp4dsd5n87z5s6rqnf455yhhsnz9f'; + expect(getFeeRecipientAddress(address, network)).toBe(address); + }); + + it('converts public key to native segwit address', () => { + const publicKey = '0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'; + const expectedAddress = 'bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4'; + expect(getFeeRecipientAddress(publicKey, network)).toBe(expectedAddress); + }); + }); + + describe('testnet', () => { + const network = testnet; + + it('accepts native segwit (p2wpkh) address', () => { + const address = 'tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx'; + expect(getFeeRecipientAddress(address, network)).toBe(address); + }); + + it('accepts taproot (p2tr) address', () => { + const address = 'tb1qqhy33peyp82mf82fktdtphfmnhtxyhtp6x9hrc'; + expect(getFeeRecipientAddress(address, network)).toBe(address); + }); + + it('converts public key to native segwit address', () => { + const publicKey = '0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'; + const expectedAddress = 'tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx'; + expect(getFeeRecipientAddress(publicKey, network)).toBe(expectedAddress); + }); + }); + + describe('regtest', () => { + const network = regtest; + + it('accepts native segwit (p2wpkh) address', () => { + const address = 'bcrt1qw508d6qejxtdg4y5r3zarvary0c5xw7kygt080'; + expect(getFeeRecipientAddress(address, network)).toBe(address); + }); + + it('accepts taproot (p2tr) address', () => { + const address = 'bcrt1qqhy33peyp82mf82fktdtphfmnhtxyhtpc0u653'; + expect(getFeeRecipientAddress(address, network)).toBe(address); + }); + + it('converts public key to native segwit address', () => { + const publicKey = '0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'; + const expectedAddress = 'bcrt1qw508d6qejxtdg4y5r3zarvary0c5xw7kygt080'; + expect(getFeeRecipientAddress(publicKey, network)).toBe(expectedAddress); + }); + }); + + describe('error cases', () => { + it('throws on invalid public key', () => { + const invalidKey = 'invalidPublicKey'; + expect(() => getFeeRecipientAddress(invalidKey, bitcoin)).toThrow( + 'P2WPKH: invalid publicKey' + ); + }); + + it('throws on invalid address', () => { + const invalidAddress = 'invalidAddress'; + expect(() => getFeeRecipientAddress(invalidAddress, bitcoin)).toThrow(); + }); + }); + }); + describe('finalizeUserInputs', () => { it('correctly finalizes inputs given a transaction and a native segwit payment script', () => { const transaction = Transaction.fromPSBT(