Skip to content

Commit

Permalink
TS: Support Blockstream's Esplora/Electrs servers for electrum protoc…
Browse files Browse the repository at this point in the history
…ol 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).
  • Loading branch information
lukasz-zimnoch authored Jan 26, 2023
2 parents e95ab1f + 4bc9085 commit 99d10e5
Show file tree
Hide file tree
Showing 7 changed files with 299 additions and 199 deletions.
4 changes: 2 additions & 2 deletions typescript/src/bitcoin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export type TransactionInput = TransactionOutpoint & {
/**
* The scriptSig that unlocks the specified outpoint for spending.
*/
scriptSig: any
scriptSig: Hex
}

/**
Expand All @@ -86,7 +86,7 @@ export interface TransactionOutput {
/**
* The receiving scriptPubKey.
*/
scriptPubKey: any
scriptPubKey: Hex
}

/**
Expand Down
113 changes: 95 additions & 18 deletions typescript/src/electrum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import Electrum from "electrum-client-js"
import sha256 from "bcrypto/lib/sha256-browser.js"
import { BigNumber } from "ethers"
import { URL } from "url"
import { Hex } from "./hex"

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

const inputs = transaction.vin.map(
if (!rawTransaction) {
throw new Error(`Transaction not found`)
}

// Decode the raw transaction.
const transaction = bcoin.TX.fromRaw(rawTransaction, "hex")

const inputs = transaction.inputs.map(
(input: any): TransactionInput => ({
transactionHash: TransactionHash.from(input.txid),
outputIndex: input.vout,
scriptSig: input.scriptSig,
transactionHash: TransactionHash.from(input.prevout.hash).reverse(),
outputIndex: input.prevout.index,
scriptSig: Hex.from(input.script.toRaw()),
})
)

const outputs = transaction.vout.map(
(output: any): TransactionOutput => ({
outputIndex: output.n,
// The `output.value` is in BTC so it must be converted to satoshis.
value: BigNumber.from((parseFloat(output.value) * 1e8).toFixed(0)),
scriptPubKey: output.scriptPubKey,
const outputs = transaction.outputs.map(
(output: any, i: number): TransactionOutput => ({
outputIndex: i,
value: BigNumber.from(output.value),
scriptPubKey: Hex.from(output.script.toRaw()),
})
)

return {
transactionHash: TransactionHash.from(transaction.txid),
transactionHash: TransactionHash.from(transaction.hash()).reverse(),
inputs: inputs,
outputs: outputs,
}
Expand Down Expand Up @@ -203,15 +214,81 @@ export class Client implements BitcoinClient {
getTransactionConfirmations(
transactionHash: TransactionHash
): Promise<number> {
// We cannot use `blockchain_transaction_get` with `verbose = true` argument
// to get the the transaction details as Esplora/Electrs doesn't support verbose
// transactions.
// See: https://github.com/Blockstream/electrs/pull/36

return this.withElectrum<number>(async (electrum: any) => {
const transaction = await electrum.blockchain_transaction_get(
const rawTransaction: string = await electrum.blockchain_transaction_get(
transactionHash.toString(),
true
false
)

// For unconfirmed transactions `confirmations` property may be undefined, so
// we will return 0 instead.
return transaction.confirmations ?? 0
// Decode the raw transaction.
const transaction = bcoin.TX.fromRaw(rawTransaction, "hex")

// As a workaround for the problem described in https://github.com/Blockstream/electrs/pull/36
// we need to calculate the number of confirmations based on the latest
// block height and block height of the transaction.
// Electrum protocol doesn't expose a function to get the transaction's block
// height (other that the `GetTransaction` that is unsupported by Esplora/Electrs).
// To get the block height of the transaction we query the history of transactions
// for the output script hash, as the history contains the transaction's block
// height.

// Initialize txBlockHeigh with minimum int32 value to identify a problem when
// a block height was not found in a history of any of the script hashes.
//
// The history is expected to return a block height for confirmed transaction.
// If a transaction is unconfirmed (is still in the mempool) the height will
// have a value of `0` or `-1`.
let txBlockHeight: number = Math.min()
for (const output of transaction.outputs) {
const scriptHash: Buffer = output.script.sha256()

type HistoryEntry = {
// eslint-disable-next-line camelcase
tx_hash: string
height: number
}

const scriptHashHistory: HistoryEntry[] =
await electrum.blockchain_scripthash_getHistory(
scriptHash.reverse().toString("hex")
)

const tx = scriptHashHistory.find(
(t) => t.tx_hash === transactionHash.toString()
)

if (tx) {
txBlockHeight = tx.height
break
}
}

// History querying didn't come up with the transaction's block height. Return
// an error.
if (txBlockHeight === Math.min()) {
throw new Error(
"failed to find the transaction block height in script hashes' histories"
)
}

// If the block height is greater than `0` the transaction is confirmed.
if (txBlockHeight > 0) {
const latestBlockHeight: number = await this.latestBlockHeight()

if (latestBlockHeight >= txBlockHeight) {
// Add `1` to the calculated difference as if the transaction block
// height equals the latest block height the transaction is already
// confirmed, so it has one confirmation.
return latestBlockHeight - txBlockHeight + 1
}
}

return 0
})
}

Expand Down
19 changes: 8 additions & 11 deletions typescript/test/data/deposit-sweep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
import { calculateDepositRefundLocktime, Deposit } from "../../src/deposit"
import { BigNumber } from "ethers"
import { Address } from "../../src/ethereum"
import { Hex } from "../../src"

export const NO_MAIN_UTXO = {
transactionHash: TransactionHash.from(""),
Expand Down Expand Up @@ -406,41 +407,37 @@ export const depositSweepProof: DepositSweepProofTestData = {
"ea4d9e45f8c1b8a187c007f36ba1e9b201e8511182c7083c4edcaf9325b2998f"
),
outputIndex: 0,
scriptSig: { asm: "", hex: "" },
scriptSig: Hex.from(""),
},
{
transactionHash: TransactionHash.from(
"c844ff4c1781c884bb5e80392398b81b984d7106367ae16675f132bd1a7f33fd"
),
outputIndex: 0,
scriptSig: { asm: "", hex: "" },
scriptSig: Hex.from(""),
},
{
transactionHash: TransactionHash.from(
"44c568bc0eac07a2a9c2b46829be5b5d46e7d00e17bfb613f506a75ccf86a473"
),
outputIndex: 0,
scriptSig: { asm: "", hex: "" },
scriptSig: Hex.from(""),
},
{
transactionHash: TransactionHash.from(
"f548c00e464764e112826450a00cf005ca771a6108a629b559b6c60a519e4378"
),
outputIndex: 0,
scriptSig: { asm: "", hex: "" },
scriptSig: Hex.from(""),
},
],
outputs: [
{
outputIndex: 0,
value: BigNumber.from(39800),
scriptPubKey: {
asm: "OP_0 8db50eb52063ea9d98b3eac91489a90f738986f6",
hex: "00148db50eb52063ea9d98b3eac91489a90f738986f6",
type: "WITNESSPUBKEYHASH",
reqSigs: 1,
addresses: ["tb1q3k6sadfqv04fmx9naty3fzdfpaecnphkfm3cf3"],
},
scriptPubKey: Hex.from(
"00148db50eb52063ea9d98b3eac91489a90f738986f6"
),
},
],
},
Expand Down
22 changes: 4 additions & 18 deletions typescript/test/data/electrum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
TransactionHash,
} from "../../src/bitcoin"
import { BigNumber } from "ethers"
import { Hex } from "../../src"

/**
* Bitcoin testnet address used for Electrum client tests.
Expand All @@ -27,35 +28,20 @@ export const testnetTransaction: Transaction = {
"c6ffe9e0f8cca057acad211023ff6b9d46604fbbcb76c6dd669c20b22985f802"
),
outputIndex: 1,
scriptSig: {
asm: "",
hex: "",
},
scriptSig: Hex.from(""),
},
],

outputs: [
{
outputIndex: 0,
value: BigNumber.from(101),
scriptPubKey: {
addresses: ["tb1qfdru0xx39mw30ha5a2vw23reymmxgucujfnc7l"],
asm: "OP_0 4b47c798d12edd17dfb4ea98e5447926f664731c",
hex: "00144b47c798d12edd17dfb4ea98e5447926f664731c",
reqSigs: 1,
type: "WITNESSPUBKEYHASH",
},
scriptPubKey: Hex.from("00144b47c798d12edd17dfb4ea98e5447926f664731c"),
},
{
outputIndex: 1,
value: BigNumber.from(9125),
scriptPubKey: {
addresses: ["tb1q78ezl08lyhuazzfz592sstenmegdns7durc4cl"],
asm: "OP_0 f1f22fbcff25f9d10922a155082f33de50d9c3cd",
hex: "0014f1f22fbcff25f9d10922a155082f33de50d9c3cd",
reqSigs: 1,
type: "WITNESSPUBKEYHASH",
},
scriptPubKey: Hex.from("0014f1f22fbcff25f9d10922a155082f33de50d9c3cd"),
},
],
}
Expand Down
Loading

0 comments on commit 99d10e5

Please sign in to comment.