From 02a3a69be8cd0e053e8c5531f74879f4816f3279 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Tue, 31 Oct 2023 16:20:07 +0100 Subject: [PATCH 1/5] Added Bitcoin client function for getting tx hashes for public key hash --- typescript/src/lib/bitcoin/client.ts | 14 +++++ typescript/src/lib/electrum/client.ts | 58 ++++++++++++++++++++ typescript/test/data/electrum.ts | 28 ++++++++++ typescript/test/lib/electrum.test.ts | 26 +++++++++ typescript/test/utils/mock-bitcoin-client.ts | 7 +++ 5 files changed, 133 insertions(+) diff --git a/typescript/src/lib/bitcoin/client.ts b/typescript/src/lib/bitcoin/client.ts index 2df7e1147..483563d5f 100644 --- a/typescript/src/lib/bitcoin/client.ts +++ b/typescript/src/lib/bitcoin/client.ts @@ -54,6 +54,20 @@ export interface BitcoinClient { */ getTransactionConfirmations(transactionHash: BitcoinTxHash): Promise + /** + * Gets hashes of confirmed transactions that pay the given public key hash + * using either a P2PKH or P2WPKH script. The returned transactions hashes are + * ordered by block height in the ascending order, i.e. the latest transaction + * hash is at the end of the list. The returned list does not contain + * unconfirmed transactions hashes living in the mempool at the moment of + * request. + * @param publicKeyHash - Hash of the public key for which to find + * corresponding transaction hashes. + * @returns Array of confirmed transaction hashes related to the provided + * public key hash. + */ + getTxHashesForPublicKeyHash(publicKeyHash: Hex): Promise + /** * Gets height of the latest mined block. * @return Height of the last mined block. diff --git a/typescript/src/lib/electrum/client.ts b/typescript/src/lib/electrum/client.ts index 881f28564..a07a16dc4 100644 --- a/typescript/src/lib/electrum/client.ts +++ b/typescript/src/lib/electrum/client.ts @@ -498,6 +498,64 @@ export class ElectrumClient implements BitcoinClient { }) } + // eslint-disable-next-line valid-jsdoc + /** + * @see {BitcoinClient#getTxHashesForPublicKeyHash} + */ + getTxHashesForPublicKeyHash(publicKeyHash: Hex): Promise { + return this.withElectrum(async (electrum: Electrum) => { + // eslint-disable-next-line camelcase + type HistoryItem = { height: number; tx_hash: string } + + const getConfirmedHistory = async ( + witnessAddress: boolean + ): Promise => { + const bitcoinNetwork = await this.getNetwork() + + const address = BitcoinAddressConverter.publicKeyHashToAddress( + publicKeyHash, + witnessAddress, + bitcoinNetwork + ) + + const script = BitcoinAddressConverter.addressToOutputScript( + address, + bitcoinNetwork + ) + + let historyItems: HistoryItem[] = await this.withBackoffRetrier< + HistoryItem[] + >()(async () => { + return await electrum.blockchain_scripthash_getHistory( + computeElectrumScriptHash(script) + ) + }) + + // According to https://electrumx.readthedocs.io/en/latest/protocol-methods.html#blockchain-scripthash-get-history + // unconfirmed items living in the mempool are appended at the end of the + // returned list and their height value is either -1 or 0. That means + // we need to take all items with height >0 to obtain a confirmed txs + // history. + historyItems = historyItems.filter((item) => item.height > 0) + + // The list returned from blockchain.scripthash.get_history is sorted by + // the block height in the ascending order though we are sorting it + // again just in case (e.g. API contract changes). + historyItems = historyItems.sort((a, b) => a.height - b.height) + + return historyItems + } + + const p2pkhItems = await getConfirmedHistory(false) + const p2wpkhItems = await getConfirmedHistory(true) + + const items = [...p2pkhItems, ...p2wpkhItems] + items.sort((a, b) => a.height - b.height) + + return items.map((item) => BitcoinTxHash.from(item.tx_hash)) + }) + } + // eslint-disable-next-line valid-jsdoc /** * @see {BitcoinClient#latestBlockHeight} diff --git a/typescript/test/data/electrum.ts b/typescript/test/data/electrum.ts index 38c12becb..3782d755e 100644 --- a/typescript/test/data/electrum.ts +++ b/typescript/test/data/electrum.ts @@ -131,3 +131,31 @@ export const testnetTransactionMerkleBranch: BitcoinTxMerkleBranch = { ], position: 176, } + +/** + * Public key hash that has associated transactions locking funds to it. + */ +export const testnetPublicKeyHash = Hex.from( + "e6f9d74726b19b75f16fe1e9feaec048aa4fa1d0" +) + +/** + * Transaction hashes corresponding to {@link testnetPublicKeyHash} + */ +export const testnetTxHashes: BitcoinTxHash[] = [ + BitcoinTxHash.from( + "f65bc5029251f0042aedb37f90dbb2bfb63a2e81694beef9cae5ec62e954c22e" + ), + BitcoinTxHash.from( + "44863a79ce2b8fec9792403d5048506e50ffa7338191db0e6c30d3d3358ea2f6" + ), + BitcoinTxHash.from( + "4c6b33b7c0550e0e536a5d119ac7189d71e1296fcb0c258e0c115356895bc0e6" + ), + BitcoinTxHash.from( + "605edd75ae0b4fa7cfc7aae8f1399119e9d7ecc212e6253156b60d60f4925d44" + ), + BitcoinTxHash.from( + "4f9affc5b418385d5aa61e23caa0b55156bf0682d5fedf2d905446f3f88aec6c" + ), +] diff --git a/typescript/test/lib/electrum.test.ts b/typescript/test/lib/electrum.test.ts index b54e50b1d..673db9a01 100644 --- a/typescript/test/lib/electrum.test.ts +++ b/typescript/test/lib/electrum.test.ts @@ -4,13 +4,16 @@ import { ElectrumClient, computeElectrumScriptHash, Hex, + BitcoinTxHash, } from "../../src" import { testnetAddress, testnetHeadersChain, + testnetPublicKeyHash, testnetRawTransaction, testnetTransaction, testnetTransactionMerkleBranch, + testnetTxHashes, testnetUTXO, } from "../data/electrum" import { expect } from "chai" @@ -180,6 +183,29 @@ describe("Electrum", () => { }) }) + describe("getTxHashesForPublicKeyHash", () => { + let actualHashes: BitcoinTxHash[] + + before(async () => { + actualHashes = await electrumClient.getTxHashesForPublicKeyHash( + testnetPublicKeyHash + ) + }) + + it("should return proper transaction hashes", async () => { + const expectedHashes = testnetTxHashes + // If the actual hashes set is greater than the expected set, we + // need to adjust them to the same length to make a comparison that + // makes sense. + if (actualHashes.length > expectedHashes.length) { + actualHashes = actualHashes.slice( + actualHashes.length - expectedHashes.length + ) + } + expect(actualHashes).to.be.deep.equal(expectedHashes) + }) + }) + describe("latestBlockHeight", () => { let result: number diff --git a/typescript/test/utils/mock-bitcoin-client.ts b/typescript/test/utils/mock-bitcoin-client.ts index 69b55e212..09bd88771 100644 --- a/typescript/test/utils/mock-bitcoin-client.ts +++ b/typescript/test/utils/mock-bitcoin-client.ts @@ -17,6 +17,7 @@ export class MockBitcoinClient implements BitcoinClient { private _rawTransactions = new Map() private _transactions = new Map() private _confirmations = new Map() + private _txHashes = new Map() private _latestHeight = 0 private _headersChain = Hex.from("") private _transactionMerkle: BitcoinTxMerkleBranch = { @@ -111,6 +112,12 @@ export class MockBitcoinClient implements BitcoinClient { }) } + getTxHashesForPublicKeyHash(publicKeyHash: Hex): Promise { + return new Promise((resolve, _) => { + resolve(this._txHashes.get(publicKeyHash.toString()) as BitcoinTxHash[]) + }) + } + latestBlockHeight(): Promise { return new Promise((resolve, _) => { resolve(this._latestHeight) From c90b155fcdc177fe5d625060e7d2183081aeb2e3 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Wed, 1 Nov 2023 10:44:09 +0100 Subject: [PATCH 2/5] Updated function for determining wallet main UTXO --- .../redemptions/redemptions-service.ts | 134 ++++++++---------- typescript/test/utils/mock-bitcoin-client.ts | 4 + 2 files changed, 63 insertions(+), 75 deletions(-) diff --git a/typescript/src/services/redemptions/redemptions-service.ts b/typescript/src/services/redemptions/redemptions-service.ts index 690037877..4ce6c6341 100644 --- a/typescript/src/services/redemptions/redemptions-service.ts +++ b/typescript/src/services/redemptions/redemptions-service.ts @@ -208,12 +208,6 @@ export class RedemptionsService { * Determines the plain-text wallet main UTXO currently registered in the * Bridge on-chain contract. The returned main UTXO can be undefined if the * wallet does not have a main UTXO registered in the Bridge at the moment. - * - * WARNING: THIS FUNCTION CANNOT DETERMINE THE MAIN UTXO IF IT COMES FROM A - * BITCOIN TRANSACTION THAT IS NOT ONE OF THE LATEST FIVE TRANSACTIONS - * TARGETING THE GIVEN WALLET PUBLIC KEY HASH. HOWEVER, SUCH A CASE IS - * VERY UNLIKELY. - * * @param walletPublicKeyHash - Public key hash of the wallet. * @param bitcoinNetwork - Bitcoin network. * @returns Promise holding the wallet main UTXO or undefined value. @@ -238,87 +232,77 @@ export class RedemptionsService { return undefined } - // Declare a helper function that will try to determine the main UTXO for - // the given wallet address type. - const determine = async ( - witnessAddress: boolean - ): Promise => { - // Build the wallet Bitcoin address based on its public key hash. - const walletAddress = BitcoinAddressConverter.publicKeyHashToAddress( + // The wallet main UTXO registered in the Bridge almost always comes + // from the latest BTC transaction made by the wallet. However, there may + // be cases where the BTC transaction was made but their SPV proof is + // not yet submitted to the Bridge thus the registered main UTXO points + // to the second last BTC transaction. In theory, such a gap between + // the actual latest BTC transaction and the registered main UTXO in + // the Bridge may be even wider. To cover the worst possible cases, we + // must rely on the full transaction history. Due to performance reasons, + // we are first taking just the transactions hashes (fast call) and then + // fetch full transaction data (time-consuming calls) starting from + // the most recent transactions as there is a high chance the main UTXO + // comes from there. + const walletTxHashes = await this.bitcoinClient.getTxHashesForPublicKeyHash( + walletPublicKeyHash + ) + + const getOutputScript = (witness: boolean): Hex => { + const address = BitcoinAddressConverter.publicKeyHashToAddress( walletPublicKeyHash, - witnessAddress, + witness, bitcoinNetwork ) - - // Get the wallet transaction history. The wallet main UTXO registered in the - // Bridge almost always comes from the latest BTC transaction made by the wallet. - // However, there may be cases where the BTC transaction was made but their - // SPV proof is not yet submitted to the Bridge thus the registered main UTXO - // points to the second last BTC transaction. In theory, such a gap between - // the actual latest BTC transaction and the registered main UTXO in the - // Bridge may be even wider. The exact behavior is a wallet implementation - // detail and not a protocol invariant so, it may be subject of changes. - // To cover the worst possible cases, we always take the five latest - // transactions made by the wallet for consideration. - const walletTransactions = await this.bitcoinClient.getTransactionHistory( - walletAddress, - 5 + return BitcoinAddressConverter.addressToOutputScript( + address, + bitcoinNetwork ) + } - // Get the wallet script based on the wallet address. This is required - // to find transaction outputs that lock funds on the wallet. - const walletScript = BitcoinAddressConverter.addressToOutputScript( - walletAddress, - bitcoinNetwork + const walletP2PKH = getOutputScript(false) + const walletP2WPKH = getOutputScript(true) + + const isWalletOutput = (output: BitcoinTxOutput) => + walletP2PKH.equals(output.scriptPubKey) || + walletP2WPKH.equals(output.scriptPubKey) + + // Start iterating from the latest transaction as the chance it matches + // the wallet main UTXO is the highest. + for (let i = walletTxHashes.length - 1; i >= 0; i--) { + const walletTxHash = walletTxHashes[i] + const walletTransaction = await this.bitcoinClient.getTransaction( + walletTxHash ) - const isWalletOutput = (output: BitcoinTxOutput) => - walletScript.equals(output.scriptPubKey) - - // Start iterating from the latest transaction as the chance it matches - // the wallet main UTXO is the highest. - for (let i = walletTransactions.length - 1; i >= 0; i--) { - const walletTransaction = walletTransactions[i] - - // Find the output that locks the funds on the wallet. Only such an output - // can be a wallet main UTXO. - const outputIndex = walletTransaction.outputs.findIndex(isWalletOutput) - - // Should never happen as all transactions come from wallet history. Just - // in case check whether the wallet output was actually found. - if (outputIndex < 0) { - console.error( - `wallet output for transaction ${walletTransaction.transactionHash.toString()} not found` - ) - continue - } - // Build a candidate UTXO instance based on the detected output. - const utxo: BitcoinUtxo = { - transactionHash: walletTransaction.transactionHash, - outputIndex: outputIndex, - value: walletTransaction.outputs[outputIndex].value, - } + // Find the output that locks the funds on the wallet. Only such an output + // can be a wallet main UTXO. + const outputIndex = walletTransaction.outputs.findIndex(isWalletOutput) - // Check whether the candidate UTXO hash matches the main UTXO hash stored - // on the Bridge. - if ( - mainUtxoHash.equals(this.tbtcContracts.bridge.buildUtxoHash(utxo)) - ) { - return utxo - } + // Should never happen as all transactions come from wallet history. Just + // in case check whether the wallet output was actually found. + if (outputIndex < 0) { + console.error( + `wallet output for transaction ${walletTransaction.transactionHash.toString()} not found` + ) + continue } - return undefined - } + // Build a candidate UTXO instance based on the detected output. + const utxo: BitcoinUtxo = { + transactionHash: walletTransaction.transactionHash, + outputIndex: outputIndex, + value: walletTransaction.outputs[outputIndex].value, + } - // The most common case is that the wallet uses a witness address for all - // operations. Try to determine the main UTXO for that address first as the - // chance for success is the highest here. - const mainUtxo = await determine(true) + // Check whether the candidate UTXO hash matches the main UTXO hash stored + // on the Bridge. + if (mainUtxoHash.equals(this.tbtcContracts.bridge.buildUtxoHash(utxo))) { + return utxo + } + } - // In case the main UTXO was not found for witness address, there is still - // a chance it exists for the legacy wallet address. - return mainUtxo ?? (await determine(false)) + return undefined } /** diff --git a/typescript/test/utils/mock-bitcoin-client.ts b/typescript/test/utils/mock-bitcoin-client.ts index 09bd88771..e6274b807 100644 --- a/typescript/test/utils/mock-bitcoin-client.ts +++ b/typescript/test/utils/mock-bitcoin-client.ts @@ -44,6 +44,10 @@ export class MockBitcoinClient implements BitcoinClient { this._confirmations = value } + set txHashes(value: Map) { + this._txHashes = value + } + set latestHeight(value: number) { this._latestHeight = value } From 6a1bffb062b7a2a7da6430de436bcc42984a7943 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Wed, 1 Nov 2023 15:08:25 +0100 Subject: [PATCH 3/5] Updated unit tests --- .../redemptions/redemptions-service.ts | 6 + typescript/test/services/redemptions.test.ts | 324 ++++++++++-------- typescript/test/utils/mock-bitcoin-client.ts | 13 +- 3 files changed, 195 insertions(+), 148 deletions(-) diff --git a/typescript/src/services/redemptions/redemptions-service.ts b/typescript/src/services/redemptions/redemptions-service.ts index 4ce6c6341..3e8e6f5c8 100644 --- a/typescript/src/services/redemptions/redemptions-service.ts +++ b/typescript/src/services/redemptions/redemptions-service.ts @@ -302,6 +302,12 @@ export class RedemptionsService { } } + // Should never happen if the wallet has the main UTXO registered in the + // Bridge. It could only happen due to some serious error, e.g. wrong main + // UTXO hash stored in the Bridge or Bitcoin blockchain data corruption. + console.error( + `main UTXO with hash ${mainUtxoHash.toPrefixedString()} not found for wallet ${walletPublicKeyHash.toString()}` + ) return undefined } diff --git a/typescript/test/services/redemptions.test.ts b/typescript/test/services/redemptions.test.ts index 2d429696f..3b36489ed 100644 --- a/typescript/test/services/redemptions.test.ts +++ b/typescript/test/services/redemptions.test.ts @@ -79,23 +79,33 @@ describe("Redemptions", () => { // Prepare wallet transaction history for main UTXO lookup. // Set only relevant fields. - const transactionHistory = new Map() - transactionHistory.set(walletAddress, [ - { - transactionHash: mainUtxo.transactionHash, - outputs: [ - { - outputIndex: mainUtxo.outputIndex, - value: mainUtxo.value, - scriptPubKey: BitcoinAddressConverter.addressToOutputScript( - walletAddress, - BitcoinNetwork.Testnet - ), - }, - ], - } as BitcoinTx, + + const transaction = { + transactionHash: mainUtxo.transactionHash, + outputs: [ + { + outputIndex: mainUtxo.outputIndex, + value: mainUtxo.value, + scriptPubKey: BitcoinAddressConverter.addressToOutputScript( + walletAddress, + BitcoinNetwork.Testnet + ), + }, + ], + } + + const walletTransactions = new Map() + walletTransactions.set( + transaction.transactionHash.toString(), + transaction as BitcoinTx + ) + bitcoinClient.transactions = walletTransactions + + const walletTransactionHashes = new Map() + walletTransactionHashes.set(walletPublicKeyHash.toString(), [ + transaction.transactionHash, ]) - bitcoinClient.transactionHistory = transactionHistory + bitcoinClient.transactionHashes = walletTransactionHashes const redemptionsService = new RedemptionsService( tbtcContracts, @@ -268,19 +278,27 @@ describe("Redemptions", () => { (wallet) => wallet.event ) - const walletsTransactionHistory = new Map() + const walletTransactions = new Map() + const walletTransactionHashes = new Map() walletsOrder.forEach((wallet) => { const { state, mainUtxoHash, walletPublicKey, - btcAddress, transactions, pendingRedemptionsValue, } = wallet.data - walletsTransactionHistory.set(btcAddress, transactions) + transactions.forEach((tx) => { + walletTransactions.set(tx.transactionHash.toString(), tx) + }) + + walletTransactionHashes.set( + wallet.event.walletPublicKeyHash.toString(), + transactions.map((tx) => tx.transactionHash) + ) + tbtcContracts.bridge.setWallet( wallet.event.walletPublicKeyHash.toPrefixedString(), { @@ -292,7 +310,8 @@ describe("Redemptions", () => { ) }) - bitcoinClient.transactionHistory = walletsTransactionHistory + bitcoinClient.transactions = walletTransactions + bitcoinClient.transactionHashes = walletTransactionHashes redemptionsService = new TestRedemptionsService( tbtcContracts, @@ -538,9 +557,10 @@ describe("Redemptions", () => { } } - // Create a fake wallet witness transaction history that consists of 6 transactions. - const walletWitnessTransactionHistory: BitcoinTx[] = [ + // Create a fake wallet transaction history. + const walletTransactionHistory: BitcoinTx[] = [ mockTransaction( + // Witness transaction "3ca4ae3f8ee3b48949192bc7a146c8d9862267816258c85e02a44678364551e1", { "0014e6f9d74726b19b75f16fe1e9feaec048aa4fa1d0": 100000, // wallet witness output @@ -548,6 +568,7 @@ describe("Redemptions", () => { } ), mockTransaction( + // Witness transaction "4c6b33b7c0550e0e536a5d119ac7189d71e1296fcb0c258e0c115356895bc0e6", { "00140000000000000000000000000000000000000001": 100000, @@ -555,6 +576,7 @@ describe("Redemptions", () => { } ), mockTransaction( + // Witness transaction "44863a79ce2b8fec9792403d5048506e50ffa7338191db0e6c30d3d3358ea2f6", { "00140000000000000000000000000000000000000001": 100000, @@ -563,6 +585,7 @@ describe("Redemptions", () => { } ), mockTransaction( + // Witness transaction "f65bc5029251f0042aedb37f90dbb2bfb63a2e81694beef9cae5ec62e954c22e", { "0014e6f9d74726b19b75f16fe1e9feaec048aa4fa1d0": 100000, // wallet witness output @@ -570,6 +593,7 @@ describe("Redemptions", () => { } ), mockTransaction( + // Witness transaction "2724545276df61f43f1e92c4b9f1dd3c9109595c022dbd9dc003efbad8ded38b", { "0014e6f9d74726b19b75f16fe1e9feaec048aa4fa1d0": 100000, // wallet witness output @@ -577,17 +601,15 @@ describe("Redemptions", () => { } ), mockTransaction( + // Witness transaction "ea374ab6842723c647c3fc0ab281ca0641eaa768576cf9df695ca5b827140214", { "00140000000000000000000000000000000000000001": 100000, "0014e6f9d74726b19b75f16fe1e9feaec048aa4fa1d0": 200000, // wallet witness output } ), - ] - - // Create a fake wallet legacy transaction history that consists of 6 transactions. - const walletLegacyTransactionHistory: BitcoinTx[] = [ mockTransaction( + // Legacy transaction "230a19d8867ff3f5b409e924d9dd6413188e215f9bb52f1c47de6154dac42267", { "00140000000000000000000000000000000000000001": 100000, @@ -595,6 +617,7 @@ describe("Redemptions", () => { } ), mockTransaction( + // Legacy transaction "b11bfc481b95769b8488bd661d5f61a35f7c3c757160494d63f6e04e532dfcb9", { "00140000000000000000000000000000000000000001": 100000, @@ -603,6 +626,7 @@ describe("Redemptions", () => { } ), mockTransaction( + // Legacy transaction "7e91580d989f8541489a37431381ff9babd596111232f1bc7a1a1ba503c27dee", { "76a914e6f9d74726b19b75f16fe1e9feaec048aa4fa1d088ac": 100000, // wallet legacy output @@ -610,6 +634,7 @@ describe("Redemptions", () => { } ), mockTransaction( + // Legacy transaction "5404e339ba82e6e52fcc24cb40029bed8425baa4c7f869626ef9de956186f910", { "76a914e6f9d74726b19b75f16fe1e9feaec048aa4fa1d088ac": 100000, // wallet legacy output @@ -617,6 +642,7 @@ describe("Redemptions", () => { } ), mockTransaction( + // Legacy transaction "05dabb0291c0a6aa522de5ded5cb6d14ee2159e7ff109d3ef0f21de128b56b94", { "76a914e6f9d74726b19b75f16fe1e9feaec048aa4fa1d088ac": 100000, // wallet legacy output @@ -624,6 +650,7 @@ describe("Redemptions", () => { } ), mockTransaction( + // Legacy transaction "00cc0cd13fc4de7a15cb41ab6d58f8b31c75b6b9b4194958c381441a67d09b08", { "00140000000000000000000000000000000000000001": 100000, @@ -669,134 +696,143 @@ describe("Redemptions", () => { }) context("when wallet main UTXO is set in the Bridge", () => { - const tests = [ - { - testName: "recent witness transaction", - // Set the main UTXO hash based on the latest transaction from walletWitnessTransactionHistory. - actualMainUtxo: { - transactionHash: Hex.from( - "ea374ab6842723c647c3fc0ab281ca0641eaa768576cf9df695ca5b827140214" - ), - outputIndex: 1, - value: BigNumber.from(200000), - }, - expectedMainUtxo: { - transactionHash: Hex.from( - "ea374ab6842723c647c3fc0ab281ca0641eaa768576cf9df695ca5b827140214" - ), - outputIndex: 1, - value: BigNumber.from(200000), - }, - }, - { - testName: "recent legacy transaction", - // Set the main UTXO hash based on the second last transaction from walletLegacyTransactionHistory. - actualMainUtxo: { - transactionHash: Hex.from( - "05dabb0291c0a6aa522de5ded5cb6d14ee2159e7ff109d3ef0f21de128b56b94" - ), - outputIndex: 0, - value: BigNumber.from(100000), - }, - expectedMainUtxo: { - transactionHash: Hex.from( - "05dabb0291c0a6aa522de5ded5cb6d14ee2159e7ff109d3ef0f21de128b56b94" - ), - outputIndex: 0, - value: BigNumber.from(100000), - }, - }, - { - testName: "old witness transaction", - // Set the main UTXO hash based on the oldest transaction from walletWitnessTransactionHistory. - actualMainUtxo: { - transactionHash: Hex.from( - "3ca4ae3f8ee3b48949192bc7a146c8d9862267816258c85e02a44678364551e1" - ), - outputIndex: 0, - value: BigNumber.from(100000), - }, - expectedMainUtxo: undefined, - }, - { - testName: "old legacy transaction", - // Set the main UTXO hash based on the oldest transaction from walletLegacyTransactionHistory. - actualMainUtxo: { - transactionHash: Hex.from( - "230a19d8867ff3f5b409e924d9dd6413188e215f9bb52f1c47de6154dac42267" - ), - outputIndex: 1, - value: BigNumber.from(200000), - }, - expectedMainUtxo: undefined, - }, - ] + context( + "when the transaction representing main UTXO could not be found", + () => { + // This scenario should never happen. It could only happen due to some + // serious error. + beforeEach(async () => { + tbtcContracts.bridge.setWallet( + walletPublicKeyHash.toPrefixedString(), + { + // Set main UTXO hash to some non-zero-filled hash. + mainUtxoHash: Hex.from( + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ), + } as Wallet + ) + }) + + it("should return undefined", async () => { + const mainUtxo = await redemptionsService.determineWalletMainUtxo( + walletPublicKeyHash, + BitcoinNetwork.Testnet + ) + + expect(mainUtxo).to.be.undefined + }) + } + ) - tests.forEach(({ testName, actualMainUtxo, expectedMainUtxo }) => { - context(`with main UTXO coming from ${testName}`, () => { - const networkTests = [ + context( + "when the transaction representing main UTXO could be found", + () => { + const tests = [ + { + testName: "recent witness transaction", + // Set the main UTXO hash based on the latest transaction from walletWitnessTransactionHistory. + mainUtxo: { + transactionHash: Hex.from( + "ea374ab6842723c647c3fc0ab281ca0641eaa768576cf9df695ca5b827140214" + ), + outputIndex: 1, + value: BigNumber.from(200000), + }, + }, + { + testName: "recent legacy transaction", + // Set the main UTXO hash based on the second last transaction from walletLegacyTransactionHistory. + mainUtxo: { + transactionHash: Hex.from( + "05dabb0291c0a6aa522de5ded5cb6d14ee2159e7ff109d3ef0f21de128b56b94" + ), + outputIndex: 0, + value: BigNumber.from(100000), + }, + }, { - networkTestName: "bitcoin testnet", - network: BitcoinNetwork.Testnet, + testName: "old witness transaction", + // Set the main UTXO hash based on the oldest transaction from walletWitnessTransactionHistory. + mainUtxo: { + transactionHash: Hex.from( + "3ca4ae3f8ee3b48949192bc7a146c8d9862267816258c85e02a44678364551e1" + ), + outputIndex: 0, + value: BigNumber.from(100000), + }, }, { - networkTestName: "bitcoin mainnet", - network: BitcoinNetwork.Mainnet, + testName: "old legacy transaction", + // Set the main UTXO hash based on the oldest transaction from walletLegacyTransactionHistory. + mainUtxo: { + transactionHash: Hex.from( + "230a19d8867ff3f5b409e924d9dd6413188e215f9bb52f1c47de6154dac42267" + ), + outputIndex: 1, + value: BigNumber.from(200000), + }, }, ] - networkTests.forEach(({ networkTestName, network }) => { - context(`with ${networkTestName} network`, () => { - beforeEach(async () => { - bitcoinNetwork = network - - const walletWitnessAddress = - BitcoinAddressConverter.publicKeyHashToAddress( - walletPublicKeyHash, - true, - bitcoinNetwork - ) - const walletLegacyAddress = - BitcoinAddressConverter.publicKeyHashToAddress( - walletPublicKeyHash, - false, - bitcoinNetwork - ) - - // Record the fake transaction history for both address types. - const transactionHistory = new Map() - transactionHistory.set( - walletWitnessAddress, - walletWitnessTransactionHistory - ) - transactionHistory.set( - walletLegacyAddress, - walletLegacyTransactionHistory - ) - bitcoinClient.transactionHistory = transactionHistory - - tbtcContracts.bridge.setWallet( - walletPublicKeyHash.toPrefixedString(), - { - mainUtxoHash: - tbtcContracts.bridge.buildUtxoHash(actualMainUtxo), - } as Wallet - ) - }) - - it("should return the expected main UTXO", async () => { - const mainUtxo = - await redemptionsService.determineWalletMainUtxo( - walletPublicKeyHash, - bitcoinNetwork - ) - - expect(mainUtxo).to.be.eql(expectedMainUtxo) + tests.forEach(({ testName, mainUtxo }) => { + context(`with main UTXO coming from ${testName}`, () => { + const networkTests = [ + { + networkTestName: "bitcoin testnet", + network: BitcoinNetwork.Testnet, + }, + { + networkTestName: "bitcoin mainnet", + network: BitcoinNetwork.Mainnet, + }, + ] + + networkTests.forEach(({ networkTestName, network }) => { + context(`with ${networkTestName} network`, () => { + beforeEach(async () => { + bitcoinNetwork = network + + // Record transaction and transaction hashes. + const transactions = new Map() + walletTransactionHistory.forEach((tx) => { + transactions.set(tx.transactionHash.toString(), tx) + }) + bitcoinClient.transactions = transactions + + const transactionHashes = new Map< + string, + BitcoinTxHash[] + >() + transactionHashes.set( + walletPublicKeyHash.toString(), + walletTransactionHistory.map((tx) => tx.transactionHash) + ) + bitcoinClient.transactionHashes = transactionHashes + + tbtcContracts.bridge.setWallet( + walletPublicKeyHash.toPrefixedString(), + { + mainUtxoHash: + tbtcContracts.bridge.buildUtxoHash(mainUtxo), + } as Wallet + ) + }) + + it("should return the expected main UTXO", async () => { + const mainUtxo = + await redemptionsService.determineWalletMainUtxo( + walletPublicKeyHash, + bitcoinNetwork + ) + + expect(mainUtxo).to.be.eql(mainUtxo) + }) + }) }) }) }) - }) - }) + } + ) }) }) }) diff --git a/typescript/test/utils/mock-bitcoin-client.ts b/typescript/test/utils/mock-bitcoin-client.ts index e6274b807..2463bee87 100644 --- a/typescript/test/utils/mock-bitcoin-client.ts +++ b/typescript/test/utils/mock-bitcoin-client.ts @@ -17,7 +17,7 @@ export class MockBitcoinClient implements BitcoinClient { private _rawTransactions = new Map() private _transactions = new Map() private _confirmations = new Map() - private _txHashes = new Map() + private _transactionHashes = new Map() private _latestHeight = 0 private _headersChain = Hex.from("") private _transactionMerkle: BitcoinTxMerkleBranch = { @@ -44,8 +44,8 @@ export class MockBitcoinClient implements BitcoinClient { this._confirmations = value } - set txHashes(value: Map) { - this._txHashes = value + set transactionHashes(value: Map) { + this._transactionHashes = value } set latestHeight(value: number) { @@ -118,7 +118,12 @@ export class MockBitcoinClient implements BitcoinClient { getTxHashesForPublicKeyHash(publicKeyHash: Hex): Promise { return new Promise((resolve, _) => { - resolve(this._txHashes.get(publicKeyHash.toString()) as BitcoinTxHash[]) + const hashes = this._transactionHashes.get(publicKeyHash.toString()) + if (hashes) { + resolve(hashes) + } else { + resolve([]) + } }) } From cbba148ebfd62f25f48a072560b2d63360d85023 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Mon, 6 Nov 2023 12:12:23 +0100 Subject: [PATCH 4/5] Reduced number of requests to Electrum server --- typescript/src/lib/electrum/client.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/typescript/src/lib/electrum/client.ts b/typescript/src/lib/electrum/client.ts index de3736a91..a773cb86f 100644 --- a/typescript/src/lib/electrum/client.ts +++ b/typescript/src/lib/electrum/client.ts @@ -507,14 +507,14 @@ export class ElectrumClient implements BitcoinClient { */ getTxHashesForPublicKeyHash(publicKeyHash: Hex): Promise { return this.withElectrum(async (electrum: Electrum) => { + const bitcoinNetwork = await this.getNetwork() + // eslint-disable-next-line camelcase type HistoryItem = { height: number; tx_hash: string } const getConfirmedHistory = async ( witnessAddress: boolean ): Promise => { - const bitcoinNetwork = await this.getNetwork() - const address = BitcoinAddressConverter.publicKeyHashToAddress( publicKeyHash, witnessAddress, From c55a7aa6f53cdb1707bf6951c267e9d5fda55df9 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Mon, 6 Nov 2023 12:29:55 +0100 Subject: [PATCH 5/5] Regenerated docs --- typescript/api-reference/README.md | 2 +- .../api-reference/classes/ElectrumClient.md | 35 +++++++++++++++-- .../classes/RedemptionsService.md | 9 +---- .../api-reference/interfaces/BitcoinClient.md | 39 +++++++++++++++++-- 4 files changed, 69 insertions(+), 16 deletions(-) diff --git a/typescript/api-reference/README.md b/typescript/api-reference/README.md index 3184171aa..b5e639aa6 100644 --- a/typescript/api-reference/README.md +++ b/typescript/api-reference/README.md @@ -685,7 +685,7 @@ Electrum script hash as a hex string. #### Defined in -[lib/electrum/client.ts:591](https://github.com/keep-network/tbtc-v2/blob/main/typescript/src/lib/electrum/client.ts#L591) +[lib/electrum/client.ts:649](https://github.com/keep-network/tbtc-v2/blob/main/typescript/src/lib/electrum/client.ts#L649) ___ diff --git a/typescript/api-reference/classes/ElectrumClient.md b/typescript/api-reference/classes/ElectrumClient.md index f617ed98e..bef3ba335 100644 --- a/typescript/api-reference/classes/ElectrumClient.md +++ b/typescript/api-reference/classes/ElectrumClient.md @@ -31,6 +31,7 @@ Electrum-based implementation of the Bitcoin client. - [getTransactionConfirmations](ElectrumClient.md#gettransactionconfirmations) - [getTransactionHistory](ElectrumClient.md#gettransactionhistory) - [getTransactionMerkle](ElectrumClient.md#gettransactionmerkle) +- [getTxHashesForPublicKeyHash](ElectrumClient.md#gettxhashesforpublickeyhash) - [latestBlockHeight](ElectrumClient.md#latestblockheight) - [withBackoffRetrier](ElectrumClient.md#withbackoffretrier) - [withElectrum](ElectrumClient.md#withelectrum) @@ -136,7 +137,7 @@ ___ #### Defined in -[lib/electrum/client.ts:574](https://github.com/keep-network/tbtc-v2/blob/main/typescript/src/lib/electrum/client.ts#L574) +[lib/electrum/client.ts:632](https://github.com/keep-network/tbtc-v2/blob/main/typescript/src/lib/electrum/client.ts#L632) ___ @@ -189,7 +190,7 @@ ___ #### Defined in -[lib/electrum/client.ts:524](https://github.com/keep-network/tbtc-v2/blob/main/typescript/src/lib/electrum/client.ts#L524) +[lib/electrum/client.ts:582](https://github.com/keep-network/tbtc-v2/blob/main/typescript/src/lib/electrum/client.ts#L582) ___ @@ -341,7 +342,33 @@ ___ #### Defined in -[lib/electrum/client.ts:543](https://github.com/keep-network/tbtc-v2/blob/main/typescript/src/lib/electrum/client.ts#L543) +[lib/electrum/client.ts:601](https://github.com/keep-network/tbtc-v2/blob/main/typescript/src/lib/electrum/client.ts#L601) + +___ + +### getTxHashesForPublicKeyHash + +▸ **getTxHashesForPublicKeyHash**(`publicKeyHash`): `Promise`\<[`BitcoinTxHash`](BitcoinTxHash.md)[]\> + +#### Parameters + +| Name | Type | +| :------ | :------ | +| `publicKeyHash` | [`Hex`](Hex.md) | + +#### Returns + +`Promise`\<[`BitcoinTxHash`](BitcoinTxHash.md)[]\> + +**`See`** + +#### Implementation of + +[BitcoinClient](../interfaces/BitcoinClient.md).[getTxHashesForPublicKeyHash](../interfaces/BitcoinClient.md#gettxhashesforpublickeyhash) + +#### Defined in + +[lib/electrum/client.ts:508](https://github.com/keep-network/tbtc-v2/blob/main/typescript/src/lib/electrum/client.ts#L508) ___ @@ -361,7 +388,7 @@ ___ #### Defined in -[lib/electrum/client.ts:508](https://github.com/keep-network/tbtc-v2/blob/main/typescript/src/lib/electrum/client.ts#L508) +[lib/electrum/client.ts:566](https://github.com/keep-network/tbtc-v2/blob/main/typescript/src/lib/electrum/client.ts#L566) ___ diff --git a/typescript/api-reference/classes/RedemptionsService.md b/typescript/api-reference/classes/RedemptionsService.md index f92529d8a..5a65618a0 100644 --- a/typescript/api-reference/classes/RedemptionsService.md +++ b/typescript/api-reference/classes/RedemptionsService.md @@ -75,11 +75,6 @@ Determines the plain-text wallet main UTXO currently registered in the Bridge on-chain contract. The returned main UTXO can be undefined if the wallet does not have a main UTXO registered in the Bridge at the moment. -WARNING: THIS FUNCTION CANNOT DETERMINE THE MAIN UTXO IF IT COMES FROM A -BITCOIN TRANSACTION THAT IS NOT ONE OF THE LATEST FIVE TRANSACTIONS -TARGETING THE GIVEN WALLET PUBLIC KEY HASH. HOWEVER, SUCH A CASE IS -VERY UNLIKELY. - #### Parameters | Name | Type | Description | @@ -95,7 +90,7 @@ Promise holding the wallet main UTXO or undefined value. #### Defined in -[services/redemptions/redemptions-service.ts:221](https://github.com/keep-network/tbtc-v2/blob/main/typescript/src/services/redemptions/redemptions-service.ts#L221) +[services/redemptions/redemptions-service.ts:215](https://github.com/keep-network/tbtc-v2/blob/main/typescript/src/services/redemptions/redemptions-service.ts#L215) ___ @@ -152,7 +147,7 @@ Throws an error if no redemption request exists for the given #### Defined in -[services/redemptions/redemptions-service.ts:337](https://github.com/keep-network/tbtc-v2/blob/main/typescript/src/services/redemptions/redemptions-service.ts#L337) +[services/redemptions/redemptions-service.ts:327](https://github.com/keep-network/tbtc-v2/blob/main/typescript/src/services/redemptions/redemptions-service.ts#L327) ___ diff --git a/typescript/api-reference/interfaces/BitcoinClient.md b/typescript/api-reference/interfaces/BitcoinClient.md index 67e3358f1..3afac0bdd 100644 --- a/typescript/api-reference/interfaces/BitcoinClient.md +++ b/typescript/api-reference/interfaces/BitcoinClient.md @@ -19,6 +19,7 @@ Represents a Bitcoin client. - [getTransactionConfirmations](BitcoinClient.md#gettransactionconfirmations) - [getTransactionHistory](BitcoinClient.md#gettransactionhistory) - [getTransactionMerkle](BitcoinClient.md#gettransactionmerkle) +- [getTxHashesForPublicKeyHash](BitcoinClient.md#gettxhashesforpublickeyhash) - [latestBlockHeight](BitcoinClient.md#latestblockheight) ## Methods @@ -41,7 +42,7 @@ Broadcasts the given transaction over the network. #### Defined in -[lib/bitcoin/client.ts:87](https://github.com/keep-network/tbtc-v2/blob/main/typescript/src/lib/bitcoin/client.ts#L87) +[lib/bitcoin/client.ts:101](https://github.com/keep-network/tbtc-v2/blob/main/typescript/src/lib/bitcoin/client.ts#L101) ___ @@ -90,7 +91,7 @@ Concatenation of block headers in a hexadecimal format. #### Defined in -[lib/bitcoin/client.ts:70](https://github.com/keep-network/tbtc-v2/blob/main/typescript/src/lib/bitcoin/client.ts#L70) +[lib/bitcoin/client.ts:84](https://github.com/keep-network/tbtc-v2/blob/main/typescript/src/lib/bitcoin/client.ts#L84) ___ @@ -232,7 +233,37 @@ Merkle branch. #### Defined in -[lib/bitcoin/client.ts:78](https://github.com/keep-network/tbtc-v2/blob/main/typescript/src/lib/bitcoin/client.ts#L78) +[lib/bitcoin/client.ts:92](https://github.com/keep-network/tbtc-v2/blob/main/typescript/src/lib/bitcoin/client.ts#L92) + +___ + +### getTxHashesForPublicKeyHash + +▸ **getTxHashesForPublicKeyHash**(`publicKeyHash`): `Promise`\<[`BitcoinTxHash`](../classes/BitcoinTxHash.md)[]\> + +Gets hashes of confirmed transactions that pay the given public key hash +using either a P2PKH or P2WPKH script. The returned transactions hashes are +ordered by block height in the ascending order, i.e. the latest transaction +hash is at the end of the list. The returned list does not contain +unconfirmed transactions hashes living in the mempool at the moment of +request. + +#### Parameters + +| Name | Type | Description | +| :------ | :------ | :------ | +| `publicKeyHash` | [`Hex`](../classes/Hex.md) | Hash of the public key for which to find corresponding transaction hashes. | + +#### Returns + +`Promise`\<[`BitcoinTxHash`](../classes/BitcoinTxHash.md)[]\> + +Array of confirmed transaction hashes related to the provided + public key hash. + +#### Defined in + +[lib/bitcoin/client.ts:69](https://github.com/keep-network/tbtc-v2/blob/main/typescript/src/lib/bitcoin/client.ts#L69) ___ @@ -250,4 +281,4 @@ Height of the last mined block. #### Defined in -[lib/bitcoin/client.ts:61](https://github.com/keep-network/tbtc-v2/blob/main/typescript/src/lib/bitcoin/client.ts#L61) +[lib/bitcoin/client.ts:75](https://github.com/keep-network/tbtc-v2/blob/main/typescript/src/lib/bitcoin/client.ts#L75)