UNPKG

@bsv/wallet-toolbox

Version:

BRC100 conforming wallet, wallet storage and wallet signer components

156 lines (138 loc) 5.81 kB
import { Beef, Script, Transaction, TransactionInput, TransactionOutput } from '@bsv/sdk' import { asBsvSdkScript, sdk, verifyTruthy } from '../../index.client' import { Wallet, PendingStorageInput } from '../../Wallet' import { makeChangeLock } from './createAction' export function buildSignableTransaction( dctr: sdk.StorageCreateActionResult, args: sdk.ValidCreateActionArgs, wallet: Wallet ): { tx: Transaction amount: number pdi: PendingStorageInput[] log: string } { const changeKeys = wallet.getClientChangeKeyPair() const inputBeef = args.inputBEEF ? Beef.fromBinary(args.inputBEEF) : undefined const { inputs: storageInputs, outputs: storageOutputs } = dctr const tx = new Transaction(args.version, [], [], args.lockTime) // The order of outputs in storageOutputs is always: // CreateActionArgs.outputs in the original order // Commission output // Change outputs // The Vout values will be randomized if args.options.randomizeOutputs is true. Default is true. const voutToIndex = Array<number>(storageOutputs.length) for (let vout = 0; vout < storageOutputs.length; vout++) { const i = storageOutputs.findIndex(o => o.vout === vout) if (i < 0) throw new sdk.WERR_INVALID_PARAMETER('output.vout', `sequential. ${vout} is missing`) voutToIndex[vout] = i } ////////////// // Add OUTPUTS ///////////// for (let vout = 0; vout < storageOutputs.length; vout++) { const i = voutToIndex[vout] const out = storageOutputs[i] if (vout !== out.vout) throw new sdk.WERR_INVALID_PARAMETER('output.vout', `equal to array index. ${out.vout} !== ${vout}`) const change = out.providedBy === 'storage' && out.purpose === 'change' const lockingScript = change ? makeChangeLock(out, dctr, args, changeKeys, wallet) : asBsvSdkScript(out.lockingScript) const output: TransactionOutput = { satoshis: out.satoshis, lockingScript, change } tx.addOutput(output) } if (storageOutputs.length === 0) { // Add a dummy output to avoid transaction rejection by processors for having no outputs. const output: TransactionOutput = { satoshis: 0, lockingScript: Script.fromASM('OP_FALSE OP_RETURN 42'), change: false } tx.addOutput(output) } ////////////// // Merge and sort INPUTS info by vin order. ///////////// const inputs: { argsInput: sdk.ValidCreateActionInput | undefined storageInput: sdk.StorageCreateTransactionSdkInput }[] = [] for (const storageInput of storageInputs) { const argsInput = storageInput.vin !== undefined && storageInput.vin < args.inputs.length ? args.inputs[storageInput.vin] : undefined inputs.push({ argsInput, storageInput }) } inputs.sort((a, b) => a.storageInput.vin! < b.storageInput.vin! ? -1 : a.storageInput.vin! === b.storageInput.vin! ? 0 : 1 ) const pendingStorageInputs: PendingStorageInput[] = [] ////////////// // Add INPUTS ///////////// let totalChangeInputs = 0 for (const { storageInput, argsInput } of inputs) { // Two types of inputs are handled: user specified wth/without unlockingScript and storage specified using SABPPP template. if (argsInput) { // Type 1: User supplied input, with or without an explicit unlockingScript. // If without, signAction must be used to provide the actual unlockScript. const hasUnlock = typeof argsInput.unlockingScript === 'string' const unlock = hasUnlock ? asBsvSdkScript(argsInput.unlockingScript!) : new Script() const sourceTransaction = args.isSignAction ? inputBeef?.findTxid(argsInput.outpoint.txid)?.tx : undefined const inputToAdd: TransactionInput = { sourceTXID: argsInput.outpoint.txid, sourceOutputIndex: argsInput.outpoint.vout, // Include the source transaction for access to the outputs locking script and output satoshis for user side fee calculation. // TODO: Make this conditional to improve performance when user can supply locking scripts themselves. sourceTransaction, unlockingScript: unlock, sequence: argsInput.sequenceNumber } tx.addInput(inputToAdd) } else { // Type2: SABPPP protocol inputs which are signed using ScriptTemplateBRC29. if (storageInput.type !== 'P2PKH') throw new sdk.WERR_INVALID_PARAMETER( 'type', `vin ${storageInput.vin}, "${storageInput.type}" is not a supported unlocking script type.` ) pendingStorageInputs.push({ vin: tx.inputs.length, derivationPrefix: verifyTruthy(storageInput.derivationPrefix), derivationSuffix: verifyTruthy(storageInput.derivationSuffix), unlockerPubKey: storageInput.senderIdentityKey, sourceSatoshis: storageInput.sourceSatoshis, lockingScript: storageInput.sourceLockingScript }) const inputToAdd: TransactionInput = { sourceTXID: storageInput.sourceTxid, sourceOutputIndex: storageInput.sourceVout, sourceTransaction: storageInput.sourceTransaction ? Transaction.fromBinary(storageInput.sourceTransaction) : undefined, unlockingScript: new Script(), sequence: 0xffffffff } tx.addInput(inputToAdd) totalChangeInputs += verifyTruthy(storageInput.sourceSatoshis) } } // The amount is the total of non-foreign inputs minus change outputs // Note that the amount can be negative when we are redeeming more inputs than we are spending const totalChangeOutputs = storageOutputs .filter(x => x.purpose === 'change') .reduce((acc, el) => acc + el.satoshis, 0) const amount = totalChangeInputs - totalChangeOutputs return { tx, amount, pdi: pendingStorageInputs, log: '' } }