Skip to content

Commit

Permalink
Added Bitcoin client function for getting tx hashes for public key hash
Browse files Browse the repository at this point in the history
  • Loading branch information
tomaszslabon committed Oct 31, 2023
1 parent 95ad1a2 commit 1413bab
Show file tree
Hide file tree
Showing 5 changed files with 135 additions and 0 deletions.
14 changes: 14 additions & 0 deletions typescript/src/lib/bitcoin/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,20 @@ export interface BitcoinClient {
*/
getTransactionConfirmations(transactionHash: BitcoinTxHash): Promise<number>

/**
* 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<BitcoinTxHash[]>

/**
* Gets height of the latest mined block.
* @return Height of the last mined block.
Expand Down
60 changes: 60 additions & 0 deletions typescript/src/lib/electrum/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,66 @@ export class ElectrumClient implements BitcoinClient {
})
}

// eslint-disable-next-line valid-jsdoc
/**
* @see {BitcoinClient#getTxHashesForPublicKeyHash}
*/
getTxHashesForPublicKeyHash(publicKeyHash: Hex): Promise<BitcoinTxHash[]> {
return this.withElectrum<BitcoinTxHash[]>(async (electrum: Electrum) => {
// eslint-disable-next-line camelcase
type HistoryItem = { height: number; tx_hash: string }

const getConfirmedHistory = async (
witnessAddress: boolean
): Promise<HistoryItem[]> => {
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}
Expand Down
28 changes: 28 additions & 0 deletions typescript/test/data/electrum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
),
]
26 changes: 26 additions & 0 deletions typescript/test/lib/electrum.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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

Expand Down
7 changes: 7 additions & 0 deletions typescript/test/utils/mock-bitcoin-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export class MockBitcoinClient implements BitcoinClient {
private _rawTransactions = new Map<string, BitcoinRawTx>()
private _transactions = new Map<string, BitcoinTx>()
private _confirmations = new Map<string, number>()
private _txHashes = new Map<string, BitcoinTxHash[]>()
private _latestHeight = 0
private _headersChain = Hex.from("")
private _transactionMerkle: BitcoinTxMerkleBranch = {
Expand Down Expand Up @@ -111,6 +112,12 @@ export class MockBitcoinClient implements BitcoinClient {
})
}

getTxHashesForPublicKeyHash(publicKeyHash: Hex): Promise<BitcoinTxHash[]> {
return new Promise<BitcoinTxHash[]>((resolve, _) => {
resolve(this._txHashes.get(publicKeyHash.toString()) as BitcoinTxHash[])
})
}

latestBlockHeight(): Promise<number> {
return new Promise<number>((resolve, _) => {
resolve(this._latestHeight)
Expand Down

0 comments on commit 1413bab

Please sign in to comment.