-
Notifications
You must be signed in to change notification settings - Fork 282
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
feat(utxo-bin): add `psbt` subcommand
- Loading branch information
Showing
38 changed files
with
867 additions
and
36 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
99 changes: 99 additions & 0 deletions
99
modules/utxo-bin/src/commands/cmdPsbt/cmdAddDescriptorInput.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
import * as utxolib from '@bitgo/utxo-lib'; | ||
import { Descriptor } from '@bitgo/wasm-miniscript'; | ||
import * as yargs from 'yargs'; | ||
|
||
import { toUtxoPsbt, toWrappedPsbt } from './wrap'; | ||
import { withPsbt, WithPsbtOptions, withPsbtOptions } from './withPsbt'; | ||
|
||
/** | ||
* Non-Final (Replaceable) | ||
* Reference: https://github.com/bitcoin/bitcoin/blob/v25.1/src/rpc/rawtransaction_util.cpp#L49 | ||
* */ | ||
const MAX_BIP125_RBF_SEQUENCE = 0xffffffff - 2; | ||
|
||
type ArgsAddDescriptorInput = WithPsbtOptions & { | ||
outputId: string; | ||
address?: string; | ||
scriptPubKey?: string; | ||
value: string; | ||
descriptor: string; | ||
descriptorIndex: number; | ||
}; | ||
|
||
function getScriptPubKey( | ||
args: { address?: string; scriptPubKey?: string }, | ||
network: utxolib.Network | ||
): Buffer | undefined { | ||
if (args.address) { | ||
return utxolib.addressFormat.toOutputScriptTryFormats(args.address, network); | ||
} | ||
if (args.scriptPubKey) { | ||
return Buffer.from(args.scriptPubKey, 'hex'); | ||
} | ||
return undefined; | ||
} | ||
|
||
function addDescriptorInput( | ||
psbt: utxolib.Psbt, | ||
outputId: string, | ||
scriptPubKey: Buffer | undefined, | ||
value: bigint, | ||
descriptorString: string, | ||
descriptorIndex: number, | ||
{ sequence = MAX_BIP125_RBF_SEQUENCE } = {} | ||
): void { | ||
const { txid, vout } = utxolib.bitgo.parseOutputId(outputId); | ||
const descriptor = Descriptor.fromString(descriptorString, 'derivable'); | ||
const derivedDescriptor = descriptor.atDerivationIndex(descriptorIndex); | ||
if (scriptPubKey === undefined) { | ||
scriptPubKey = Buffer.from(derivedDescriptor.scriptPubkey()); | ||
} | ||
psbt.addInput({ | ||
hash: txid, | ||
index: vout, | ||
sequence, | ||
witnessUtxo: { | ||
script: scriptPubKey, | ||
value, | ||
}, | ||
}); | ||
const inputIndex = psbt.txInputs.length - 1; | ||
const wrappedPsbt = toWrappedPsbt(psbt); | ||
wrappedPsbt.updateInputWithDescriptor(inputIndex, derivedDescriptor); | ||
const utxoPsbt = toUtxoPsbt(wrappedPsbt); | ||
psbt.data.inputs[inputIndex] = utxoPsbt.data.inputs[inputIndex]; | ||
} | ||
|
||
export const cmdAddDescriptorInput: yargs.CommandModule<unknown, ArgsAddDescriptorInput> = { | ||
command: 'addDescriptorInput', | ||
describe: 'add descriptor input to psbt', | ||
builder(b: yargs.Argv<unknown>) { | ||
return b | ||
.options(withPsbtOptions) | ||
.option('outputId', { type: 'string', demandOption: true }) | ||
.option('address', { type: 'string' }) | ||
.option('scriptPubKey', { type: 'string' }) | ||
.option('value', { type: 'string', demandOption: true }) | ||
.option('descriptor', { type: 'string', demandOption: true }) | ||
.option('descriptorIndex', { type: 'number', demandOption: true }); | ||
}, | ||
async handler(argv) { | ||
await withPsbt(argv, async function (psbt) { | ||
addDescriptorInput( | ||
psbt, | ||
argv.outputId, | ||
getScriptPubKey( | ||
{ | ||
address: argv.address, | ||
scriptPubKey: argv.scriptPubKey, | ||
}, | ||
argv.network | ||
), | ||
BigInt(argv.value), | ||
argv.descriptor, | ||
argv.descriptorIndex | ||
); | ||
return psbt; | ||
}); | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
import * as yargs from 'yargs'; | ||
import * as utxolib from '@bitgo/utxo-lib'; | ||
import { Dimensions } from '@bitgo/unspents'; | ||
import { Buffer } from 'buffer'; | ||
import { Descriptor, Miniscript } from '@bitgo/wasm-miniscript'; | ||
|
||
import { withPsbt, withPsbtOptions, WithPsbtOptions } from './withPsbt'; | ||
|
||
function toScriptPubKey( | ||
params: { | ||
address?: string; | ||
scriptPubKey?: string; | ||
}, | ||
network: utxolib.Network | ||
): Buffer { | ||
if (params.address) { | ||
return utxolib.addressFormat.toOutputScriptTryFormats(params.address, network); | ||
} | ||
if (params.scriptPubKey) { | ||
return Buffer.from(params.scriptPubKey, 'hex'); | ||
} | ||
throw new Error('address or scriptPubKey is required'); | ||
} | ||
|
||
type ArgsAddOutput = WithPsbtOptions & { | ||
address?: string; | ||
scriptPubKey?: string; | ||
amount: string; | ||
feeRateSatB?: number; | ||
}; | ||
|
||
function getInputWeight(psbt: utxolib.Psbt, inputIndex?: number): number { | ||
if (inputIndex === undefined) { | ||
return psbt.txInputs.reduce((sum, input, inputIndex) => sum + getInputWeight(psbt, inputIndex), 0); | ||
} | ||
const { redeemScript, witnessScript } = psbt.data.inputs[inputIndex]; | ||
if (redeemScript) { | ||
throw new Error('redeemScript is not supported'); | ||
} | ||
if (!witnessScript) { | ||
throw new Error('witnessScript is required'); | ||
} | ||
const witnessMiniscript = Miniscript.fromBitcoinScript(witnessScript, 'segwitv0'); | ||
const descriptor = Descriptor.fromString(`wsh(${witnessMiniscript.toString()})`, 'definite'); | ||
return descriptor.maxWeightToSatisfy(); | ||
} | ||
|
||
function getOutputVsize(psbt: utxolib.Psbt, outputIndex?: number): number { | ||
if (outputIndex === undefined) { | ||
return psbt.txOutputs.reduce((sum, output, outputIndex) => sum + getOutputVsize(psbt, outputIndex), 0); | ||
} | ||
const { script } = psbt.txOutputs[outputIndex]; | ||
return Dimensions.getVSizeForOutputWithScriptLength(script.length); | ||
} | ||
|
||
function getMaxOutputValue( | ||
psbt: utxolib.Psbt, | ||
{ | ||
scriptPubKey, | ||
feeRateSatB, | ||
}: { | ||
scriptPubKey: Buffer; | ||
feeRateSatB: number; | ||
} | ||
): bigint { | ||
const inputSum = psbt.data.inputs.reduce((sum, input) => { | ||
if (!input.witnessUtxo) { | ||
throw new Error('witnessUtxo is required'); | ||
} | ||
return sum + input.witnessUtxo.value; | ||
}, BigInt(0)); | ||
const outputSum = psbt.txOutputs.reduce((sum, output) => sum + output.value, BigInt(0)); | ||
const inputVsize = Math.ceil(getInputWeight(psbt) / 4); | ||
const outputVsize = getOutputVsize(psbt) + Dimensions.getVSizeForOutputWithScriptLength(scriptPubKey.length); | ||
const totalVsize = inputVsize + outputVsize + 11; | ||
const fee = BigInt(totalVsize * feeRateSatB); | ||
if (inputSum < outputSum + fee) { | ||
throw new Error(`insufficient funds: [inputSum=${inputSum}, outputSum=${outputSum}, fee=${fee}]`); | ||
} | ||
return inputSum - outputSum - fee; | ||
} | ||
|
||
function getOutputValue( | ||
amount: string, | ||
{ | ||
scriptPubKey, | ||
psbt, | ||
feeRateSatB, | ||
}: { | ||
scriptPubKey: Buffer; | ||
psbt: utxolib.Psbt; | ||
feeRateSatB?: number; | ||
} | ||
): bigint { | ||
if (amount === 'max') { | ||
if (!feeRateSatB) { | ||
throw new Error('feeRateSatB is required'); | ||
} | ||
return getMaxOutputValue(psbt, { scriptPubKey, feeRateSatB }); | ||
} | ||
return BigInt(parseFloat(amount)); | ||
} | ||
|
||
export const cmdAddOutput: yargs.CommandModule<unknown, ArgsAddOutput> = { | ||
command: 'addOutput', | ||
describe: 'add output to psbt', | ||
builder(b: yargs.Argv<unknown>) { | ||
return b | ||
.options(withPsbtOptions) | ||
.option('address', { type: 'string' }) | ||
.option('scriptPubKey', { type: 'string' }) | ||
.option('amount', { type: 'string', demandOption: true }) | ||
.option('feeRateSatB', { type: 'number' }); | ||
}, | ||
async handler(argv) { | ||
await withPsbt(argv, async function (psbt) { | ||
const scriptPubKey = toScriptPubKey( | ||
{ | ||
address: argv.address, | ||
scriptPubKey: argv.scriptPubKey, | ||
}, | ||
argv.network | ||
); | ||
const value = getOutputValue(argv.amount, { scriptPubKey, psbt, feeRateSatB: argv.feeRateSatB }); | ||
psbt.addOutput({ script: scriptPubKey, value }); | ||
return psbt; | ||
}); | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import * as yargs from 'yargs'; | ||
import { withPsbt, withPsbtOptions, WithPsbtOptions } from './withPsbt'; | ||
|
||
type ArgsCreatePsbt = WithPsbtOptions & { | ||
txVersion?: number; | ||
txLocktime?: number; | ||
}; | ||
|
||
export const cmdCreate: yargs.CommandModule<unknown, ArgsCreatePsbt> = { | ||
builder(b: yargs.Argv<unknown>) { | ||
return b.options(withPsbtOptions).option('txVersion', { type: 'number' }).option('txLocktime', { type: 'number' }); | ||
}, | ||
command: 'create', | ||
describe: 'create empty psbt without inputs or outputs', | ||
async handler(argv) { | ||
return withPsbt({ ...argv, create: true, expectEmpty: true }, async function (psbt) { | ||
if (argv.txVersion !== undefined) { | ||
psbt.setVersion(argv.txVersion); | ||
} | ||
if (argv.txLocktime !== undefined) { | ||
psbt.setLocktime(argv.txLocktime); | ||
} | ||
return psbt; | ||
}); | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
import { Argv, CommandModule } from 'yargs'; | ||
import { withPsbt, WithPsbtOptions, withPsbtOptions } from './withPsbt'; | ||
import * as utxolib from '@bitgo/utxo-lib'; | ||
import { toUtxoPsbt, toWrappedPsbt } from './wrap'; | ||
|
||
type ArgsFinalizePsbt = WithPsbtOptions & { | ||
extract: boolean; | ||
}; | ||
|
||
export function finalizeWithWrappedPsbt(psbt: utxolib.bitgo.UtxoPsbt | utxolib.Psbt): void { | ||
const wrappedPsbt = toWrappedPsbt(psbt); | ||
wrappedPsbt.finalize(); | ||
const unwrappedPsbt = toUtxoPsbt(wrappedPsbt); | ||
for (let i = 0; i < psbt.data.inputs.length; i++) { | ||
psbt.data.inputs[i] = unwrappedPsbt.data.inputs[i]; | ||
} | ||
} | ||
|
||
export const cmdFinalize: CommandModule<unknown, ArgsFinalizePsbt> = { | ||
command: 'finalize [psbt]', | ||
describe: 'finalize psbt', | ||
builder(b: Argv<unknown>): Argv<ArgsFinalizePsbt> { | ||
return b.options(withPsbtOptions).option('extract', { type: 'boolean', default: false }); | ||
}, | ||
async handler(argv) { | ||
await withPsbt(argv, async function (psbt) { | ||
finalizeWithWrappedPsbt(psbt); | ||
if (argv.extract) { | ||
return psbt.extractTransaction().toBuffer(); | ||
} | ||
return psbt; | ||
}); | ||
}, | ||
}; | ||
|
||
export const cmdExtract: CommandModule<unknown, WithPsbtOptions> = { | ||
command: 'extract [psbt]', | ||
describe: 'extract transaction from psbt', | ||
builder(b: Argv<unknown>): Argv<WithPsbtOptions> { | ||
return b.options(withPsbtOptions); | ||
}, | ||
async handler(argv) { | ||
await withPsbt(argv, async function (psbt) { | ||
return psbt.extractTransaction().toBuffer(); | ||
}); | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import { Argv, CommandModule } from 'yargs'; | ||
import * as utxolib from '@bitgo/utxo-lib'; | ||
|
||
import { withPsbt, WithPsbtOptions, withPsbtOptions } from './withPsbt'; | ||
import { getNetworkOptionsDemand } from '../../args'; | ||
|
||
export type ArgsSignPsbt = WithPsbtOptions & { | ||
key: string; | ||
}; | ||
|
||
export const cmdSign: CommandModule<unknown, ArgsSignPsbt> = { | ||
command: 'sign [psbt]', | ||
describe: 'sign psbt', | ||
builder(b: Argv<unknown>): Argv<ArgsSignPsbt> { | ||
return b | ||
.options(getNetworkOptionsDemand('bitcoin')) | ||
.options(withPsbtOptions) | ||
.option('key', { type: 'string', demandOption: true }); | ||
}, | ||
async handler(argv) { | ||
const key = utxolib.bip32.fromBase58(argv.key, argv.network); | ||
await withPsbt(argv, async function (psbt) { | ||
psbt.signAllInputsHD(key); | ||
return psbt; | ||
}); | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import { Argv, CommandModule } from 'yargs'; | ||
|
||
import { cmdSign } from './cmdSign'; | ||
import { cmdAddOutput } from './cmdAddOutput'; | ||
import { cmdAddDescriptorInput } from './cmdAddDescriptorInput'; | ||
import { cmdExtract, cmdFinalize } from './cmdFinalize'; | ||
import { cmdCreate } from './cmdCreate'; | ||
|
||
export const cmdPsbt = { | ||
command: 'psbt <command>', | ||
describe: 'psbt commands', | ||
builder(b: Argv<unknown>): Argv<unknown> { | ||
return b | ||
.strict() | ||
.command(cmdCreate) | ||
.command(cmdAddDescriptorInput) | ||
.command(cmdAddOutput) | ||
.command(cmdSign) | ||
.command(cmdFinalize) | ||
.command(cmdExtract) | ||
.demandCommand(); | ||
}, | ||
handler(): void { | ||
// do nothing | ||
}, | ||
} satisfies CommandModule<unknown, unknown>; |
Oops, something went wrong.