From e4bafee5103a79d5d1ef0793cb9ac2593d573588 Mon Sep 17 00:00:00 2001 From: Polybius93 Date: Tue, 28 May 2024 16:50:06 +0200 Subject: [PATCH] feat: add wallet handlers --- src/bitgo-functions.ts | 215 ---------- src/constants.ts | 38 -- .../ethereum-constants.ts} | 29 -- src/constants/ledger-constants.ts | 7 + src/dlc-handlers/ledger-dlc-handler.ts | 377 +++++++++++++++++ src/dlc-handlers/private-key-dlc-handler.ts | 224 +++++++++++ .../software-wallet-dlc-handler.ts | 184 +++++++++ src/ethereum-observer.ts | 1 - src/{ => functions}/attestor-functions.ts | 2 +- src/{ => functions}/bitcoin-functions.ts | 76 ++-- src/{ => functions}/ethereum-functions.ts | 8 +- src/{ => functions}/psbt-functions.ts | 6 +- src/index.ts | 121 ++++-- src/ledger-functions.ts | 378 +----------------- src/models/bitcoin-models.ts | 14 + src/models/bitgo-models.ts | 12 - src/network-handlers/ethereum-handler.ts | 0 src/payment-functions.ts | 120 ------ src/private-key-dlc-handler.ts | 188 --------- 19 files changed, 962 insertions(+), 1038 deletions(-) delete mode 100644 src/bitgo-functions.ts delete mode 100644 src/constants.ts rename src/{ethereum-network.ts => constants/ethereum-constants.ts} (53%) create mode 100644 src/constants/ledger-constants.ts create mode 100644 src/dlc-handlers/ledger-dlc-handler.ts create mode 100644 src/dlc-handlers/private-key-dlc-handler.ts create mode 100644 src/dlc-handlers/software-wallet-dlc-handler.ts delete mode 100644 src/ethereum-observer.ts rename src/{ => functions}/attestor-functions.ts (97%) rename src/{ => functions}/bitcoin-functions.ts (84%) rename src/{ => functions}/ethereum-functions.ts (98%) rename src/{ => functions}/psbt-functions.ts (98%) delete mode 100644 src/models/bitgo-models.ts create mode 100644 src/network-handlers/ethereum-handler.ts delete mode 100644 src/payment-functions.ts delete mode 100644 src/private-key-dlc-handler.ts diff --git a/src/bitgo-functions.ts b/src/bitgo-functions.ts deleted file mode 100644 index 627f9fb..0000000 --- a/src/bitgo-functions.ts +++ /dev/null @@ -1,215 +0,0 @@ -/** @format */ - -// /** @format */ - -// import { CoinConstructor, EnvironmentName, Wallet } from '@bitgo/sdk-core'; -// import { BitGoAddress } from './models/bitgo-models.js'; -// import { bitcoinToSats } from './utilities.js'; -// import { Network } from 'bitcoinjs-lib'; -// import { Btc, Tbtc } from '@bitgo/sdk-coin-btc'; -// import { bitcoin, testnet } from 'bitcoinjs-lib/src/networks.js'; -// import { BitGoAPI } from '@bitgo/sdk-api'; - -// function findBitGoAddress(bitGoAddresses: BitGoAddress[], targetAddress: string): BitGoAddress { -// const bitGoAddress = bitGoAddresses.find((address) => address.address === targetAddress); -// if (!bitGoAddress) { -// throw new Error(`Address ${targetAddress} not found.`); -// } -// return bitGoAddress; -// } - -// async function createTaprootAddress(bitGoWallet: Wallet, label: string) { -// try { -// const taprootAddress = await bitGoWallet.createAddress({ -// chain: 30, -// label, -// }); -// 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, label: string) { -// try { -// const nativeSegwitAddress = await bitGoWallet.createAddress({ -// chain: 20, -// label, -// }); -// console.log(`Created Native Segwit Address: ${JSON.stringify(nativeSegwitAddress, null, 2)}`); -// } catch (error) { -// throw new Error(`Error while creating Native Segwit address: ${error}`); -// } -// } - -// async function createMultisigWallet() { -// const { -// BITCOIN_NETWORK, -// BITGO_ACCESS_TOKEN, -// BITGO_WALLET_PASSPHRASE, -// BITGO_WALLET_ID, -// BITGO_NATIVE_SEGWIT_ADDRESS, -// BITGO_TAPROOT_ADDRESS, -// USER_XPUB, -// BACKUP_XPUB, -// BITGO_XPUB, -// } = process.env; - -// if ( -// !BITCOIN_NETWORK || -// !BITGO_ACCESS_TOKEN || -// !BITGO_WALLET_PASSPHRASE || -// !BITGO_WALLET_ID || -// !BITGO_NATIVE_SEGWIT_ADDRESS || -// !BITGO_TAPROOT_ADDRESS || -// !USER_XPUB || -// !BACKUP_XPUB || -// !BITGO_XPUB -// ) { -// throw new Error('Please provide all the required Environment Variables.'); -// } - -// let environmentName: EnvironmentName; -// let coinType: string; -// let coinInstance: CoinConstructor; -// let bitcoinNetwork: Network; - -// switch (BITCOIN_NETWORK) { -// case 'bitcoin': -// environmentName = 'prod'; -// coinType = 'btc'; -// coinInstance = Btc.createInstance; -// bitcoinNetwork = bitcoin; -// break; -// case 'testnet': -// environmentName = 'test'; -// coinType = 'tbtc'; -// coinInstance = Tbtc.createInstance; -// bitcoinNetwork = testnet; -// break; -// default: -// throw new Error('Invalid BITCOIN_NETWORK Value. Please provide either "bitcoin" or "testnet".'); -// } - -// const attestorGroupXPublicKey = 'xpub43f9a14c790c0b86ce78bec919e96725e56aee8e0a0fdd8138aa7b351930b3c1'; - -// let bitGoAPI: BitGoAPI; -// try { -// bitGoAPI = new BitGoAPI({ accessToken: BITGO_ACCESS_TOKEN, env: environmentName }); -// } catch (error) { -// throw new Error(`Error while initializing BitGo API: ${error}`); -// } - -// bitGoAPI.coin(coinType).wallets().generateWallet({ label: 'Test Wallet' }); - -// bitGoAPI.register(coinType, coinInstance); -// } - -// async function getBitGoDetails() { -// const { -// BITCOIN_NETWORK, -// BITGO_ACCESS_TOKEN, -// BITGO_WALLET_PASSPHRASE, -// BITGO_WALLET_ID, -// BITGO_NATIVE_SEGWIT_ADDRESS, -// BITGO_TAPROOT_ADDRESS, -// USER_XPUB, -// BACKUP_XPUB, -// BITGO_XPUB, -// } = process.env; - -// if ( -// !BITCOIN_NETWORK || -// !BITGO_ACCESS_TOKEN || -// !BITGO_WALLET_PASSPHRASE || -// !BITGO_WALLET_ID || -// !BITGO_NATIVE_SEGWIT_ADDRESS || -// !BITGO_TAPROOT_ADDRESS || -// !USER_XPUB || -// !BACKUP_XPUB || -// !BITGO_XPUB -// ) { -// throw new Error('Please provide all the required Environment Variables.'); -// } - -// let environmentName: EnvironmentName; -// let coinType: string; -// let coinInstance: CoinConstructor; -// let bitcoinNetwork: Network; - -// switch (BITCOIN_NETWORK) { -// case 'bitcoin': -// environmentName = 'prod'; -// coinType = 'btc'; -// coinInstance = Btc.createInstance; -// bitcoinNetwork = bitcoin; -// break; -// case 'testnet': -// environmentName = 'test'; -// coinType = 'tbtc'; -// coinInstance = Tbtc.createInstance; -// bitcoinNetwork = testnet; -// break; -// default: -// throw new Error('Invalid BITCOIN_NETWORK Value. Please provide either "bitcoin" or "testnet".'); -// } - -// let bitGoAPI: BitGoAPI; -// try { -// bitGoAPI = new BitGoAPI({ accessToken: BITGO_ACCESS_TOKEN, env: environmentName }); -// } catch (error) { -// throw new Error(`Error while initializing BitGo API: ${error}`); -// } - -// bitGoAPI.coin(coinType).wallets().generateWallet({ label: 'Test Wallet' }); - -// bitGoAPI.register(coinType, coinInstance); - -// let bitGoWallet: Wallet; -// try { -// bitGoWallet = await bitGoAPI.coin(coinType).wallets().getWallet({ id: BITGO_WALLET_ID }); -// } catch (error) { -// throw new Error(`Error while retrieving BitGo wallet: ${error}`); -// } - -// const bitGoKeyChain = await bitGoAPI.coin(coinType).keychains().getKeysForSigning({ wallet: bitGoWallet }); - -// return { -// bitGoAPI, -// bitGoWallet, -// bitGoKeyChain, -// bitGoWalletPassphrase: BITGO_WALLET_PASSPHRASE, -// nativeSegwitAddress: BITGO_NATIVE_SEGWIT_ADDRESS, -// taprootAddress: BITGO_TAPROOT_ADDRESS, -// userXPUB: USER_XPUB, -// backupXPUB: BACKUP_XPUB, -// bitGoXPUB: BITGO_XPUB, -// bitcoinNetwork, -// }; -// } - -// /** -// * Creates a Funding Transaction to fund the Multisig Transaction. -// * -// * @param bitcoinAmount - The amount of Bitcoin to fund the Transaction with. -// * @param multisigAddress - The Multisig Address. -// * @param feeRecipientAddress - The Fee Recipient's Address. -// * @param feeBasisPoints - The Fee Basis Points. -// * @returns The Funding Transaction Info. -// */ -// export function getFundingTransactionRecipients( -// bitcoinAmount: number, -// multisigAddress: string, -// feeRecipientAddress: string, -// feeBasisPoints: number -// ) { -// const recipients = [ -// { amount: bitcoinToSats(bitcoinAmount), address: multisigAddress }, -// { -// amount: bitcoinToSats(bitcoinAmount) * feeBasisPoints, -// address: feeRecipientAddress, -// }, -// ]; - -// return recipients; -// } diff --git a/src/constants.ts b/src/constants.ts deleted file mode 100644 index 0c69fac..0000000 --- a/src/constants.ts +++ /dev/null @@ -1,38 +0,0 @@ -/** @format */ - -// test values for the integration tests -export const TEST_BITCOIN_AMOUNT = 1000000n; -export const TEST_FEE_AMOUNT = 100n; -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 = 350n; - -// 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/`; - -export const TEST_EXTENDED_PRIVATE_KEY_1 = - 'tprv8ZgxMBicQKsPdUfw7LM946yzMWhPrDtmBpB3R5Czx3u98TB2bXgUnkGQbPrNaQ8VQsbjNYseSsggRETuFExqhHoAoqCbrcpVj8pWShR5eQy'; -export const TEST_EXTENDED_PUBLIC_KEY_1 = - 'tpubD6NzVbkrYhZ4Wwhizz1jTWe6vYDL1Z5fm7mphbFJNKhXxwRoDvW4yEtGmWJ6n9JE86wpvQsDpzn5t49uenYStgAqwgmKNjDe1D71TdAjy8o'; -export const TEST_MASTER_FINGERPRINT_1 = '8400dc04'; - -export const TEST_EXTENDED_PRIVATE_KEY_2 = - 'tprv8ZgxMBicQKsPfJ6T1H5ErNLa1fZyj2fxCR7vRqVokCLvWg9JypYJoGVdvU6UNkj59o6qDdB97QFk7CQa2XnKZGSzQGhfoc4hCGXrviFuxwP'; -export const TEST_EXTENDED_PUBLIC_KEY_2 = - 'tpubD6NzVbkrYhZ4Ym8EtvjqFmzgah5utMrrmiihiMY7AU9KMAQ5cDMtym7W6ccSUinTVbDqK1Vno96HNhaqhS1DuVCrjHoFG9bFa3DKUUMErCv'; -export const TEST_MASTER_FINGERPRINT_2 = 'b2cd3e18'; - -export const TAPROOT_UNSPENDABLE_KEY_STRING = '50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0'; - -export const TAPROOT_DERIVATION_PATH = "86'"; -export const NATIVE_SEGWIT_DERIVATION_PATH = "84'"; - -export const LEDGER_APPS_MAP = { - BITCOIN_MAINNET: 'Bitcoin', - BITCOIN_TESTNET: 'Bitcoin Test', - MAIN_MENU: 'BOLOS', -} as const; diff --git a/src/ethereum-network.ts b/src/constants/ethereum-constants.ts similarity index 53% rename from src/ethereum-network.ts rename to src/constants/ethereum-constants.ts index bf74e51..15a0ba3 100644 --- a/src/ethereum-network.ts +++ b/src/constants/ethereum-constants.ts @@ -32,32 +32,3 @@ export const hexChainIDs: { [key in EthereumNetworkID]: string } = { [EthereumNetworkID.ArbSepolia]: '0x66eee', [EthereumNetworkID.Arbitrum]: '0xa4b1', }; - -export const addNetworkParams = { - [EthereumNetworkID.ArbSepolia]: [ - { - chainId: '0x66eee', - rpcUrls: ['https://sepolia-rollup.arbitrum.io/rpc', 'https://arb-sepolia.infura.io/v3/'], - chainName: 'Arbitrum Sepolia Testnet', - nativeCurrency: { - name: 'ETH', - symbol: 'ETH', - decimals: 18, - }, - blockExplorerUrls: ['https://sepolia.arbiscan.io/'], - }, - ], - [EthereumNetworkID.Arbitrum]: [ - { - chainId: '42161', - rpcUrls: ['https://arb1.arbitrum.io/rpc', 'https://arbitrum-mainnet.infura.io'], - chainName: 'Arbitrum One', - nativeCurrency: { - name: 'ETH', - symbol: 'ETH', - decimals: 18, - }, - blockExplorerUrls: ['https://arbiscan.io/'], - }, - ], -}; diff --git a/src/constants/ledger-constants.ts b/src/constants/ledger-constants.ts new file mode 100644 index 0000000..6fc09d1 --- /dev/null +++ b/src/constants/ledger-constants.ts @@ -0,0 +1,7 @@ +/** @format */ + +export const LEDGER_APPS_MAP = { + BITCOIN_MAINNET: 'Bitcoin', + BITCOIN_TESTNET: 'Bitcoin Test', + MAIN_MENU: 'BOLOS', +} as const; diff --git a/src/dlc-handlers/ledger-dlc-handler.ts b/src/dlc-handlers/ledger-dlc-handler.ts new file mode 100644 index 0000000..f497322 --- /dev/null +++ b/src/dlc-handlers/ledger-dlc-handler.ts @@ -0,0 +1,377 @@ +/** @format */ + +import { Transaction } from '@scure/btc-signer'; +import { P2Ret, P2TROut, p2wpkh } from '@scure/btc-signer/payment'; +import { Network, Psbt } from 'bitcoinjs-lib'; +import { bitcoin, regtest, testnet } from 'bitcoinjs-lib/src/networks.js'; +import { AppClient, DefaultWalletPolicy, WalletPolicy } from 'ledger-bitcoin'; +import { + createBitcoinInputSigningConfiguration, + createTaprootMultisigPayment, + deriveUnhardenedPublicKey, + getBalance, + getFeeRate, + getInputByPaymentTypeArray, + getUnspendableKeyCommittedToUUID, +} from '../functions/bitcoin-functions.js'; +import { RawVault } from '../models/ethereum-models.js'; +import { + addNativeSegwitSignaturesToPSBT, + addTaprootInputSignaturesToPSBT, + createClosingTransaction, + createFundingTransaction, + getNativeSegwitInputsToSign, + getTaprootInputsToSign, + updateNativeSegwitInputs, + updateTaprootInputs, +} from '../functions/psbt-functions.js'; +import { truncateAddress } from '../utilities.js'; +import { PaymentInformation } from '../models/bitcoin-models.js'; + +interface LedgerPolicyInformation { + nativeSegwitWalletPolicy: DefaultWalletPolicy; + taprootMultisigWalletPolicy: WalletPolicy; + taprootMultisigWalletPolicyHMac: Buffer; +} + +export class LedgerDLCHandler { + private ledgerApp: AppClient; + private masterFingerprint: string; + private walletAccountIndex: number; + private policyInformation: LedgerPolicyInformation | undefined; + private paymentInformation: PaymentInformation | undefined; + private bitcoinNetwork: Network; + private bitcoinBlockchainAPI: string; + private bitcoinBlockchainFeeRecommendationAPI: string; + + constructor( + ledgerApp: AppClient, + masterFingerprint: string, + walletAccountIndex: number, + bitcoinNetwork: Network, + bitcoinBlockchainAPI?: string, + bitcoinBlockchainFeeRecommendationAPI?: string + ) { + switch (bitcoinNetwork) { + case bitcoin: + this.bitcoinBlockchainAPI = 'https://mempool.space/api'; + this.bitcoinBlockchainFeeRecommendationAPI = 'https://mempool.space/api/v1/fees/recommended'; + break; + case testnet: + this.bitcoinBlockchainAPI = 'https://mempool.space/testnet/api'; + this.bitcoinBlockchainFeeRecommendationAPI = 'https://mempool.space/testnet/api/v1/fees/recommended'; + break; + case regtest: + if (bitcoinBlockchainAPI === undefined || bitcoinBlockchainFeeRecommendationAPI === undefined) { + throw new Error('Regtest requires a Bitcoin Blockchain API and a Bitcoin Blockchain Fee Recommendation API'); + } + this.bitcoinBlockchainAPI = bitcoinBlockchainAPI; + this.bitcoinBlockchainFeeRecommendationAPI = bitcoinBlockchainFeeRecommendationAPI; + break; + default: + throw new Error('Invalid Bitcoin Network'); + } + this.ledgerApp = ledgerApp; + this.masterFingerprint = masterFingerprint; + this.walletAccountIndex = walletAccountIndex; + this.bitcoinNetwork = bitcoinNetwork; + } + + private setPolicyInformation( + nativeSegwitWalletPolicy: DefaultWalletPolicy, + taprootMultisigWalletPolicy: WalletPolicy, + taprootMultisigWalletPolicyHMac: Buffer + ): void { + this.policyInformation = { + nativeSegwitWalletPolicy, + taprootMultisigWalletPolicy, + taprootMultisigWalletPolicyHMac, + }; + } + private setPaymentInformation( + nativeSegwitPayment: P2Ret, + nativeSegwitDerivedPublicKey: Buffer, + taprootMultisigPayment: P2TROut, + taprootDerivedPublicKey: Buffer + ): void { + this.paymentInformation = { + nativeSegwitPayment, + nativeSegwitDerivedPublicKey, + taprootMultisigPayment, + taprootDerivedPublicKey, + }; + } + + private getPolicyInformation(): LedgerPolicyInformation { + if (!this.policyInformation) { + throw new Error('Policy Information not set'); + } + return this.policyInformation; + } + + private getPaymentInformation(): PaymentInformation { + if (!this.paymentInformation) { + throw new Error('Payment Information not set'); + } + return this.paymentInformation; + } + + getVaultRelatedAddress(paymentType: 'p2wpkh' | 'p2tr'): string { + const payment = this.getPaymentInformation(); + + if (payment === undefined) { + throw new Error('Payment objects have not been set'); + } + + let address: string; + + switch (paymentType) { + case 'p2wpkh': + if (!payment.nativeSegwitPayment.address) { + throw new Error('Native Segwit Payment Address is undefined'); + } + address = payment.nativeSegwitPayment.address; + return address; + case 'p2tr': + if (!payment.taprootMultisigPayment.address) { + throw new Error('Taproot Multisig Payment Address is undefined'); + } + address = payment.taprootMultisigPayment.address; + return address; + default: + throw new Error('Invalid Payment Type'); + } + } + + async createPayment(vaultUUID: string, attestorGroupPublicKey: string): Promise { + try { + const networkIndex = this.bitcoinNetwork === bitcoin ? 0 : 1; + + const nativeSegwitExtendedPublicKey = await this.ledgerApp.getExtendedPubkey( + `m/84'/${networkIndex}'/${this.walletAccountIndex}'` + ); + + const nativeSegwitKeyinfo = `[${this.masterFingerprint}/84'/${networkIndex}'/${this.walletAccountIndex}']${nativeSegwitExtendedPublicKey}`; + + const nativeSegwitWalletPolicy = new DefaultWalletPolicy('wpkh(@0/**)', nativeSegwitKeyinfo); + + const nativeSegwitAddress = await this.ledgerApp.getWalletAddress(nativeSegwitWalletPolicy, null, 0, 0, false); + + const nativeSegwitDerivedPublicKey = deriveUnhardenedPublicKey( + nativeSegwitExtendedPublicKey, + this.bitcoinNetwork + ); + const nativeSegwitPayment = p2wpkh(nativeSegwitDerivedPublicKey, this.bitcoinNetwork); + + if (nativeSegwitPayment.address !== nativeSegwitAddress) { + throw new Error(`[Ledger] Recreated Native Segwit Address does not match the Ledger Native Segwit Address`); + } + + const unspendablePublicKey = getUnspendableKeyCommittedToUUID(vaultUUID, this.bitcoinNetwork); + const unspendableDerivedPublicKey = deriveUnhardenedPublicKey(unspendablePublicKey, this.bitcoinNetwork); + + const attestorDerivedPublicKey = deriveUnhardenedPublicKey(attestorGroupPublicKey, this.bitcoinNetwork); + + const taprootExtendedPublicKey = await this.ledgerApp.getExtendedPubkey( + `m/86'/${networkIndex}'/${this.walletAccountIndex}'` + ); + + const ledgerTaprootKeyInfo = `[${this.masterFingerprint}/86'/${networkIndex}'/${this.walletAccountIndex}']${taprootExtendedPublicKey}`; + + const taprootDerivedPublicKey = deriveUnhardenedPublicKey(taprootExtendedPublicKey, this.bitcoinNetwork); + + const descriptors = + taprootDerivedPublicKey.toString('hex') < attestorDerivedPublicKey.toString('hex') + ? [ledgerTaprootKeyInfo, attestorGroupPublicKey] + : [attestorGroupPublicKey, ledgerTaprootKeyInfo]; + + const taprootMultisigAccountPolicy = new WalletPolicy( + `Taproot Multisig Wallet for Vault: ${truncateAddress(vaultUUID)}`, + `tr(@0/**,and_v(v:pk(@1/**),pk(@2/**)))`, + [unspendablePublicKey, ...descriptors] + ); + + const [, taprootMultisigPolicyHMac] = await this.ledgerApp.registerWallet(taprootMultisigAccountPolicy); + + const taprootMultisigAddress = await this.ledgerApp.getWalletAddress( + taprootMultisigAccountPolicy, + taprootMultisigPolicyHMac, + 0, + 0, + false + ); + + const taprootMultisigPayment = createTaprootMultisigPayment( + unspendableDerivedPublicKey, + attestorDerivedPublicKey, + taprootDerivedPublicKey, + this.bitcoinNetwork + ); + + if (taprootMultisigAddress !== taprootMultisigPayment.address) { + throw new Error(`Recreated Multisig Address does not match the Ledger Multisig Address`); + } + + this.setPolicyInformation(nativeSegwitWalletPolicy, taprootMultisigAccountPolicy, taprootMultisigPolicyHMac); + this.setPaymentInformation( + nativeSegwitPayment, + nativeSegwitDerivedPublicKey, + taprootMultisigPayment, + taprootDerivedPublicKey + ); + } catch (error: any) { + throw new Error(`Error creating required wallet information: ${error}`); + } + } + + async createFundingPSBT(vault: RawVault, feeRateMultiplier?: number, customFeeRate?: bigint): Promise { + try { + const { nativeSegwitPayment, nativeSegwitDerivedPublicKey, taprootMultisigPayment, taprootDerivedPublicKey } = + this.getPaymentInformation(); + + if (taprootMultisigPayment.address === undefined || nativeSegwitPayment.address === undefined) { + throw new Error('Payment Address is undefined'); + } + + const feeRate = + customFeeRate ?? BigInt(await getFeeRate(this.bitcoinBlockchainFeeRecommendationAPI, feeRateMultiplier)); + + const addressBalance = await getBalance(nativeSegwitPayment.address, this.bitcoinBlockchainAPI); + + if (BigInt(addressBalance) < vault.valueLocked.toBigInt()) { + throw new Error('Insufficient Funds'); + } + + const fundingPSBT = await createFundingTransaction( + vault.valueLocked.toBigInt(), + this.bitcoinNetwork, + taprootMultisigPayment.address, + nativeSegwitPayment, + feeRate, + vault.btcFeeRecipient, + vault.btcMintFeeBasisPoints.toBigInt(), + this.bitcoinBlockchainAPI + ); + + const signingConfiguration = createBitcoinInputSigningConfiguration( + fundingPSBT, + this.walletAccountIndex, + this.bitcoinNetwork + ); + + const formattedFundingPSBT = Psbt.fromBuffer(Buffer.from(fundingPSBT), { + network: this.bitcoinNetwork, + }); + + const inputByPaymentTypeArray = getInputByPaymentTypeArray( + signingConfiguration, + formattedFundingPSBT.toBuffer(), + this.bitcoinNetwork + ); + + const nativeSegwitInputsToSign = getNativeSegwitInputsToSign(inputByPaymentTypeArray); + + await updateNativeSegwitInputs( + nativeSegwitInputsToSign, + nativeSegwitDerivedPublicKey, + this.masterFingerprint, + formattedFundingPSBT, + this.bitcoinBlockchainAPI + ); + + return formattedFundingPSBT; + } catch (error: any) { + throw new Error(`Error creating Funding PSBT: ${error}`); + } + } + + async createClosingPSBT( + vault: RawVault, + fundingTransactionID: string, + feeRateMultiplier?: number, + customFeeRate?: bigint + ): Promise { + try { + const { nativeSegwitPayment, taprootMultisigPayment, taprootDerivedPublicKey } = this.getPaymentInformation(); + + if (nativeSegwitPayment.address === undefined) { + throw new Error('Could not get Addresses from Payments'); + } + + const feeRate = + customFeeRate ?? BigInt(await getFeeRate(this.bitcoinBlockchainFeeRecommendationAPI, feeRateMultiplier)); + + const closingPSBT = createClosingTransaction( + vault.valueLocked.toBigInt(), + this.bitcoinNetwork, + fundingTransactionID, + taprootMultisigPayment, + nativeSegwitPayment.address, + feeRate, + vault.btcFeeRecipient, + vault.btcRedeemFeeBasisPoints.toBigInt() + ); + + const closingTransactionSigningConfiguration = createBitcoinInputSigningConfiguration( + closingPSBT, + this.walletAccountIndex, + this.bitcoinNetwork + ); + + const formattedClosingPSBT = Psbt.fromBuffer(Buffer.from(closingPSBT), { + network: this.bitcoinNetwork, + }); + + const closingInputByPaymentTypeArray = getInputByPaymentTypeArray( + closingTransactionSigningConfiguration, + formattedClosingPSBT.toBuffer(), + this.bitcoinNetwork + ); + + const taprootInputsToSign = getTaprootInputsToSign(closingInputByPaymentTypeArray); + + await updateTaprootInputs( + taprootInputsToSign, + taprootDerivedPublicKey, + this.masterFingerprint, + formattedClosingPSBT + ); + + return formattedClosingPSBT; + } catch (error: any) { + throw new Error(`Error creating Closing PSBT: ${error}`); + } + } + + async signPSBT(psbt: Psbt, transactionType: 'funding' | 'closing'): Promise { + try { + const { nativeSegwitWalletPolicy, taprootMultisigWalletPolicy, taprootMultisigWalletPolicyHMac } = + this.getPolicyInformation(); + + let signatures; + let transaction: Transaction; + + switch (transactionType) { + case 'funding': + signatures = await this.ledgerApp.signPsbt(psbt.toBase64(), nativeSegwitWalletPolicy, null); + addNativeSegwitSignaturesToPSBT(psbt, signatures); + transaction = Transaction.fromPSBT(psbt.toBuffer()); + transaction.finalize(); + return transaction; + case 'closing': + signatures = await this.ledgerApp.signPsbt( + psbt.toBase64(), + taprootMultisigWalletPolicy, + taprootMultisigWalletPolicyHMac + ); + addTaprootInputSignaturesToPSBT(psbt, signatures); + transaction = Transaction.fromPSBT(psbt.toBuffer()); + return transaction; + default: + throw new Error('Invalid Transaction Type'); + } + } catch (error: any) { + throw new Error(`Error signing PSBT: ${error}`); + } + } +} diff --git a/src/dlc-handlers/private-key-dlc-handler.ts b/src/dlc-handlers/private-key-dlc-handler.ts new file mode 100644 index 0000000..fd84fcc --- /dev/null +++ b/src/dlc-handlers/private-key-dlc-handler.ts @@ -0,0 +1,224 @@ +/** @format */ + +import { Transaction, p2wpkh } from '@scure/btc-signer'; +import { P2Ret, P2TROut } from '@scure/btc-signer/payment'; +import { Signer } from '@scure/btc-signer/transaction'; +import { BIP32Interface } from 'bip32'; +import { Network } from 'bitcoinjs-lib'; +import { bitcoin, regtest, testnet } from 'bitcoinjs-lib/src/networks.js'; +import { + createTaprootMultisigPayment, + deriveUnhardenedKeyPairFromRootPrivateKey, + deriveUnhardenedPublicKey, + getBalance, + getFeeRate, + getUnspendableKeyCommittedToUUID, +} from '../functions/bitcoin-functions.js'; +import { RawVault } from '../models/ethereum-models.js'; +import { createClosingTransaction, createFundingTransaction } from '../functions/psbt-functions.js'; +import { RequiredPayment } from '../models/bitcoin-models.js'; + +interface RequiredKeyPair { + nativeSegwitDerivedKeyPair: BIP32Interface; + taprootDerivedKeyPair: BIP32Interface; +} + +export class PrivateKeyDLCHandler { + private derivedKeyPair: RequiredKeyPair; + public payment: RequiredPayment | undefined; + private bitcoinNetwork: Network; + private bitcoinBlockchainAPI: string; + private bitcoinBlockchainFeeRecommendationAPI: string; + + constructor( + bitcoinWalletPrivateKey: string, + walletAccountIndex: number, + bitcoinNetwork: Network, + bitcoinBlockchainAPI?: string, + bitcoinBlockchainFeeRecommendationAPI?: string + ) { + switch (bitcoinNetwork) { + case bitcoin: + this.bitcoinBlockchainAPI = 'https://mempool.space/api'; + this.bitcoinBlockchainFeeRecommendationAPI = 'https://mempool.space/api/v1/fees/recommended'; + break; + case testnet: + this.bitcoinBlockchainAPI = 'https://mempool.space/testnet/api'; + this.bitcoinBlockchainFeeRecommendationAPI = 'https://mempool.space/testnet/api/v1/fees/recommended'; + break; + case regtest: + if (bitcoinBlockchainAPI === undefined || bitcoinBlockchainFeeRecommendationAPI === undefined) { + throw new Error('Regtest requires a Bitcoin Blockchain API and a Bitcoin Blockchain Fee Recommendation API'); + } + this.bitcoinBlockchainAPI = bitcoinBlockchainAPI; + this.bitcoinBlockchainFeeRecommendationAPI = bitcoinBlockchainFeeRecommendationAPI; + break; + default: + throw new Error('Invalid Bitcoin Network'); + } + this.bitcoinNetwork = bitcoinNetwork; + const nativeSegwitDerivedKeyPair = deriveUnhardenedKeyPairFromRootPrivateKey( + bitcoinWalletPrivateKey, + bitcoinNetwork, + 'p2wpkh', + walletAccountIndex + ); + const taprootDerivedKeyPair = deriveUnhardenedKeyPairFromRootPrivateKey( + bitcoinWalletPrivateKey, + bitcoinNetwork, + 'p2tr', + walletAccountIndex + ); + + this.derivedKeyPair = { + taprootDerivedKeyPair, + nativeSegwitDerivedKeyPair, + }; + } + + private setPayment(nativeSegwitPayment: P2Ret, taprootMultisigPayment: P2TROut): void { + this.payment = { + nativeSegwitPayment, + taprootMultisigPayment, + }; + } + + getVaultRelatedAddress(paymentType: 'p2wpkh' | 'p2tr'): string { + const payment = this.payment; + + if (payment === undefined) { + throw new Error('Payment objects have not been set'); + } + + let address: string; + + switch (paymentType) { + case 'p2wpkh': + if (!payment.nativeSegwitPayment.address) { + throw new Error('Native Segwit Payment Address is undefined'); + } + address = payment.nativeSegwitPayment.address; + return address; + case 'p2tr': + if (!payment.taprootMultisigPayment.address) { + throw new Error('Taproot Multisig Payment Address is undefined'); + } + address = payment.taprootMultisigPayment.address; + return address; + default: + throw new Error('Invalid Payment Type'); + } + } + + private getPrivateKey(paymentType: 'p2wpkh' | 'p2tr'): Signer { + const privateKey = + paymentType === 'p2wpkh' + ? this.derivedKeyPair.nativeSegwitDerivedKeyPair.privateKey + : this.derivedKeyPair.taprootDerivedKeyPair.privateKey; + + if (!privateKey) { + throw new Error('Private Key is Undefined'); + } + + return privateKey; + } + private handlePayment(vaultUUID: string, attestorGroupPublicKey: string): RequiredPayment { + try { + const unspendablePublicKey = getUnspendableKeyCommittedToUUID(vaultUUID, this.bitcoinNetwork); + const unspendableDerivedPublicKey = deriveUnhardenedPublicKey(unspendablePublicKey, this.bitcoinNetwork); + + const attestorDerivedPublicKey = deriveUnhardenedPublicKey(attestorGroupPublicKey, this.bitcoinNetwork); + + const nativeSegwitPayment = p2wpkh(this.derivedKeyPair.nativeSegwitDerivedKeyPair.publicKey, this.bitcoinNetwork); + const taprootMultisigPayment = createTaprootMultisigPayment( + unspendableDerivedPublicKey, + attestorDerivedPublicKey, + this.derivedKeyPair.taprootDerivedKeyPair.publicKey, + this.bitcoinNetwork + ); + + this.setPayment(nativeSegwitPayment, taprootMultisigPayment); + + return { + nativeSegwitPayment, + taprootMultisigPayment, + }; + } catch (error: any) { + throw new Error(`Error creating required Payment objects: ${error}`); + } + } + + async createFundingPSBT( + vault: RawVault, + attestorGroupPublicKey: string, + feeRateMultiplier?: number, + customFeeRate?: bigint + ): Promise { + const { nativeSegwitPayment, taprootMultisigPayment } = this.handlePayment(vault.uuid, attestorGroupPublicKey); + + if (nativeSegwitPayment.address === undefined || taprootMultisigPayment.address === undefined) { + throw new Error('Could not get Addresses from Payments'); + } + + const addressBalance = await getBalance(nativeSegwitPayment.address, this.bitcoinBlockchainAPI); + + if (BigInt(addressBalance) < vault.valueLocked.toBigInt()) { + throw new Error('Insufficient Funds'); + } + + const feeRate = + customFeeRate ?? BigInt(await getFeeRate(this.bitcoinBlockchainFeeRecommendationAPI, feeRateMultiplier)); + + const fundingPSBT = await createFundingTransaction( + vault.valueLocked.toBigInt(), + this.bitcoinNetwork, + taprootMultisigPayment.address, + nativeSegwitPayment, + feeRate, + vault.btcFeeRecipient, + vault.btcMintFeeBasisPoints.toBigInt(), + this.bitcoinBlockchainAPI + ); + + return Transaction.fromPSBT(fundingPSBT); + } + + async createClosingPSBT( + vault: RawVault, + fundingTransactionID: string, + feeRateMultiplier?: number, + customFeeRate?: bigint + ): Promise { + if (this.payment === undefined) { + throw new Error('Payment objects have not been set'); + } + + const { nativeSegwitPayment, taprootMultisigPayment } = this.payment; + + if (nativeSegwitPayment.address === undefined) { + throw new Error('Could not get Addresses from Payments'); + } + + const feeRate = + customFeeRate ?? BigInt(await getFeeRate(this.bitcoinBlockchainFeeRecommendationAPI, feeRateMultiplier)); + + const closingPSBT = createClosingTransaction( + vault.valueLocked.toBigInt(), + this.bitcoinNetwork, + fundingTransactionID, + taprootMultisigPayment, + nativeSegwitPayment.address, + feeRate, + vault.btcFeeRecipient, + vault.btcRedeemFeeBasisPoints.toBigInt() + ); + + return Transaction.fromPSBT(closingPSBT); + } + + signPSBT(psbt: Transaction, transactionType: 'funding' | 'closing', finalize: boolean = false): Transaction { + psbt.sign(this.getPrivateKey(transactionType === 'funding' ? 'p2wpkh' : 'p2tr')); + if (transactionType === 'funding') psbt.finalize(); + return psbt; + } +} diff --git a/src/dlc-handlers/software-wallet-dlc-handler.ts b/src/dlc-handlers/software-wallet-dlc-handler.ts new file mode 100644 index 0000000..b79fcf0 --- /dev/null +++ b/src/dlc-handlers/software-wallet-dlc-handler.ts @@ -0,0 +1,184 @@ +/** @format */ + +import { Transaction, p2wpkh } from '@scure/btc-signer'; +import { Network } from 'bitcoinjs-lib'; +import { bitcoin, regtest, testnet } from 'bitcoinjs-lib/src/networks.js'; +import { + createTaprootMultisigPayment, + deriveUnhardenedPublicKey, + getBalance, + getFeeRate, + getUnspendableKeyCommittedToUUID, +} from '../functions/bitcoin-functions.js'; +import { RequiredPayment } from '../models/bitcoin-models.js'; +import { P2Ret, P2TROut } from '@scure/btc-signer/payment'; +import { RawVault } from '../models/ethereum-models.js'; +import { createClosingTransaction, createFundingTransaction } from '../functions/psbt-functions.js'; + +export class SoftwareWalletDLCHandler { + private nativeSegwitDerivedPublicKey: string; + private taprootDerivedPublicKey: string; + public paymentInformation: RequiredPayment | undefined; + private bitcoinNetwork: Network; + private bitcoinBlockchainAPI: string; + private bitcoinBlockchainFeeRecommendationAPI: string; + + constructor( + nativeSegwitDerivedPublicKey: string, + taprootDerivedPublicKey: string, + bitcoinNetwork: Network, + bitcoinBlockchainAPI?: string, + bitcoinBlockchainFeeRecommendationAPI?: string + ) { + switch (bitcoinNetwork) { + case bitcoin: + this.bitcoinBlockchainAPI = 'https://mempool.space/api'; + this.bitcoinBlockchainFeeRecommendationAPI = 'https://mempool.space/api/v1/fees/recommended'; + break; + case testnet: + this.bitcoinBlockchainAPI = 'https://mempool.space/testnet/api'; + this.bitcoinBlockchainFeeRecommendationAPI = 'https://mempool.space/testnet/api/v1/fees/recommended'; + break; + case regtest: + if (bitcoinBlockchainAPI === undefined || bitcoinBlockchainFeeRecommendationAPI === undefined) { + throw new Error('Regtest requires a Bitcoin Blockchain API and a Bitcoin Blockchain Fee Recommendation API'); + } + this.bitcoinBlockchainAPI = bitcoinBlockchainAPI; + this.bitcoinBlockchainFeeRecommendationAPI = bitcoinBlockchainFeeRecommendationAPI; + break; + default: + throw new Error('Invalid Bitcoin Network'); + } + this.bitcoinNetwork = bitcoinNetwork; + this.nativeSegwitDerivedPublicKey = nativeSegwitDerivedPublicKey; + this.taprootDerivedPublicKey = taprootDerivedPublicKey; + } + + private setPaymentInformation(nativeSegwitPayment: P2Ret, taprootMultisigPayment: P2TROut): void { + this.paymentInformation = { + nativeSegwitPayment, + taprootMultisigPayment, + }; + } + + private getPaymentInformation(): RequiredPayment { + if (!this.paymentInformation) { + throw new Error('Payment Information not set'); + } + return this.paymentInformation; + } + + etVaultRelatedAddress(paymentType: 'p2wpkh' | 'p2tr'): string { + const payment = this.getPaymentInformation(); + + if (payment === undefined) { + throw new Error('Payment objects have not been set'); + } + + let address: string; + + switch (paymentType) { + case 'p2wpkh': + if (!payment.nativeSegwitPayment.address) { + throw new Error('Native Segwit Payment Address is undefined'); + } + address = payment.nativeSegwitPayment.address; + return address; + case 'p2tr': + if (!payment.taprootMultisigPayment.address) { + throw new Error('Taproot Multisig Payment Address is undefined'); + } + address = payment.taprootMultisigPayment.address; + return address; + default: + throw new Error('Invalid Payment Type'); + } + } + + async createPayment(vaultUUID: string, attestorGroupPublicKey: string): Promise { + try { + const nativeSegwitPayment = p2wpkh(Buffer.from(this.nativeSegwitDerivedPublicKey, 'hex'), this.bitcoinNetwork); + + const unspendablePublicKey = getUnspendableKeyCommittedToUUID(vaultUUID, this.bitcoinNetwork); + const unspendableDerivedPublicKey = deriveUnhardenedPublicKey(unspendablePublicKey, this.bitcoinNetwork); + + const attestorDerivedPublicKey = deriveUnhardenedPublicKey(attestorGroupPublicKey, this.bitcoinNetwork); + + const taprootMultisigPayment = createTaprootMultisigPayment( + unspendableDerivedPublicKey, + attestorDerivedPublicKey, + Buffer.from(this.taprootDerivedPublicKey), + this.bitcoinNetwork + ); + + this.setPaymentInformation(nativeSegwitPayment, taprootMultisigPayment); + } catch (error: any) { + throw new Error(`Error creating required wallet information: ${error}`); + } + } + + async createFundingPSBT(vault: RawVault, feeRateMultiplier?: number, customFeeRate?: bigint): Promise { + try { + const { nativeSegwitPayment, taprootMultisigPayment } = this.getPaymentInformation(); + + if (taprootMultisigPayment.address === undefined || nativeSegwitPayment.address === undefined) { + throw new Error('Payment Address is undefined'); + } + + const feeRate = + customFeeRate ?? BigInt(await getFeeRate(this.bitcoinBlockchainFeeRecommendationAPI, feeRateMultiplier)); + + const addressBalance = await getBalance(nativeSegwitPayment.address, this.bitcoinBlockchainAPI); + + if (BigInt(addressBalance) < vault.valueLocked.toBigInt()) { + throw new Error('Insufficient Funds'); + } + + const fundingPSBT = await createFundingTransaction( + vault.valueLocked.toBigInt(), + this.bitcoinNetwork, + taprootMultisigPayment.address, + nativeSegwitPayment, + feeRate, + vault.btcFeeRecipient, + vault.btcMintFeeBasisPoints.toBigInt(), + this.bitcoinBlockchainAPI + ); + return Transaction.fromPSBT(fundingPSBT); + } catch (error: any) { + throw new Error(`Error creating Funding PSBT: ${error}`); + } + } + + async createClosingPSBT( + vault: RawVault, + fundingTransactionID: string, + feeRateMultiplier?: number, + customFeeRate?: bigint + ): Promise { + try { + const { nativeSegwitPayment, taprootMultisigPayment } = this.getPaymentInformation(); + + if (taprootMultisigPayment.address === undefined || nativeSegwitPayment.address === undefined) { + throw new Error('Payment Address is undefined'); + } + + const feeRate = + customFeeRate ?? BigInt(await getFeeRate(this.bitcoinBlockchainFeeRecommendationAPI, feeRateMultiplier)); + + const closingTransaction = createClosingTransaction( + vault.valueLocked.toBigInt(), + this.bitcoinNetwork, + fundingTransactionID, + taprootMultisigPayment, + nativeSegwitPayment.address!, + feeRate, + vault.btcFeeRecipient, + vault.btcRedeemFeeBasisPoints.toBigInt() + ); + return Transaction.fromPSBT(closingTransaction); + } catch (error: any) { + throw new Error(`Error creating Closing PSBT: ${error}`); + } + } +} diff --git a/src/ethereum-observer.ts b/src/ethereum-observer.ts deleted file mode 100644 index 1a1f0eb..0000000 --- a/src/ethereum-observer.ts +++ /dev/null @@ -1 +0,0 @@ -/** @format */ diff --git a/src/attestor-functions.ts b/src/functions/attestor-functions.ts similarity index 97% rename from src/attestor-functions.ts rename to src/functions/attestor-functions.ts index fd4eb15..e2acdcd 100644 --- a/src/attestor-functions.ts +++ b/src/functions/attestor-functions.ts @@ -1,6 +1,6 @@ /** @format */ -import { AttestorError } from './models/errors.js'; +import { AttestorError } from '../models/errors.js'; export async function getExtendedAttestorGroupPublicKey(attestorURL: string): Promise { const attestorExtendedPublicKeyEndpoint = `${attestorURL}/tss/get-extended-group-publickey`; diff --git a/src/bitcoin-functions.ts b/src/functions/bitcoin-functions.ts similarity index 84% rename from src/bitcoin-functions.ts rename to src/functions/bitcoin-functions.ts index bdfb633..72e5188 100644 --- a/src/bitcoin-functions.ts +++ b/src/functions/bitcoin-functions.ts @@ -2,12 +2,12 @@ import { Address, OutScript, Transaction, p2ms, p2pk, p2tr, p2tr_ns, p2wpkh } from '@scure/btc-signer'; import { P2Ret, P2TROut } from '@scure/btc-signer/payment'; import { TransactionInput } from '@scure/btc-signer/psbt'; -import { BIP32Factory } from 'bip32'; +import { BIP32Factory, BIP32Interface } from 'bip32'; import { Network } from 'bitcoinjs-lib'; -import { bitcoin, testnet } from 'bitcoinjs-lib/src/networks.js'; +import { bitcoin, regtest, testnet } from 'bitcoinjs-lib/src/networks.js'; import * as ellipticCurveCryptography from 'tiny-secp256k1'; -import { BitcoinInputSigningConfig, FeeRates, PaymentTypes, UTXO } from './models/bitcoin-models.js'; -import { createRangeFromLength, isDefined, isUndefined, unshiftValue } from './utilities.js'; +import { BitcoinInputSigningConfig, FeeRates, PaymentTypes, UTXO } from '../models/bitcoin-models.js'; +import { createRangeFromLength, isDefined, isUndefined, unshiftValue } from '../utilities.js'; const TAPROOT_UNSPENDABLE_KEY_HEX = '0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0'; const ECDSA_PUBLIC_KEY_LENGTH = 33; @@ -15,16 +15,55 @@ const ECDSA_PUBLIC_KEY_LENGTH = 33; const bip32 = BIP32Factory(ellipticCurveCryptography); /** - * Gets the derived public key from the extended public key. - * @param extendedPublicKey - The Extended Public Key. + * Derives the Public Key at the Unhardened Path (0/0) from a given Extended Public Key. + * @param extendedPublicKey - The base58-encoded Extended Public Key. * @param bitcoinNetwork - The Bitcoin Network to use. - * @returns The Derived Public Key. + * @returns The Public Key derived at the Unhardened Path. */ -export function getDerivedPublicKey(extendedPublicKey: string, bitcoinNetwork: Network): Buffer { +export function deriveUnhardenedPublicKey(extendedPublicKey: string, bitcoinNetwork: Network): Buffer { return bip32.fromBase58(extendedPublicKey, bitcoinNetwork).derivePath('0/0').publicKey; } -function getXOnlyPublicKey(publicKey: Buffer): Buffer { +/** + * Derives the Account Key Pair from the Root Private Key. + * @param rootPrivateKey - The Root Private Key. + * @param bitcoinNetwork - The Bitcoin Network to use. + * @param paymentType - The Payment Type to use. + * @param accountIndex - The Account Index to use. + * @returns The Account Key Pair. + */ +export function deriveUnhardenedKeyPairFromRootPrivateKey( + rootPrivateKey: string, + bitcoinNetwork: Network, + paymentType: 'p2tr' | 'p2wpkh', + accountIndex: number +): BIP32Interface { + switch (bitcoinNetwork) { + case bitcoin: + switch (paymentType) { + case 'p2wpkh': + return bip32.fromBase58(rootPrivateKey, bitcoinNetwork).derivePath(`m/84'/0'/${accountIndex}'/0/0`); + case 'p2tr': + return bip32.fromBase58(rootPrivateKey, bitcoinNetwork).derivePath(`m/86'/0'/${accountIndex}'/0/0`); + default: + throw new Error('Unsupported Payment Type'); + } + case testnet: + case regtest: + switch (paymentType) { + case 'p2wpkh': + return bip32.fromBase58(rootPrivateKey, bitcoinNetwork).derivePath(`m/84'/1'/${accountIndex}'/0/0`); + case 'p2tr': + return bip32.fromBase58(rootPrivateKey, bitcoinNetwork).derivePath(`m/86'/1'/${accountIndex}'/0/0`); + default: + throw new Error('Unsupported Payment Type'); + } + default: + throw new Error('Unsupported Bitcoin Network'); + } +} + +export function getXOnlyPublicKey(publicKey: Buffer): Buffer { return publicKey.length === 32 ? publicKey : publicKey.subarray(1); } @@ -271,24 +310,13 @@ function getAddressFromOutScript(script: Uint8Array, bitcoinNetwork: Network): s */ export function createBitcoinInputSigningConfiguration( psbt: Uint8Array, - derivationPath: string, + walletAccountIndex: number, bitcoinNetwork: Network ): BitcoinInputSigningConfig[] { - let nativeSegwitDerivationPath = ''; - let taprootDerivationPath = ''; + const networkIndex = bitcoinNetwork === bitcoin ? 0 : 1; - switch (bitcoinNetwork) { - case bitcoin: - nativeSegwitDerivationPath = `m/${derivationPath}/0/0`; - taprootDerivationPath = `m/${derivationPath}/0/0`; - break; - case testnet: - nativeSegwitDerivationPath = `m/${derivationPath}/0/0`; - taprootDerivationPath = `m/${derivationPath}/0/0`; - break; - default: - throw new Error('Unsupported Bitcoin Network'); - } + const nativeSegwitDerivationPath = `m/84'/${networkIndex}'/${walletAccountIndex}'/0/0`; + const taprootDerivationPath = `m/86'/${networkIndex}'/${walletAccountIndex}'/0/0`; const transaction = Transaction.fromPSBT(psbt); const indexesToSign = createRangeFromLength(transaction.inputsLength); diff --git a/src/ethereum-functions.ts b/src/functions/ethereum-functions.ts similarity index 98% rename from src/ethereum-functions.ts rename to src/functions/ethereum-functions.ts index ecca040..dd064c1 100644 --- a/src/ethereum-functions.ts +++ b/src/functions/ethereum-functions.ts @@ -1,10 +1,10 @@ /** @format */ import chalk from 'chalk'; import { Contract, ethers } from 'ethers'; -import { EthereumNetwork, ethereumArbitrum, ethereumArbitrumSepolia } from './ethereum-network.js'; -import { EthereumError } from './models/errors.js'; -import { DisplayVault, ExtendedDisplayVault, RawVault, VaultState } from './models/ethereum-models.js'; -import { customShiftValue, shiftValue, truncateAddress, unshiftValue } from './utilities.js'; +import { EthereumNetwork, ethereumArbitrum, ethereumArbitrumSepolia } from '../constants/ethereum-constants.js'; +import { EthereumError } from '../models/errors.js'; +import { DisplayVault, ExtendedDisplayVault, RawVault, VaultState } from '../models/ethereum-models.js'; +import { customShiftValue, shiftValue, truncateAddress, unshiftValue } from '../utilities.js'; const SOLIDITY_CONTRACT_URL = 'https://raw.githubusercontent.com/DLC-link/dlc-solidity'; interface EthereumContracts { diff --git a/src/psbt-functions.ts b/src/functions/psbt-functions.ts similarity index 98% rename from src/psbt-functions.ts rename to src/functions/psbt-functions.ts index 7172a35..a280bbb 100644 --- a/src/psbt-functions.ts +++ b/src/functions/psbt-functions.ts @@ -4,10 +4,10 @@ import { p2wpkh, selectUTXO } from '@scure/btc-signer'; import { Network, Psbt } from 'bitcoinjs-lib'; import { PartialSignature } from 'ledger-bitcoin/build/main/lib/appClient.js'; -import { ecdsaPublicKeyToSchnorr, getFeeRecipientAddressFromPublicKey, getUTXOs } from './bitcoin-functions.js'; import { P2Ret, P2TROut } from '@scure/btc-signer/payment'; -import { BitcoinInputSigningConfig, PaymentTypes } from './models/bitcoin-models.js'; -import { reverseBytes } from './utilities.js'; +import { BitcoinInputSigningConfig, PaymentTypes } from '../models/bitcoin-models.js'; +import { reverseBytes } from '../utilities.js'; +import { ecdsaPublicKeyToSchnorr, getFeeRecipientAddressFromPublicKey, getUTXOs } from './bitcoin-functions.js'; /** * Creates a Funding Transaction to fund the Multisig Transaction. diff --git a/src/index.ts b/src/index.ts index 82a97d5..4f1d43a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,17 +1,21 @@ /** @format */ -import dotenv from 'dotenv'; -import { getAttestorGroupPublicKey, getRawVault, setupEthereum, setupVault } from './ethereum-functions.js'; import { bytesToHex } from '@noble/hashes/utils'; -import { createPSBTEvent } from './attestor-functions.js'; -import { broadcastTransaction } from './bitcoin-functions.js'; -import { ethereumArbitrumSepolia } from './ethereum-network.js'; -import { PrivateKeyDLCHandler } from './handle-functions.js'; +import { regtest, testnet } from 'bitcoinjs-lib/src/networks.js'; +import dotenv from 'dotenv'; +import { ethereumArbitrumSepolia } from './constants/ethereum-constants.js'; +import { LEDGER_APPS_MAP } from './constants/ledger-constants.js'; +import { LedgerDLCHandler } from './dlc-handlers/ledger-dlc-handler.js'; +import { PrivateKeyDLCHandler } from './dlc-handlers/private-key-dlc-handler.js'; +import { broadcastTransaction } from './functions/bitcoin-functions.js'; +import { getAttestorGroupPublicKey, getRawVault, setupEthereum, setupVault } from './functions/ethereum-functions.js'; +import { getLedgerApp } from './ledger-functions.js'; +import { createPSBTEvent } from './functions/attestor-functions.js'; dotenv.config(); async function runFlowWithPrivateKey() { - const exampleNetwork = 'Regtest'; + const exampleNetwork = regtest; const exampleBitcoinBlockchainAPI = 'https://devnet.dlc.link/electrs'; const exampleBitcoinBlockchainFeeRecommendationAPI = 'https://devnet.dlc.link/electrs/fee-estimates'; const exampleAttestorURLs = [ @@ -41,6 +45,7 @@ async function runFlowWithPrivateKey() { // Setup DLC Handler (with Private Key) const dlcHandler = new PrivateKeyDLCHandler( examplePrivateKey, + 0, exampleNetwork, exampleBitcoinBlockchainAPI, exampleBitcoinBlockchainFeeRecommendationAPI @@ -52,48 +57,107 @@ async function runFlowWithPrivateKey() { // Fetch Attestor Group Public Key const attestorGroupPublicKey = await getAttestorGroupPublicKey(ethereumArbitrumSepolia); - // Setup Payment and Key Pair Information - const { nativeSegwitPayment, nativeSegwitDerivedKeyPair, taprootMultisigPayment, taprootDerivedKeyPair } = - dlcHandler.handlePayment(vault.uuid, 0, attestorGroupPublicKey); - // Create Funding Transaction - const fundingPSBT = await dlcHandler.createFundingPSBT(vault, nativeSegwitPayment, taprootMultisigPayment, 2n); - - if (!nativeSegwitDerivedKeyPair.privateKey) { - throw new Error('Could not get Private Key from Native Segwit Derived Key Pair'); - } + const fundingPSBT = await dlcHandler.createFundingPSBT(vault, attestorGroupPublicKey, 2); // Sign Funding Transaction - const fundingTransaction = dlcHandler.signPSBT(fundingPSBT, nativeSegwitDerivedKeyPair.privateKey, true); + const fundingTransaction = dlcHandler.signPSBT(fundingPSBT, 'funding', true); // Create Closing Transaction - const closingTransaction = await dlcHandler.createClosingPSBT( - vault, - nativeSegwitPayment, - taprootMultisigPayment, - fundingTransaction.id, - 2n + const closingTransaction = await dlcHandler.createClosingPSBT(vault, fundingTransaction.id, 2); + + // Sign Closing Transaction + const partiallySignedClosingTransaction = dlcHandler.signPSBT(closingTransaction, 'closing'); + const partiallySignedClosingTransactionHex = bytesToHex(partiallySignedClosingTransaction.toPSBT()); + + const nativeSegwitAddress = dlcHandler.getVaultRelatedAddress('p2wpkh'); + + // Send Required Information to Attestors to Create PSBT Event + await createPSBTEvent( + exampleAttestorURLs, + vaultUUID, + fundingTransaction.hex, + partiallySignedClosingTransactionHex, + nativeSegwitAddress ); - if (!taprootDerivedKeyPair.privateKey) { - throw new Error('Could not get Private Key from Taproot Derived Key Pair'); + // Broadcast Funding Transaction + const fundingTransactionID = await broadcastTransaction(fundingTransaction.hex, exampleBitcoinBlockchainAPI); + + console.log('Funding Transaction ID:', fundingTransactionID); + console.log('Success'); +} + +async function runFlowWithLedger() { + const exampleNetwork = testnet; + const exampleAttestorURLs = [ + 'https://testnet.dlc.link/attestor-1', + 'https://testnet.dlc.link/attestor-2', + 'https://testnet.dlc.link/attestor-3', + ]; + const exampleBitcoinAmount = 0.01; + + // Setup Ethereum + const { ethereumContracts, ethereumNetworkName } = await setupEthereum(); + const { protocolContract } = ethereumContracts; + + // Setup Vault + const setupVaultTransactionReceipt: any = await setupVault( + protocolContract, + ethereumNetworkName, + exampleBitcoinAmount + ); + if (!setupVaultTransactionReceipt) { + throw new Error('Could not setup Vault'); } + const vaultUUID = setupVaultTransactionReceipt.events.find((event: any) => event.event === 'SetupVault').args[0]; + // const vaultUUID = '0x1e0bf7ac4dc3886bcdb1d4bd1813a0b0d923f83d61ad1776e45677cec83e4a65'; + + const ledgerApp = await getLedgerApp(LEDGER_APPS_MAP.BITCOIN_TESTNET); + + if (!ledgerApp) { + throw new Error('Could not get Ledger App'); + } + + const masterFingerprint = await ledgerApp.getMasterFingerprint(); + + // Setup DLC Handler (with Private Key) + const dlcHandler = new LedgerDLCHandler(ledgerApp, masterFingerprint, 1, testnet); + + // Fetch Vault + const vault = await getRawVault(protocolContract, vaultUUID); + + // Fetch Attestor Group Public Key + const attestorGroupPublicKey = await getAttestorGroupPublicKey(ethereumArbitrumSepolia); + + await dlcHandler.createPayment(vaultUUID, attestorGroupPublicKey); + + // Create Funding Transaction + const fundingPSBT = await dlcHandler.createFundingPSBT(vault, 2); + + // Sign Funding Transaction + const fundingTransaction = await dlcHandler.signPSBT(fundingPSBT, 'funding'); + + // Create Closing Transaction + const closingTransaction = await dlcHandler.createClosingPSBT(vault, fundingTransaction.id, 2); // Sign Closing Transaction - const partiallySignedClosingTransaction = dlcHandler.signPSBT(closingTransaction, taprootDerivedKeyPair.privateKey); + const partiallySignedClosingTransaction = await dlcHandler.signPSBT(closingTransaction, 'closing'); const partiallySignedClosingTransactionHex = bytesToHex(partiallySignedClosingTransaction.toPSBT()); + const nativeSegwitAddress = dlcHandler.getVaultRelatedAddress('p2wpkh'); + // Send Required Information to Attestors to Create PSBT Event await createPSBTEvent( exampleAttestorURLs, vaultUUID, fundingTransaction.hex, partiallySignedClosingTransactionHex, - nativeSegwitPayment.address! + nativeSegwitAddress ); // Broadcast Funding Transaction - const fundingTransactionID = await broadcastTransaction(fundingTransaction.hex, exampleBitcoinBlockchainAPI); + const fundingTransactionID = await broadcastTransaction(fundingTransaction.hex, 'https://mempool.space/testnet/api'); console.log('Funding Transaction ID:', fundingTransactionID); console.log('Success'); @@ -102,6 +166,7 @@ async function runFlowWithPrivateKey() { async function example() { try { await runFlowWithPrivateKey(); + // await runFlowWithLedger(); } catch (error) { throw new Error(`Error: ${error}`); } diff --git a/src/ledger-functions.ts b/src/ledger-functions.ts index 3bad817..76ef34a 100644 --- a/src/ledger-functions.ts +++ b/src/ledger-functions.ts @@ -1,33 +1,12 @@ /** @format */ import Transport from '@ledgerhq/hw-transport-node-hid'; -import { p2wpkh } from '@scure/btc-signer'; -import { P2Ret, P2TROut, p2tr, p2tr_ns } from '@scure/btc-signer/payment'; -import { BIP32Factory } from 'bip32'; -import { Network, initEccLib } from 'bitcoinjs-lib'; -import { AppClient, DefaultWalletPolicy, WalletPolicy } from 'ledger-bitcoin'; -import * as ellipticCurveCryptography from 'tiny-secp256k1'; -import { - // broadcastTransaction, - getBalance, - // getBitcoinNetwork, - getUnspendableKeyCommittedToUUID, -} from './bitcoin-functions.js'; -import { LEDGER_APPS_MAP, NATIVE_SEGWIT_DERIVATION_PATH, TAPROOT_DERIVATION_PATH, TEST_FEE_RATE } from './constants.js'; +import { AppClient } from 'ledger-bitcoin'; +import { LEDGER_APPS_MAP } from './constants/ledger-constants.js'; +import { delay } from './utilities.js'; type TransportInstance = Awaited>; -import prompts from 'prompts'; - -// import { createPSBTEvent, getAttestorURLs, getExtendedAttestorGroupPublicKey } from './attestor-functions.js'; -// import { LedgerError } from './models/errors.js'; -// import { RawVault } from './models/ethereum-models.js'; -// import { handleClosingTransaction, handleFundingTransaction } from './psbt-functions.js'; -import { delay, truncateAddress } from './utilities.js'; - -initEccLib(ellipticCurveCryptography); -const bip32 = BIP32Factory(ellipticCurveCryptography); - export async function getLedgerApp(appName: string) { const transport = await Transport.default.create(); const ledgerApp = new AppClient(transport); @@ -62,354 +41,3 @@ async function quitApp(transport: TransportInstance): Promise { async function openApp(transport: TransportInstance, name: string): Promise { await transport.send(0xe0, 0xd8, 0x00, 0x00, Buffer.from(name, 'ascii')); } - -export async function getLedgerAddressIndexAndDerivationPath( - ledgerApp: AppClient, - fpr: string, - bitcoinNetworkName: string, - bitcoinNetworkIndex: string, - paymentType: 'wpkh' | 'tr', - paymentDerivationPath: string, - bitcoinBlockchainAPIURL: string -) { - const nativeSegwitAddressesWithBalances = await getLedgerAddressesWithBalances( - ledgerApp, - fpr, - bitcoinNetworkName, - paymentType, - paymentDerivationPath, - bitcoinNetworkIndex, - bitcoinBlockchainAPIURL - ); - - const addressSelectPrompt = await prompts({ - type: 'select', - name: 'addressIndex', - message: `Select Native Segwit Address to withdraw from`, - choices: nativeSegwitAddressesWithBalances.map((address, index) => ({ - title: `Address: ${address[0]} | Balance: ${address[1]}`, - value: index, - })), - }); - const addressIndex = addressSelectPrompt.addressIndex; - const rootDerivationPath = `${paymentDerivationPath}/${bitcoinNetworkIndex}/${addressIndex}'`; - - return { addressIndex, rootDerivationPath }; -} - -export async function getLedgerAddressesWithBalances( - ledgerApp: AppClient, - fpr: string, - bitcoinNetworkName: string, - paymentType: 'wpkh' | 'tr', - rootDerivationPath: string, - bitcoinNetworkIndex: string, - bitcoinBlockchainAPIURL: string -): Promise<[string, number][]> { - const indices = [0, 1, 2, 3, 4]; // Replace with your actual indices - const addresses = []; - - for (const index of indices) { - const derivationPath = `${rootDerivationPath}/${bitcoinNetworkIndex}/${index}'`; - console.log('derivationPath', derivationPath); - const extendedPublicKey = await ledgerApp.getExtendedPubkey(`m${derivationPath}`); - - const accountPolicy = new DefaultWalletPolicy( - `${paymentType}(@0/**)`, - `[${fpr}/${derivationPath}]${extendedPublicKey}` - ); - - const address = await ledgerApp.getWalletAddress(accountPolicy, null, 0, 0, false); - - addresses.push(address); - - console.log( - `[Ledger][${bitcoinNetworkName}] Retrieving ${paymentType === 'wpkh' ? 'Native Segwit' : 'Taproot'} Addresses ${index + 1} / ${indices.length}` - ); - } - - const addressesWithBalances = await Promise.all( - addresses.map(async (address) => { - const balance = await getBalance(address, bitcoinBlockchainAPIURL); // Replace with your actual function to get balance - return [address, balance] as [string, number]; - }) - ); - - return addressesWithBalances; -} - -export async function getNativeSegwitAccount( - ledgerApp: AppClient, - fpr: string, - bitcoinNetwork: Network, - bitcoinNetworkName: string, - rootNativeSegwitDerivationPath: string -): Promise<{ - ledgerNativeSegwitAccountPolicy: DefaultWalletPolicy; - nativeSegwitAddress: string; - nativeSegwitDerivedPublicKey: Buffer; - nativeSegwitPayment: P2Ret; -}> { - // ==> Get Ledger First Native Segwit Extended Public Key - const ledgerFirstNativeSegwitExtendedPublicKey = await ledgerApp.getExtendedPubkey( - `m${rootNativeSegwitDerivationPath}` - ); - console.log( - `[Ledger][${bitcoinNetworkName}] Ledger First Native Segwit Extended Public Key: ${ledgerFirstNativeSegwitExtendedPublicKey}` - ); - - // ==> Get Ledger First Native Segwit Account Policy - const ledgerNativeSegwitAccountPolicy = new DefaultWalletPolicy( - 'wpkh(@0/**)', - `[${fpr}/${rootNativeSegwitDerivationPath}]${ledgerFirstNativeSegwitExtendedPublicKey}` - ); - - // ==> Get Ledger First Native Segwit Address - const ledgerNativeSegwitAccountAddress = await ledgerApp.getWalletAddress( - ledgerNativeSegwitAccountPolicy, - null, - 0, - 0, - false - ); - console.log( - `[Ledger][${bitcoinNetworkName}] Ledger First Native Segwit Account Address: ${ledgerNativeSegwitAccountAddress}` - ); - - const nativeSegwitDerivedPublicKey = bip32 - .fromBase58(ledgerFirstNativeSegwitExtendedPublicKey, bitcoinNetwork) - .derivePath('0/0').publicKey; - - // ==> Get derivation path for Ledger Native Segwit Address - const nativeSegwitPayment = p2wpkh(nativeSegwitDerivedPublicKey, bitcoinNetwork); - - console.log(`[Ledger][${bitcoinNetworkName}] Recreated Native Segwit Address: ${nativeSegwitPayment.address}`); - - if (nativeSegwitPayment.address !== ledgerNativeSegwitAccountAddress) { - throw new Error( - `[Ledger][${bitcoinNetworkName}] Recreated Native Segwit Address does not match the Ledger Native Segwit Address` - ); - } - - return { - ledgerNativeSegwitAccountPolicy, - nativeSegwitAddress: ledgerNativeSegwitAccountAddress, - nativeSegwitDerivedPublicKey, - nativeSegwitPayment, - }; -} - -export async function getTaprootMultisigAccount( - ledgerApp: AppClient, - fpr: string, - bitcoinNetwork: Network, - bitcoinNetworkName: string, - rootTaprootDerivationPath: string, - vaultUUID: string -): Promise<{ - ledgerTaprootMultisigAccountPolicy: WalletPolicy; - ledgerTaprootMultisigPolicyHMac: Buffer; - taprootMultisigAddress: string; - taprootDerivedPublicKey: Buffer; - taprootMultisigPayment: P2TROut; -}> { - // ==> Get Ledger Derived Public Key - const ledgerExtendedPublicKey = await ledgerApp.getExtendedPubkey(`m/${rootTaprootDerivationPath}`); - - // ==> Get External Derived Public Keys - - const unspendableExtendedPublicKey = getUnspendableKeyCommittedToUUID(vaultUUID, bitcoinNetwork); - // const unspendableExtendedPublicKey = bip32 - // .fromBase58(TEST_EXTENDED_PRIVATE_KEY_1, bitcoinNetwork) - // .derivePath(`m/${rootTaprootDerivationPath}`) - // .neutered() - // .toBase58(); - - // const externalExtendedPublicKey = bip32 - // .fromBase58(TEST_EXTENDED_PRIVATE_KEY_2, bitcoinNetwork) - // .derivePath(`m/${rootTaprootDerivationPath}`) - // .neutered() - // .toBase58(); - - // const attestorURLs = getAttestorURLs(); - // const attestorExtendedPublicKey = await getExtendedAttestorGroupPublicKey(attestorURLs[0]); - - console.log(`[Ledger][${bitcoinNetworkName}] Ledger Extended Public Key: ${ledgerExtendedPublicKey}`); - console.log(`[Ledger][${bitcoinNetworkName}] Unspendable Extended Public Key: ${unspendableExtendedPublicKey}`); - console.log(`[Ledger][${bitcoinNetworkName}] Attestor Extended Public Key: ${attestorExtendedPublicKey}`); - - // ==> Create Key Info - const ledgerKeyInfo = `[${fpr}/${rootTaprootDerivationPath}]${ledgerExtendedPublicKey}`; - console.log(`[Ledger][${bitcoinNetworkName}] Ledger Key Info: ${ledgerKeyInfo}`); - - // ==> Create Multisig Wallet Policy - const ledgerTaprootMultisigAccountPolicy = new WalletPolicy( - `Taproot Multisig Wallet for Vault: ${truncateAddress(vaultUUID)}`, - `tr(@0/**,and_v(v:pk(@1/**),pk(@2/**)))`, - [unspendableExtendedPublicKey, attestorExtendedPublicKey, ledgerKeyInfo] - ); - - // ==> Register Wallet - const [policyId, policyHmac] = await ledgerApp.registerWallet(ledgerTaprootMultisigAccountPolicy); - - console.log(`[Ledger][${bitcoinNetworkName}] Policy HMac: ${policyHmac.toString('hex')}`); - - // => Assert Policy ID - console.assert(policyId.compare(ledgerTaprootMultisigAccountPolicy.getId()) == 0); // - - // ==> Get Wallet Address from Ledger - const ledgerTaprootMultisigAddress = await ledgerApp.getWalletAddress( - ledgerTaprootMultisigAccountPolicy, - policyHmac, - 0, - 0, - false - ); - console.log( - `[Ledger][${bitcoinNetworkName}] Ledger Taproot Multisig Wallet Address: ${ledgerTaprootMultisigAddress}` - ); - - const attestorDerivedPublicKey = bip32 - .fromBase58(attestorExtendedPublicKey, bitcoinNetwork) - .derivePath('0/0').publicKey; - - console.log( - `[Ledger][${bitcoinNetworkName}] Attestor Derived Public Key: ${attestorDerivedPublicKey.toString('hex')}` - ); - const unspendableDerivedPublicKey = bip32 - .fromBase58(unspendableExtendedPublicKey, bitcoinNetwork) - .derivePath('0/0').publicKey; - - console.log( - `[Ledger][${bitcoinNetworkName}] Unspendable Derived Public Key: ${unspendableDerivedPublicKey.toString('hex')}` - ); - - const ledgerDerivedPublicKey = bip32.fromBase58(ledgerExtendedPublicKey, bitcoinNetwork).derivePath('0/0').publicKey; - - // ==> Recreate Multisig Address to retrieve script - const taprootMultiLeafWallet = p2tr_ns(2, [attestorDerivedPublicKey.subarray(1), ledgerDerivedPublicKey.subarray(1)]); - - const taprootMultisigPayment = p2tr(unspendableDerivedPublicKey.subarray(1), taprootMultiLeafWallet, bitcoinNetwork); - - if (ledgerTaprootMultisigAddress !== taprootMultisigPayment.address) { - throw new Error( - `[Ledger][${bitcoinNetworkName}] Recreated Multisig Address does not match the Ledger Multisig Address` - ); - } - - return { - ledgerTaprootMultisigAccountPolicy, - ledgerTaprootMultisigPolicyHMac: policyHmac, - taprootMultisigAddress: ledgerTaprootMultisigAddress, - taprootDerivedPublicKey: ledgerDerivedPublicKey, - taprootMultisigPayment, - }; -} - -export async function signFundingAndClosingTransactionWithLedger(userVault: RawVault, bitcoinBlockchainAPIURL: string) { - try { - // ==> Get Bitcoin Network - const [bitcoinNetworkName, bitcoinNetwork, bitcoinNetworkIndex, ledgerAppName] = getBitcoinNetwork(); - - const rootTaprootDerivationPath = `${TAPROOT_DERIVATION_PATH}/${bitcoinNetworkIndex}/0'`; - - // ==> Open Ledger App - const ledgerApp = await getLedgerApp(ledgerAppName); - - if (!ledgerApp) { - throw new Error(`[Ledger][${bitcoinNetworkName}] Could not open Ledger ${ledgerAppName} App`); - } - - // ==> Get Ledger Master Fingerprint - const fpr = await ledgerApp.getMasterFingerprint(); - - const { addressIndex: nativeSegwitAddressIndex, rootDerivationPath: rootNativeSegwitDerivationPath } = - await getLedgerAddressIndexAndDerivationPath( - ledgerApp, - fpr, - bitcoinNetworkName, - bitcoinNetworkIndex, - 'wpkh', - NATIVE_SEGWIT_DERIVATION_PATH, - bitcoinBlockchainAPIURL - ); - - console.log( - `[Ledger][${bitcoinNetworkName}] Selected Native Segwit Address Index: ${[nativeSegwitAddressIndex][0]}` - ); - - // ==> Get Native Segwit Account - const { ledgerNativeSegwitAccountPolicy, nativeSegwitAddress, nativeSegwitDerivedPublicKey, nativeSegwitPayment } = - await getNativeSegwitAccount(ledgerApp, fpr, bitcoinNetwork, bitcoinNetworkName, rootNativeSegwitDerivationPath); - - // ==> Get Taproot Multisig Account - const { - ledgerTaprootMultisigAccountPolicy, - ledgerTaprootMultisigPolicyHMac, - taprootMultisigAddress, - taprootDerivedPublicKey, - taprootMultisigPayment, - } = await getTaprootMultisigAccount( - ledgerApp, - fpr, - bitcoinNetwork, - bitcoinNetworkName, - rootTaprootDerivationPath, - userVault.uuid - ); - - // ==> Handle Funding Transaction - const fundingTransaction = await handleFundingTransaction( - ledgerApp, - bitcoinNetwork, - bitcoinNetworkName, - userVault.valueLocked.toBigInt(), - fpr, - taprootMultisigPayment, - nativeSegwitDerivedPublicKey, - nativeSegwitPayment, - ledgerNativeSegwitAccountPolicy, - TEST_FEE_RATE, - userVault.btcFeeRecipient, - userVault.btcMintFeeBasisPoints.toBigInt() - ); - - // ==> Handle Closing Transaction - const closingTransaction = await handleClosingTransaction( - ledgerApp, - bitcoinNetwork, - bitcoinNetworkName, - userVault.valueLocked.toBigInt(), - fpr, - fundingTransaction, - taprootMultisigPayment, - taprootDerivedPublicKey, - ledgerTaprootMultisigAccountPolicy, - ledgerTaprootMultisigPolicyHMac, - nativeSegwitPayment, - TEST_FEE_RATE, - userVault.btcFeeRecipient, - userVault.btcRedeemFeeBasisPoints.toBigInt() - ); - console.log(`[Ledger][${bitcoinNetworkName}] Signed Funding and Closing Transaction`); - - // ==> Send PSBT to Attestors - const attestorURLs = getAttestorURLs(); - - await createPSBTEvent( - attestorURLs, - userVault.uuid, - fundingTransaction.hex, - closingTransaction, - nativeSegwitAddress - ); - - const fundingTransactionID = await broadcastTransaction(fundingTransaction.hex); - - console.log(`[Ledger][${bitcoinNetworkName}] Broadcasted Funding Transaction: ${fundingTransactionID}`); - - ledgerApp.transport.close(); - } catch (error) { - throw new LedgerError(`Error running PSBT signing flow with Ledger: ${error}`); - } -} diff --git a/src/models/bitcoin-models.ts b/src/models/bitcoin-models.ts index 42fd974..137d440 100644 --- a/src/models/bitcoin-models.ts +++ b/src/models/bitcoin-models.ts @@ -1,5 +1,7 @@ /** @format */ +import { P2Ret, P2TROut } from '@scure/btc-signer/payment'; + interface TransactionStatus { confirmed: boolean; block_height: number; @@ -27,6 +29,18 @@ export interface FeeRates { minimumFee: number; } +export interface RequiredPayment { + nativeSegwitPayment: P2Ret; + taprootMultisigPayment: P2TROut; +} + +export interface PaymentInformation { + nativeSegwitPayment: P2Ret; + nativeSegwitDerivedPublicKey: Buffer; + taprootMultisigPayment: P2TROut; + taprootDerivedPublicKey: Buffer; +} + export type PaymentTypes = 'p2pkh' | 'p2sh' | 'p2wpkh-p2sh' | 'p2wpkh' | 'p2tr'; export type BitcoinNetworkName = 'Mainnet' | 'Testnet' | 'Regtest'; diff --git a/src/models/bitgo-models.ts b/src/models/bitgo-models.ts deleted file mode 100644 index 8a8ebe8..0000000 --- a/src/models/bitgo-models.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** @format */ - -export interface BitGoAddress { - id: string; - address: string; - chain: number; - index: number; - coin: string; - wallet: string; - label: string; - coinSpecific: Record; -} diff --git a/src/network-handlers/ethereum-handler.ts b/src/network-handlers/ethereum-handler.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/payment-functions.ts b/src/payment-functions.ts deleted file mode 100644 index 4d00e81..0000000 --- a/src/payment-functions.ts +++ /dev/null @@ -1,120 +0,0 @@ -/** @format */ - -import { p2ms, p2tr, p2tr_ns, p2wpkh, p2wsh } from '@scure/btc-signer'; -import { BIP32Factory } from 'bip32'; -import * as ecc from 'tiny-secp256k1'; -import { Network } from 'bitcoinjs-lib'; -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/bitgo-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/src/private-key-dlc-handler.ts b/src/private-key-dlc-handler.ts deleted file mode 100644 index b8b942f..0000000 --- a/src/private-key-dlc-handler.ts +++ /dev/null @@ -1,188 +0,0 @@ -/** @format */ - -import { Transaction, p2wpkh } from '@scure/btc-signer'; -import { P2Ret, P2TROut } from '@scure/btc-signer/payment'; -import { BIP32Factory, BIP32Interface } from 'bip32'; -import { Network } from 'bitcoinjs-lib'; -import { bitcoin, regtest, testnet } from 'bitcoinjs-lib/src/networks.js'; -import * as ellipticCurveCryptography from 'tiny-secp256k1'; -import { - createTaprootMultisigPayment, - getBalance, - getDerivedPublicKey, - getFeeRate, - getUnspendableKeyCommittedToUUID, -} from './bitcoin-functions.js'; -import { BitcoinNetworkName } from './models/bitcoin-models.js'; -import { RawVault } from './models/ethereum-models.js'; -import { createClosingTransaction, createFundingTransaction } from './psbt-functions.js'; - -interface BitcoinDerivationPath { - nativeSegwitDerivationPathRoot: string; - taprootDerivationPathRoot: string; -} - -interface PaymentInformation { - nativeSegwitPayment: P2Ret; - nativeSegwitDerivedKeyPair: BIP32Interface; - taprootMultisigPayment: P2TROut; - taprootDerivedKeyPair: BIP32Interface; -} - -export class PrivateKeyDLCHandler { - private bip32: BIP32Interface; - private bitcoinNetwork: Network; - private bitcoinDerivationPath: BitcoinDerivationPath; - private bitcoinBlockchainAPI: string; - private bitcoinBlockchainFeeRecommendationAPI: string; - - constructor( - bitcoinWalletPrivateKey: string, - bitcoinNetworkName: BitcoinNetworkName, - bitcoinBlockchainAPI?: string, - bitcoinBlockchainFeeRecommendationAPI?: string - ) { - const bip32 = BIP32Factory(ellipticCurveCryptography); - - switch (bitcoinNetworkName) { - case 'Mainnet': - this.bip32 = bip32.fromBase58(bitcoinWalletPrivateKey, bitcoin); - this.bitcoinNetwork = bitcoin; - this.bitcoinBlockchainAPI = ''; - this.bitcoinBlockchainFeeRecommendationAPI = ''; - this.bitcoinDerivationPath = { - nativeSegwitDerivationPathRoot: `m/84'/0'`, - taprootDerivationPathRoot: `m/86'/0'`, - }; - break; - case 'Testnet': - this.bip32 = bip32.fromBase58(bitcoinWalletPrivateKey, testnet); - this.bitcoinNetwork = testnet; - this.bitcoinBlockchainAPI = ''; - this.bitcoinBlockchainFeeRecommendationAPI = ''; - this.bitcoinDerivationPath = { - nativeSegwitDerivationPathRoot: `m/84'/1'`, - taprootDerivationPathRoot: `m/86'/1'`, - }; - break; - case 'Regtest': - if (bitcoinBlockchainAPI === undefined || bitcoinBlockchainFeeRecommendationAPI === undefined) { - throw new Error('Regtest requires a Bitcoin Blockchain API and a Bitcoin Blockchain Fee Recommendation API'); - } - this.bip32 = bip32.fromBase58(bitcoinWalletPrivateKey, regtest); - this.bitcoinNetwork = regtest; - this.bitcoinBlockchainAPI = bitcoinBlockchainAPI; - this.bitcoinBlockchainFeeRecommendationAPI = bitcoinBlockchainFeeRecommendationAPI; - this.bitcoinDerivationPath = { - nativeSegwitDerivationPathRoot: `m/84'/1'`, - taprootDerivationPathRoot: `m/86'/1'`, - }; - break; - default: - throw new Error('Invalid Bitcoin Network'); - } - } - - handlePayment(vaultUUID: string, accountIndex: number, attestorGroupPublicKey: string): PaymentInformation { - const { nativeSegwitDerivationPathRoot, taprootDerivationPathRoot } = this.bitcoinDerivationPath; - - const nativeSegwitDerivationPath = `${nativeSegwitDerivationPathRoot}/${accountIndex}`; - const taprootDerivationPath = `${taprootDerivationPathRoot}/${accountIndex}`; - - const nativeSegwitDerivedKeyPair = this.bip32.derivePath(`${nativeSegwitDerivationPath}/0/0`); - const taprootDerivedKeyPair = this.bip32.derivePath(`${taprootDerivationPath}/0/0`); - - if (nativeSegwitDerivedKeyPair.privateKey === undefined || taprootDerivedKeyPair.privateKey === undefined) { - throw new Error('Could not get Private Key'); - } - - const unspendablePublicKey = getUnspendableKeyCommittedToUUID(vaultUUID, this.bitcoinNetwork); - const unspendableDerivedPublicKey = getDerivedPublicKey(unspendablePublicKey, this.bitcoinNetwork); - - const attestorDerivedPublicKey = getDerivedPublicKey(attestorGroupPublicKey, this.bitcoinNetwork); - - const nativeSegwitPayment = p2wpkh(nativeSegwitDerivedKeyPair.publicKey, this.bitcoinNetwork); - const taprootMultisigPayment = createTaprootMultisigPayment( - unspendableDerivedPublicKey, - attestorDerivedPublicKey, - taprootDerivedKeyPair.publicKey, - this.bitcoinNetwork - ); - - return { - nativeSegwitPayment, - nativeSegwitDerivedKeyPair, - taprootMultisigPayment, - taprootDerivedKeyPair, - }; - } - - async createFundingPSBT( - vault: RawVault, - nativeSegwitPayment: P2Ret, - taprootMultisigPayment: P2TROut, - customFeeRate?: bigint, - feeRateMultiplier?: number - ): Promise { - if (nativeSegwitPayment.address === undefined || taprootMultisigPayment.address === undefined) { - throw new Error('Could not get Addresses from Payments'); - } - - const addressBalance = await getBalance(nativeSegwitPayment.address, this.bitcoinBlockchainAPI); - - if (BigInt(addressBalance) < vault.valueLocked.toBigInt()) { - throw new Error('Insufficient Funds'); - } - - const feeRate = - customFeeRate ?? BigInt(await getFeeRate(this.bitcoinBlockchainFeeRecommendationAPI, feeRateMultiplier)); - - const fundingPSBT = await createFundingTransaction( - vault.valueLocked.toBigInt(), - this.bitcoinNetwork, - taprootMultisigPayment.address, - nativeSegwitPayment, - feeRate, - vault.btcFeeRecipient, - vault.btcMintFeeBasisPoints.toBigInt(), - this.bitcoinBlockchainAPI - ); - - return Transaction.fromPSBT(fundingPSBT); - } - - async createClosingPSBT( - vault: RawVault, - nativeSegwitPayment: P2Ret, - taprootMultisigPayment: P2TROut, - fundingTransactionID: string, - customFeeRate?: bigint, - feeRateMultiplier?: number - ): Promise { - if (nativeSegwitPayment.address === undefined) { - throw new Error('Could not get Addresses from Payments'); - } - - const feeRate = - customFeeRate ?? BigInt(await getFeeRate(this.bitcoinBlockchainFeeRecommendationAPI, feeRateMultiplier)); - - const closingPSBT = createClosingTransaction( - vault.valueLocked.toBigInt(), - this.bitcoinNetwork, - fundingTransactionID, - taprootMultisigPayment, - nativeSegwitPayment.address, - feeRate, - vault.btcFeeRecipient, - vault.btcRedeemFeeBasisPoints.toBigInt() - ); - - return Transaction.fromPSBT(closingPSBT); - } - - signPSBT(psbt: Transaction, privateKey: Buffer, finalize: boolean = false): Transaction { - psbt.sign(privateKey); - if (finalize) psbt.finalize(); - return psbt; - } -}