diff --git a/typescript/src/lib/bitcoin/ecdsa-key.ts b/typescript/src/lib/bitcoin/ecdsa-key.ts index 16a6b1ec7..f7b845840 100644 --- a/typescript/src/lib/bitcoin/ecdsa-key.ts +++ b/typescript/src/lib/bitcoin/ecdsa-key.ts @@ -1,7 +1,8 @@ -import bcoin from "bcoin" -import wif from "wif" import { BigNumber } from "ethers" import { Hex } from "../utils" +import { ECPairFactory, ECPairInterface } from "ecpair" +import * as tinysecp from "tiny-secp256k1" +import { BitcoinNetwork, toBitcoinJsLibNetwork } from "./network" /** * Checks whether given public key is a compressed Bitcoin public key. @@ -59,25 +60,25 @@ export const BitcoinPublicKeyUtils = { } /** - * Creates a Bitcoin key ring based on the given private key. - * @param privateKey Private key that should be used to create the key ring - * @param witness Flag indicating whether the key ring will create witness - * or non-witness addresses + * Creates a Bitcoin key pair based on the given private key. + * @param privateKey Private key that should be used to create the key pair. + * @param bitcoinNetwork Bitcoin network the given key pair is relevant for. * @returns Bitcoin key ring. */ -function createKeyRing(privateKey: string, witness: boolean = true): any { - const decodedPrivateKey = wif.decode(privateKey) - - return new bcoin.KeyRing({ - witness: witness, - privateKey: decodedPrivateKey.privateKey, - compressed: decodedPrivateKey.compressed, - }) +function createKeyPair( + privateKey: string, + bitcoinNetwork: BitcoinNetwork +): ECPairInterface { + // eslint-disable-next-line new-cap + return ECPairFactory(tinysecp).fromWIF( + privateKey, + toBitcoinJsLibNetwork(bitcoinNetwork) + ) } /** * Utility functions allowing to perform Bitcoin ECDSA public keys. */ export const BitcoinPrivateKeyUtils = { - createKeyRing, + createKeyPair, } diff --git a/typescript/src/services/deposits/funding.ts b/typescript/src/services/deposits/funding.ts index ff250d096..c520f5e83 100644 --- a/typescript/src/services/deposits/funding.ts +++ b/typescript/src/services/deposits/funding.ts @@ -3,6 +3,7 @@ import { BitcoinAddressConverter, BitcoinClient, BitcoinNetwork, + BitcoinPrivateKeyUtils, BitcoinRawTx, BitcoinScriptUtils, BitcoinTxHash, @@ -11,8 +12,6 @@ import { } from "../../lib/bitcoin" import { BigNumber } from "ethers" import { Psbt, Transaction } from "bitcoinjs-lib" -import { ECPairFactory } from "ecpair" -import * as tinysecp from "tiny-secp256k1" import { Hex } from "../../lib/utils" /** @@ -72,10 +71,10 @@ export class DepositFunding { rawTransaction: BitcoinRawTx }> { const network = toBitcoinJsLibNetwork(bitcoinNetwork) - // eslint-disable-next-line new-cap - const depositorKeyPair = ECPairFactory(tinysecp).fromWIF( + + const depositorKeyPair = BitcoinPrivateKeyUtils.createKeyPair( depositorPrivateKey, - network + bitcoinNetwork ) const psbt = new Psbt({ network }) diff --git a/typescript/src/services/deposits/refund.ts b/typescript/src/services/deposits/refund.ts index 4582c7f1c..6b896e0e9 100644 --- a/typescript/src/services/deposits/refund.ts +++ b/typescript/src/services/deposits/refund.ts @@ -1,7 +1,6 @@ import bcoin from "bcoin" import { BigNumber } from "ethers" import { - BitcoinPrivateKeyUtils, BitcoinRawTx, BitcoinClient, BitcoinTxHash, @@ -11,6 +10,7 @@ import { } from "../../lib/bitcoin" import { validateDepositReceipt } from "../../lib/contracts" import { DepositScript } from "./" +import wif from "wif" /** * Component allowing to craft and submit the Bitcoin refund transaction using @@ -106,8 +106,13 @@ export class DepositRefund { }> { validateDepositReceipt(this.script.receipt) - const refunderKeyRing = - BitcoinPrivateKeyUtils.createKeyRing(refunderPrivateKey) + const decodedPrivateKey = wif.decode(refunderPrivateKey) + + const refunderKeyRing = new bcoin.KeyRing({ + witness: true, + privateKey: decodedPrivateKey.privateKey, + compressed: decodedPrivateKey.compressed, + }) const transaction = new bcoin.MTX() diff --git a/typescript/src/services/maintenance/wallet-tx.ts b/typescript/src/services/maintenance/wallet-tx.ts index 4c017bebb..552929510 100644 --- a/typescript/src/services/maintenance/wallet-tx.ts +++ b/typescript/src/services/maintenance/wallet-tx.ts @@ -17,10 +17,7 @@ import { RedemptionRequest, TBTCContracts, } from "../../lib/contracts" -import bcoin from "bcoin" import { DepositScript } from "../deposits" -import { ECPairFactory } from "ecpair" -import * as tinysecp from "tiny-secp256k1" import { Hex } from "../../lib/utils" import { payments, @@ -29,6 +26,7 @@ import { Stack, Transaction, TxOutput, + Psbt, } from "bitcoinjs-lib" /** @@ -189,11 +187,9 @@ class DepositSweep { ) } - const network = toBitcoinJsLibNetwork(bitcoinNetwork) - // eslint-disable-next-line new-cap - const walletKeyPair = ECPairFactory(tinysecp).fromWIF( + const walletKeyPair = BitcoinPrivateKeyUtils.createKeyPair( walletPrivateKey, - network + bitcoinNetwork ) const walletAddress = BitcoinAddressConverter.publicKeyToAddress( Hex.from(walletKeyPair.publicKey), @@ -566,11 +562,14 @@ class Redemption { transactionHex: mainUtxoRawTransaction.transactionHex, } - const walletPublicKey = BitcoinPrivateKeyUtils.createKeyRing( - walletPrivateKey + const bitcoinNetwork = await this.bitcoinClient.getNetwork() + + const walletKeyPair = BitcoinPrivateKeyUtils.createKeyPair( + walletPrivateKey, + bitcoinNetwork ) - .getPublicKey() - .toString("hex") + + const walletPublicKey = walletKeyPair.publicKey.toString("hex") const redemptionRequests: RedemptionRequest[] = [] @@ -593,6 +592,7 @@ class Redemption { const { transactionHash, newMainUtxo, rawTransaction } = await this.assembleTransaction( + bitcoinNetwork, walletPrivateKey, mainUtxoWithRaw, redemptionRequests @@ -617,6 +617,7 @@ class Redemption { * - there is at least one redemption * - the `requestedAmount` in each redemption request is greater than * the sum of its `txFee` and `treasuryFee` + * @param bitcoinNetwork The target Bitcoin network. * @param walletPrivateKey - The private key of the wallet in the WIF format * @param mainUtxo - The main UTXO of the wallet. Must match the main UTXO held * by the on-chain Bridge contract @@ -627,6 +628,7 @@ class Redemption { * - the redemption transaction in the raw format */ async assembleTransaction( + bitcoinNetwork: BitcoinNetwork, walletPrivateKey: string, mainUtxo: BitcoinUtxo & BitcoinRawTx, redemptionRequests: RedemptionRequest[] @@ -639,22 +641,45 @@ class Redemption { throw new Error("There must be at least one request to redeem") } - const walletKeyRing = BitcoinPrivateKeyUtils.createKeyRing( + const walletKeyPair = BitcoinPrivateKeyUtils.createKeyPair( walletPrivateKey, + bitcoinNetwork + ) + const walletAddress = BitcoinAddressConverter.publicKeyToAddress( + Hex.from(walletKeyPair.publicKey), + bitcoinNetwork, this.witness ) - const walletAddress = walletKeyRing.getAddress("string") - - // Use the main UTXO as the single transaction input - const inputCoins = [ - bcoin.Coin.fromTX( - bcoin.MTX.fromRaw(mainUtxo.transactionHex, "hex"), - mainUtxo.outputIndex, - -1 - ), - ] - const transaction = new bcoin.MTX() + const network = toBitcoinJsLibNetwork(bitcoinNetwork) + const psbt = new Psbt({ network }) + psbt.setVersion(1) + + // Add input (current main UTXO). + const previousOutput = Transaction.fromHex(mainUtxo.transactionHex).outs[ + mainUtxo.outputIndex + ] + const previousOutputScript = previousOutput.script + const previousOutputValue = previousOutput.value + + if (BitcoinScriptUtils.isP2PKHScript(previousOutputScript)) { + psbt.addInput({ + hash: mainUtxo.transactionHash.reverse().toBuffer(), + index: mainUtxo.outputIndex, + nonWitnessUtxo: Buffer.from(mainUtxo.transactionHex, "hex"), + }) + } else if (BitcoinScriptUtils.isP2WPKHScript(previousOutputScript)) { + psbt.addInput({ + hash: mainUtxo.transactionHash.reverse().toBuffer(), + index: mainUtxo.outputIndex, + witnessUtxo: { + script: previousOutputScript, + value: previousOutputValue, + }, + }) + } else { + throw new Error("Unexpected main UTXO type") + } let txTotalFee = BigNumber.from(0) let totalOutputsValue = BigNumber.from(0) @@ -673,44 +698,34 @@ class Redemption { // Add the fee for this particular request to the overall transaction fee txTotalFee = txTotalFee.add(request.txMaxFee) - transaction.addOutput({ - script: bcoin.Script.fromRaw( - Buffer.from(request.redeemerOutputScript, "hex") - ), + psbt.addOutput({ + script: Buffer.from(request.redeemerOutputScript, "hex"), value: outputValue.toNumber(), }) } - // If there is a change output, add it explicitly to the transaction. - // If we did not add this output explicitly, the bcoin library would add it - // anyway during funding, but if the value of the change output was very low, - // the library would consider it "dust" and add it to the fee rather than - // create a new output. + // If there is a change output, add it to the transaction. const changeOutputValue = mainUtxo.value .sub(totalOutputsValue) .sub(txTotalFee) if (changeOutputValue.gt(0)) { - transaction.addOutput({ - script: bcoin.Script.fromAddress(walletAddress), + psbt.addOutput({ + address: walletAddress, value: changeOutputValue.toNumber(), }) } - await transaction.fund(inputCoins, { - changeAddress: walletAddress, - hardFee: txTotalFee.toNumber(), - subtractFee: false, - }) - - transaction.sign(walletKeyRing) + psbt.signAllInputs(walletKeyPair) + psbt.finalizeAllInputs() - const transactionHash = BitcoinTxHash.from(transaction.txid()) + const transaction = psbt.extractTransaction() + const transactionHash = BitcoinTxHash.from(transaction.getId()) // If there is a change output, it will be the new wallet's main UTXO. const newMainUtxo = changeOutputValue.gt(0) ? { transactionHash, // It was the last output added to the transaction. - outputIndex: transaction.outputs.length - 1, + outputIndex: transaction.outs.length - 1, value: changeOutputValue, } : undefined @@ -719,7 +734,7 @@ class Redemption { transactionHash, newMainUtxo, rawTransaction: { - transactionHex: transaction.toRaw().toString("hex"), + transactionHex: transaction.toHex(), }, } } diff --git a/typescript/test/redemption.test.ts b/typescript/test/redemption.test.ts index 468360684..371662f2c 100644 --- a/typescript/test/redemption.test.ts +++ b/typescript/test/redemption.test.ts @@ -61,8 +61,6 @@ describe("Redemption", () => { let bitcoinClient: MockBitcoinClient beforeEach(async () => { - bcoin.set("testnet") - tbtcContracts = new MockTBTCContracts() bitcoinClient = new MockBitcoinClient() @@ -818,8 +816,6 @@ describe("Redemption", () => { let bitcoinClient: MockBitcoinClient beforeEach(async () => { - bcoin.set("testnet") - tbtcContracts = new MockTBTCContracts() bitcoinClient = new MockBitcoinClient() }) @@ -1272,6 +1268,7 @@ describe("Redemption", () => { newMainUtxo, rawTransaction: transaction, } = await walletTx.redemption.assembleTransaction( + BitcoinNetwork.Testnet, walletPrivateKey, data.mainUtxo, redemptionRequests @@ -1396,6 +1393,7 @@ describe("Redemption", () => { newMainUtxo, rawTransaction: transaction, } = await walletTx.redemption.assembleTransaction( + BitcoinNetwork.Testnet, walletPrivateKey, data.mainUtxo, redemptionRequests @@ -1519,6 +1517,7 @@ describe("Redemption", () => { newMainUtxo, rawTransaction: transaction, } = await walletTx.redemption.assembleTransaction( + BitcoinNetwork.Testnet, walletPrivateKey, data.mainUtxo, redemptionRequests @@ -1642,6 +1641,7 @@ describe("Redemption", () => { newMainUtxo, rawTransaction: transaction, } = await walletTx.redemption.assembleTransaction( + BitcoinNetwork.Testnet, walletPrivateKey, data.mainUtxo, redemptionRequests @@ -1764,6 +1764,7 @@ describe("Redemption", () => { newMainUtxo, rawTransaction: transaction, } = await walletTx.redemption.assembleTransaction( + BitcoinNetwork.Testnet, walletPrivateKey, data.mainUtxo, redemptionRequests @@ -1930,6 +1931,7 @@ describe("Redemption", () => { newMainUtxo, rawTransaction: transaction, } = await walletTx.redemption.assembleTransaction( + BitcoinNetwork.Testnet, walletPrivateKey, data.mainUtxo, redemptionRequests @@ -2045,6 +2047,7 @@ describe("Redemption", () => { newMainUtxo, rawTransaction: transaction, } = await walletTx.redemption.assembleTransaction( + BitcoinNetwork.Testnet, walletPrivateKey, data.mainUtxo, redemptionRequests @@ -2140,6 +2143,7 @@ describe("Redemption", () => { await expect( walletTx.redemption.assembleTransaction( + BitcoinNetwork.Testnet, walletPrivateKey, data.mainUtxo, [] // empty list of redemption requests @@ -2166,8 +2170,6 @@ describe("Redemption", () => { let maintenanceService: MaintenanceService beforeEach(async () => { - bcoin.set("testnet") - bitcoinClient = new MockBitcoinClient() tbtcContracts = new MockTBTCContracts()