Skip to content

Commit 99d10e5

Browse files
TS: Support Blockstream's Esplora/Electrs servers for electrum protocol integration (#501)
Closes: #422 In this PR we update the implementation of TypeScript Electrum integration to support Esplora/Electrs servers. As per Blockstream/electrs#36 verbose transactions are not supported by Esplora/Electrs. This affects our implementation of `getTransaction` and `getTransactionConfirmations` functions. For a consistent code in the client without alternative paths for different Electrum servers implementations I decided to not use verbose transactions at all. ### [getTransactionConfirmations](26d4f01) 1. Get the raw transaction 2. Deserialize the raw transaction 3. Find transaction block height by finding it in a history of transactions for the output script included in the transaction. 4. Get the latest block height 5. Calculate number of confirmations by subtracting the transaction block height from the latest block height and adding one. ### [getTransaction](a7aedd1) We get a raw transaction and deserialize it with `bcoin`. This lets us define a consistent type for returned transactions (093d4ec). Before these changes, I observed that Electrum server implementations are not consistent with data returned in verbose JSON. ### Electrum tests We can test electrum integration against different kinds of servers. The most popular implementations are: - ElectrumX - Fulcrum - Electrs/Esplora We can find a list of public servers here: https://1209k.com/bitcoin-eye/ele.php?chain=tbtc The electrs-esplora server seems pretty unstable, so we don't want to enable it in tests until we add retries (#485).
2 parents e95ab1f + 4bc9085 commit 99d10e5

File tree

7 files changed

+299
-199
lines changed

7 files changed

+299
-199
lines changed

typescript/src/bitcoin.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ export type TransactionInput = TransactionOutpoint & {
6666
/**
6767
* The scriptSig that unlocks the specified outpoint for spending.
6868
*/
69-
scriptSig: any
69+
scriptSig: Hex
7070
}
7171

7272
/**
@@ -86,7 +86,7 @@ export interface TransactionOutput {
8686
/**
8787
* The receiving scriptPubKey.
8888
*/
89-
scriptPubKey: any
89+
scriptPubKey: Hex
9090
}
9191

9292
/**

typescript/src/electrum.ts

Lines changed: 95 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import Electrum from "electrum-client-js"
1313
import sha256 from "bcrypto/lib/sha256-browser.js"
1414
import { BigNumber } from "ethers"
1515
import { URL } from "url"
16+
import { Hex } from "./hex"
1617

1718
/**
1819
* Represents a set of credentials required to establish an Electrum connection.
@@ -149,30 +150,40 @@ export class Client implements BitcoinClient {
149150
*/
150151
getTransaction(transactionHash: TransactionHash): Promise<Transaction> {
151152
return this.withElectrum<Transaction>(async (electrum: any) => {
152-
const transaction = await electrum.blockchain_transaction_get(
153+
// We cannot use `blockchain_transaction_get` with `verbose = true` argument
154+
// to get the the transaction details as Esplora/Electrs doesn't support verbose
155+
// transactions.
156+
// See: https://github.com/Blockstream/electrs/pull/36
157+
const rawTransaction = await electrum.blockchain_transaction_get(
153158
transactionHash.toString(),
154-
true
159+
false
155160
)
156161

157-
const inputs = transaction.vin.map(
162+
if (!rawTransaction) {
163+
throw new Error(`Transaction not found`)
164+
}
165+
166+
// Decode the raw transaction.
167+
const transaction = bcoin.TX.fromRaw(rawTransaction, "hex")
168+
169+
const inputs = transaction.inputs.map(
158170
(input: any): TransactionInput => ({
159-
transactionHash: TransactionHash.from(input.txid),
160-
outputIndex: input.vout,
161-
scriptSig: input.scriptSig,
171+
transactionHash: TransactionHash.from(input.prevout.hash).reverse(),
172+
outputIndex: input.prevout.index,
173+
scriptSig: Hex.from(input.script.toRaw()),
162174
})
163175
)
164176

165-
const outputs = transaction.vout.map(
166-
(output: any): TransactionOutput => ({
167-
outputIndex: output.n,
168-
// The `output.value` is in BTC so it must be converted to satoshis.
169-
value: BigNumber.from((parseFloat(output.value) * 1e8).toFixed(0)),
170-
scriptPubKey: output.scriptPubKey,
177+
const outputs = transaction.outputs.map(
178+
(output: any, i: number): TransactionOutput => ({
179+
outputIndex: i,
180+
value: BigNumber.from(output.value),
181+
scriptPubKey: Hex.from(output.script.toRaw()),
171182
})
172183
)
173184

174185
return {
175-
transactionHash: TransactionHash.from(transaction.txid),
186+
transactionHash: TransactionHash.from(transaction.hash()).reverse(),
176187
inputs: inputs,
177188
outputs: outputs,
178189
}
@@ -203,15 +214,81 @@ export class Client implements BitcoinClient {
203214
getTransactionConfirmations(
204215
transactionHash: TransactionHash
205216
): Promise<number> {
217+
// We cannot use `blockchain_transaction_get` with `verbose = true` argument
218+
// to get the the transaction details as Esplora/Electrs doesn't support verbose
219+
// transactions.
220+
// See: https://github.com/Blockstream/electrs/pull/36
221+
206222
return this.withElectrum<number>(async (electrum: any) => {
207-
const transaction = await electrum.blockchain_transaction_get(
223+
const rawTransaction: string = await electrum.blockchain_transaction_get(
208224
transactionHash.toString(),
209-
true
225+
false
210226
)
211227

212-
// For unconfirmed transactions `confirmations` property may be undefined, so
213-
// we will return 0 instead.
214-
return transaction.confirmations ?? 0
228+
// Decode the raw transaction.
229+
const transaction = bcoin.TX.fromRaw(rawTransaction, "hex")
230+
231+
// As a workaround for the problem described in https://github.com/Blockstream/electrs/pull/36
232+
// we need to calculate the number of confirmations based on the latest
233+
// block height and block height of the transaction.
234+
// Electrum protocol doesn't expose a function to get the transaction's block
235+
// height (other that the `GetTransaction` that is unsupported by Esplora/Electrs).
236+
// To get the block height of the transaction we query the history of transactions
237+
// for the output script hash, as the history contains the transaction's block
238+
// height.
239+
240+
// Initialize txBlockHeigh with minimum int32 value to identify a problem when
241+
// a block height was not found in a history of any of the script hashes.
242+
//
243+
// The history is expected to return a block height for confirmed transaction.
244+
// If a transaction is unconfirmed (is still in the mempool) the height will
245+
// have a value of `0` or `-1`.
246+
let txBlockHeight: number = Math.min()
247+
for (const output of transaction.outputs) {
248+
const scriptHash: Buffer = output.script.sha256()
249+
250+
type HistoryEntry = {
251+
// eslint-disable-next-line camelcase
252+
tx_hash: string
253+
height: number
254+
}
255+
256+
const scriptHashHistory: HistoryEntry[] =
257+
await electrum.blockchain_scripthash_getHistory(
258+
scriptHash.reverse().toString("hex")
259+
)
260+
261+
const tx = scriptHashHistory.find(
262+
(t) => t.tx_hash === transactionHash.toString()
263+
)
264+
265+
if (tx) {
266+
txBlockHeight = tx.height
267+
break
268+
}
269+
}
270+
271+
// History querying didn't come up with the transaction's block height. Return
272+
// an error.
273+
if (txBlockHeight === Math.min()) {
274+
throw new Error(
275+
"failed to find the transaction block height in script hashes' histories"
276+
)
277+
}
278+
279+
// If the block height is greater than `0` the transaction is confirmed.
280+
if (txBlockHeight > 0) {
281+
const latestBlockHeight: number = await this.latestBlockHeight()
282+
283+
if (latestBlockHeight >= txBlockHeight) {
284+
// Add `1` to the calculated difference as if the transaction block
285+
// height equals the latest block height the transaction is already
286+
// confirmed, so it has one confirmation.
287+
return latestBlockHeight - txBlockHeight + 1
288+
}
289+
}
290+
291+
return 0
215292
})
216293
}
217294

typescript/test/data/deposit-sweep.ts

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
import { calculateDepositRefundLocktime, Deposit } from "../../src/deposit"
1111
import { BigNumber } from "ethers"
1212
import { Address } from "../../src/ethereum"
13+
import { Hex } from "../../src"
1314

1415
export const NO_MAIN_UTXO = {
1516
transactionHash: TransactionHash.from(""),
@@ -406,41 +407,37 @@ export const depositSweepProof: DepositSweepProofTestData = {
406407
"ea4d9e45f8c1b8a187c007f36ba1e9b201e8511182c7083c4edcaf9325b2998f"
407408
),
408409
outputIndex: 0,
409-
scriptSig: { asm: "", hex: "" },
410+
scriptSig: Hex.from(""),
410411
},
411412
{
412413
transactionHash: TransactionHash.from(
413414
"c844ff4c1781c884bb5e80392398b81b984d7106367ae16675f132bd1a7f33fd"
414415
),
415416
outputIndex: 0,
416-
scriptSig: { asm: "", hex: "" },
417+
scriptSig: Hex.from(""),
417418
},
418419
{
419420
transactionHash: TransactionHash.from(
420421
"44c568bc0eac07a2a9c2b46829be5b5d46e7d00e17bfb613f506a75ccf86a473"
421422
),
422423
outputIndex: 0,
423-
scriptSig: { asm: "", hex: "" },
424+
scriptSig: Hex.from(""),
424425
},
425426
{
426427
transactionHash: TransactionHash.from(
427428
"f548c00e464764e112826450a00cf005ca771a6108a629b559b6c60a519e4378"
428429
),
429430
outputIndex: 0,
430-
scriptSig: { asm: "", hex: "" },
431+
scriptSig: Hex.from(""),
431432
},
432433
],
433434
outputs: [
434435
{
435436
outputIndex: 0,
436437
value: BigNumber.from(39800),
437-
scriptPubKey: {
438-
asm: "OP_0 8db50eb52063ea9d98b3eac91489a90f738986f6",
439-
hex: "00148db50eb52063ea9d98b3eac91489a90f738986f6",
440-
type: "WITNESSPUBKEYHASH",
441-
reqSigs: 1,
442-
addresses: ["tb1q3k6sadfqv04fmx9naty3fzdfpaecnphkfm3cf3"],
443-
},
438+
scriptPubKey: Hex.from(
439+
"00148db50eb52063ea9d98b3eac91489a90f738986f6"
440+
),
444441
},
445442
],
446443
},

typescript/test/data/electrum.ts

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
TransactionHash,
77
} from "../../src/bitcoin"
88
import { BigNumber } from "ethers"
9+
import { Hex } from "../../src"
910

1011
/**
1112
* Bitcoin testnet address used for Electrum client tests.
@@ -27,35 +28,20 @@ export const testnetTransaction: Transaction = {
2728
"c6ffe9e0f8cca057acad211023ff6b9d46604fbbcb76c6dd669c20b22985f802"
2829
),
2930
outputIndex: 1,
30-
scriptSig: {
31-
asm: "",
32-
hex: "",
33-
},
31+
scriptSig: Hex.from(""),
3432
},
3533
],
3634

3735
outputs: [
3836
{
3937
outputIndex: 0,
4038
value: BigNumber.from(101),
41-
scriptPubKey: {
42-
addresses: ["tb1qfdru0xx39mw30ha5a2vw23reymmxgucujfnc7l"],
43-
asm: "OP_0 4b47c798d12edd17dfb4ea98e5447926f664731c",
44-
hex: "00144b47c798d12edd17dfb4ea98e5447926f664731c",
45-
reqSigs: 1,
46-
type: "WITNESSPUBKEYHASH",
47-
},
39+
scriptPubKey: Hex.from("00144b47c798d12edd17dfb4ea98e5447926f664731c"),
4840
},
4941
{
5042
outputIndex: 1,
5143
value: BigNumber.from(9125),
52-
scriptPubKey: {
53-
addresses: ["tb1q78ezl08lyhuazzfz592sstenmegdns7durc4cl"],
54-
asm: "OP_0 f1f22fbcff25f9d10922a155082f33de50d9c3cd",
55-
hex: "0014f1f22fbcff25f9d10922a155082f33de50d9c3cd",
56-
reqSigs: 1,
57-
type: "WITNESSPUBKEYHASH",
58-
},
44+
scriptPubKey: Hex.from("0014f1f22fbcff25f9d10922a155082f33de50d9c3cd"),
5945
},
6046
],
6147
}

0 commit comments

Comments
 (0)