Skip to content

Commit

Permalink
fix(sdk)!: add missing address type to input generator (#33)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
iamcrazycoder authored Aug 9, 2023
1 parent 733eb4e commit a2db600
Showing 1 changed file with 55 additions and 103 deletions.
158 changes: 55 additions & 103 deletions packages/sdk/src/transactions/psbt.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,30 @@
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"
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,
Expand All @@ -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 }
Expand All @@ -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
Expand All @@ -88,91 +91,66 @@ export async function createPsbt({
}
}

export async function processInput({
utxo,
pubKey,
network,
enableRBF = true,
...options
}: Omit<ProcessInputOptions, "rawTx">): Promise<InputType> {
const { rawTx } = await OrditApi.fetchTx({ txId: utxo.txid, network, hex: true })

export async function processInput({ utxo, pubKey, network, sighashType }: ProcessInputOptions): Promise<InputType> {
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<ProcessInputOptions, "pubKey" | "network">): 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")
Expand All @@ -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)
Expand All @@ -193,41 +170,26 @@ function generateNestedSegwitInput({

async function generateLegacyInput({
utxo,
enableRBF,
sighashType,
rawTx
}: Omit<ProcessInputOptions, "pubKey" | "network">): Promise<LegacyInputType> {
network
}: Omit<ProcessInputOptions, "pubKey">): Promise<BaseInputType> {
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
Expand All @@ -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
}

0 comments on commit a2db600

Please sign in to comment.