From f062132636d9f9b0f9497177bc7e1faf7937aa07 Mon Sep 17 00:00:00 2001 From: junderw Date: Mon, 5 Sep 2022 23:13:45 +0900 Subject: [PATCH 1/6] Feature: Add endpoint for PSBT nonWitnessUtxo inclusion --- backend/src/api/bitcoin/bitcoin.routes.ts | 44 +++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/backend/src/api/bitcoin/bitcoin.routes.ts b/backend/src/api/bitcoin/bitcoin.routes.ts index 2c3fd9467..1b805d7f0 100644 --- a/backend/src/api/bitcoin/bitcoin.routes.ts +++ b/backend/src/api/bitcoin/bitcoin.routes.ts @@ -1,5 +1,6 @@ import { Application, Request, Response } from 'express'; import axios from 'axios'; +import * as bitcoinjs from 'bitcoinjs-lib'; import config from '../../config'; import websocketHandler from '../websocket-handler'; import mempool from '../mempool'; @@ -95,6 +96,7 @@ class BitcoinRoutes { .get(config.MEMPOOL.API_URL_PREFIX + 'mempool', this.getMempool) .get(config.MEMPOOL.API_URL_PREFIX + 'mempool/txids', this.getMempoolTxIds) .get(config.MEMPOOL.API_URL_PREFIX + 'mempool/recent', this.getRecentMempoolTransactions) + .post(config.MEMPOOL.API_URL_PREFIX + 'psbt/addparents', this.postPsbtCompletion) .get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId', this.getTransaction) .post(config.MEMPOOL.API_URL_PREFIX + 'tx', this.$postTransaction) .get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/hex', this.getRawTransaction) @@ -241,6 +243,48 @@ class BitcoinRoutes { } } + /** + * Takes the PSBT as text/plain body, parses it, and adds the full + * parent transaction to each input that doesn't already have it. + * This is used for BTCPayServer / Trezor users which need access to + * the full parent transaction even with segwit inputs. + * It will respond with a text/plain PSBT in the same format (hex|base64). + */ + private async postPsbtCompletion(req: Request, res: Response) { + res.setHeader('content-type', 'text/plain'); + try { + let psbt: bitcoinjs.Psbt; + let format: 'hex' | 'base64'; + try { + psbt = bitcoinjs.Psbt.fromBase64(req.body); + format = 'base64'; + } catch(e1) { + try { + psbt = bitcoinjs.Psbt.fromHex(req.body); + format = 'hex'; + } catch(e2) { + throw new Error(`Unable to parse PSBT`); + } + } + for (const [index, input] of psbt.data.inputs.entries()) { + if (!input.nonWitnessUtxo) { + // Buffer.from ensures it won't be modified in place by reverse() + const txid = Buffer.from(psbt.txInputs[index].hash).reverse().toString('hex'); + const transaction: IEsploraApi.Transaction = await bitcoinApi.$getRawTransaction(txid, true); + if (!transaction.hex) { + throw new Error(`Couldn't get transaction hex for ${txid}`); + } + psbt.updateInput(index, { + nonWitnessUtxo: Buffer.from(transaction.hex, 'hex'), + }); + } + } + res.send(format === 'hex' ? psbt.toHex() : psbt.toBase64()); + } catch (e: any) { + res.status(500).send(e instanceof Error ? e.message : e); + } + } + private async getTransactionStatus(req: Request, res: Response) { try { const transaction = await transactionUtils.$getTransactionExtended(req.params.txId, true); From 9b1fc1e000560706b1406612dee0386557da7019 Mon Sep 17 00:00:00 2001 From: junderw Date: Sat, 10 Sep 2022 16:03:31 +0900 Subject: [PATCH 2/6] Fix response codes for various error states --- backend/src/api/bitcoin/bitcoin.routes.ts | 42 ++++++++++++++++++----- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/backend/src/api/bitcoin/bitcoin.routes.ts b/backend/src/api/bitcoin/bitcoin.routes.ts index 1b805d7f0..e783a4864 100644 --- a/backend/src/api/bitcoin/bitcoin.routes.ts +++ b/backend/src/api/bitcoin/bitcoin.routes.ts @@ -252,36 +252,62 @@ class BitcoinRoutes { */ private async postPsbtCompletion(req: Request, res: Response) { res.setHeader('content-type', 'text/plain'); + const notFoundError = `Couldn't get transaction hex for parent of input`; try { let psbt: bitcoinjs.Psbt; let format: 'hex' | 'base64'; + let isModified = false; try { psbt = bitcoinjs.Psbt.fromBase64(req.body); format = 'base64'; - } catch(e1) { + } catch (e1) { try { psbt = bitcoinjs.Psbt.fromHex(req.body); format = 'hex'; - } catch(e2) { + } catch (e2) { throw new Error(`Unable to parse PSBT`); } } for (const [index, input] of psbt.data.inputs.entries()) { if (!input.nonWitnessUtxo) { // Buffer.from ensures it won't be modified in place by reverse() - const txid = Buffer.from(psbt.txInputs[index].hash).reverse().toString('hex'); - const transaction: IEsploraApi.Transaction = await bitcoinApi.$getRawTransaction(txid, true); - if (!transaction.hex) { - throw new Error(`Couldn't get transaction hex for ${txid}`); + const txid = Buffer.from(psbt.txInputs[index].hash) + .reverse() + .toString('hex'); + + let transaction: IEsploraApi.Transaction; + // If missing transaction, return 404 status error + try { + transaction = await bitcoinApi.$getRawTransaction(txid, true); + if (!transaction.hex) { + throw new Error(''); + } + } catch (err) { + throw new Error(`${notFoundError} #${index} @ ${txid}`); } + psbt.updateInput(index, { nonWitnessUtxo: Buffer.from(transaction.hex, 'hex'), }); + if (!isModified) { + isModified = true; + } } } - res.send(format === 'hex' ? psbt.toHex() : psbt.toBase64()); + if (isModified) { + res.send(format === 'hex' ? psbt.toHex() : psbt.toBase64()); + } else { + // Not modified + // 422 Unprocessable Entity + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/422 + res.status(422).send(`Psbt had no missing nonUtxoWitnesses.`); + } } catch (e: any) { - res.status(500).send(e instanceof Error ? e.message : e); + if (e instanceof Error && new RegExp(notFoundError).test(e.message)) { + res.status(404).send(e.message); + } else { + res.status(500).send(e instanceof Error ? e.message : e); + } } } From bd4cf980bd9d8b60c6806f7fd816a357df7abf6f Mon Sep 17 00:00:00 2001 From: junderw Date: Sat, 10 Sep 2022 16:09:43 +0900 Subject: [PATCH 3/6] Spelling fix --- backend/src/api/bitcoin/bitcoin.routes.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/api/bitcoin/bitcoin.routes.ts b/backend/src/api/bitcoin/bitcoin.routes.ts index e783a4864..e774a0ded 100644 --- a/backend/src/api/bitcoin/bitcoin.routes.ts +++ b/backend/src/api/bitcoin/bitcoin.routes.ts @@ -300,7 +300,7 @@ class BitcoinRoutes { // Not modified // 422 Unprocessable Entity // https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/422 - res.status(422).send(`Psbt had no missing nonUtxoWitnesses.`); + res.status(422).send(`Psbt had no missing nonWitnessUtxos.`); } } catch (e: any) { if (e instanceof Error && new RegExp(notFoundError).test(e.message)) { From 4f3296566af0801e0243df64433fa53269a45478 Mon Sep 17 00:00:00 2001 From: softsimon Date: Tue, 22 Nov 2022 19:08:09 +0900 Subject: [PATCH 4/6] Make api available on all backends --- backend/src/api/bitcoin/bitcoin.routes.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/api/bitcoin/bitcoin.routes.ts b/backend/src/api/bitcoin/bitcoin.routes.ts index e774a0ded..433f4bdb7 100644 --- a/backend/src/api/bitcoin/bitcoin.routes.ts +++ b/backend/src/api/bitcoin/bitcoin.routes.ts @@ -88,7 +88,8 @@ class BitcoinRoutes { .get(config.MEMPOOL.API_URL_PREFIX + 'blocks', this.getBlocks.bind(this)) .get(config.MEMPOOL.API_URL_PREFIX + 'blocks/:height', this.getBlocks.bind(this)) .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash', this.getBlock) - .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/summary', this.getStrippedBlockTransactions); + .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/summary', this.getStrippedBlockTransactions) + .post(config.MEMPOOL.API_URL_PREFIX + 'psbt/addparents', this.postPsbtCompletion) ; if (config.MEMPOOL.BACKEND !== 'esplora') { @@ -96,7 +97,6 @@ class BitcoinRoutes { .get(config.MEMPOOL.API_URL_PREFIX + 'mempool', this.getMempool) .get(config.MEMPOOL.API_URL_PREFIX + 'mempool/txids', this.getMempoolTxIds) .get(config.MEMPOOL.API_URL_PREFIX + 'mempool/recent', this.getRecentMempoolTransactions) - .post(config.MEMPOOL.API_URL_PREFIX + 'psbt/addparents', this.postPsbtCompletion) .get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId', this.getTransaction) .post(config.MEMPOOL.API_URL_PREFIX + 'tx', this.$postTransaction) .get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/hex', this.getRawTransaction) From 584f443f56cad1f7297bd5d4b646100b5d384c19 Mon Sep 17 00:00:00 2001 From: softsimon Date: Tue, 22 Nov 2022 21:45:05 +0900 Subject: [PATCH 5/6] Adding new getTransactionHex api --- .../src/api/bitcoin/bitcoin-api-abstract-factory.ts | 1 + backend/src/api/bitcoin/bitcoin-api.ts | 5 +++++ backend/src/api/bitcoin/bitcoin.routes.ts | 10 +++++----- backend/src/api/bitcoin/esplora-api.ts | 5 +++++ 4 files changed, 16 insertions(+), 5 deletions(-) diff --git a/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts b/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts index 358bd29e4..aa9fe5d15 100644 --- a/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts +++ b/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts @@ -3,6 +3,7 @@ import { IEsploraApi } from './esplora-api.interface'; export interface AbstractBitcoinApi { $getRawMempool(): Promise; $getRawTransaction(txId: string, skipConversion?: boolean, addPrevout?: boolean, lazyPrevouts?: boolean): Promise; + $getTransactionHex(txId: string): Promise; $getBlockHeightTip(): Promise; $getBlockHashTip(): Promise; $getTxIdsForBlock(hash: string): Promise; diff --git a/backend/src/api/bitcoin/bitcoin-api.ts b/backend/src/api/bitcoin/bitcoin-api.ts index ebde5cc07..0a3d674ec 100644 --- a/backend/src/api/bitcoin/bitcoin-api.ts +++ b/backend/src/api/bitcoin/bitcoin-api.ts @@ -57,6 +57,11 @@ class BitcoinApi implements AbstractBitcoinApi { }); } + $getTransactionHex(txId: string): Promise { + return this.$getRawTransaction(txId, true) + .then((tx) => tx.hex || ''); + } + $getBlockHeightTip(): Promise { return this.bitcoindClient.getChainTips() .then((result: IBitcoinApi.ChainTips[]) => { diff --git a/backend/src/api/bitcoin/bitcoin.routes.ts b/backend/src/api/bitcoin/bitcoin.routes.ts index 433f4bdb7..3740cccd4 100644 --- a/backend/src/api/bitcoin/bitcoin.routes.ts +++ b/backend/src/api/bitcoin/bitcoin.routes.ts @@ -250,7 +250,7 @@ class BitcoinRoutes { * the full parent transaction even with segwit inputs. * It will respond with a text/plain PSBT in the same format (hex|base64). */ - private async postPsbtCompletion(req: Request, res: Response) { + private async postPsbtCompletion(req: Request, res: Response): Promise { res.setHeader('content-type', 'text/plain'); const notFoundError = `Couldn't get transaction hex for parent of input`; try { @@ -275,11 +275,11 @@ class BitcoinRoutes { .reverse() .toString('hex'); - let transaction: IEsploraApi.Transaction; + let transactionHex: string; // If missing transaction, return 404 status error try { - transaction = await bitcoinApi.$getRawTransaction(txid, true); - if (!transaction.hex) { + transactionHex = await bitcoinApi.$getTransactionHex(txid); + if (!transactionHex) { throw new Error(''); } } catch (err) { @@ -287,7 +287,7 @@ class BitcoinRoutes { } psbt.updateInput(index, { - nonWitnessUtxo: Buffer.from(transaction.hex, 'hex'), + nonWitnessUtxo: Buffer.from(transactionHex, 'hex'), }); if (!isModified) { isModified = true; diff --git a/backend/src/api/bitcoin/esplora-api.ts b/backend/src/api/bitcoin/esplora-api.ts index ebaf2f6a0..3662347d6 100644 --- a/backend/src/api/bitcoin/esplora-api.ts +++ b/backend/src/api/bitcoin/esplora-api.ts @@ -20,6 +20,11 @@ class ElectrsApi implements AbstractBitcoinApi { .then((response) => response.data); } + $getTransactionHex(txId: string): Promise { + return axios.get(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/hex', this.axiosConfig) + .then((response) => response.data); + } + $getBlockHeightTip(): Promise { return axios.get(config.ESPLORA.REST_API_URL + '/blocks/tip/height', this.axiosConfig) .then((response) => response.data); From 74dbd6cee12be3ac7096b2b19da1066fa18dd01c Mon Sep 17 00:00:00 2001 From: softsimon Date: Wed, 23 Nov 2022 18:43:37 +0900 Subject: [PATCH 6/6] Add support for application/base64 content type --- backend/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/index.ts b/backend/src/index.ts index 09a12e200..cd81e4994 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -83,7 +83,7 @@ class Server { next(); }) .use(express.urlencoded({ extended: true })) - .use(express.text()) + .use(express.text({ type: ['text/plain', 'application/base64'] })) ; this.server = http.createServer(this.app);