From 92a43b0df1391f97323f38dbb7ce2218e5b8e27e Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Tue, 10 Aug 2021 15:24:04 +0200 Subject: [PATCH 1/4] Removed unused functions --- src/BitcoinHelpers.js | 49 -------------------------------- src/lib/BitcoinSPV.js | 39 -------------------------- src/lib/ElectrumClient.js | 57 -------------------------------------- test/BitcoinSPVTest.js | 15 ---------- test/ElectrumClientTest.js | 55 ------------------------------------ 5 files changed, 215 deletions(-) diff --git a/src/BitcoinHelpers.js b/src/BitcoinHelpers.js index d58696a1..8ce57950 100644 --- a/src/BitcoinHelpers.js +++ b/src/BitcoinHelpers.js @@ -661,26 +661,6 @@ const BitcoinHelpers = { return transaction.toRaw().toString("hex") }, - /** - * Finds all transactions containing unspent outputs received - * by the `bitcoinAddress`. - * - * @param {string} bitcoinAddress Bitcoin address to check. - * - * @return {Promise} A promise to an array of - * transactions with accompanying information about the output - * position and value pointed at the specified receiver script. - * Resolves with an empty array if no such transactions exist. - */ - findAllUnspent: async function(bitcoinAddress) { - return await BitcoinHelpers.withElectrumClient(async electrumClient => { - const script = BitcoinHelpers.Address.toScript(bitcoinAddress) - return BitcoinHelpers.Transaction.findAllUnspentWithClient( - electrumClient, - script - ) - }) - }, /** * Gets the confirmed balance of the `bitcoinAddress`. * @@ -798,35 +778,6 @@ const BitcoinHelpers = { }) return transactions.length > 0 ? transactions[0] : null - }, - /** - * Finds all transactions to the given `receiverScript` using the - * given `electrumClient`. - * - * @param {ElectrumClient} electrumClient An already-initialized Electrum client. - * @param {string} receiverScript A receiver script. - * - * @return {Promise} A promise to an array of - * transactions with accompanying information about the output - * position and value pointed at the specified receiver script. - * Resolves with an empty array if no such transactions exist. - */ - findAllUnspentWithClient: async function(electrumClient, receiverScript) { - const unspentTransactions = await electrumClient.getUnspentToScript( - receiverScript - ) - - const result = [] - - for (const tx of unspentTransactions.reverse()) { - result.push({ - transactionID: tx.tx_hash, - outputPosition: tx.tx_pos, - value: tx.value - }) - } - - return result } } } diff --git a/src/lib/BitcoinSPV.js b/src/lib/BitcoinSPV.js index a66d328b..90769547 100644 --- a/src/lib/BitcoinSPV.js +++ b/src/lib/BitcoinSPV.js @@ -103,43 +103,4 @@ export class BitcoinSPV { return { proof: proof.toString("hex"), position: merkle.pos } } - - /** - * Verifies merkle proof of transaction inclusion in the block. It expects proof - * as a concatenation of 32-byte values in a hexadecimal form. The proof should - * include the merkle tree branches, with transaction hash merkle tree root omitted. - * @param {string} proofHex hexadecimal representation of the proof - * @param {string} txHash Transaction hash. - * @param {number} index is transaction index in the block (1-indexed) - * @param {number} blockHeight Height of the block where transaction was confirmed. - * @return {Promise} true if verification passed, else false - */ - async verifyMerkleProof(proofHex, txHash, index, blockHeight) { - const proof = Buffer.from(proofHex, "hex") - - // Retrieve merkle tree root. - const actualRoot = await this.client - .getMerkleRoot(blockHeight) - .catch(err => { - throw new Error(`failed to get merkle root: [${err}]`) - }) - - // Extract tree branches - const branches = [] - for (let i = 0; i < Math.floor(proof.length / 32); i++) { - const branch = proof.slice(i * 32, (i + 1) * 32) - branches.push(branch) - } - - // Derive expected root from branches and transaction. - const txHashBuffer = Buffer.from(txHash, "hex").reverse() - const expectedRoot = deriveRoot(Hash256, txHashBuffer, branches, index) - - // Validate if calculated root is equal to the one returned from client. - if (actualRoot.equals(expectedRoot)) { - return true - } else { - return false - } - } } diff --git a/src/lib/ElectrumClient.js b/src/lib/ElectrumClient.js index c546eeda..5288b378 100644 --- a/src/lib/ElectrumClient.js +++ b/src/lib/ElectrumClient.js @@ -150,24 +150,6 @@ export default class Client { * @property {number} value The value of the unspent output in satoshis. */ - /** - * Get unspent outputs sent to a script. - * @param {string} script ScriptPubKey in a hexadecimal format. - * @return {Promise} List of unspent outputs. It includes - * transactions in the mempool. - */ - async getUnspentToScript(script) { - const scriptHash = Client.scriptToHash(script) - - const listUnspent = await this.electrumClient - .blockchain_scripthash_listunspent(scriptHash) - .catch(err => { - throw new Error(JSON.stringify(err)) - }) - - return listUnspent - } - /** * Get balance of a script. * @@ -350,21 +332,6 @@ export default class Client { }) } - /** - * Get merkle root hash for block. - * @param {number} blockHeight Block height. - * @return {Promise} Merkle root hash. - */ - async getMerkleRoot(blockHeight) { - const header = await this.electrumClient - .blockchain_block_header(blockHeight) - .catch(err => { - throw new Error(`failed to get block header: [${err}]`) - }) - - return Buffer.from(header, "hex").slice(36, 68) - } - /** * Get concatenated chunk of block headers built on a starting block. * @param {number} blockHeight Starting block height. @@ -412,30 +379,6 @@ export default class Client { })) } - /** - * Finds index of output in a transaction for a given address. - * @param {string} txHash Hash of a transaction. - * @param {string} address Bitcoin address for the output. - * @return {Promise} Index of output in the transaction (0-indexed). - */ - async findOutputForAddress(txHash, address) { - const tx = await this.getTransaction(txHash).catch(err => { - throw new Error(`failed to get transaction: [${err}]`) - }) - - const outputs = tx.vout - - for (let index = 0; index < outputs.length; index++) { - for (const a of outputs[index].scriptPubKey.addresses) { - if (a == address) { - return index - } - } - } - - throw new Error(`output for address ${address} not found`) - } - /** * Gets a history of all transactions the script is involved in. * @param {string} script The script in raw hexadecimal format. diff --git a/test/BitcoinSPVTest.js b/test/BitcoinSPVTest.js index 13de00ac..1d501a78 100644 --- a/test/BitcoinSPVTest.js +++ b/test/BitcoinSPVTest.js @@ -37,21 +37,6 @@ describe("BitcoinSPV", async () => { assert.deepEqual(result, expectedResult) }) - it("verifyMerkleProof", async () => { - const proofHex = tx.merkleProof - const index = tx.indexInBlock - const txHash = tx.hash - const blockHeight = tx.blockHeight - const result = await bitcoinSPV.verifyMerkleProof( - proofHex, - txHash, - index, - blockHeight - ) - - assert.isTrue(result) - }) - it("getMerkleProofInfo", async () => { const expectedResult = tx.merkleProof const expectedPosition = tx.indexInBlock diff --git a/test/ElectrumClientTest.js b/test/ElectrumClientTest.js index 39c5c808..536ea02c 100644 --- a/test/ElectrumClientTest.js +++ b/test/ElectrumClientTest.js @@ -26,23 +26,6 @@ describe("ElectrumClient", async () => { assert.equal(result.hex, expectedTx, "unexpected result") }) - it("getUnspentToScript", async () => { - const script = "00144b47c798d12edd17dfb4ea98e5447926f664731c" - - const result = await client.getUnspentToScript(script) - const expectedResult = [ - { - tx_hash: - "72e7fd57c2adb1ed2305c4247486ff79aec363296f02ec65be141904f80d214e", - tx_pos: 0, - height: 1569342, - value: 101 - } - ] - - assert.deepEqual(result, expectedResult) - }) - it("getHeadersChain", async () => { const confirmations = tx.chainHeadersNumber const expectedResult = tx.chainHeaders @@ -51,44 +34,6 @@ describe("ElectrumClient", async () => { assert.equal(result, expectedResult, "unexpected result") }) - describe("findOutputForAddress", async () => { - it("finds first element", async () => { - const address = "tb1qfdru0xx39mw30ha5a2vw23reymmxgucujfnc7l" - const expectedResult = 0 - - const result = await client.findOutputForAddress(tx.hash, address) - - assert.equal(result, expectedResult) - }) - - it("finds second element", async () => { - const address = "tb1q78ezl08lyhuazzfz592sstenmegdns7durc4cl" - const expectedResult = 1 - - const result = await client.findOutputForAddress(tx.hash, address) - - assert.equal(result, expectedResult) - }) - - it("fails for missing address", async () => { - const address = "NOT_EXISTING_ADDRESS" - - await client.findOutputForAddress(tx.hash, address).then( - value => { - // onFulfilled - assert.fail("not failed as expected") - }, - reason => { - // onRejected - assert.include( - reason.toString(), - `output for address ${address} not found` - ) - } - ) - }) - }) - describe("onTransactionToScript", async () => { it("subscribe for script hash when transaction already exists", async () => { const script = "00144b47c798d12edd17dfb4ea98e5447926f664731c" From b342e0158a80fd524612ac8a6d525dac877711a4 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Tue, 10 Aug 2021 15:30:03 +0200 Subject: [PATCH 2/4] Installed node-fetch --- package-lock.json | 13 ++++++++++--- package.json | 1 + 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index dbbea1af..1f3ba248 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5239,6 +5239,13 @@ "requires": { "node-fetch": "2.6.0", "whatwg-fetch": "3.0.0" + }, + "dependencies": { + "node-fetch": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", + "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==" + } } }, "cross-spawn": { @@ -17790,9 +17797,9 @@ } }, "node-fetch": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", - "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==" + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==" }, "node-gyp-build": { "version": "4.2.3", diff --git a/package.json b/package.json index 58d1487a..bb508213 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "bcrypto": "git+https://github.com/bcoin-org/bcrypto.git#semver:~5.3.0", "bufio": "^1.0.6", "electrum-client-js": "git+https://github.com/keep-network/electrum-client-js.git#v0.1.1", + "node-fetch": "^2.6.1", "p-wait-for": "^3.1.0", "web3-utils": "^1.2.8" }, From dc3c0b26865ad60f3bd777d8e7d312aae5511318 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Tue, 10 Aug 2021 15:39:31 +0200 Subject: [PATCH 3/4] Removed unused imports --- src/lib/BitcoinSPV.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/lib/BitcoinSPV.js b/src/lib/BitcoinSPV.js index 90769547..6b518cd0 100644 --- a/src/lib/BitcoinSPV.js +++ b/src/lib/BitcoinSPV.js @@ -1,10 +1,6 @@ // JS implementation of merkle.py script from [summa-tx/bitcoin-spv] repository. // // [summa-tx/bitcoin-spv]: https://github.com/summa-tx/bitcoin-spv/ -import Hash256 from "bcrypto/lib/hash256-browser.js" -import BcryptoMerkle from "bcrypto/lib/merkle.js" -const { deriveRoot } = BcryptoMerkle - /** @typedef { import("./ElectrumClient.js").default } ElectrumClient */ /** From ac81e583926bd7cc6ae056e39fa92a03c71d5d01 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Tue, 10 Aug 2021 18:35:02 +0200 Subject: [PATCH 4/4] Added implementation for getTransaction --- src/BitcoinHelpers.js | 5 +++- src/lib/ElectrumClient.js | 60 ++++++++++++++++++++++++++++++++++---- test/BitcoinSPVTest.js | 7 +++-- test/ElectrumClientTest.js | 13 +++++++-- test/config/network.js | 6 ++++ 5 files changed, 80 insertions(+), 11 deletions(-) diff --git a/src/BitcoinHelpers.js b/src/BitcoinHelpers.js index 8ce57950..de133d40 100644 --- a/src/BitcoinHelpers.js +++ b/src/BitcoinHelpers.js @@ -290,7 +290,10 @@ const BitcoinHelpers = { throw new Error("Electrum client not configured.") } - const electrumClient = new ElectrumClient(BitcoinHelpers.electrumConfig) + const electrumClient = new ElectrumClient( + BitcoinHelpers.electrumConfig, + BitcoinHelpers.electrsConfig + ) await electrumClient.connect() diff --git a/src/lib/ElectrumClient.js b/src/lib/ElectrumClient.js index 5288b378..de3078ac 100644 --- a/src/lib/ElectrumClient.js +++ b/src/lib/ElectrumClient.js @@ -1,3 +1,4 @@ +import fetch from "node-fetch" import ElectrumClient from "electrum-client-js" import sha256 from "bcrypto/lib/sha256-browser.js" const { digest } = sha256 @@ -63,20 +64,26 @@ export default class Client { /** * Initializes Electrum Client instance with provided configuration. * @param {Config} config Electrum client connection configuration. + * @param {string} apiUrl Url to the electrs server */ - constructor(config) { + constructor(config, apiUrl) { + // TODO: config will be removed once all ported this.electrumClient = new ElectrumClient( config.server, config.port, config.protocol, config.options ) + + this.apiUrl = apiUrl + // TODO: Check connectivity here } /** * Establish connection with the server. */ async connect() { + // TODO: Remove when done with electrum client console.log("Connecting to electrum server...") await this.electrumClient.connect("tbtc", "1.4.2").catch(err => { @@ -88,6 +95,7 @@ export default class Client { * Disconnect from the server. */ async close() { + // TODO: Remove when done with electrum client console.log("Closing connection to electrum server...") this.electrumClient.close() } @@ -106,17 +114,59 @@ export default class Client { return header.height } + /** + * Get details of the transaction. + * @param {string} txHash Hash of a transaction. + * @return {Promise} Transaction details. + */ + // async getTransaction(txHash) { + // const tx = await this.electrumClient + // .blockchain_transaction_get(txHash, true) + // .catch(err => { + // throw new Error(`failed to get transaction ${txHash}: [${err}]`) + // }) + + // return tx + // } + /** * Get details of the transaction. * @param {string} txHash Hash of a transaction. * @return {Promise} Transaction details. */ async getTransaction(txHash) { - const tx = await this.electrumClient - .blockchain_transaction_get(txHash, true) - .catch(err => { - throw new Error(`failed to get transaction ${txHash}: [${err}]`) + const getTxUrl = `${this.apiUrl}/tx/${txHash}` + const tx = await fetch(getTxUrl).then(resp => { + if (!resp.ok) { + throw new Error(`failed to get transaction ${txHash} at ${getTxUrl}`) + } + return resp.json() + }) + + // append hex data to transaction + const getTxRawUrl = `${this.apiUrl}/tx/${txHash}/hex` + tx.hex = await fetch(getTxRawUrl).then(resp => { + if (!resp.ok) { + throw new Error( + `failed to get hex transaction ${txHash} at ${getTxRawUrl}` + ) + } + return resp.text() + }) + + // append confirmations + if (tx.status.confirmed) { + const heightUrl = `${this.apiUrl}/blocks/tip/height` + const height = await fetch(heightUrl).then(resp => { + if (!resp.ok) { + throw new Error(`failed to get blockchain height at ${heightUrl}`) + } + return resp.text() }) + tx.confirmations = parseInt(height) - tx.status.block_height + 1 + } else { + tx.confirmations = 0 + } return tx } diff --git a/test/BitcoinSPVTest.js b/test/BitcoinSPVTest.js index 1d501a78..6dcc0f07 100644 --- a/test/BitcoinSPVTest.js +++ b/test/BitcoinSPVTest.js @@ -1,6 +1,6 @@ import { BitcoinSPV } from "../src/lib/BitcoinSPV.js" import ElectrumClient from "../src/lib/ElectrumClient.js" -import { electrumConfig } from "./config/network.js" +import { electrsConfig, electrumConfig } from "./config/network.js" import { readFileSync } from "fs" import { assert } from "chai" @@ -12,7 +12,10 @@ describe("BitcoinSPV", async () => { before(async () => { const txData = readFileSync("./test/data/tx.json", "utf8") tx = JSON.parse(txData) - electrumClient = new ElectrumClient(electrumConfig["testnet"]) + electrumClient = new ElectrumClient( + electrumConfig["testnet"], + electrsConfig["testnet"] + ) bitcoinSPV = new BitcoinSPV(electrumClient) await electrumClient.connect() }) diff --git a/test/ElectrumClientTest.js b/test/ElectrumClientTest.js index 536ea02c..cb8fb03f 100644 --- a/test/ElectrumClientTest.js +++ b/test/ElectrumClientTest.js @@ -1,5 +1,5 @@ import ElectrumClient from "../src/lib/ElectrumClient.js" -import { electrumConfig } from "./config/network.js" +import { electrumConfig, electrsConfig } from "./config/network.js" import { readFileSync } from "fs" import { assert } from "chai" @@ -11,7 +11,11 @@ describe("ElectrumClient", async () => { const txData = readFileSync("./test/data/tx.json", "utf8") tx = JSON.parse(txData) - client = new ElectrumClient(electrumConfig["testnet"]) + // TODO: Remove client + client = new ElectrumClient( + electrumConfig["testnet"], + electrsConfig["testnet"] + ) await client.connect() }) @@ -179,7 +183,10 @@ describe("ElectrumClient", async () => { }, reason => { // onRejected - assert.include(reason.toString(), "Transaction not found.") + assert.include( + reason.toString(), + "failed to get transaction 02437d2f0fedd7cb11766dc6aefbc1dc8c171ef2daebddb02a32349318cc6289" + ) } ) diff --git a/test/config/network.js b/test/config/network.js index b122c8e5..9038d961 100644 --- a/test/config/network.js +++ b/test/config/network.js @@ -15,3 +15,9 @@ export const electrumConfig = { protocol: "ws" } } + +export const electrsConfig = { + main: "https://blockstream.info/api/", + testnet: "https://blockstream.info/testnet/api/", + regtest: "" // TODO: Fill +}