Merge pull request #2514 from mempool/junderw/psbt-complete-inputs
Feature: Add endpoint for PSBT nonWitnessUtxo inclusion
This commit is contained in:
commit
fe8cdb5867
@ -3,6 +3,7 @@ import { IEsploraApi } from './esplora-api.interface';
|
|||||||
export interface AbstractBitcoinApi {
|
export interface AbstractBitcoinApi {
|
||||||
$getRawMempool(): Promise<IEsploraApi.Transaction['txid'][]>;
|
$getRawMempool(): Promise<IEsploraApi.Transaction['txid'][]>;
|
||||||
$getRawTransaction(txId: string, skipConversion?: boolean, addPrevout?: boolean, lazyPrevouts?: boolean): Promise<IEsploraApi.Transaction>;
|
$getRawTransaction(txId: string, skipConversion?: boolean, addPrevout?: boolean, lazyPrevouts?: boolean): Promise<IEsploraApi.Transaction>;
|
||||||
|
$getTransactionHex(txId: string): Promise<string>;
|
||||||
$getBlockHeightTip(): Promise<number>;
|
$getBlockHeightTip(): Promise<number>;
|
||||||
$getBlockHashTip(): Promise<string>;
|
$getBlockHashTip(): Promise<string>;
|
||||||
$getTxIdsForBlock(hash: string): Promise<string[]>;
|
$getTxIdsForBlock(hash: string): Promise<string[]>;
|
||||||
|
@ -57,6 +57,11 @@ class BitcoinApi implements AbstractBitcoinApi {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$getTransactionHex(txId: string): Promise<string> {
|
||||||
|
return this.$getRawTransaction(txId, true)
|
||||||
|
.then((tx) => tx.hex || '');
|
||||||
|
}
|
||||||
|
|
||||||
$getBlockHeightTip(): Promise<number> {
|
$getBlockHeightTip(): Promise<number> {
|
||||||
return this.bitcoindClient.getChainTips()
|
return this.bitcoindClient.getChainTips()
|
||||||
.then((result: IBitcoinApi.ChainTips[]) => {
|
.then((result: IBitcoinApi.ChainTips[]) => {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { Application, Request, Response } from 'express';
|
import { Application, Request, Response } from 'express';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import * as bitcoinjs from 'bitcoinjs-lib';
|
||||||
import config from '../../config';
|
import config from '../../config';
|
||||||
import websocketHandler from '../websocket-handler';
|
import websocketHandler from '../websocket-handler';
|
||||||
import mempool from '../mempool';
|
import mempool from '../mempool';
|
||||||
@ -87,7 +88,8 @@ class BitcoinRoutes {
|
|||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks', this.getBlocks.bind(this))
|
.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 + 'blocks/:height', this.getBlocks.bind(this))
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash', this.getBlock)
|
.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') {
|
if (config.MEMPOOL.BACKEND !== 'esplora') {
|
||||||
@ -241,6 +243,74 @@ 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): Promise<void> {
|
||||||
|
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) {
|
||||||
|
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');
|
||||||
|
|
||||||
|
let transactionHex: string;
|
||||||
|
// If missing transaction, return 404 status error
|
||||||
|
try {
|
||||||
|
transactionHex = await bitcoinApi.$getTransactionHex(txid);
|
||||||
|
if (!transactionHex) {
|
||||||
|
throw new Error('');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error(`${notFoundError} #${index} @ ${txid}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
psbt.updateInput(index, {
|
||||||
|
nonWitnessUtxo: Buffer.from(transactionHex, 'hex'),
|
||||||
|
});
|
||||||
|
if (!isModified) {
|
||||||
|
isModified = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 nonWitnessUtxos.`);
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async getTransactionStatus(req: Request, res: Response) {
|
private async getTransactionStatus(req: Request, res: Response) {
|
||||||
try {
|
try {
|
||||||
const transaction = await transactionUtils.$getTransactionExtended(req.params.txId, true);
|
const transaction = await transactionUtils.$getTransactionExtended(req.params.txId, true);
|
||||||
|
@ -20,6 +20,11 @@ class ElectrsApi implements AbstractBitcoinApi {
|
|||||||
.then((response) => response.data);
|
.then((response) => response.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$getTransactionHex(txId: string): Promise<string> {
|
||||||
|
return axios.get<string>(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/hex', this.axiosConfig)
|
||||||
|
.then((response) => response.data);
|
||||||
|
}
|
||||||
|
|
||||||
$getBlockHeightTip(): Promise<number> {
|
$getBlockHeightTip(): Promise<number> {
|
||||||
return axios.get<number>(config.ESPLORA.REST_API_URL + '/blocks/tip/height', this.axiosConfig)
|
return axios.get<number>(config.ESPLORA.REST_API_URL + '/blocks/tip/height', this.axiosConfig)
|
||||||
.then((response) => response.data);
|
.then((response) => response.data);
|
||||||
|
@ -83,7 +83,7 @@ class Server {
|
|||||||
next();
|
next();
|
||||||
})
|
})
|
||||||
.use(express.urlencoded({ extended: true }))
|
.use(express.urlencoded({ extended: true }))
|
||||||
.use(express.text())
|
.use(express.text({ type: ['text/plain', 'application/base64'] }))
|
||||||
;
|
;
|
||||||
|
|
||||||
this.server = http.createServer(this.app);
|
this.server = http.createServer(this.app);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user