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..eba15cf78 100644 --- a/typescript/src/lib/electrum/client.ts +++ b/typescript/src/lib/electrum/client.ts @@ -498,6 +498,66 @@ 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( + (item1, item2) => item1.height - item2.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)