diff --git a/.env.template b/.env.template new file mode 100644 index 0000000..36811c6 --- /dev/null +++ b/.env.template @@ -0,0 +1,10 @@ +BITCOIN_NETWORK=testnet +BITGO_ACCESS_TOKEN= +BITGO_WALLET_PASSPHRASE= +BITGO_WALLET_ID= +BITGO_NATIVE_SEGWIT_ADDRESS= +BITGO_TAPROOT_ADDRESS= +USER_XPUB= +BACKUP_XPUB= +BITGO_XPUB= +BITCOIN_BLOCKCHAIN_API_URL=https://testnet.dlc.link/electrs diff --git a/.gitignore b/.gitignore index c6bba59..2b3f55c 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,10 @@ lerna-debug.log* # Diagnostic reports (https://nodejs.org/api/report.html) report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json +# Example JSON files +tb1ppxcqqtaxxqwclxudhervdnngzehpyr5mahczne64rkp2x7kvzfvqgda4ja.json +tb1q3ekr0u3s6clpag3tzaqk23f6edltxq3xl00hq8fjgugq5ds87jqsza5k55.json + # Runtime data pids *.pid diff --git a/package.json b/package.json index bb66b50..3ba42c9 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "decimal.js": "^10.4.3", "dotenv": "^16.4.5", "lint": "^0.8.19", + "lodash": "^4.17.21", "ls-lint": "^0.1.2", "noble": "^1.9.1", "prettier-eslint": "^16.3.0", diff --git a/src/bitcoin-functions.ts b/src/bitcoin-functions.ts index 8dbf69a..2c7d1a8 100644 --- a/src/bitcoin-functions.ts +++ b/src/bitcoin-functions.ts @@ -1,17 +1,14 @@ /** @format */ -import { bytesToHex, hexToBytes } from '@noble/hashes/utils'; +import { hexToBytes } from '@noble/hashes/utils'; import { hex } from '@scure/base'; -import { Transaction, selectUTXO } from '@scure/btc-signer'; -import { P2TROut, p2ms, p2tr, p2tr_ns, p2wpkh, p2wsh } from '@scure/btc-signer/payment'; +import { selectUTXO } from '@scure/btc-signer'; +import { P2TROut, p2tr, p2tr_ns, p2wpkh } from '@scure/btc-signer/payment'; import { taprootTweakPubkey } from '@scure/btc-signer/utils'; -import BIP32Factory, { TinySecp256k1Interface } from 'bip32'; -import { Network, Payment, payments } from 'bitcoinjs-lib'; -import * as ecc from 'tiny-secp256k1'; +import { Network, Psbt } from 'bitcoinjs-lib'; +import { getNativeSegwitMultisigScript } from './payment-functions.js'; import { bitcoinToSats } from './utilities.js'; -import { fromOutputScript, fromBech32 } from 'bitcoinjs-lib/src/address.js'; -import { TARGET_CHILD_NODES, TARGET_NATIVE_SEGWIT_MULTISIG_PUBLICKEYS } from './index.js'; interface TransactionStatus { confirmed: boolean; @@ -27,68 +24,6 @@ interface UTXO { value: number; } -export function getAddress(publicKey: string, network: Network) { - const publicKeyBuffer = Buffer.from(publicKey, 'hex'); - const { address } = p2wpkh(publicKeyBuffer, network); - console.log('Address:', address); - return p2wpkh(publicKeyBuffer, network).address; -} - -export function derivePublicKeyFromMasterPublicKey(masterPublicKey: string, derivationPathRoot: string, index: string) { - console.log('Derivation Path:', `${derivationPathRoot}${index}`); - const bip32 = BIP32Factory.BIP32Factory(ecc); - const derivationPath = `${derivationPathRoot}${index}`; - const masterNode = bip32.fromBase58(masterPublicKey); - const publicKey = masterNode.derivePath(derivationPath).publicKey; - const publicKeyString = publicKey.toString('hex'); - return publicKeyString; -} - -export function getTaprootAddress(publicKey: string, bitcoinNetwork: Network) { - console.log('Public Key:', publicKey); - const publicKeyBuffer = Buffer.from(publicKey, 'hex'); - const { address } = p2tr(publicKeyBuffer, undefined, bitcoinNetwork); - console.log('Address:', address); - return address; -} - -export function getPublicKeyFromTaprootAddress(address: string, network: Network): Buffer { - const { data } = fromBech32(address); - return data; -} - -// export function createP2WSHMultisigAddress(pubkeys: string[], network: Network): string { -// const publicKeyBuffers = TARGET_NATIVE_SEGWIT_MULTISIG_PUBLICKEYS.map((hex) => Buffer.from(hex, 'hex')); -// const redeemScript = p2ms(2, publicKeyBuffers).redeemScript; -// if (!redeemScript) throw new Error('Could not create redeem script'); -// const scriptPubKey = p2wsh(redeemScript, network); -// return fromOutputScript(scriptPubKey!, network); -// } -export function createMultisigSpend(publicKeys: string[], bitcoinNetwork: Network) { - const publicKeysBuffer = publicKeys.map((hex) => Buffer.from(hex, 'hex')); - const redeemScript = p2ms(2, publicKeysBuffer); - const spendScript = p2wsh(redeemScript, bitcoinNetwork); - return spendScript; -} - -export function getMultisigNativeSegwitAddress(publicKeys: string[], bitcoinNetwork: Network) { - const publicKeysBuffer = publicKeys.map((hex) => Buffer.from(hex, 'hex')); - const redeemScript = p2ms(2, publicKeysBuffer); - const multisigAddress = p2wsh(redeemScript, bitcoinNetwork).address; - return multisigAddress; -} - -export function getMultisigTaprootTransaction(publicKeys: string[], bitcoinNetwork: Network) { - const publicKeysBuffer = publicKeys.map((hex) => { - const buffer = Buffer.from(hex, 'hex'); - const uncompressedPublicKey = buffer.slice(1); - return uncompressedPublicKey; - }); - const multisig = p2tr_ns(2, publicKeysBuffer); - const bitGoMultisig = p2tr(undefined, multisig, bitcoinNetwork); - return bitGoMultisig; -} - /** * Gets the UTXOs of the User's Native Segwit Address. * @@ -112,7 +47,7 @@ export async function getUTXOs( const allUTXOs = await response.json(); - const spend = createMultisigSpend(publicKeys, bitcoinNetwork); + const spend = getNativeSegwitMultisigScript(publicKeys, bitcoinNetwork); const utxos = await Promise.all( allUTXOs.map(async (utxo: UTXO) => { @@ -160,56 +95,60 @@ export function createMultisigTransaction( return multisigTransaction; } -// /** -// * Creates a Funding Transaction to fund the Multisig Transaction. -// * -// * @param bitcoinAmount - The amount of Bitcoin to fund the Transaction with. -// * @param bitcoinNetwork - The Bitcoin Network to use. -// * @param multisigAddress - The Multisig Address. -// * @param utxos - The UTXOs to use for the Transaction. -// * @param userChangeAddress - The user's Change Address. -// * @param feeRate - The Fee Rate to use for the Transaction. -// * @param feePublicKey - The Fee Recipient's Public Key. -// * @param feeBasisPoints - The Fee Basis Points. -// * @returns The Funding Transaction. -// */ -// export function createFundingTransaction( -// bitcoinAmount: number, -// bitcoinNetwork: Network, -// multisigAddress: string, -// utxos: any[], -// userChangeAddress: string, -// feeRate: bigint, -// feePublicKey: string, -// feeBasisPoints: number -// ): Transaction { -// const feePublicKeyBuffer = Buffer.from(feePublicKey, 'hex'); -// const { address: feeAddress } = p2wpkh(feePublicKeyBuffer, bitcoinNetwork); - -// if (!feeAddress) throw new Error('Could not create Fee Address'); - -// const outputs = [ -// { address: multisigAddress, amount: BigInt(satsToBitcoin(bitcoinAmount)) }, -// { -// address: feeAddress, -// amount: BigInt(satsToBitcoin(bitcoinAmount) * feeBasisPoints), -// }, -// ]; - -// const selected = selectUTXO(utxos, outputs, 'default', { -// changeAddress: userChangeAddress, -// feePerByte: feeRate, -// bip69: false, -// createTx: true, -// network: bitcoinNetwork, -// }); - -// const fundingTX = selected?.tx; - -// if (!fundingTX) throw new Error('Could not create Funding Transaction'); - -// return fundingTX; -// } +/** + * Creates a Funding Transaction to fund the Multisig Transaction. + * + * @param bitcoinAmount - The amount of Bitcoin to fund the Transaction with. + * @param bitcoinNetwork - The Bitcoin Network to use. + * @param multisigAddress - The Multisig Address. + * @param utxos - The UTXOs to use for the Transaction. + * @param userChangeAddress - The user's Change Address. + * @param feeRate - The Fee Rate to use for the Transaction. + * @param feePublicKey - The Fee Recipient's Public Key. + * @param feeBasisPoints - The Fee Basis Points. + * @returns The Funding Transaction. + */ +export async function createFundingTransaction( + bitcoinAmount: number, + bitcoinNetwork: Network, + multisigAddress: string, + nativeSegwitAddress: string, + nativeSegwitPublicKeys: string[], + feeRate: bigint, + feePublicKey: string, + feeBasisPoints: number +): Promise { + const feePublicKeyBuffer = Buffer.from(feePublicKey, 'hex'); + const { address: feeAddress } = p2wpkh(feePublicKeyBuffer, bitcoinNetwork); + + if (!feeAddress) throw new Error('Could not create Fee Address'); + + const utxos = await getUTXOs(nativeSegwitAddress, nativeSegwitPublicKeys, bitcoinNetwork); + + const outputs = [ + { address: multisigAddress, amount: BigInt(bitcoinToSats(bitcoinAmount)) }, + { + address: feeAddress, + amount: BigInt(bitcoinToSats(bitcoinAmount) * feeBasisPoints), + }, + ]; + + const selected = selectUTXO(utxos, outputs, 'default', { + changeAddress: nativeSegwitAddress, + feePerByte: feeRate, + bip69: false, + createTx: true, + network: bitcoinNetwork, + }); + + const fundingTX = selected?.tx; + + if (!fundingTX) throw new Error('Could not create Funding Transaction'); + + const fundingPSBT = fundingTX.toPSBT(); + + return fundingPSBT; +} export function getFeeRecipientAddress(feePublicKey: string, bitcoinNetwork: Network): string { const feePublicKeyBuffer = Buffer.from(feePublicKey, 'hex'); @@ -227,7 +166,7 @@ export function getFeeRecipientAddress(feePublicKey: string, bitcoinNetwork: Net * @param feeBasisPoints - The Fee Basis Points. * @returns The Funding Transaction Info. */ -export function createFundingTransactionInfo( +export function getFundingTransactionRecipients( bitcoinAmount: number, multisigAddress: string, feeRecipientAddress: string, @@ -244,69 +183,69 @@ export function createFundingTransactionInfo( return recipients; } -// /** -// * Creates the Closing Transaction. -// * Uses the Funding Transaction's ID to create the Closing Transaction. -// * The Closing Transaction is sent to the User's Native Segwit Address. -// * -// * @param bitcoinAmount - The Amount of Bitcoin to fund the Transaction with. -// * @param bitcoinNetwork - The Bitcoin Network to use. -// * @param fundingTransactionID - The ID of the Funding Transaction. -// * @param multisigTransaction - The Multisig Transaction. -// * @param userNativeSegwitAddress - The User's Native Segwit Address. -// * @param feeRate - The Fee Rate to use for the Transaction. -// * @param feePublicKey - The Fee Recipient's Public Key. -// * @param feeBasisPoints - The Fee Basis Points. -// * @returns The Closing Transaction. -// */ -// async function createClosingTransaction( -// bitcoinAmount: number, -// bitcoinNetwork: Network, -// fundingTransactionID: string, -// multisigTransaction: P2TROut, -// userNativeSegwitAddress: string, -// feeRate: bigint, -// feePublicKey: string, -// feeBasisPoints: number -// ): Promise { -// const feePublicKeyBuffer = Buffer.from(feePublicKey, 'hex'); -// const { address: feeAddress } = p2wpkh(feePublicKeyBuffer, bitcoinNetwork); - -// if (!feeAddress) throw new Error('Could not create Fee Address'); - -// const inputs = [ -// { -// txid: hexToBytes(fundingTransactionID), -// index: 0, -// witnessUtxo: { -// amount: BigInt(satsToBitcoin(bitcoinAmount)), -// script: multisigTransaction.script, -// }, -// ...multisigTransaction, -// }, -// ]; - -// const outputs = [ -// { -// address: feeAddress, -// amount: BigInt(satsToBitcoin(bitcoinAmount) * feeBasisPoints), -// }, -// ]; - -// const selected = selectUTXO(inputs, outputs, 'default', { -// changeAddress: userNativeSegwitAddress, -// feePerByte: feeRate, -// bip69: false, -// createTx: true, -// network: bitcoinNetwork, -// }); - -// if (!selected?.tx) throw new Error('Could not create Closing Transaction'); - -// const closingPSBT = selected.tx.toPSBT(); - -// return closingPSBT; -// } +/** + * Creates the Closing Transaction. + * Uses the Funding Transaction's ID to create the Closing Transaction. + * The Closing Transaction is sent to the User's Native Segwit Address. + * + * @param bitcoinAmount - The Amount of Bitcoin to fund the Transaction with. + * @param bitcoinNetwork - The Bitcoin Network to use. + * @param fundingTransactionID - The ID of the Funding Transaction. + * @param multisigTransaction - The Multisig Transaction. + * @param userNativeSegwitAddress - The User's Native Segwit Address. + * @param feeRate - The Fee Rate to use for the Transaction. + * @param feePublicKey - The Fee Recipient's Public Key. + * @param feeBasisPoints - The Fee Basis Points. + * @returns The Closing Transaction. + */ +export async function createClosingTransaction( + bitcoinAmount: number, + bitcoinNetwork: Network, + fundingTransactionID: string, + multisigTransaction: P2TROut, + userNativeSegwitAddress: string, + feeRate: bigint, + feePublicKey: string, + feeBasisPoints: number +): Promise { + const feePublicKeyBuffer = Buffer.from(feePublicKey, 'hex'); + const { address: feeAddress } = p2wpkh(feePublicKeyBuffer, bitcoinNetwork); + + if (!feeAddress) throw new Error('Could not create Fee Address'); + + const inputs = [ + { + txid: hexToBytes(fundingTransactionID), + index: 0, + witnessUtxo: { + amount: BigInt(bitcoinToSats(bitcoinAmount)), + script: multisigTransaction.script, + }, + ...multisigTransaction, + }, + ]; + + const outputs = [ + { + address: feeAddress, + amount: BigInt(bitcoinToSats(bitcoinAmount) * feeBasisPoints), + }, + ]; + + const selected = selectUTXO(inputs, outputs, 'default', { + changeAddress: userNativeSegwitAddress, + feePerByte: feeRate, + bip69: false, + createTx: true, + network: bitcoinNetwork, + }); + + if (!selected?.tx) throw new Error('Could not create Closing Transaction'); + + const closingPSBT = selected.tx.toPSBT(); + + return closingPSBT; +} /** * Broadcasts the Transaction to the Bitcoin Network. @@ -314,17 +253,17 @@ export function createFundingTransactionInfo( * @param transaction - The Transaction to broadcast. * @returns A Promise that resolves to the Response from the Broadcast Request. */ -async function broadcastTransaction(transaction: Transaction): Promise { +export async function broadcastTransaction(transaction: string): Promise { const bitcoinBlockchainAPIURL = process.env.BITCOIN_BLOCKCHAIN_API_URL; try { const response = await fetch(`${bitcoinBlockchainAPIURL}/tx`, { method: 'POST', - body: bytesToHex(transaction.extract()), + body: transaction, }); if (!response.ok) { - throw new Error(`HTTP Error! Status: ${response.status}`); + throw new Error(`HTTP Error! Status: ${await response.text()}`); } const transactionID = await response.text(); diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 0000000..d8d3c12 --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,15 @@ +/** @format */ + +// test values for the integration tests +export const TEST_BITCOIN_AMOUNT = 0.01; +export const TEST_FEE_AMOUNT = 0.01; +export const TEST_ATTESTOR_PUBLIC_KEY = '4caaf4bb366239b0a8b7a5e5a44d043b5f66ae7364895317af8847ac6fadbd2b'; +export const TEST_FEE_PUBLIC_KEY = '03c9fc819e3c26ec4a58639add07f6372e810513f5d3d7374c25c65fdf1aefe4c5'; +export const TEST_VAULT_UUID = '0xcf5f227dd384a590362b417153876d9d22b31b2ed1e22065e270b82437cf1880'; +export const TEST_FEE_RATE = 147n; + +// derivation paths for BitGo wallets +export const DERIVATION_PATH_NATIVE_SEGWIT_FROM_MASTER = `m/0/0/20/`; +export const DERIVATION_PATH_NATIVE_SEGWIT_FROM_CHILD = `0/0/20/`; +export const DERIVATION_PATH_TAPROOT_FROM_MASTER = `m/0/0/30/`; +export const DERIVATION_PATH_TAPROOT_FROM_CHILD = `0/0/30/`; diff --git a/src/index.ts b/src/index.ts index 3b89c7b..ead5ba4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,62 +2,35 @@ import { BitGoAPI } from '@bitgo/sdk-api'; import { Btc, Tbtc } from '@bitgo/sdk-coin-btc'; -import { CoinConstructor, EnvironmentName, Wallet } from '@bitgo/sdk-core'; +import { CoinConstructor, EnvironmentName, FullySignedTransaction, Wallet } from '@bitgo/sdk-core'; import { bytesToHex, hexToBytes } from '@noble/hashes/utils'; +import { Transaction } from '@scure/btc-signer'; +import { Network } from 'bitcoinjs-lib'; +import { bitcoin, testnet } from 'bitcoinjs-lib/src/networks.js'; import dotenv from 'dotenv'; import { - createFundingTransactionInfo, + broadcastTransaction, + createClosingTransaction, + createFundingTransaction, createMultisigTransaction, - derivePublicKeyFromMasterPublicKey, - getAddress, getFeeRecipientAddress, - getMultisigNativeSegwitAddress, - getMultisigTaprootTransaction, - getPublicKeyFromTaprootAddress, - getTaprootAddress, getUTXOs, } from './bitcoin-functions.js'; -import { Network } from 'bitcoinjs-lib'; -import { bitcoin, testnet } from 'bitcoinjs-lib/src/networks.js'; +import { + TEST_ATTESTOR_PUBLIC_KEY, + TEST_BITCOIN_AMOUNT, + TEST_FEE_AMOUNT, + TEST_FEE_PUBLIC_KEY, + TEST_FEE_RATE, + TEST_VAULT_UUID, +} from './constants.js'; +import { BitGoAddress } from './models.js'; +import { getNativeSegwitPublicKeys, getTaprootMultisigScript, getTaprootPublicKeys } from './payment-functions.js'; +import { bitcoinToSats } from './utilities.js'; dotenv.config(); -interface BitGoAddress { - id: string; - address: string; - chain: number; - index: number; - coin: string; - wallet: string; - label: string; - coinSpecific: Record; -} - -export const TARGET_NATIVE_SEGWIT_MULTISIG_PUBLICKEYS = [ - '0299edd7076a15f848a969b1ddbb5f89bc03bc272825e5a7c195ad4e14df2aa22a', - '02c60d785bb90f86e928af586327570d64ebb8ea5b1d8f588afe8dbf999c859400', - '021c10ce56ed56cc1cf12c04c75e1fa6ba973b8666e5c570b42af063708ae7abea', -]; - -export const TARGET_CHILD_NODES = [ - 'xpub69AcSTtvBCuMep1Pz5S1ik3xYTPFLqh81NNf3zBgThLcdHKSuZDSxhMwc9A4b2DM8DDC78si5af1kvYcCGpiyXCKcD8zwdsd6mKQK6Y5iFZ', - 'xpub661MyMwAqRbcFq1F9XgMepGcNmBQgeA3Ue7XhvJ4xtZ9u8iDR6uMNbGZLHzF9Xy7aR9ALbLdWPCngzUue6VtDFFv9aHzPkw7iUhHuTYMNSN', -]; - -const TESTNET_FEE_PUBLIC_KEY = '03c9fc819e3c26ec4a58639add07f6372e810513f5d3d7374c25c65fdf1aefe4c5'; -const TESTNET_ATTESTOR_PUBLIC_KEY = '4caaf4bb366239b0a8b7a5e5a44d043b5f66ae7364895317af8847ac6fadbd2b'; - -const TEST_VAULT_UUID = '0xcf5f227dd384a590362b417153876d9d22b31b2ed1e22065e270b82437cf1880'; - -const TARGET_NATIVE_SEGWIT_MULTISIG_ADDRESS = 'tb1q3ekr0u3s6clpag3tzaqk23f6edltxq3xl00hq8fjgugq5ds87jqsza5k55'; - -const DERIVATION_PATH_NATIVE_SEGWIT_FROM_MASTER = `m/0/0/20/`; -const DERIVATION_PATH_NATIVE_SEGWIT_FROM_CHILD = `0/0/20/`; - -const DERIVATION_PATH_TAPROOT_FROM_MASTER = `m/0/0/30/`; -const DERIVATION_PATH_TAPROOT_FROM_CHILD = `0/0/30/`; - function findBitGoAddress(bitGoAddresses: BitGoAddress[], targetAddress: string): BitGoAddress { const bitGoAddress = bitGoAddresses.find((address) => address.address === targetAddress); if (!bitGoAddress) { @@ -66,27 +39,25 @@ function findBitGoAddress(bitGoAddresses: BitGoAddress[], targetAddress: string) return bitGoAddress; } -async function createTaprootAddress(bitGoWallet: Wallet) { +async function createTaprootAddress(bitGoWallet: Wallet, label: string) { try { const taprootAddress = await bitGoWallet.createAddress({ chain: 30, - label: 'Taproot Address 1', + label, }); - console.log(`Created Native Segwit Address: ${JSON.stringify(taprootAddress, null, 2)}`); - console.log(`Created Taproot Address: ${taprootAddress.address}`); + console.log(`Created Taproot Address: ${JSON.stringify(taprootAddress, null, 2)}`); } catch (error) { throw new Error(`Error while creating Taproot address: ${error}`); } } -async function createNativeSegwitAddress(bitGoWallet: Wallet) { +async function createNativeSegwitAddress(bitGoWallet: Wallet, label: string) { try { const nativeSegwitAddress = await bitGoWallet.createAddress({ chain: 20, - label: 'Native Segwit Address 1', + label, }); console.log(`Created Native Segwit Address: ${JSON.stringify(nativeSegwitAddress, null, 2)}`); - console.log(`Created Native Segwit Address: ${nativeSegwitAddress.address}`); } catch (error) { throw new Error(`Error while creating Native Segwit address: ${error}`); } @@ -96,6 +67,7 @@ async function getBitGoDetails() { const { BITCOIN_NETWORK, BITGO_ACCESS_TOKEN, + BITGO_WALLET_PASSPHRASE, BITGO_WALLET_ID, BITGO_NATIVE_SEGWIT_ADDRESS, BITGO_TAPROOT_ADDRESS, @@ -107,6 +79,7 @@ async function getBitGoDetails() { if ( !BITCOIN_NETWORK || !BITGO_ACCESS_TOKEN || + !BITGO_WALLET_PASSPHRASE || !BITGO_WALLET_ID || !BITGO_NATIVE_SEGWIT_ADDRESS || !BITGO_TAPROOT_ADDRESS || @@ -116,6 +89,7 @@ async function getBitGoDetails() { ) { throw new Error('Please provide all the required Environment Variables.'); } + let environmentName: EnvironmentName; let coinType: string; let coinInstance: CoinConstructor; @@ -160,6 +134,7 @@ async function getBitGoDetails() { bitGoAPI, bitGoWallet, bitGoKeyChain, + bitGoWalletPassphrase: BITGO_WALLET_PASSPHRASE, nativeSegwitAddress: BITGO_NATIVE_SEGWIT_ADDRESS, taprootAddress: BITGO_TAPROOT_ADDRESS, userXPUB: USER_XPUB, @@ -175,6 +150,7 @@ async function main() { bitGoAPI, bitGoWallet, bitGoKeyChain, + bitGoWalletPassphrase, nativeSegwitAddress, taprootAddress, userXPUB, @@ -186,116 +162,123 @@ async function main() { console.log(`Current Balance: ${bitGoWallet.balance()}`); const { addresses: bitGoAddresses } = await bitGoWallet.addresses(); + const bitGoNativeSegwitAddress = findBitGoAddress(bitGoAddresses, nativeSegwitAddress); const bitGoTaprootAddress = findBitGoAddress(bitGoAddresses, taprootAddress); - const userDerivedNativeSegwitPublicKey = derivePublicKeyFromMasterPublicKey( + // this returns the public keys of the user, backup and bitgo for the native segwit multisig address + const nativeSegwitPublicKeys = getNativeSegwitPublicKeys( + bitGoNativeSegwitAddress, userXPUB, - DERIVATION_PATH_NATIVE_SEGWIT_FROM_MASTER, - bitGoNativeSegwitAddress.index.toString() - ); - - console.log('userDerivedNativeSegwitPublicKey', userDerivedNativeSegwitPublicKey); - - const bitGoDerivedNativeSegwitPublicKey = derivePublicKeyFromMasterPublicKey( + backupXPUB, bitGoXPUB, - DERIVATION_PATH_NATIVE_SEGWIT_FROM_MASTER, - bitGoNativeSegwitAddress.index.toString() + bitcoinNetwork ); - console.log('bitGoderivedPublicKey', bitGoDerivedNativeSegwitPublicKey); + // this returns the public keys of the user, backup and bitgo for a new taproot multisig address + const taprootPublicKeys = getTaprootPublicKeys(bitGoTaprootAddress, userXPUB, backupXPUB, bitGoXPUB); - // Backup XPUB is a child of the user XPUB, so we use the child derivation path - const backupDerivedNativeSegwitPublicKey = derivePublicKeyFromMasterPublicKey( - backupXPUB, - DERIVATION_PATH_NATIVE_SEGWIT_FROM_CHILD, - bitGoNativeSegwitAddress.index.toString() + // not sure if this is the correct way to create a taproot multisig address from the public keys + const taprootMultisig = getTaprootMultisigScript(taprootPublicKeys, bitcoinNetwork); + + // this creates a multisig transaction using the user's taproot public key and the attestor group's public key + const multisigTransaction = await createMultisigTransaction( + taprootMultisig.tweakedPubkey, // this is the user's taproot public key + Buffer.from(TEST_ATTESTOR_PUBLIC_KEY, 'hex'), + TEST_VAULT_UUID, + bitcoinNetwork ); - console.log('backupDerivedPublicKey', backupDerivedNativeSegwitPublicKey); + if (!multisigTransaction.address) throw new Error('Error while creating Multisig Transaction.'); - // To recreate the multisig address created by BitGo, we need to use the public keys in the same order - const nativeSegwitPublicKeys = [ - userDerivedNativeSegwitPublicKey, - backupDerivedNativeSegwitPublicKey, - bitGoDerivedNativeSegwitPublicKey, - ]; + // ### ORIGINAL FUNDING TRANSACTION FLOW ### - const multisigAddress = getMultisigNativeSegwitAddress(nativeSegwitPublicKeys, bitcoinNetwork); + // #1 Create a Funding Transaction to fund the Multisig Transaction + // const fundingTransaction = await createFundingTransaction( + // TEST_BITCOIN_AMOUNT, + // bitcoinNetwork, + // multisigTransaction.address, + // bitGoNativeSegwitAddress.address, + // nativeSegwitPublicKeys, + // TEST_FEE_RATE, + // TEST_FEE_PUBLIC_KEY, + // TEST_FEE_AMOUNT + // ); - if (multisigAddress !== TARGET_NATIVE_SEGWIT_MULTISIG_ADDRESS) { - throw new Error('Multisig Address does not match the target address.'); - } + // #2 Sign the Funding Transaction - const userDerivedTaprootPublicKey = derivePublicKeyFromMasterPublicKey( - userXPUB, - DERIVATION_PATH_TAPROOT_FROM_MASTER, - bitGoTaprootAddress.index.toString() - ); + // #3 Finalize the Funding Transaction + // const transaction = Transaction.fromPSBT(fundingTransaction); + // transaction.finalize(); - console.log('userDerivedTaprootPublicKey', userDerivedTaprootPublicKey); + // #4 Broadcast the Funding Transaction + // const broadcastResponse = await broadcastTransaction(bytesToHex(fundingTransaction)); - const bitGoDerivedTaprootPublicKey = derivePublicKeyFromMasterPublicKey( - bitGoXPUB, - DERIVATION_PATH_TAPROOT_FROM_MASTER, - bitGoTaprootAddress.index.toString() - ); + // ### BITGO API FLOW ### - console.log('bitGoDerivedTaprootPublicKey', bitGoDerivedTaprootPublicKey); + // #1 Create an Array of Recipients for the Transaction + const feeRecipientAddress = getFeeRecipientAddress(TEST_FEE_PUBLIC_KEY, bitcoinNetwork); + const recipients = [ + { amount: bitcoinToSats(TEST_BITCOIN_AMOUNT), address: multisigTransaction.address }, + { amount: bitcoinToSats(TEST_BITCOIN_AMOUNT * TEST_FEE_AMOUNT), address: feeRecipientAddress }, + ]; - // Backup XPUB is a child of the user XPUB, so we use the child derivation path - const backupDerivedTaprootPublicKey = derivePublicKeyFromMasterPublicKey( - backupXPUB, - DERIVATION_PATH_TAPROOT_FROM_CHILD, - bitGoTaprootAddress.index.toString() - ); + // #2 Prebuild the Transaction + const transactionPrebuild = await bitGoWallet.prebuildTransaction({ + recipients: recipients, + walletPassphrase: bitGoWalletPassphrase, + }); - console.log('backupDerivedTaprootPublicKey', backupDerivedTaprootPublicKey); + // #3 Sign the Transaction + const signedTransaction = await bitGoWallet.signTransaction({ + txPrebuild: transactionPrebuild, + keychain: bitGoKeyChain[0], + walletPassphrase: bitGoWalletPassphrase, + }); - // To recreate the multisig address created by BitGo, we need to use the public keys in the same order - const taprootPublicKeys = [ - userDerivedTaprootPublicKey, - bitGoDerivedTaprootPublicKey, - backupDerivedTaprootPublicKey, - ]; + const FullySignedTransaction = signedTransaction as FullySignedTransaction; - // Create a Taproot Multisig Address from the User's, BitGo's and Backup's Public Keys - const taprootMultisig = getMultisigTaprootTransaction(taprootPublicKeys, bitcoinNetwork); + // #4 Submit the Transaction + const submitResponse = await bitGoWallet.submitTransaction({ txHex: FullySignedTransaction.txHex }); - // Create a Taproot Multisig Transaction from the Taproot Multisig Address and the Attestor's Group Public Key - const multisigTransaction = await createMultisigTransaction( - taprootMultisig.tweakedPubkey, - Buffer.from(TESTNET_ATTESTOR_PUBLIC_KEY, 'hex'), - TEST_VAULT_UUID, - bitcoinNetwork + // ### ORIGINAL CLOSING TRANSACTION FLOW ### + + // #1 Create a Closing Transaction + const closingTransaction = await createClosingTransaction( + TEST_BITCOIN_AMOUNT, + bitcoinNetwork, + 'fundingTransactionID', // this is the ID of the funding transaction + multisigTransaction, + bitGoNativeSegwitAddress.address, + TEST_FEE_RATE, + TEST_FEE_PUBLIC_KEY, + TEST_FEE_AMOUNT ); - if (!multisigTransaction.address) throw new Error('Error while creating Multisig Transaction.'); + // #2 Sign the Closing Transaction - // Get the Fee Recipient Address from the Fee Recipient's Public Key - const feeRecipientAddress = getFeeRecipientAddress(TESTNET_FEE_PUBLIC_KEY, bitcoinNetwork); + // #3 Send the Closing Transaction PSBT to the Attestor Group - // Create an array of Recipients for the Funding Transaction - const fundingTransactionRecipients = createFundingTransactionInfo( - 0.001, - multisigTransaction.address, - feeRecipientAddress, - 0.01 - ); + // ### BITGO API FLOW ### - // Create a Prebuild Transaction from the Recipients - const preBuild = await bitGoWallet.prebuildTransaction({ recipients: fundingTransactionRecipients }); + // #1 Prebuild the Transaction + const closingTransactionPreBuild = await bitGoWallet.prebuildTransaction({ + recipients: [ + { amount: bitcoinToSats(TEST_BITCOIN_AMOUNT), address: bitGoNativeSegwitAddress.address }, + { amount: bitcoinToSats(TEST_BITCOIN_AMOUNT * TEST_FEE_AMOUNT), address: feeRecipientAddress }, + ], + unspents: ['fundingTransactionID:index'], // this is how the unspent can be referenced in the BitGo API + walletPassphrase: bitGoWalletPassphrase, + }); - // Sign the Prebuild Transaction with the BitGo Keychain - const signFundingTransactionResponse = await bitGoWallet.signTransaction({ - txPrebuild: preBuild, + // #2 Sign the Transaction + const signedClosingTransaction = await bitGoWallet.signTransaction({ + txPrebuild: transactionPrebuild, keychain: bitGoKeyChain[0], - walletPassphrase: '7B%w^F%dWPFRE4', + walletPassphrase: bitGoWalletPassphrase, }); - await bitGoWallet.prebuildTransaction({}); - - console.log('Funding Transaction Signing Response', signFundingTransactionResponse); + // #3 Send the Closing Transaction PSBT to the Attestor Group } catch (error) { console.error(`Error while running BitGoAPI flow: ${error}`); } diff --git a/src/models.ts b/src/models.ts new file mode 100644 index 0000000..8a8ebe8 --- /dev/null +++ b/src/models.ts @@ -0,0 +1,12 @@ +/** @format */ + +export interface BitGoAddress { + id: string; + address: string; + chain: number; + index: number; + coin: string; + wallet: string; + label: string; + coinSpecific: Record; +} diff --git a/src/payment-functions.ts b/src/payment-functions.ts new file mode 100644 index 0000000..e3399ba --- /dev/null +++ b/src/payment-functions.ts @@ -0,0 +1,120 @@ +/** @format */ + +import { p2ms, p2tr, p2tr_ns, p2wpkh, p2wsh } from '@scure/btc-signer'; +import { BIP32Factory } from 'bip32'; +import { Network } from 'bitcoinjs-lib'; +import * as ecc from 'tiny-secp256k1'; +import { + DERIVATION_PATH_NATIVE_SEGWIT_FROM_CHILD, + DERIVATION_PATH_NATIVE_SEGWIT_FROM_MASTER, + DERIVATION_PATH_TAPROOT_FROM_CHILD, + DERIVATION_PATH_TAPROOT_FROM_MASTER, +} from './constants.js'; +import { BitGoAddress } from './models.js'; + +const bip32 = BIP32Factory(ecc); + +export function derivePublicKeyFromExtendedPublicKey( + extendedPublicKey: string, + derivationPathRoot: string, + index: string +) { + const root = bip32.fromBase58(extendedPublicKey); + const child = root.derivePath(`${derivationPathRoot}${index}`); + return child.publicKey.toString('hex'); +} + +export function getNativeSegwitAddress(publicKey: string, bitcoinNetwork: Network) { + const publicKeyBuffer = Buffer.from(publicKey, 'hex'); + return p2wpkh(publicKeyBuffer, bitcoinNetwork).address; +} + +export function getTaprootAddress(publicKey: string, bitcoinNetwork: Network) { + const publicKeyBuffer = Buffer.from(publicKey, 'hex'); + return p2tr(publicKeyBuffer, undefined, bitcoinNetwork).address; +} + +export function getNativeSegwitMultisigScript(publicKeys: string[], bitcoinNetwork: Network) { + const redeemScript = p2ms( + 2, + publicKeys.map((hex) => Buffer.from(hex, 'hex')) + ); + return p2wsh(redeemScript, bitcoinNetwork); +} + +export function getTaprootMultisigScript(publicKeys: string[], bitcoinNetwork: Network) { + const publicKeysBuffer = publicKeys.map((hex) => { + return Buffer.from(hex, 'hex').subarray(1); + }); + const multisig = p2tr_ns(2, publicKeysBuffer); + return p2tr(undefined, multisig, bitcoinNetwork); +} + +export function getNativeSegwitPublicKeys( + bitGoNativeSegwitAddress: BitGoAddress, + userXPUB: string, + backupXPUB: string, + bitGoXPUB: string, + bitcoinNetwork: Network +): string[] { + const userDerivedNativeSegwitPublicKey = derivePublicKeyFromExtendedPublicKey( + userXPUB, + DERIVATION_PATH_NATIVE_SEGWIT_FROM_MASTER, + bitGoNativeSegwitAddress.index.toString() + ); + + const bitGoDerivedNativeSegwitPublicKey = derivePublicKeyFromExtendedPublicKey( + bitGoXPUB, + DERIVATION_PATH_NATIVE_SEGWIT_FROM_MASTER, + bitGoNativeSegwitAddress.index.toString() + ); + + const backupDerivedNativeSegwitPublicKey = derivePublicKeyFromExtendedPublicKey( + backupXPUB, + DERIVATION_PATH_NATIVE_SEGWIT_FROM_CHILD, + bitGoNativeSegwitAddress.index.toString() + ); + + const nativeSegwitPublicKeys = [ + userDerivedNativeSegwitPublicKey, + backupDerivedNativeSegwitPublicKey, + bitGoDerivedNativeSegwitPublicKey, + ]; + + const multisigAddress = getNativeSegwitMultisigScript(nativeSegwitPublicKeys, bitcoinNetwork).address; + + if (multisigAddress !== bitGoNativeSegwitAddress.address) { + throw new Error('Multisig Address does not match the target address.'); + } + + return nativeSegwitPublicKeys; +} + +export function getTaprootPublicKeys( + bitGoTaprootAddress: BitGoAddress, + userXPUB: string, + backupXPUB: string, + bitGoXPUB: string +): string[] { + const userDerivedTaprootPublicKey = derivePublicKeyFromExtendedPublicKey( + userXPUB, + DERIVATION_PATH_TAPROOT_FROM_MASTER, + bitGoTaprootAddress.index.toString() + ); + + const bitGoDerivedTaprootPublicKey = derivePublicKeyFromExtendedPublicKey( + bitGoXPUB, + DERIVATION_PATH_TAPROOT_FROM_MASTER, + bitGoTaprootAddress.index.toString() + ); + + const backupDerivedTaprootPublicKey = derivePublicKeyFromExtendedPublicKey( + backupXPUB, + DERIVATION_PATH_TAPROOT_FROM_CHILD, + bitGoTaprootAddress.index.toString() + ); + + const taprootPublicKeys = [userDerivedTaprootPublicKey, bitGoDerivedTaprootPublicKey, backupDerivedTaprootPublicKey]; + + return taprootPublicKeys; +} diff --git a/tb1ppxcqqtaxxqwclxudhervdnngzehpyr5mahczne64rkp2x7kvzfvqgda4ja.json b/tb1ppxcqqtaxxqwclxudhervdnngzehpyr5mahczne64rkp2x7kvzfvqgda4ja.json deleted file mode 100644 index f5d752b..0000000 --- a/tb1ppxcqqtaxxqwclxudhervdnngzehpyr5mahczne64rkp2x7kvzfvqgda4ja.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "id": "6628cd7dbad63539229ad37359751670", - "address": "tb1ppxcqqtaxxqwclxudhervdnngzehpyr5mahczne64rkp2x7kvzfvqgda4ja", - "chain": 30, - "index": 1, - "coin": "tbtc", - "wallet": "6628cc36e43fd24b95b63e5ed1838b7c", - "label": "Taproot Address 1", - "coinSpecific": {}, - "addressType": "p2tr", - "keychains": [ - { - "id": "6628cc10e0763eec6451c7c2c61da72a", - "pub": "xpub661MyMwAqRbcFzH9LyJ7e5dYdUmECDf8CyE6GRaFZqe1h9YAKvHAGp519ES3ftEovPH7G368mdVJstvWkjnSrYMtYDyyMKRHoHKaLL9op7r", - "ethAddress": "0x99ab1784e2abd980993e39a6769c23d7af2b7989", - "source": "user", - "type": "independent", - "encryptedPrv": "{\"iv\":\"B01RxkiCbwKmSGVlpx2HJQ==\",\"v\":1,\"iter\":10000,\"ks\":256,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"k1Q65rUKUWY=\",\"ct\":\"8yFiRcW6Nph35rcSVBCstzKAyQukcLrGLLkweN9RQlV7KlSNojAC7EgKx4YG0POzhu4IYt+ySrX2aHo72VK6pw/9D36qsCtmTbdnKxS1a6M6wHmKWYMDENTZ/sgPgApcDsc9TmNVPpLe70sVSbOHUKTqFrma68A=\"}" - }, - { - "id": "6628cc1130c091bd3c7939aa1b70a57d", - "pub": "xpub69AcSTtvBCuMep1Pz5S1ik3xYTPFLqh81NNf3zBgThLcdHKSuZDSxhMwc9A4b2DM8DDC78si5af1kvYcCGpiyXCKcD8zwdsd6mKQK6Y5iFZ", - "ethAddress": "0xb8e49c3953081fe25614ef5fee8b68ce64faa770", - "source": "backup", - "type": "independent", - "provider": "dai" - }, - { - "id": "6628cc13e43fd24b95b61ed46d97c9f0", - "pub": "xpub661MyMwAqRbcFq1F9XgMepGcNmBQgeA3Ue7XhvJ4xtZ9u8iDR6uMNbGZLHzF9Xy7aR9ALbLdWPCngzUue6VtDFFv9aHzPkw7iUhHuTYMNSN", - "ethAddress": "0x184e1e2d03d64ec0dcce87eddf2457615922a2f1", - "source": "bitgo", - "type": "independent", - "isBitGo": true - } - ] -} diff --git a/tb1q3ekr0u3s6clpag3tzaqk23f6edltxq3xl00hq8fjgugq5ds87jqsza5k55.json b/tb1q3ekr0u3s6clpag3tzaqk23f6edltxq3xl00hq8fjgugq5ds87jqsza5k55.json deleted file mode 100644 index 5f8249e..0000000 --- a/tb1q3ekr0u3s6clpag3tzaqk23f6edltxq3xl00hq8fjgugq5ds87jqsza5k55.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "Created Native Segwit Address": { - "id": "6628cc98363c0fc30976cd8a6edda1e3", - "address": "tb1q3ekr0u3s6clpag3tzaqk23f6edltxq3xl00hq8fjgugq5ds87jqsza5k55", - "chain": 20, - "index": 2, - "coin": "tbtc", - "wallet": "6628cc36e43fd24b95b63e5ed1838b7c", - "label": "Native Segwit Address 1", - "coinSpecific": { - "witnessScript": "52210299edd7076a15f848a969b1ddbb5f89bc03bc272825e5a7c195ad4e14df2aa22a2102c60d785bb90f86e928af586327570d64ebb8ea5b1d8f588afe8dbf999c85940021021c10ce56ed56cc1cf12c04c75e1fa6ba973b8666e5c570b42af063708ae7abea53ae" - }, - "addressType": "p2wsh", - "keychains": [ - { - "id": "6628cc10e0763eec6451c7c2c61da72a", - "pub": "xpub661MyMwAqRbcFzH9LyJ7e5dYdUmECDf8CyE6GRaFZqe1h9YAKvHAGp519ES3ftEovPH7G368mdVJstvWkjnSrYMtYDyyMKRHoHKaLL9op7r", - "ethAddress": "0x99ab1784e2abd980993e39a6769c23d7af2b7989", - "source": "user", - "type": "independent", - "encryptedPrv": "{\"iv\":\"B01RxkiCbwKmSGVlpx2HJQ==\",\"v\":1,\"iter\":10000,\"ks\":256,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"k1Q65rUKUWY=\",\"ct\":\"8yFiRcW6Nph35rcSVBCstzKAyQukcLrGLLkweN9RQlV7KlSNojAC7EgKx4YG0POzhu4IYt+ySrX2aHo72VK6pw/9D36qsCtmTbdnKxS1a6M6wHmKWYMDENTZ/sgPgApcDsc9TmNVPpLe70sVSbOHUKTqFrma68A=\"}" - }, - { - "id": "6628cc1130c091bd3c7939aa1b70a57d", - "pub": "xpub69AcSTtvBCuMep1Pz5S1ik3xYTPFLqh81NNf3zBgThLcdHKSuZDSxhMwc9A4b2DM8DDC78si5af1kvYcCGpiyXCKcD8zwdsd6mKQK6Y5iFZ", - "ethAddress": "0xb8e49c3953081fe25614ef5fee8b68ce64faa770", - "source": "backup", - "type": "independent", - "provider": "dai" - }, - { - "id": "6628cc13e43fd24b95b61ed46d97c9f0", - "pub": "xpub661MyMwAqRbcFq1F9XgMepGcNmBQgeA3Ue7XhvJ4xtZ9u8iDR6uMNbGZLHzF9Xy7aR9ALbLdWPCngzUue6VtDFFv9aHzPkw7iUhHuTYMNSN", - "ethAddress": "0x184e1e2d03d64ec0dcce87eddf2457615922a2f1", - "source": "bitgo", - "type": "independent", - "isBitGo": true - } - ] - } -} diff --git a/yarn.lock b/yarn.lock index 48caf52..fea1493 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2682,11 +2682,16 @@ lodash.merge@^4.6.0, lodash.merge@^4.6.2: resolved "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== -lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.21, lodash@~4.17.21: +lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.14, lodash@^4.17.15, lodash@~4.17.21: version "4.17.21" resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== +lodash@^4.17.21: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + log-symbols@^2.2.0: version "2.2.0" resolved "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz"