From a2db6005190d0faaaad659efcca48ebfb3530a6d Mon Sep 17 00:00:00 2001 From: Nishant Ghodke <64554492+iamcrazycoder@users.noreply.github.com> Date: Wed, 9 Aug 2023 11:34:29 +0530 Subject: [PATCH] fix(sdk)!: add missing address type to input generator (#33) * fix: consider witness_v0_keyhash in input generation * refactor: move RBF enabling logic to higher level also, use utxo.script.hex and update type interfaces * refactor: update arg options of createPsbt fn * refactor: update fn arg option types --- packages/sdk/src/transactions/psbt.ts | 158 +++++++++----------------- 1 file changed, 55 insertions(+), 103 deletions(-) diff --git a/packages/sdk/src/transactions/psbt.ts b/packages/sdk/src/transactions/psbt.ts index 84ba48b7..536266ca 100644 --- a/packages/sdk/src/transactions/psbt.ts +++ b/packages/sdk/src/transactions/psbt.ts @@ -1,6 +1,6 @@ import * as ecc from "@bitcoinerlab/secp256k1" import { BIP32Factory } from "bip32" -import { Psbt, Transaction } from "bitcoinjs-lib" +import { Psbt } from "bitcoinjs-lib" import { getAddressType } from "../addresses" import { addressTypeToName } from "../addresses/formats" @@ -8,24 +8,23 @@ import { OrditApi } from "../api" import { Network } from "../config/types" import { MINIMUM_AMOUNT_IN_SATS } from "../constants" import { calculateTxFee, createTransaction, getNetwork, toXOnly } from "../utils" -import { GetWalletOptions } from "../wallet" +import { OnOffUnion } from "../wallet" import { UTXO } from "./types" const bip32 = BIP32Factory(ecc) export async function createPsbt({ - network, pubKey, - ins, - outs, - satsPerByte = 10, + network, + address, + outputs, + satsPerByte, safeMode = "on", enableRBF = true }: CreatePsbtOptions) { - if (!ins.length || !outs.length) { + if (!outputs.length) { throw new Error("Invalid request") } - const { address } = ins[0] const { spendableUTXOs, unspendableUTXOs, totalUTXOs } = await OrditApi.fetchUnspentUTXOs({ address, network, @@ -41,21 +40,25 @@ export async function createPsbt({ const inputSats = spendableUTXOs .concat(safeMode === "off" ? unspendableUTXOs : []) .reduce((acc, utxo) => (acc += utxo.sats), 0) - const outputSats = outs.reduce((acc, utxo) => (acc += utxo.cardinals), 0) + const outputSats = outputs.reduce((acc, utxo) => (acc += utxo.cardinals), 0) // add inputs const witnessScripts: Buffer[] = [] - for (const utxo of spendableUTXOs) { + for (const [index, utxo] of spendableUTXOs.entries()) { if (utxo.scriptPubKey.address !== address) continue - const payload = await processInput({ utxo, pubKey, network, enableRBF }) + const payload = await processInput({ utxo, pubKey, network }) payload.witnessUtxo?.script && witnessScripts.push(payload.witnessUtxo?.script) psbt.addInput(payload) + + if (enableRBF) { + psbt.setInputSequence(index, 0xfffffffd) + } } const fees = calculateTxFee({ totalInputs: totalUTXOs, // select only relevant utxos to spend. NOT ALL! - totalOutputs: outs.length, + totalOutputs: outputs.length, satsPerByte, type: addressTypeToName[getAddressType(address, network)], additional: { witnessScripts } @@ -68,14 +71,14 @@ export async function createPsbt({ const isChangeOwed = remainingBalance > MINIMUM_AMOUNT_IN_SATS if (isChangeOwed) { - outs.push({ + outputs.push({ address, cardinals: remainingBalance }) } // add outputs - outs.forEach((out) => { + outputs.forEach((out) => { psbt.addOutput({ address: out.address, value: out.cardinals @@ -88,91 +91,66 @@ export async function createPsbt({ } } -export async function processInput({ - utxo, - pubKey, - network, - enableRBF = true, - ...options -}: Omit): Promise { - const { rawTx } = await OrditApi.fetchTx({ txId: utxo.txid, network, hex: true }) - +export async function processInput({ utxo, pubKey, network, sighashType }: ProcessInputOptions): Promise { switch (utxo.scriptPubKey.type) { case "witness_v1_taproot": - return generateTaprootInput({ utxo, pubKey, network, rawTx, enableRBF, ...options }) + return generateTaprootInput({ utxo, pubKey, network, sighashType }) case "witness_v0_scripthash": - return generateSegwitInput({ utxo, pubKey, network, rawTx, enableRBF, ...options }) + case "witness_v0_keyhash": + return generateSegwitInput({ utxo, sighashType }) case "scripthash": - return generateNestedSegwitInput({ utxo, pubKey, network, rawTx, enableRBF, ...options }) + return generateNestedSegwitInput({ utxo, pubKey, network, sighashType }) case "pubkeyhash": - return generateLegacyInput({ utxo, rawTx, enableRBF, ...options }) + return generateLegacyInput({ utxo, sighashType, network }) default: throw new Error("invalid script pub type") } } -function generateTaprootInput({ - utxo, - pubKey, - network, - enableRBF, - sighashType, - rawTx -}: ProcessInputOptions): TaprootInputType { +function generateTaprootInput({ utxo, pubKey, network, sighashType }: ProcessInputOptions): TaprootInputType { const chainCode = Buffer.alloc(32) chainCode.fill(1) const key = bip32.fromPublicKey(Buffer.from(pubKey, "hex"), chainCode, getNetwork(network)) - const childNodeXOnlyPubkey = toXOnly(key.publicKey) + const xOnlyPubKey = toXOnly(key.publicKey) - const p2tr = createTransaction(childNodeXOnlyPubkey, "p2tr", network) - if (!p2tr || !p2tr.output) { + if (!utxo.scriptPubKey.hex) { throw new Error("Unable to process p2tr input") } return { hash: utxo.txid, index: utxo.n, - sequence: enableRBF ? 0xfffffffd : undefined, - tapInternalKey: childNodeXOnlyPubkey, - nonWitnessUtxo: rawTx?.toBuffer() ?? undefined, + tapInternalKey: xOnlyPubKey, witnessUtxo: { - script: p2tr.output, + script: Buffer.from(utxo.scriptPubKey.hex, "hex"), value: utxo.sats }, ...(sighashType ? { sighashType } : undefined) } } -function generateSegwitInput({ utxo, pubKey, network, enableRBF, sighashType }: ProcessInputOptions): SegwitInputType { - const p2wpkh = createTransaction(Buffer.from(pubKey, "hex"), "p2wpkh", network) - if (!p2wpkh || !p2wpkh.output) { +function generateSegwitInput({ utxo, sighashType }: Omit): BaseInputType { + if (!utxo.scriptPubKey.hex) { throw new Error("Unable to process Segwit input") } return { hash: utxo.txid, index: utxo.n, - sequence: enableRBF ? 0xfffffffd : undefined, witnessUtxo: { - script: p2wpkh.output, + script: Buffer.from(utxo.scriptPubKey.hex, "hex"), value: utxo.sats }, ...(sighashType ? { sighashType } : undefined) } } -function generateNestedSegwitInput({ - utxo, - pubKey, - network, - enableRBF, - sighashType -}: ProcessInputOptions): NestedSegwitInputType { +function generateNestedSegwitInput({ utxo, pubKey, network, sighashType }: ProcessInputOptions): NestedSegwitInputType { const p2sh = createTransaction(Buffer.from(pubKey, "hex"), "p2sh", network) if (!p2sh || !p2sh.output || !p2sh.redeem) { throw new Error("Unable to process Segwit input") @@ -181,10 +159,9 @@ function generateNestedSegwitInput({ return { hash: utxo.txid, index: utxo.n, - sequence: enableRBF ? 0xfffffffd : undefined, - redeemScript: p2sh.redeem.output, + redeemScript: p2sh.redeem.output!, witnessUtxo: { - script: p2sh.output, + script: Buffer.from(utxo.scriptPubKey.hex, "hex"), value: utxo.sats }, ...(sighashType ? { sighashType } : undefined) @@ -193,41 +170,26 @@ function generateNestedSegwitInput({ async function generateLegacyInput({ utxo, - enableRBF, sighashType, - rawTx -}: Omit): Promise { + network +}: Omit): Promise { + const { rawTx } = await OrditApi.fetchTx({ txId: utxo.txid, network, hex: true }) if (!rawTx) { - throw new Error("Unable to process Legacy input") + throw new Error("Unable to process legacy input") } return { hash: utxo.txid, index: utxo.n, - sequence: enableRBF ? 0xfffffffd : undefined, - nonWitnessUtxo: rawTx.toBuffer(), + nonWitnessUtxo: rawTx?.toBuffer(), ...(sighashType ? { sighashType } : undefined) } } // TODO: replace below interfaces and custom types w/ PsbtInputExtended from bitcoinjs-lib -interface TaprootInputType { - hash: string - index: number - sequence?: number - sighashType?: number - tapInternalKey?: Buffer - witnessUtxo?: { - script: Buffer - value: number - } - nonWitnessUtxo?: Buffer -} - -interface SegwitInputType { +interface BaseInputType { hash: string index: number - sequence?: number sighashType?: number witnessUtxo?: { script: Buffer @@ -236,42 +198,32 @@ interface SegwitInputType { nonWitnessUtxo?: Buffer } -interface NestedSegwitInputType { - hash: string - index: number - sequence?: number - sighashType?: number - redeemScript?: Buffer | undefined - witnessUtxo?: { - script: Buffer - value: number - } - nonWitnessUtxo?: Buffer +type TaprootInputType = BaseInputType & { + tapInternalKey: Buffer } -interface LegacyInputType { - hash: string - index: number - sequence?: number - sighashType?: number - nonWitnessUtxo?: Buffer - witnessUtxo?: never +type NestedSegwitInputType = BaseInputType & { + redeemScript: Buffer } -export type InputType = TaprootInputType | SegwitInputType | NestedSegwitInputType | LegacyInputType +export type InputType = BaseInputType | TaprootInputType | NestedSegwitInputType -export type CreatePsbtOptions = GetWalletOptions & { - satsPerByte?: number - ins: any[] - outs: any[] +export type CreatePsbtOptions = { + satsPerByte: number + address: string + outputs: { + address: string + cardinals: number + }[] enableRBF: boolean + pubKey: string + network: Network + safeMode?: OnOffUnion } interface ProcessInputOptions { utxo: UTXO pubKey: string network: Network - enableRBF?: boolean sighashType?: number - rawTx?: Transaction }