diff --git a/package.json b/package.json index 6a499c6..2669fb2 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "type": "module", "name": "dlc-btc-lib", - "version": "2.4.21", + "version": "2.5.0", "description": "This library provides a comprehensive set of interfaces and functions for minting dlcBTC tokens on supported blockchains.", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/src/constants/dlc-handler.constants.ts b/src/constants/dlc-handler.constants.ts new file mode 100644 index 0000000..7502a28 --- /dev/null +++ b/src/constants/dlc-handler.constants.ts @@ -0,0 +1,7 @@ +export const DLCHandlers = { + KEYPAIR: 'keypair', + LEATHER: 'leather', + UNISAT_FORDEFI: 'unisat/fordefi', + LEDGER: 'ledger', + DFNS: 'dfns', +} as const; diff --git a/src/dlc-handlers/abstract-dlc-handler.ts b/src/dlc-handlers/abstract-dlc-handler.ts new file mode 100644 index 0000000..495355e --- /dev/null +++ b/src/dlc-handlers/abstract-dlc-handler.ts @@ -0,0 +1,252 @@ +import { Transaction } from '@scure/btc-signer'; +import { P2Ret, P2TROut } from '@scure/btc-signer/payment'; +import { Network } from 'bitcoinjs-lib'; + +import { + createNativeSegwitPayment, + createTaprootMultisigPayment, + createTaprootPayment, + deriveUnhardenedPublicKey, + ecdsaPublicKeyToSchnorr, + finalizeUserInputs, + getBalance, + getFeeRate, + getUnspendableKeyCommittedToUUID, +} from '../functions/bitcoin/bitcoin-functions.js'; +import { + createDepositTransaction, + createFundingTransaction, + createWithdrawTransaction, +} from '../functions/bitcoin/psbt-functions.js'; +import { PaymentInformation } from '../models/bitcoin-models.js'; +import { + DLCHandlerType, + FundingPaymentType, + PaymentType, + TransactionType, +} from '../models/dlc-handler.models.js'; +import { + AddressNotFoundError, + InsufficientFundsError, + InvalidPaymentTypeError, + PaymentNotSetError, +} from '../models/errors/dlc-handler.errors.models.js'; +import { RawVault } from '../models/ethereum-models.js'; + +export abstract class AbstractDLCHandler { + abstract readonly dlcHandlerType: DLCHandlerType; + protected fundingPaymentType: FundingPaymentType; + protected _payment?: PaymentInformation; + protected readonly bitcoinNetwork: Network; + protected readonly bitcoinBlockchainAPI: string; + protected readonly bitcoinBlockchainFeeRecommendationAPI: string; + + constructor( + fundingPaymentType: FundingPaymentType, + bitcoinNetwork: Network, + bitcoinBlockchainAPI: string, + bitcoinBlockchainFeeRecommendationAPI: string + ) { + this.fundingPaymentType = fundingPaymentType; + this.bitcoinNetwork = bitcoinNetwork; + this.bitcoinBlockchainAPI = bitcoinBlockchainAPI; + this.bitcoinBlockchainFeeRecommendationAPI = bitcoinBlockchainFeeRecommendationAPI; + } + + protected set payment(payment: PaymentInformation) { + this._payment = payment; + } + + protected get payment(): PaymentInformation { + if (!this._payment) { + throw new PaymentNotSetError(); + } + return this._payment; + } + + getVaultRelatedAddress(paymentType: PaymentType): string { + switch (paymentType) { + case 'funding': + if (!this.payment.fundingPayment.address) { + throw new AddressNotFoundError('funding'); + } + return this.payment.fundingPayment.address; + case 'multisig': + if (!this.payment.multisigPayment.address) { + throw new AddressNotFoundError('multisig'); + } + return this.payment.multisigPayment.address; + default: + throw new InvalidPaymentTypeError(paymentType); + } + } + + protected async createPaymentInformation( + vaultUUID: string, + attestorGroupPublicKey: string + ): Promise { + let fundingPayment: P2Ret | P2TROut; + + if (this.fundingPaymentType === 'wpkh') { + const fundingPublicKeyBuffer = Buffer.from(this.getUserFundingPublicKey(), 'hex'); + fundingPayment = createNativeSegwitPayment(fundingPublicKeyBuffer, this.bitcoinNetwork); + } else { + const fundingPublicKeyBuffer = Buffer.from(this.getUserFundingPublicKey(), 'hex'); + const fundingSchnorrPublicKeyBuffer = ecdsaPublicKeyToSchnorr(fundingPublicKeyBuffer); + fundingPayment = createTaprootPayment(fundingSchnorrPublicKeyBuffer, this.bitcoinNetwork); + } + + const unspendablePublicKey = getUnspendableKeyCommittedToUUID(vaultUUID, this.bitcoinNetwork); + const unspendableDerivedPublicKey = deriveUnhardenedPublicKey( + unspendablePublicKey, + this.bitcoinNetwork + ); + + const attestorDerivedPublicKey = deriveUnhardenedPublicKey( + attestorGroupPublicKey, + this.bitcoinNetwork + ); + + const taprootPublicKeyBuffer = Buffer.from(this.getUserTaprootPublicKey(), 'hex'); + + const multisigPayment = createTaprootMultisigPayment( + unspendableDerivedPublicKey, + attestorDerivedPublicKey, + taprootPublicKeyBuffer, + this.bitcoinNetwork + ); + + const paymentInformation = { fundingPayment, multisigPayment }; + + this.payment = paymentInformation; + return paymentInformation; + } + + private async validateFundsAvailability( + fundingPayment: P2Ret | P2TROut, + requiredAmount: bigint + ): Promise { + const currentBalance = BigInt(await getBalance(fundingPayment, this.bitcoinBlockchainAPI)); + + if (currentBalance < requiredAmount) { + throw new InsufficientFundsError(currentBalance, requiredAmount); + } + } + + private async getFeeRate(feeRateMultiplier?: number, customFeeRate?: bigint): Promise { + return ( + customFeeRate ?? + BigInt(await getFeeRate(this.bitcoinBlockchainFeeRecommendationAPI, feeRateMultiplier)) + ); + } + + async createFundingPSBT( + vault: RawVault, + depositAmount: bigint, + attestorGroupPublicKey: string, + feeRateMultiplier?: number, + customFeeRate?: bigint + ): Promise { + const { fundingPayment, multisigPayment } = await this.createPaymentInformation( + vault.uuid, + attestorGroupPublicKey + ); + + const feeRate = await this.getFeeRate(feeRateMultiplier, customFeeRate); + + await this.validateFundsAvailability(fundingPayment, vault.valueLocked.toBigInt()); + + return await createFundingTransaction( + this.bitcoinBlockchainAPI, + this.bitcoinNetwork, + depositAmount, + multisigPayment, + fundingPayment, + feeRate, + vault.btcFeeRecipient, + vault.btcMintFeeBasisPoints.toBigInt() + ); + } + + async createWithdrawPSBT( + vault: RawVault, + withdrawAmount: bigint, + attestorGroupPublicKey: string, + fundingTransactionID: string, + feeRateMultiplier?: number, + customFeeRate?: bigint + ): Promise { + const { fundingPayment, multisigPayment } = await this.createPaymentInformation( + vault.uuid, + attestorGroupPublicKey + ); + + const feeRate = await this.getFeeRate(feeRateMultiplier, customFeeRate); + + return await createWithdrawTransaction( + this.bitcoinBlockchainAPI, + this.bitcoinNetwork, + withdrawAmount, + fundingTransactionID, + multisigPayment, + fundingPayment, + feeRate, + vault.btcFeeRecipient, + vault.btcRedeemFeeBasisPoints.toBigInt() + ); + } + + async createDepositPSBT( + vault: RawVault, + depositAmount: bigint, + attestorGroupPublicKey: string, + fundingTransactionID: string, + feeRateMultiplier?: number, + customFeeRate?: bigint + ): Promise { + const { fundingPayment, multisigPayment } = await this.createPaymentInformation( + vault.uuid, + attestorGroupPublicKey + ); + + const feeRate = await this.getFeeRate(feeRateMultiplier, customFeeRate); + + return await createDepositTransaction( + this.bitcoinBlockchainAPI, + this.bitcoinNetwork, + depositAmount, + fundingTransactionID, + multisigPayment, + fundingPayment, + feeRate, + vault.btcFeeRecipient, + vault.btcMintFeeBasisPoints.toBigInt() + ); + } + + private readonly transactionFinalizers: Record< + TransactionType, + (transaction: Transaction, payment: P2Ret | P2TROut) => void + > = { + funding: transaction => transaction.finalize(), + deposit: (transaction, payment) => finalizeUserInputs(transaction, payment), + withdraw: () => {}, + }; + + protected finalizeTransaction( + signedTransaction: Transaction, + transactionType: TransactionType, + fundingPayment: P2Ret | P2TROut + ): void { + this.transactionFinalizers[transactionType](signedTransaction, fundingPayment); + } + + abstract signPSBT( + transaction: Transaction, + transactionType: TransactionType + ): Promise; + + abstract getUserTaprootPublicKey(tweaked?: boolean): string; + + abstract getUserFundingPublicKey(): string; +} diff --git a/src/dlc-handlers/dfns-dlc-handler.ts b/src/dlc-handlers/dfns-dlc-handler.ts index ea07230..9fcb3f5 100644 --- a/src/dlc-handlers/dfns-dlc-handler.ts +++ b/src/dlc-handlers/dfns-dlc-handler.ts @@ -2,66 +2,59 @@ import { DfnsDelegatedApiClient } from '@dfns/sdk'; import { WebAuthnSigner } from '@dfns/sdk-browser'; import { GenerateSignatureBody, ListWalletsResponse } from '@dfns/sdk/generated/wallets/types.js'; import { bytesToHex, hexToBytes } from '@noble/hashes/utils'; -import { Transaction, p2tr } from '@scure/btc-signer'; -import { P2TROut } from '@scure/btc-signer/payment'; +import { Transaction } from '@scure/btc-signer'; import { Network } from 'bitcoinjs-lib'; +import { FundingPaymentType, TransactionType } from '../models/dlc-handler.models.js'; import { - createTaprootMultisigPayment, - deriveUnhardenedPublicKey, - ecdsaPublicKeyToSchnorr, - getBalance, - getFeeRate, - getInputIndicesByScript, - getUnspendableKeyCommittedToUUID, -} from '../functions/bitcoin/bitcoin-functions.js'; -import { - createDepositTransaction, - createFundingTransaction, - createWithdrawTransaction, -} from '../functions/bitcoin/psbt-functions.js'; -import { PaymentInformation } from '../models/bitcoin-models.js'; -import { RawVault } from '../models/ethereum-models.js'; - -export class DFNSDLCHandler { + DFNSWalletIDNotSetError, + PaymentNotSetError, + SignatureGenerationFailed, + TaprootDerivedPublicKeyNotSet, +} from '../models/errors/dlc-handler.errors.models.js'; +import { AbstractDLCHandler } from './abstract-dlc-handler.js'; + +export class DFNSDLCHandler extends AbstractDLCHandler { + readonly dlcHandlerType = 'dfns' as const; private readonly dfnsDelegatedAPIClient: DfnsDelegatedApiClient; - private readonly bitcoinNetwork: Network; - private readonly bitcoinBlockchainAPI: string; - private readonly bitcoinBlockchainFeeRecommendationAPI: string; - private taprootDerivedPublicKey?: string; - private dfnsWalletID?: string; - public payment?: PaymentInformation; + private _taprootDerivedPublicKey?: string; + private _dfnsWalletID?: string; constructor( - dfnsAppID: string, - appBaseURL: string, - dfnsAuthToken: string, + fundingPaymentType: FundingPaymentType = 'tr', bitcoinNetwork: Network, bitcoinBlockchainAPI: string, - bitcoinBlockchainFeeRecommendationAPI: string + bitcoinBlockchainFeeRecommendationAPI: string, + dfnsAppID: string, + appBaseURL: string, + dfnsAuthToken: string ) { + super( + fundingPaymentType, + bitcoinNetwork, + bitcoinBlockchainAPI, + bitcoinBlockchainFeeRecommendationAPI + ); this.dfnsDelegatedAPIClient = new DfnsDelegatedApiClient({ baseUrl: appBaseURL, appId: dfnsAppID, authToken: dfnsAuthToken, }); - this.bitcoinBlockchainAPI = bitcoinBlockchainAPI; - this.bitcoinBlockchainFeeRecommendationAPI = bitcoinBlockchainFeeRecommendationAPI; - this.bitcoinNetwork = bitcoinNetwork; } - private setPayment(fundingPayment: P2TROut, multisigPayment: P2TROut): void { - this.payment = { - fundingPayment, - multisigPayment, - }; + set dfnsWalletID(dfnsWalletID: string) { + this._dfnsWalletID = dfnsWalletID; } - private getPayment(): PaymentInformation { - if (!this.payment) { - throw new Error('Payment Information not set'); + get dfnsWalletID(): string { + if (!this._dfnsWalletID) { + throw new DFNSWalletIDNotSetError(); } - return this.payment; + return this._dfnsWalletID; + } + + set taprootDerivedPublicKey(taprootDerivedPublicKey: string) { + this._taprootDerivedPublicKey = taprootDerivedPublicKey; } async getWallets(): Promise { @@ -77,293 +70,75 @@ export class DFNSDLCHandler { const dfnsWallet = await this.dfnsDelegatedAPIClient.wallets.getWallet({ walletId: dfnsWalletID, }); - this.setDFNSWalletID(dfnsWallet.id); - this.setTaprootDerivedPublicKey(dfnsWallet.signingKey.publicKey); + this.dfnsWalletID = dfnsWallet.id; + this.taprootDerivedPublicKey = dfnsWallet.signingKey.publicKey; } catch (error: any) { throw new Error(`Error fetching wallet: ${error}`); } } - setDFNSWalletID(dfnsWalletID: string): void { - this.dfnsWalletID = dfnsWalletID; - } - - getDFNSWalletID(): string { - if (!this.dfnsWalletID) { - throw new Error('DFNS Wallet ID not set'); - } - return this.dfnsWalletID; - } - - setTaprootDerivedPublicKey(taprootDerivedPublicKey: string): void { - this.taprootDerivedPublicKey = taprootDerivedPublicKey; - } - - getTaprootDerivedPublicKey(): string { - if (!this.taprootDerivedPublicKey) { - throw new Error('Taproot Derived Public Key not set'); + getUserTaprootPublicKey(tweaked: boolean = false): string { + if (!tweaked) { + if (!this._taprootDerivedPublicKey) { + throw new TaprootDerivedPublicKeyNotSet(); + } + return this._taprootDerivedPublicKey; } - return this.taprootDerivedPublicKey; - } - getTaprootTweakedPublicKey(): string { if (!this.payment) { - throw new Error('Payment Information not set'); + throw new PaymentNotSetError(); } - return bytesToHex((this.payment.fundingPayment as P2TROut).tweakedPubkey); - } - - getVaultRelatedAddress(paymentType: 'funding' | 'multisig'): string { - const payment = this.getPayment(); - if (payment === undefined) { - throw new Error('Payment Objects have not been set'); - } - - switch (paymentType) { - case 'funding': - if (!payment.fundingPayment.address) { - throw new Error('Funding Payment Address is undefined'); - } - return payment.fundingPayment.address; - case 'multisig': - if (!payment.multisigPayment.address) { - throw new Error('Taproot Multisig Payment Address is undefined'); - } - return payment.multisigPayment.address; - default: - throw new Error('Invalid Payment Type'); - } + return bytesToHex(this.payment.multisigPayment.tweakedPubkey); } - private async createPayments( - vaultUUID: string, - attestorGroupPublicKey: string - ): Promise { - try { - if (!this.taprootDerivedPublicKey) { - throw new Error('Taproot Derived Public Key not set'); - } - - const fundingPayment = p2tr( - ecdsaPublicKeyToSchnorr(Buffer.from(this.taprootDerivedPublicKey, 'hex')), - undefined, - this.bitcoinNetwork - ); - - const unspendablePublicKey = getUnspendableKeyCommittedToUUID(vaultUUID, this.bitcoinNetwork); - const unspendableDerivedPublicKey = deriveUnhardenedPublicKey( - unspendablePublicKey, - this.bitcoinNetwork - ); - - const attestorDerivedPublicKey = deriveUnhardenedPublicKey( - attestorGroupPublicKey, - this.bitcoinNetwork - ); - - const multisigPayment = createTaprootMultisigPayment( - unspendableDerivedPublicKey, - attestorDerivedPublicKey, - Buffer.from(fundingPayment.tweakedPubkey), - this.bitcoinNetwork - ); - - this.setPayment(fundingPayment, multisigPayment); - - return { - fundingPayment, - multisigPayment, - }; - } catch (error: any) { - throw new Error(`Error creating required wallet information: ${error}`); + getUserFundingPublicKey(): string { + if (!this._taprootDerivedPublicKey) { + throw new TaprootDerivedPublicKeyNotSet(); } + return this._taprootDerivedPublicKey; } - async createFundingPSBT( - vault: RawVault, - bitcoinAmount: bigint, - attestorGroupPublicKey: string, - feeRateMultiplier?: number, - customFeeRate?: bigint - ): Promise { - try { - const { fundingPayment, multisigPayment } = await this.createPayments( - vault.uuid, - attestorGroupPublicKey - ); - - const feeRate = - customFeeRate ?? - BigInt(await getFeeRate(this.bitcoinBlockchainFeeRecommendationAPI, feeRateMultiplier)); + async signPSBT(transaction: Transaction, transactionType: TransactionType): Promise { + const dfnsWalletID = this.dfnsWalletID; - const addressBalance = await getBalance(fundingPayment, this.bitcoinBlockchainAPI); + const generateSignatureBody: GenerateSignatureBody = { + kind: 'Psbt', + psbt: bytesToHex(transaction.toPSBT()), + }; - if (BigInt(addressBalance) < vault.valueLocked.toBigInt()) { - throw new Error('Insufficient Funds'); - } + const generateSignatureRequest = { + walletId: dfnsWalletID, + body: generateSignatureBody, + }; - const fundingTransaction = await createFundingTransaction( - this.bitcoinBlockchainAPI, - this.bitcoinNetwork, - bitcoinAmount, - multisigPayment, - fundingPayment, - feeRate, - vault.btcFeeRecipient, - vault.btcMintFeeBasisPoints.toBigInt() - ); + const generateSignatureInitResponse = + await this.dfnsDelegatedAPIClient.wallets.generateSignatureInit(generateSignatureRequest); - return fundingTransaction; - } catch (error: any) { - throw new Error(`Error creating Funding PSBT: ${error}`); - } - } + const webAuthenticator = new WebAuthnSigner(); + const assertion = await webAuthenticator.sign(generateSignatureInitResponse); - async createWithdrawPSBT( - vault: RawVault, - withdrawAmount: bigint, - attestorGroupPublicKey: string, - fundingTransactionID: string, - feeRateMultiplier?: number, - customFeeRate?: bigint - ): Promise { - try { - const { fundingPayment, multisigPayment } = await this.createPayments( - vault.uuid, - attestorGroupPublicKey + const generateSignatureCompleteResponse = + await this.dfnsDelegatedAPIClient.wallets.generateSignatureComplete( + generateSignatureRequest, + { + challengeIdentifier: generateSignatureInitResponse.challengeIdentifier, + firstFactor: assertion, + } ); - const feeRate = - customFeeRate ?? - BigInt(await getFeeRate(this.bitcoinBlockchainFeeRecommendationAPI, feeRateMultiplier)); + const signedPSBT = generateSignatureCompleteResponse.signedData; - const withdrawTransaction = await createWithdrawTransaction( - this.bitcoinBlockchainAPI, - this.bitcoinNetwork, - withdrawAmount, - fundingTransactionID, - multisigPayment, - fundingPayment, - feeRate, - vault.btcFeeRecipient, - vault.btcRedeemFeeBasisPoints.toBigInt() - ); - - return withdrawTransaction; - } catch (error: any) { - throw new Error(`Error creating Withdraw PSBT: ${error}`); + if (!signedPSBT) { + throw new SignatureGenerationFailed(); } - } - - async createDepositPSBT( - depositAmount: bigint, - vault: RawVault, - attestorGroupPublicKey: string, - fundingTransactionID: string, - feeRateMultiplier?: number, - customFeeRate?: bigint - ): Promise { - const { fundingPayment, multisigPayment } = await this.createPayments( - vault.uuid, - attestorGroupPublicKey - ); - - const feeRate = - customFeeRate ?? - BigInt(await getFeeRate(this.bitcoinBlockchainFeeRecommendationAPI, feeRateMultiplier)); - - const depositTransaction = await createDepositTransaction( - this.bitcoinBlockchainAPI, - this.bitcoinNetwork, - depositAmount, - fundingTransactionID, - multisigPayment, - fundingPayment, - feeRate, - vault.btcFeeRecipient, - vault.btcMintFeeBasisPoints.toBigInt() - ); - - return depositTransaction; - } - - async signPSBT( - transaction: Transaction, - transactionType: 'funding' | 'deposit' | 'withdraw' - ): Promise { - try { - const dfnsWalletID = this.getDFNSWalletID(); - - const generateSignatureBody: GenerateSignatureBody = { - kind: 'Psbt', - psbt: bytesToHex(transaction.toPSBT()), - }; - - const generateSignatureRequest = { - walletId: dfnsWalletID, - body: generateSignatureBody, - }; - - const generateSignatureInitResponse = - await this.dfnsDelegatedAPIClient.wallets.generateSignatureInit(generateSignatureRequest); - - const webAuthenticator = new WebAuthnSigner(); - const assertion = await webAuthenticator.sign(generateSignatureInitResponse); - const generateSignatureCompleteResponse = - await this.dfnsDelegatedAPIClient.wallets.generateSignatureComplete( - generateSignatureRequest, - { - challengeIdentifier: generateSignatureInitResponse.challengeIdentifier, - firstFactor: assertion, - } - ); + const signedTransaction = Transaction.fromPSBT(hexToBytes(signedPSBT.slice(2))); - const signedPSBT = generateSignatureCompleteResponse.signedData; + const fundingPayment = this.payment.fundingPayment; - if (!signedPSBT) { - throw new Error('No signed data returned'); - } - - const signedTransaction = Transaction.fromPSBT(hexToBytes(signedPSBT.slice(2))); - - const fundingPayment = this.getPayment().fundingPayment; - - this.finalizeTransaction(signedTransaction, transactionType, fundingPayment.script); - - return signedTransaction; - } catch (error: any) { - throw new Error(`Error signing PSBT: ${error}`); - } - } + this.finalizeTransaction(signedTransaction, transactionType, fundingPayment); - private finalizeTransaction( - signedTransaction: Transaction, - transactionType: 'funding' | 'deposit' | 'withdraw', - fundingPaymentScript: Uint8Array - ): Transaction { - switch (transactionType) { - case 'funding': - // finalize all inputs in the funding transaction since we have - // collected all required signatures at this point. - signedTransaction.finalize(); - break; - case 'deposit': - // only finalize inputs that spend from the funding address, - // multisig inputs will be finalized after attestor signatures are added. - getInputIndicesByScript(fundingPaymentScript, signedTransaction).forEach(index => - signedTransaction.finalizeIdx(index) - ); - break; - case 'withdraw': - // skip finalization since withdraw transaction requires additional - // attestor signatures before it can be finalized. - break; - - default: - throw new Error(`Invalid Transaction Type: ${transactionType}`); - } return signedTransaction; } } diff --git a/src/dlc-handlers/keypair-dlc-handler.ts b/src/dlc-handlers/keypair-dlc-handler.ts new file mode 100644 index 0000000..fe7372f --- /dev/null +++ b/src/dlc-handlers/keypair-dlc-handler.ts @@ -0,0 +1,109 @@ +import { bytesToHex } from '@noble/hashes/utils'; +import { Transaction } from '@scure/btc-signer'; +import { Signer } from '@scure/btc-signer/transaction'; +import { BIP32Interface } from 'bip32'; +import { Network } from 'bitcoinjs-lib'; + +import { + deriveUnhardenedKeyPairFromRootPrivateKey, + getInputIndicesByScript, +} from '../functions/bitcoin/bitcoin-functions.js'; +import { FundingPaymentType, PaymentType, TransactionType } from '../models/dlc-handler.models.js'; +import { + InvalidTransactionTypeError, + PaymentNotSetError, +} from '../models/errors/dlc-handler.errors.models.js'; +import { AbstractDLCHandler } from './abstract-dlc-handler.js'; + +export class KeyPairDLCHandler extends AbstractDLCHandler { + readonly dlcHandlerType = 'keypair' as const; + private fundingDerivedKeyPair: BIP32Interface; + private taprootDerivedKeyPair: BIP32Interface; + + constructor( + bitcoinWalletPrivateKey: string, + walletAccountIndex: number, + fundingPaymentType: FundingPaymentType, + bitcoinNetwork: Network, + bitcoinBlockchainAPI: string, + bitcoinBlockchainFeeRecommendationAPI: string + ) { + super( + fundingPaymentType, + bitcoinNetwork, + bitcoinBlockchainAPI, + bitcoinBlockchainFeeRecommendationAPI + ); + const fundingDerivedKeyPair = deriveUnhardenedKeyPairFromRootPrivateKey( + bitcoinWalletPrivateKey, + bitcoinNetwork, + fundingPaymentType === 'wpkh' ? 'p2wpkh' : 'p2tr', + walletAccountIndex + ); + const taprootDerivedKeyPair = deriveUnhardenedKeyPairFromRootPrivateKey( + bitcoinWalletPrivateKey, + bitcoinNetwork, + 'p2tr', + walletAccountIndex + ); + + this.fundingDerivedKeyPair = fundingDerivedKeyPair; + this.taprootDerivedKeyPair = taprootDerivedKeyPair; + } + + getUserTaprootPublicKey(tweaked: boolean = false): string { + if (!tweaked) { + return bytesToHex(this.taprootDerivedKeyPair.publicKey); + } + + if (!this.payment) { + throw new PaymentNotSetError(); + } + + return bytesToHex(this.payment.multisigPayment.tweakedPubkey); + } + + getUserFundingPublicKey(): string { + return bytesToHex(this.fundingDerivedKeyPair.publicKey); + } + + private getPrivateKey(paymentType: PaymentType): Signer { + const keyPairMap: Record = { + funding: this.fundingDerivedKeyPair, + multisig: this.taprootDerivedKeyPair, + }; + + const keyPair = keyPairMap[paymentType]; + + if (!keyPair?.privateKey) { + throw new Error(`Private key not found for payment type: ${paymentType}`); + } + + return keyPair.privateKey; + } + + async signPSBT(transaction: Transaction, transactionType: TransactionType): Promise { + switch (transactionType) { + case 'funding': + transaction.sign(this.getPrivateKey('funding')); + break; + case 'deposit': + getInputIndicesByScript(this.payment.fundingPayment.script, transaction).forEach(index => { + transaction.signIdx(this.getPrivateKey('funding'), index); + }); + getInputIndicesByScript(this.payment.multisigPayment.script, transaction).forEach(index => { + transaction.signIdx(this.getPrivateKey('multisig'), index); + }); + break; + case 'withdraw': + transaction.sign(this.getPrivateKey('multisig')); + break; + default: + throw new InvalidTransactionTypeError(transactionType); + } + + this.finalizeTransaction(transaction, transactionType, this.payment.fundingPayment); + + return transaction; + } +} diff --git a/src/dlc-handlers/leather-dlc-handler.ts b/src/dlc-handlers/leather-dlc-handler.ts new file mode 100644 index 0000000..10f0e56 --- /dev/null +++ b/src/dlc-handlers/leather-dlc-handler.ts @@ -0,0 +1,84 @@ +import { bytesToHex, hexToBytes } from '@noble/hashes/utils'; +import { Transaction } from '@scure/btc-signer'; +import { Network } from 'bitcoinjs-lib'; + +import { FundingPaymentType, TransactionType } from '../models/dlc-handler.models.js'; +import { PaymentNotSetError } from '../models/errors/dlc-handler.errors.models.js'; +import { AbstractDLCHandler } from './abstract-dlc-handler.js'; + +const networkModes = ['mainnet', 'testnet', 'regtest'] as const; + +type NetworkModes = (typeof networkModes)[number]; + +declare enum SignatureHash { + ALL = 1, + NONE = 2, + SINGLE = 3, + ALL_ANYONECANPAY = 129, + NONE_ANYONECANPAY = 130, + SINGLE_ANYONECANPAY = 131, +} + +interface SignPsbtRequestParams { + hex: string; + allowedSighash?: SignatureHash[]; + signAtIndex?: number | number[]; + network?: NetworkModes; + account?: number; + broadcast?: boolean; +} + +export class LeatherDLCHandler extends AbstractDLCHandler { + readonly dlcHandlerType = 'leather' as const; + private taprootDerivedPublicKey: string; + private fundingDerivedPublicKey: string; + + constructor( + fundingPaymentType: FundingPaymentType, + bitcoinNetwork: Network, + bitcoinBlockchainAPI: string, + bitcoinBlockchainFeeRecommendationAPI: string, + fundingDerivedPublicKey: string, + taprootDerivedPublicKey: string + ) { + super( + fundingPaymentType, + bitcoinNetwork, + bitcoinBlockchainAPI, + bitcoinBlockchainFeeRecommendationAPI + ); + this.fundingDerivedPublicKey = fundingDerivedPublicKey; + this.taprootDerivedPublicKey = taprootDerivedPublicKey; + } + + getUserTaprootPublicKey(tweaked: boolean = false): string { + if (!tweaked) { + return this.taprootDerivedPublicKey; + } + + if (!this.payment) { + throw new PaymentNotSetError(); + } + + return bytesToHex(this.payment.multisigPayment.tweakedPubkey); + } + + getUserFundingPublicKey(): string { + return this.fundingDerivedPublicKey; + } + + async signPSBT(transaction: Transaction, transactionType: TransactionType): Promise { + const requestParams: SignPsbtRequestParams = { + hex: bytesToHex(transaction.toPSBT()), + }; + const response = await (window as any).btc.request('signPsbt', requestParams); + + const signedTransactionHex = response.result.hex; + + const signedTransaction = Transaction.fromPSBT(hexToBytes(signedTransactionHex)); + + this.finalizeTransaction(signedTransaction, transactionType, this.payment.fundingPayment); + + return signedTransaction; + } +} diff --git a/src/dlc-handlers/ledger-dlc-handler.ts b/src/dlc-handlers/ledger-dlc-handler.ts index 53aadd9..c99bb76 100644 --- a/src/dlc-handlers/ledger-dlc-handler.ts +++ b/src/dlc-handlers/ledger-dlc-handler.ts @@ -1,8 +1,7 @@ -import { bytesToHex } from '@noble/hashes/utils'; import { Transaction } from '@scure/btc-signer'; -import { P2Ret, P2TROut, p2tr, p2wpkh } from '@scure/btc-signer/payment'; +import { p2tr, p2wpkh } from '@scure/btc-signer/payment'; import { Network, Psbt } from 'bitcoinjs-lib'; -import { bitcoin, regtest, testnet } from 'bitcoinjs-lib/src/networks.js'; +import { bitcoin } from 'bitcoinjs-lib/src/networks.js'; import { AppClient, DefaultWalletPolicy, WalletPolicy } from 'ledger-bitcoin'; import { @@ -13,7 +12,6 @@ import { getBalance, getFeeRate, getInputByPaymentTypeArray, - getInputIndicesByScript, getUnspendableKeyCommittedToUUID, } from '../functions/bitcoin/bitcoin-functions.js'; import { @@ -28,8 +26,16 @@ import { updateTaprootInputs, } from '../functions/bitcoin/psbt-functions.js'; import { ExtendedPaymentInformation } from '../models/bitcoin-models.js'; +import { FundingPaymentType, TransactionType } from '../models/dlc-handler.models.js'; +import { + FundingDerivedPublicKeyNotSet, + IncompatibleTransactionArgument, + PolicyInformationNotSet, + TaprootDerivedPublicKeyNotSet, +} from '../models/errors/dlc-handler.errors.models.js'; import { RawVault } from '../models/ethereum-models.js'; import { truncateAddress } from '../utilities/index.js'; +import { AbstractDLCHandler } from './abstract-dlc-handler.js'; interface LedgerPolicyInformation { fundingWalletPolicy: DefaultWalletPolicy; @@ -37,134 +43,73 @@ interface LedgerPolicyInformation { multisigWalletPolicyHMac: Buffer; } -export class LedgerDLCHandler { +export class LedgerDLCHandler extends AbstractDLCHandler { + readonly dlcHandlerType = 'ledger' as const; private ledgerApp: AppClient; private masterFingerprint: string; private walletAccountIndex: number; private walletAddressIndex: number; - private fundingPaymentType: 'wpkh' | 'tr'; - private policyInformation: LedgerPolicyInformation | undefined; - public payment: ExtendedPaymentInformation | undefined; - private bitcoinNetwork: Network; private bitcoinNetworkIndex: number; - private bitcoinBlockchainAPI: string; - private bitcoinBlockchainFeeRecommendationAPI: string; + private _policyInformation: LedgerPolicyInformation | undefined; + private _fundingDerivedPublicKey?: string; + private _taprootDerivedPublicKey?: string; constructor( ledgerApp: AppClient, masterFingerprint: string, walletAccountIndex: number, walletAddressIndex: number, - fundingPaymentType: 'wpkh' | 'tr', + fundingPaymentType: FundingPaymentType, bitcoinNetwork: Network, - bitcoinBlockchainAPI?: string, - bitcoinBlockchainFeeRecommendationAPI?: string + bitcoinBlockchainAPI: string, + bitcoinBlockchainFeeRecommendationAPI: string ) { - switch (bitcoinNetwork) { - case bitcoin: - this.bitcoinBlockchainAPI = 'https://mempool.space/api'; - this.bitcoinBlockchainFeeRecommendationAPI = - 'https://mempool.space/api/v1/fees/recommended'; - this.bitcoinNetworkIndex = 0; - break; - case testnet: - this.bitcoinBlockchainAPI = 'https://mempool.space/testnet/api'; - this.bitcoinBlockchainFeeRecommendationAPI = - 'https://mempool.space/testnet/api/v1/fees/recommended'; - this.bitcoinNetworkIndex = 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.bitcoinBlockchainAPI = bitcoinBlockchainAPI; - this.bitcoinBlockchainFeeRecommendationAPI = bitcoinBlockchainFeeRecommendationAPI; - this.bitcoinNetworkIndex = 1; - break; - default: - throw new Error('Invalid Bitcoin Network'); - } + super( + fundingPaymentType, + bitcoinNetwork, + bitcoinBlockchainAPI, + bitcoinBlockchainFeeRecommendationAPI + ); + this.bitcoinNetworkIndex = bitcoinNetwork === bitcoin ? 0 : 1; this.ledgerApp = ledgerApp; this.masterFingerprint = masterFingerprint; this.walletAccountIndex = walletAccountIndex; this.walletAddressIndex = walletAddressIndex; this.fundingPaymentType = fundingPaymentType; - this.bitcoinNetwork = bitcoinNetwork; } - private setPolicyInformation( - fundingWalletPolicy: DefaultWalletPolicy, - multisigWalletPolicy: WalletPolicy, - multisigWalletPolicyHMac: Buffer - ): void { - this.policyInformation = { - fundingWalletPolicy, - multisigWalletPolicy, - multisigWalletPolicyHMac, - }; - } - private setPayment( - fundingPayment: P2Ret | P2TROut, - fundingDerivedPublicKey: Buffer, - multisigPayment: P2TROut, - taprootDerivedPublicKey: Buffer - ): void { - this.payment = { - fundingPayment, - fundingDerivedPublicKey, - multisigPayment, - taprootDerivedPublicKey, - }; + set policyInformation(policyInformation: LedgerPolicyInformation) { + this._policyInformation = policyInformation; } - private getPolicyInformation(): LedgerPolicyInformation { - if (!this.policyInformation) { - throw new Error('Policy Information not set'); + get policyInformation(): LedgerPolicyInformation { + if (!this._policyInformation) { + throw new PolicyInformationNotSet(); } - return this.policyInformation; + return this._policyInformation; } - private getPayment(): ExtendedPaymentInformation { - if (!this.payment) { - throw new Error('Payment Information not set'); - } - return this.payment; + set taprootDerivedPublicKey(taprootDerivedPublicKey: string) { + this._taprootDerivedPublicKey = taprootDerivedPublicKey; } - getTaprootDerivedPublicKey(): string { - return bytesToHex(this.getPayment().taprootDerivedPublicKey); + getUserTaprootPublicKey(): string { + if (!this._taprootDerivedPublicKey) { + throw new TaprootDerivedPublicKeyNotSet(); + } + return this._taprootDerivedPublicKey; } - getVaultRelatedAddress(paymentType: 'funding' | 'multisig'): string { - const payment = this.getPayment(); + set fundingDerivedPublicKey(fundingDerivedPublicKey: string) { + this._fundingDerivedPublicKey = fundingDerivedPublicKey; + } - if (payment === undefined) { - throw new Error('Payment objects have not been set'); + getUserFundingPublicKey(): string { + if (!this._fundingDerivedPublicKey) { + throw new FundingDerivedPublicKeyNotSet(); } - let address: string; - - switch (paymentType) { - case 'funding': - if (!payment.fundingPayment.address) { - throw new Error('Funding Payment Address is undefined'); - } - address = payment.fundingPayment.address; - return address; - case 'multisig': - if (!payment.multisigPayment.address) { - throw new Error('Taproot Multisig Payment Address is undefined'); - } - address = payment.multisigPayment.address; - return address; - default: - throw new Error('Invalid Payment Type'); - } + return this._fundingDerivedPublicKey; } private async createPayment( @@ -237,19 +182,18 @@ export class LedgerDLCHandler { ? [ledgerTaprootKeyInfo, attestorGroupPublicKey] : [attestorGroupPublicKey, ledgerTaprootKeyInfo]; - const taprootMultisigAccountPolicy = new WalletPolicy( + const multisigWalletPolicy = 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 [, multisigWalletPolicyHMac] = + await this.ledgerApp.registerWallet(multisigWalletPolicy); const taprootMultisigAddress = await this.ledgerApp.getWalletAddress( - taprootMultisigAccountPolicy, - taprootMultisigPolicyHMac, + multisigWalletPolicy, + multisigWalletPolicyHMac, 0, 0, false @@ -266,17 +210,17 @@ export class LedgerDLCHandler { throw new Error(`Recreated Multisig Address does not match the Ledger Multisig Address`); } - this.setPolicyInformation( + this.policyInformation = { fundingWalletPolicy, - taprootMultisigAccountPolicy, - taprootMultisigPolicyHMac - ); - this.setPayment( + multisigWalletPolicy, + multisigWalletPolicyHMac, + }; + this.payment = { fundingPayment, - fundingDerivedPublicKey, multisigPayment, - taprootDerivedPublicKey - ); + }; + this.taprootDerivedPublicKey = taprootDerivedPublicKey.toString('hex'); + this.fundingDerivedPublicKey = fundingDerivedPublicKey.toString('hex'); return { fundingPayment, @@ -291,11 +235,11 @@ export class LedgerDLCHandler { async createFundingPSBT( vault: RawVault, - bitcoinAmount: bigint, + depositAmount: bigint, attestorGroupPublicKey: string, feeRateMultiplier?: number, customFeeRate?: bigint - ): Promise { + ): Promise { try { const { fundingPayment, fundingDerivedPublicKey, multisigPayment } = await this.createPayment( vault.uuid, @@ -315,7 +259,7 @@ export class LedgerDLCHandler { const fundingTransaction = await createFundingTransaction( this.bitcoinBlockchainAPI, this.bitcoinNetwork, - bitcoinAmount, + depositAmount, multisigPayment, fundingPayment, feeRate, @@ -363,7 +307,7 @@ export class LedgerDLCHandler { ); } - return formattedFundingPSBT; + return Transaction.fromPSBT(formattedFundingPSBT.toBuffer()); } catch (error: any) { throw new Error(`Error creating Funding PSBT: ${error}`); } @@ -376,7 +320,7 @@ export class LedgerDLCHandler { fundingTransactionID: string, feeRateMultiplier?: number, customFeeRate?: bigint - ): Promise { + ): Promise { try { const { fundingPayment, taprootDerivedPublicKey, multisigPayment } = await this.createPayment( vault.uuid, @@ -427,20 +371,20 @@ export class LedgerDLCHandler { formattedWithdrawPSBT ); - return formattedWithdrawPSBT; + return Transaction.fromPSBT(formattedWithdrawPSBT.toBuffer()); } catch (error: any) { throw new Error(`Error creating Withdraw PSBT: ${error}`); } } async createDepositPSBT( - depositAmount: bigint, vault: RawVault, + depositAmount: bigint, attestorGroupPublicKey: string, fundingTransactionID: string, feeRateMultiplier?: number, customFeeRate?: bigint - ) { + ): Promise { const { fundingPayment, taprootDerivedPublicKey, fundingDerivedPublicKey, multisigPayment } = await this.createPayment(vault.uuid, attestorGroupPublicKey); @@ -517,17 +461,21 @@ export class LedgerDLCHandler { ); } - return formattedDepositPSBT; + return Transaction.fromPSBT(formattedDepositPSBT.toBuffer()); } - async signPSBT( - psbt: Psbt, - transactionType: 'funding' | 'deposit' | 'withdraw' + async signPSBT( + transaction: T, + transactionType: TransactionType ): Promise { - let transaction: Transaction; + if (transaction instanceof Psbt) { + throw new IncompatibleTransactionArgument(); + } + + const psbt = Psbt.fromBuffer(Buffer.from(transaction.toPSBT())); const { fundingWalletPolicy, multisigWalletPolicy, multisigWalletPolicyHMac } = - this.getPolicyInformation(); + this.policyInformation; switch (transactionType) { case 'funding': @@ -536,8 +484,6 @@ export class LedgerDLCHandler { this.fundingPaymentType, await this.ledgerApp.signPsbt(psbt.toBase64(), fundingWalletPolicy, null) ); - transaction = Transaction.fromPSBT(psbt.toBuffer()); - transaction.finalize(); break; case 'deposit': addTaprooMultisigInputSignaturesToPSBT( @@ -554,14 +500,6 @@ export class LedgerDLCHandler { this.fundingPaymentType, await this.ledgerApp.signPsbt(psbt.toBase64(), fundingWalletPolicy, null) ); - - transaction = Transaction.fromPSBT(psbt.toBuffer()); - - getInputIndicesByScript(this.getPayment().fundingPayment.script, transaction).forEach( - index => { - transaction.finalizeIdx(index); - } - ); break; case 'withdraw': addTaprooMultisigInputSignaturesToPSBT( @@ -572,11 +510,15 @@ export class LedgerDLCHandler { multisigWalletPolicyHMac ) ); - transaction = Transaction.fromPSBT(psbt.toBuffer()); break; default: throw new Error('Invalid Transaction Type'); } - return transaction; + + const signedTransaction = Transaction.fromPSBT(psbt.toBuffer()); + + this.finalizeTransaction(signedTransaction, transactionType, this.payment.fundingPayment); + + return signedTransaction; } } diff --git a/src/dlc-handlers/private-key-dlc-handler.ts b/src/dlc-handlers/private-key-dlc-handler.ts deleted file mode 100644 index 26d16a1..0000000 --- a/src/dlc-handlers/private-key-dlc-handler.ts +++ /dev/null @@ -1,335 +0,0 @@ -import { bytesToHex } from '@noble/hashes/utils'; -import { Transaction, p2wpkh } from '@scure/btc-signer'; -import { P2Ret, P2TROut, p2tr } 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, - ecdsaPublicKeyToSchnorr, - finalizeUserInputs, - getBalance, - getFeeRate, - getUnspendableKeyCommittedToUUID, -} from '../functions/bitcoin/bitcoin-functions.js'; -import { - createDepositTransaction, - createFundingTransaction, - createWithdrawTransaction, -} from '../functions/bitcoin/psbt-functions.js'; -import { PaymentInformation } from '../models/bitcoin-models.js'; -import { RawVault } from '../models/ethereum-models.js'; - -interface RequiredKeyPair { - fundingDerivedKeyPair: BIP32Interface; - taprootDerivedKeyPair: BIP32Interface; -} - -export class PrivateKeyDLCHandler { - private derivedKeyPair: RequiredKeyPair; - public payment: PaymentInformation | undefined; - private fundingPaymentType: 'wpkh' | 'tr'; - private bitcoinNetwork: Network; - private bitcoinBlockchainAPI: string; - private bitcoinBlockchainFeeRecommendationAPI: string; - - constructor( - bitcoinWalletPrivateKey: string, - walletAccountIndex: number, - fundingPaymentType: 'wpkh' | 'tr', - 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.fundingPaymentType = fundingPaymentType; - this.bitcoinNetwork = bitcoinNetwork; - const fundingDerivedKeyPair = deriveUnhardenedKeyPairFromRootPrivateKey( - bitcoinWalletPrivateKey, - bitcoinNetwork, - fundingPaymentType === 'wpkh' ? 'p2wpkh' : 'p2tr', - walletAccountIndex - ); - const taprootDerivedKeyPair = deriveUnhardenedKeyPairFromRootPrivateKey( - bitcoinWalletPrivateKey, - bitcoinNetwork, - 'p2tr', - walletAccountIndex - ); - - this.derivedKeyPair = { - taprootDerivedKeyPair, - fundingDerivedKeyPair, - }; - } - - private setPayment(fundingPayment: P2Ret | P2TROut, multisigPayment: P2TROut): void { - this.payment = { - fundingPayment, - multisigPayment, - }; - } - - private getPayment(): PaymentInformation { - if (!this.payment) { - throw new Error('Payment Information not set'); - } - return this.payment; - } - - getTaprootDerivedPublicKey(): string { - return bytesToHex(this.derivedKeyPair.taprootDerivedKeyPair.publicKey); - } - - getVaultRelatedAddress(paymentType: 'funding' | 'multisig'): string { - const payment = this.payment; - - if (payment === undefined) { - throw new Error('Payment objects have not been set'); - } - - let address: string; - - switch (paymentType) { - case 'funding': - if (!payment.fundingPayment.address) { - throw new Error('Funding Address is undefined'); - } - address = payment.fundingPayment.address; - return address; - case 'multisig': - if (!payment.multisigPayment.address) { - throw new Error('Taproot Multisig Payment Address is undefined'); - } - address = payment.multisigPayment.address; - return address; - default: - throw new Error('Invalid Payment Type'); - } - } - - private getPrivateKey(paymentType: 'funding' | 'taproot'): Signer { - const privateKey = - paymentType === 'funding' - ? this.derivedKeyPair.fundingDerivedKeyPair.privateKey - : this.derivedKeyPair.taprootDerivedKeyPair.privateKey; - - if (!privateKey) { - throw new Error('Private Key is Undefined'); - } - - return privateKey; - } - - private createPayments(vaultUUID: string, attestorGroupPublicKey: string): PaymentInformation { - try { - const unspendablePublicKey = getUnspendableKeyCommittedToUUID(vaultUUID, this.bitcoinNetwork); - const unspendableDerivedPublicKey = deriveUnhardenedPublicKey( - unspendablePublicKey, - this.bitcoinNetwork - ); - - const attestorDerivedPublicKey = deriveUnhardenedPublicKey( - attestorGroupPublicKey, - this.bitcoinNetwork - ); - - let fundingPayment: P2Ret | P2TROut; - - switch (this.fundingPaymentType) { - case 'wpkh': - fundingPayment = p2wpkh( - this.derivedKeyPair.fundingDerivedKeyPair.publicKey, - this.bitcoinNetwork - ); - break; - case 'tr': - fundingPayment = p2tr( - ecdsaPublicKeyToSchnorr(this.derivedKeyPair.taprootDerivedKeyPair.publicKey), - undefined, - this.bitcoinNetwork - ); - break; - default: - throw new Error('Invalid Funding Payment Type'); - } - - const multisigPayment = createTaprootMultisigPayment( - unspendableDerivedPublicKey, - attestorDerivedPublicKey, - this.derivedKeyPair.taprootDerivedKeyPair.publicKey, - this.bitcoinNetwork - ); - - this.setPayment(fundingPayment, multisigPayment); - - return { - fundingPayment, - multisigPayment, - }; - } catch (error: any) { - throw new Error(`Error creating required Payment objects: ${error}`); - } - } - - async createFundingPSBT( - vault: RawVault, - bitcoinAmount: bigint, - attestorGroupPublicKey: string, - feeRateMultiplier?: number, - customFeeRate?: bigint - ): Promise { - const { fundingPayment, multisigPayment } = this.createPayments( - vault.uuid, - attestorGroupPublicKey - ); - - const addressBalance = await getBalance(fundingPayment, this.bitcoinBlockchainAPI); - - if (BigInt(addressBalance) < vault.valueLocked.toBigInt()) { - throw new Error('Insufficient Funds'); - } - - const feeRate = - customFeeRate ?? - BigInt(await getFeeRate(this.bitcoinBlockchainFeeRecommendationAPI, feeRateMultiplier)); - - const fundingTransaction = await createFundingTransaction( - this.bitcoinBlockchainAPI, - this.bitcoinNetwork, - bitcoinAmount, - multisigPayment, - fundingPayment, - feeRate, - vault.btcFeeRecipient, - vault.btcMintFeeBasisPoints.toBigInt() - ); - - return fundingTransaction; - } - - async createWithdrawPSBT( - vault: RawVault, - withdrawAmount: bigint, - attestorGroupPublicKey: string, - fundingTransactionID: string, - feeRateMultiplier?: number, - customFeeRate?: bigint - ): Promise { - try { - const { fundingPayment, multisigPayment } = this.createPayments( - vault.uuid, - attestorGroupPublicKey - ); - - const feeRate = - customFeeRate ?? - BigInt(await getFeeRate(this.bitcoinBlockchainFeeRecommendationAPI, feeRateMultiplier)); - - const withdrawTransaction = await createWithdrawTransaction( - this.bitcoinBlockchainAPI, - this.bitcoinNetwork, - withdrawAmount, - fundingTransactionID, - multisigPayment, - fundingPayment, - feeRate, - vault.btcFeeRecipient, - vault.btcRedeemFeeBasisPoints.toBigInt() - ); - return withdrawTransaction; - } catch (error: any) { - throw new Error(`Error creating Withdraw PSBT: ${error}`); - } - } - - signPSBT(psbt: Transaction, transactionType: 'funding' | 'deposit' | 'withdraw'): Transaction { - switch (transactionType) { - case 'funding': - psbt.sign(this.getPrivateKey('funding')); - psbt.finalize(); - break; - case 'deposit': - try { - psbt.sign(this.getPrivateKey('funding')); - } catch (error: any) { - // this can happen if there are no tr inputs to sign - } - try { - psbt.sign(this.getPrivateKey('taproot')); - } catch (error: any) { - // this can happen if there are no p2wpkh inputs to sign - } - finalizeUserInputs(psbt, this.getPayment().fundingPayment); - break; - case 'withdraw': - psbt.sign(this.getPrivateKey('taproot')); - break; - default: - throw new Error('Invalid Transaction Type'); - } - - return psbt; - } - - async createDepositPSBT( - depositAmount: bigint, - vault: RawVault, - attestorGroupPublicKey: string, - fundingTransactionID: string, - feeRateMultiplier?: number, - customFeeRate?: bigint - ) { - const { fundingPayment, multisigPayment } = this.createPayments( - vault.uuid, - attestorGroupPublicKey - ); - - const feeRate = - customFeeRate ?? - BigInt(await getFeeRate(this.bitcoinBlockchainFeeRecommendationAPI, feeRateMultiplier)); - - const depositTransaction = await createDepositTransaction( - this.bitcoinBlockchainAPI, - this.bitcoinNetwork, - depositAmount, - fundingTransactionID, - multisigPayment, - fundingPayment, - feeRate, - vault.btcFeeRecipient, - vault.btcMintFeeBasisPoints.toBigInt() - ); - - return depositTransaction; - } -} diff --git a/src/dlc-handlers/software-wallet-dlc-handler.ts b/src/dlc-handlers/software-wallet-dlc-handler.ts deleted file mode 100644 index 963a53c..0000000 --- a/src/dlc-handlers/software-wallet-dlc-handler.ts +++ /dev/null @@ -1,239 +0,0 @@ -import { Transaction, p2tr, p2wpkh } from '@scure/btc-signer'; -import { P2Ret, P2TROut } from '@scure/btc-signer/payment'; -import { Network } from 'bitcoinjs-lib'; - -import { - createTaprootMultisigPayment, - deriveUnhardenedPublicKey, - ecdsaPublicKeyToSchnorr, - getBalance, - getFeeRate, - getUnspendableKeyCommittedToUUID, -} from '../functions/bitcoin/bitcoin-functions.js'; -import { - createDepositTransaction, - createFundingTransaction, - createWithdrawTransaction, -} from '../functions/bitcoin/psbt-functions.js'; -import { PaymentInformation } from '../models/bitcoin-models.js'; -import { RawVault } from '../models/ethereum-models.js'; - -export class SoftwareWalletDLCHandler { - private fundingDerivedPublicKey: string; - private taprootDerivedPublicKey: string; - private fundingPaymentType: 'wpkh' | 'tr'; - public payment: PaymentInformation | undefined; - private bitcoinNetwork: Network; - private bitcoinBlockchainAPI: string; - private bitcoinBlockchainFeeRecommendationAPI: string; - - constructor( - fundingDerivedPublicKey: string, - taprootDerivedPublicKey: string, - fundingPaymentType: 'wpkh' | 'tr', - bitcoinNetwork: Network, - bitcoinBlockchainAPI: string, - bitcoinBlockchainFeeRecommendationAPI: string - ) { - this.bitcoinBlockchainAPI = bitcoinBlockchainAPI; - this.bitcoinBlockchainFeeRecommendationAPI = bitcoinBlockchainFeeRecommendationAPI; - this.fundingPaymentType = fundingPaymentType; - this.bitcoinNetwork = bitcoinNetwork; - this.fundingDerivedPublicKey = fundingDerivedPublicKey; - this.taprootDerivedPublicKey = taprootDerivedPublicKey; - } - - private setPayment(fundingPayment: P2Ret | P2TROut, multisigPayment: P2TROut): void { - this.payment = { - fundingPayment, - multisigPayment, - }; - } - - private getPayment(): PaymentInformation { - if (!this.payment) { - throw new Error('Payment Information not set'); - } - return this.payment; - } - - getTaprootDerivedPublicKey(): string { - return this.taprootDerivedPublicKey; - } - - getVaultRelatedAddress(paymentType: 'funding' | 'multisig'): string { - const payment = this.getPayment(); - - if (payment === undefined) { - throw new Error('Payment Objects have not been set'); - } - - let address: string; - - switch (paymentType) { - case 'funding': - if (!payment.fundingPayment.address) { - throw new Error('Funding Payment Address is undefined'); - } - address = payment.fundingPayment.address; - return address; - case 'multisig': - if (!payment.multisigPayment.address) { - throw new Error('Taproot Multisig Payment Address is undefined'); - } - address = payment.multisigPayment.address; - return address; - default: - throw new Error('Invalid Payment Type'); - } - } - - private async createPayments( - vaultUUID: string, - attestorGroupPublicKey: string - ): Promise { - try { - const fundingPayment = - this.fundingPaymentType === 'wpkh' - ? p2wpkh(Buffer.from(this.fundingDerivedPublicKey, 'hex'), this.bitcoinNetwork) - : p2tr( - ecdsaPublicKeyToSchnorr(Buffer.from(this.fundingDerivedPublicKey, 'hex')), - undefined, - this.bitcoinNetwork - ); - - const unspendablePublicKey = getUnspendableKeyCommittedToUUID(vaultUUID, this.bitcoinNetwork); - const unspendableDerivedPublicKey = deriveUnhardenedPublicKey( - unspendablePublicKey, - this.bitcoinNetwork - ); - - const attestorDerivedPublicKey = deriveUnhardenedPublicKey( - attestorGroupPublicKey, - this.bitcoinNetwork - ); - - const multisigPayment = createTaprootMultisigPayment( - unspendableDerivedPublicKey, - attestorDerivedPublicKey, - Buffer.from(this.taprootDerivedPublicKey, 'hex'), - this.bitcoinNetwork - ); - - this.setPayment(fundingPayment, multisigPayment); - - return { - fundingPayment, - multisigPayment, - }; - } catch (error: any) { - throw new Error(`Error creating required wallet information: ${error}`); - } - } - - async createFundingPSBT( - vault: RawVault, - bitcoinAmount: bigint, - attestorGroupPublicKey: string, - feeRateMultiplier?: number, - customFeeRate?: bigint - ): Promise { - try { - const { fundingPayment, multisigPayment } = await this.createPayments( - vault.uuid, - attestorGroupPublicKey - ); - - const feeRate = - customFeeRate ?? - BigInt(await getFeeRate(this.bitcoinBlockchainFeeRecommendationAPI, feeRateMultiplier)); - - const addressBalance = await getBalance(fundingPayment, this.bitcoinBlockchainAPI); - - if (BigInt(addressBalance) < vault.valueLocked.toBigInt()) { - throw new Error('Insufficient Funds'); - } - - const fundingTransaction = await createFundingTransaction( - this.bitcoinBlockchainAPI, - this.bitcoinNetwork, - bitcoinAmount, - multisigPayment, - fundingPayment, - feeRate, - vault.btcFeeRecipient, - vault.btcMintFeeBasisPoints.toBigInt() - ); - return fundingTransaction; - } catch (error: any) { - throw new Error(`Error creating Funding PSBT: ${error}`); - } - } - - async createWithdrawPSBT( - vault: RawVault, - withdrawAmount: bigint, - attestorGroupPublicKey: string, - fundingTransactionID: string, - feeRateMultiplier?: number, - customFeeRate?: bigint - ): Promise { - try { - const { fundingPayment, multisigPayment } = await this.createPayments( - vault.uuid, - attestorGroupPublicKey - ); - - const feeRate = - customFeeRate ?? - BigInt(await getFeeRate(this.bitcoinBlockchainFeeRecommendationAPI, feeRateMultiplier)); - - const withdrawTransaction = await createWithdrawTransaction( - this.bitcoinBlockchainAPI, - this.bitcoinNetwork, - withdrawAmount, - fundingTransactionID, - multisigPayment, - fundingPayment, - feeRate, - vault.btcFeeRecipient, - vault.btcRedeemFeeBasisPoints.toBigInt() - ); - return withdrawTransaction; - } catch (error: any) { - throw new Error(`Error creating Withdraw PSBT: ${error}`); - } - } - - async createDepositPSBT( - depositAmount: bigint, - vault: RawVault, - attestorGroupPublicKey: string, - fundingTransactionID: string, - feeRateMultiplier?: number, - customFeeRate?: bigint - ) { - const { fundingPayment, multisigPayment } = await this.createPayments( - vault.uuid, - attestorGroupPublicKey - ); - - const feeRate = - customFeeRate ?? - BigInt(await getFeeRate(this.bitcoinBlockchainFeeRecommendationAPI, feeRateMultiplier)); - - const depositTransaction = await createDepositTransaction( - this.bitcoinBlockchainAPI, - this.bitcoinNetwork, - depositAmount, - fundingTransactionID, - multisigPayment, - fundingPayment, - feeRate, - vault.btcFeeRecipient, - vault.btcMintFeeBasisPoints.toBigInt() - ); - - return depositTransaction; - } -} diff --git a/src/dlc-handlers/unisat-fordefi-dlc-handler.ts b/src/dlc-handlers/unisat-fordefi-dlc-handler.ts new file mode 100644 index 0000000..053925a --- /dev/null +++ b/src/dlc-handlers/unisat-fordefi-dlc-handler.ts @@ -0,0 +1,100 @@ +import { bytesToHex, hexToBytes } from '@noble/hashes/utils'; +import { Transaction } from '@scure/btc-signer'; +import { Network } from 'bitcoinjs-lib'; + +import { getInputIndicesByScript } from '../functions/bitcoin/bitcoin-functions.js'; +import { FundingPaymentType, TransactionType } from '../models/dlc-handler.models.js'; +import { PaymentNotSetError } from '../models/errors/dlc-handler.errors.models.js'; +import { AbstractDLCHandler } from './abstract-dlc-handler.js'; + +export interface UnisatToSignInput { + index: number; + address?: string; + publicKey?: string; + sighashTypes?: number[]; + disableTweakSigner?: boolean; +} + +export interface UnisatSignPsbtRequestOptions { + autoFinalized?: boolean; + toSignInputs?: UnisatToSignInput[]; +} + +export class UnisatFordefiDLCHandler extends AbstractDLCHandler { + readonly dlcHandlerType = 'unisat/fordefi' as const; + private taprootDerivedPublicKey: string; + private fundingDerivedPublicKey: string; + + constructor( + fundingPaymentType: FundingPaymentType, + bitcoinNetwork: Network, + bitcoinBlockchainAPI: string, + bitcoinBlockchainFeeRecommendationAPI: string, + fundingDerivedPublicKey: string, + taprootDerivedPublicKey: string + ) { + super( + fundingPaymentType, + bitcoinNetwork, + bitcoinBlockchainAPI, + bitcoinBlockchainFeeRecommendationAPI + ); + this.fundingDerivedPublicKey = fundingDerivedPublicKey; + this.taprootDerivedPublicKey = taprootDerivedPublicKey; + } + + getUserTaprootPublicKey(tweaked: boolean = false): string { + if (!tweaked) { + return this.taprootDerivedPublicKey; + } + + if (!this.payment) { + throw new PaymentNotSetError(); + } + + return bytesToHex(this.payment.multisigPayment.tweakedPubkey); + } + + getUserFundingPublicKey(): string { + return this.fundingDerivedPublicKey; + } + + private getInputsToSign(transaction: Transaction): UnisatToSignInput[] { + const { multisigPayment, fundingPayment } = this.payment; + + const multisigInputIndices = getInputIndicesByScript(multisigPayment.script, transaction); + const fundingInputIndices = getInputIndicesByScript(fundingPayment.script, transaction); + + const multisigInputsToSign: UnisatToSignInput[] = multisigInputIndices.map(index => ({ + index, + publicKey: this.getUserTaprootPublicKey(), + disableTweakSigner: true, + })); + + const fundingInputsToSign: UnisatToSignInput[] = fundingInputIndices.map(index => ({ + index, + address: fundingPayment.address, + })); + + return multisigInputsToSign.concat(fundingInputsToSign); + } + + async signPSBT(transaction: Transaction, transactionType: TransactionType): Promise { + const inputsToSign = this.getInputsToSign(transaction); + + const options: UnisatSignPsbtRequestOptions = { + autoFinalized: false, + toSignInputs: inputsToSign, + }; + const signedTransactionHex = await (window as any).unisat.signPsbt( + bytesToHex(transaction.toPSBT()), + options + ); + + const signedTransaction = Transaction.fromPSBT(hexToBytes(signedTransactionHex)); + + this.finalizeTransaction(signedTransaction, transactionType, this.payment.fundingPayment); + + return signedTransaction; + } +} diff --git a/src/functions/bitcoin/bitcoin-functions.ts b/src/functions/bitcoin/bitcoin-functions.ts index beeb01a..24beecb 100644 --- a/src/functions/bitcoin/bitcoin-functions.ts +++ b/src/functions/bitcoin/bitcoin-functions.ts @@ -59,6 +59,14 @@ export function deriveUnhardenedPublicKey( .publicKey; } +export function createTaprootPayment(publicKey: Buffer, bitcoinNetwork: Network): P2TROut { + return p2tr(publicKey, undefined, bitcoinNetwork); +} + +export function createNativeSegwitPayment(publicKey: Buffer, bitcoinNetwork: Network): P2Ret { + return p2wpkh(publicKey, bitcoinNetwork); +} + /** * Derives the Account Key Pair from the Root Private Key. * @param rootPrivateKey - The Root Private Key. diff --git a/src/index.ts b/src/index.ts index 12d1d6a..e01d81f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,8 @@ import { DFNSDLCHandler } from './dlc-handlers/dfns-dlc-handler.js'; +import { KeyPairDLCHandler } from './dlc-handlers/keypair-dlc-handler.js'; +import { LeatherDLCHandler } from './dlc-handlers/leather-dlc-handler.js'; import { LedgerDLCHandler } from './dlc-handlers/ledger-dlc-handler.js'; -import { PrivateKeyDLCHandler } from './dlc-handlers/private-key-dlc-handler.js'; -import { SoftwareWalletDLCHandler } from './dlc-handlers/software-wallet-dlc-handler.js'; +import { UnisatFordefiDLCHandler } from './dlc-handlers/unisat-fordefi-dlc-handler.js'; import { EthereumHandler } from './network-handlers/ethereum-handler.js'; import { RippleHandler } from './network-handlers/ripple-handler.js'; import { GemXRPHandler } from './network-handlers/xrp-gem-wallet-handler.js'; @@ -9,9 +10,10 @@ import { LedgerXRPHandler } from './network-handlers/xrp-ledger-handler.js'; import { ProofOfReserveHandler } from './proof-of-reserve-handlers/proof-of-reserve-handler.js'; export { - PrivateKeyDLCHandler, + KeyPairDLCHandler, LedgerDLCHandler, - SoftwareWalletDLCHandler, + LeatherDLCHandler, + UnisatFordefiDLCHandler, LedgerXRPHandler, GemXRPHandler, EthereumHandler, diff --git a/src/models/dlc-handler.models.ts b/src/models/dlc-handler.models.ts new file mode 100644 index 0000000..ccc14f8 --- /dev/null +++ b/src/models/dlc-handler.models.ts @@ -0,0 +1,6 @@ +import { DLCHandlers } from '../constants/dlc-handler.constants.js'; + +export type DLCHandlerType = (typeof DLCHandlers)[keyof typeof DLCHandlers]; +export type FundingPaymentType = 'wpkh' | 'tr'; +export type PaymentType = 'funding' | 'multisig'; +export type TransactionType = 'funding' | 'deposit' | 'withdraw'; diff --git a/src/models/errors/dlc-handler.errors.models.ts b/src/models/errors/dlc-handler.errors.models.ts new file mode 100644 index 0000000..94e2f07 --- /dev/null +++ b/src/models/errors/dlc-handler.errors.models.ts @@ -0,0 +1,97 @@ +import { PaymentType, TransactionType } from '../dlc-handler.models.js'; + +export class DLCHandlerError extends Error { + constructor(message: string) { + super(message); + this.name = 'DLCHandlerError'; + } +} + +export class PaymentNotSetError extends DLCHandlerError { + constructor( + message: string = 'Payment information not initialized. Make sure to create payments before attempting to access them.' + ) { + super(message); + this.name = 'PaymentNotSetError'; + } +} + +export class AddressNotFoundError extends DLCHandlerError { + constructor(paymentType: PaymentType) { + super(`Address not found for ${paymentType} payment`); + this.name = 'AddressNotFoundError'; + } +} + +export class InvalidPaymentTypeError extends DLCHandlerError { + constructor(paymentType: PaymentType) { + super(`Invalid payment type: ${paymentType}`); + this.name = 'InvalidPaymentTypeError'; + } +} + +export class InvalidTransactionTypeError extends DLCHandlerError { + constructor(transactionType: TransactionType) { + super(`Invalid transaction type: ${transactionType}`); + this.name = 'InvalidTransactionTypeError'; + } +} + +export class InsufficientFundsError extends DLCHandlerError { + constructor(available: bigint, required: bigint) { + super(`Insufficient funds: have ${available}, need ${required}`); + this.name = 'InsufficientFundsError'; + } +} + +export class IncompatibleTransactionArgument extends DLCHandlerError { + constructor() { + super('Incompatible transaction argument'); + this.name = 'IncompatibleTransactionArgument'; + } +} + +export class PolicyInformationNotSet extends DLCHandlerError { + constructor( + message: string = 'Policy Information not initialized. Make sure to create payments before attempting to access them.' + ) { + super(message); + this.name = 'PolicyInformationNotSet'; + } +} + +export class TaprootDerivedPublicKeyNotSet extends DLCHandlerError { + constructor( + message: string = 'Taproot Derived Public Key not set. Make sure to initialize the wallet before attempting to access it.' + ) { + super(message); + this.name = 'TaprootDerivedPublicKeyNotSet'; + } +} + +export class FundingDerivedPublicKeyNotSet extends DLCHandlerError { + constructor( + message: string = 'Funding Derived Public Key not set. Make sure to initialize the wallet before attempting to access it.' + ) { + super(message); + this.name = 'FundingDerivedPublicKeyNotSet'; + } +} + +export class DFNSWalletIDNotSetError extends DLCHandlerError { + constructor( + message: string = 'DFNS Wallet ID not set. Make sure to initialize the wallet before attempting to access it.' + ) { + super(message); + this.name = 'DFNSWalletIDNotSetError'; + } +} + +export class SignatureGenerationFailed extends DLCHandlerError { + constructor( + message: string = 'Signature generation failed. Make sure to initialize the wallet before attempting to access it.' + ) { + super(message); + this.name = 'SignatureGenerationFailed'; + } +} diff --git a/tests/unit/sign-transactions.test.ts b/tests/unit/sign-transactions.test.ts deleted file mode 100644 index 893ac27..0000000 --- a/tests/unit/sign-transactions.test.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { hexToBytes } from '@noble/hashes/utils'; -import { Transaction } from '@scure/btc-signer'; -import { regtest } from 'bitcoinjs-lib/src/networks.js'; - -import { PrivateKeyDLCHandler } from '../../src/index.js'; -import { - TEST_BITCOIN_BLOCKCHAIN_FEE_RECOMMENDATION_API, - TEST_REGTEST_BITCOIN_BLOCKCHAIN_API, -} from '../mocks/api.test.constants.js'; -import { TEST_FUNDING_PSBT_PARTIALLY_SIGNED_WITHDRAW_PSBT_1 } from '../mocks/bitcoin-transaction.test.constants.js'; -import { - TEST_BITCOIN_EXTENDED_PRIVATE_KEY, - TEST_BITCOIN_WALLET_ACCOUNT_INDEX, - TEST_FUNDING_PAYMENT_TYPE, -} from '../mocks/bitcoin.test.constants.js'; - -describe('Create and Sign Vault related Transactions', () => { - let dlcHandler: PrivateKeyDLCHandler; - - it('should initialize a Private Key DLC Handler', async () => { - dlcHandler = new PrivateKeyDLCHandler( - TEST_BITCOIN_EXTENDED_PRIVATE_KEY, - TEST_BITCOIN_WALLET_ACCOUNT_INDEX, - TEST_FUNDING_PAYMENT_TYPE, - regtest, - TEST_REGTEST_BITCOIN_BLOCKCHAIN_API, - TEST_BITCOIN_BLOCKCHAIN_FEE_RECOMMENDATION_API - ); - }); - - it('should sign a funding transaction', async () => { - const signedFundingTransaction = dlcHandler.signPSBT( - Transaction.fromPSBT(hexToBytes(TEST_FUNDING_PSBT_PARTIALLY_SIGNED_WITHDRAW_PSBT_1)), - 'funding' - ); - - expect(signedFundingTransaction.isFinal).toBeTruthy(); - }); -});