diff --git a/packages/wasm-utxo/js/fixedScriptWallet/BitGoPsbt.ts b/packages/wasm-utxo/js/fixedScriptWallet/BitGoPsbt.ts index fae66000..084644a3 100644 --- a/packages/wasm-utxo/js/fixedScriptWallet/BitGoPsbt.ts +++ b/packages/wasm-utxo/js/fixedScriptWallet/BitGoPsbt.ts @@ -4,7 +4,7 @@ import { type PsbtOutputData, type PsbtOutputDataWithAddress, } from "../wasm/wasm_utxo.js"; -import type { IPsbtIntrospectionWithAddress } from "../psbt.js"; +import type { IPsbtWithAddress } from "../psbt.js"; import { type WalletKeysArg, RootWalletKeys } from "./RootWalletKeys.js"; import { type ReplayProtectionArg, ReplayProtection } from "./ReplayProtection.js"; import { type BIP32Arg, BIP32, isBIP32Arg } from "../bip32.js"; @@ -124,7 +124,7 @@ export type ParseOutputsOptions = { payGoPubkeys?: ECPairArg[]; }; -export class BitGoPsbt implements IPsbtIntrospectionWithAddress { +export class BitGoPsbt implements IPsbtWithAddress { protected constructor(protected _wasm: WasmBitGoPsbt) {} /** @@ -202,6 +202,45 @@ export class BitGoPsbt implements IPsbtIntrospectionWithAddress { * }, outputScript); * ``` */ + addInputAtIndex( + index: number, + txid: string, + vout: number, + value: bigint, + script: Uint8Array, + sequence?: number, + ): number; + addInputAtIndex(index: number, options: AddInputOptions, script: Uint8Array): number; + addInputAtIndex( + index: number, + txidOrOptions: string | AddInputOptions, + voutOrScript: number | Uint8Array, + value?: bigint, + script?: Uint8Array, + sequence?: number, + ): number { + if (typeof txidOrOptions === "string") { + return this._wasm.add_input_at_index( + index, + txidOrOptions, + voutOrScript as number, + value, + script, + sequence, + ); + } + const options = txidOrOptions; + return this._wasm.add_input_at_index( + index, + options.txid, + options.vout, + options.value, + voutOrScript as Uint8Array, + options.sequence, + options.prevTx, + ); + } + addInput(options: AddInputOptions, script: Uint8Array): number { return this._wasm.add_input( options.txid, @@ -225,41 +264,36 @@ export class BitGoPsbt implements IPsbtIntrospectionWithAddress { * const outputIndex = psbt.addOutput(outputScript, 50000n); * ``` */ + addOutputAtIndex(index: number, script: Uint8Array, value: bigint): number; + addOutputAtIndex(index: number, address: string, value: bigint): number; + addOutputAtIndex(index: number, options: AddOutputOptions): number; + addOutputAtIndex( + index: number, + scriptOrOptions: Uint8Array | string | AddOutputOptions, + value?: bigint, + ): number { + if (scriptOrOptions instanceof Uint8Array || typeof scriptOrOptions === "string") { + if (value === undefined) { + throw new Error("Value is required when passing a script or address"); + } + if (scriptOrOptions instanceof Uint8Array) { + return this._wasm.add_output_at_index(index, scriptOrOptions, value); + } + return this._wasm.add_output_with_address_at_index(index, scriptOrOptions, value); + } + + const options = scriptOrOptions; + if ("script" in options) { + return this._wasm.add_output_at_index(index, options.script, options.value); + } + if ("address" in options) { + return this._wasm.add_output_with_address_at_index(index, options.address, options.value); + } + throw new Error("Invalid output options"); + } + addOutput(script: Uint8Array, value: bigint): number; - /** - * Add an output to the PSBT by address - * - * @param address - The destination address - * @param value - Value in satoshis - * @returns The index of the newly added output - * - * @example - * ```typescript - * const outputIndex = psbt.addOutput("bc1q...", 50000n); - * ``` - */ addOutput(address: string, value: bigint): number; - /** - * Add an output to the PSBT - * - * @param options - Output options (script or address, and value) - * @returns The index of the newly added output - * - * @example - * ```typescript - * // Using script - * const outputIndex = psbt.addOutput({ - * script: outputScript, - * value: 50000n, - * }); - * - * // Using address - * const outputIndex = psbt.addOutput({ - * address: "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4", - * value: 50000n, - * }); - * ``` - */ addOutput(options: AddOutputOptions): number; addOutput(scriptOrOptions: Uint8Array | string | AddOutputOptions, value?: bigint): number { if (scriptOrOptions instanceof Uint8Array || typeof scriptOrOptions === "string") { @@ -321,6 +355,28 @@ export class BitGoPsbt implements IPsbtIntrospectionWithAddress { * ); * ``` */ + addWalletInputAtIndex( + index: number, + inputOptions: AddInputOptions, + walletKeys: WalletKeysArg, + walletOptions: AddWalletInputOptions, + ): number { + const keys = RootWalletKeys.from(walletKeys); + return this._wasm.add_wallet_input_at_index( + index, + inputOptions.txid, + inputOptions.vout, + inputOptions.value, + keys.wasm, + walletOptions.scriptId.chain, + walletOptions.scriptId.index, + walletOptions.signPath?.signer, + walletOptions.signPath?.cosigner, + inputOptions.sequence, + inputOptions.prevTx, + ); + } + addWalletInput( inputOptions: AddInputOptions, walletKeys: WalletKeysArg, @@ -371,6 +427,21 @@ export class BitGoPsbt implements IPsbtIntrospectionWithAddress { * }); * ``` */ + addWalletOutputAtIndex( + index: number, + walletKeys: WalletKeysArg, + options: AddWalletOutputOptions, + ): number { + const keys = RootWalletKeys.from(walletKeys); + return this._wasm.add_wallet_output_at_index( + index, + options.chain, + options.index, + options.value, + keys.wasm, + ); + } + addWalletOutput(walletKeys: WalletKeysArg, options: AddWalletOutputOptions): number { const keys = RootWalletKeys.from(walletKeys); return this._wasm.add_wallet_output(options.chain, options.index, options.value, keys.wasm); @@ -395,6 +466,23 @@ export class BitGoPsbt implements IPsbtIntrospectionWithAddress { * ); * ``` */ + addReplayProtectionInputAtIndex( + index: number, + inputOptions: AddInputOptions, + key: ECPairArg, + ): number { + const ecpair = ECPair.from(key); + return this._wasm.add_replay_protection_input_at_index( + index, + ecpair.wasm, + inputOptions.txid, + inputOptions.vout, + inputOptions.value, + inputOptions.sequence, + inputOptions.prevTx, + ); + } + addReplayProtectionInput(inputOptions: AddInputOptions, key: ECPairArg): number { const ecpair = ECPair.from(key); return this._wasm.add_replay_protection_input( @@ -407,11 +495,19 @@ export class BitGoPsbt implements IPsbtIntrospectionWithAddress { ); } + removeInput(index: number): void { + this._wasm.remove_input(index); + } + + removeOutput(index: number): void { + this._wasm.remove_output(index); + } + /** * Get the unsigned transaction ID * @returns The unsigned transaction ID */ - unsignedTxid(): string { + unsignedTxId(): string { return this._wasm.unsigned_txid(); } @@ -419,15 +515,11 @@ export class BitGoPsbt implements IPsbtIntrospectionWithAddress { * Get the transaction version * @returns The transaction version number */ - get version(): number { + version(): number { return this._wasm.version(); } - /** - * Get the transaction lock time - * @returns The transaction lock time - */ - get lockTime(): number { + lockTime(): number { return this._wasm.lock_time(); } @@ -828,15 +920,11 @@ export class BitGoPsbt implements IPsbtIntrospectionWithAddress { * Get the number of inputs in the PSBT * @returns The number of inputs */ - get inputCount(): number { + inputCount(): number { return this._wasm.input_count(); } - /** - * Get the number of outputs in the PSBT - * @returns The number of outputs - */ - get outputCount(): number { + outputCount(): number { return this._wasm.output_count(); } diff --git a/packages/wasm-utxo/js/index.ts b/packages/wasm-utxo/js/index.ts index 85d135c3..8c41079a 100644 --- a/packages/wasm-utxo/js/index.ts +++ b/packages/wasm-utxo/js/index.ts @@ -119,6 +119,19 @@ declare module "./wasm/wasm_utxo.js" { // Extraction methods extractTransaction(): WasmTransaction; + // Mutation methods + addInputAtIndex( + index: number, + txid: string, + vout: number, + value: bigint, + script: Uint8Array, + sequence?: number, + ): number; + addOutputAtIndex(index: number, script: Uint8Array, value: bigint): number; + removeInput(index: number): void; + removeOutput(index: number): void; + // Metadata methods unsignedTxId(): string; lockTime(): number; @@ -130,8 +143,4 @@ export { WrapDescriptor as Descriptor } from "./wasm/wasm_utxo.js"; export { WrapMiniscript as Miniscript } from "./wasm/wasm_utxo.js"; export { WrapPsbt as Psbt } from "./wasm/wasm_utxo.js"; export { DashTransaction, Transaction, ZcashTransaction } from "./transaction.js"; -export { - hasPsbtMagic, - type IPsbtIntrospection, - type IPsbtIntrospectionWithAddress, -} from "./psbt.js"; +export { hasPsbtMagic, type IPsbt, type IPsbtWithAddress } from "./psbt.js"; diff --git a/packages/wasm-utxo/js/psbt.ts b/packages/wasm-utxo/js/psbt.ts index 005bae59..a8067a54 100644 --- a/packages/wasm-utxo/js/psbt.ts +++ b/packages/wasm-utxo/js/psbt.ts @@ -1,15 +1,29 @@ import type { PsbtInputData, PsbtOutputData, PsbtOutputDataWithAddress } from "./wasm/wasm_utxo.js"; -/** Common interface for PSBT introspection methods */ -export interface IPsbtIntrospection { - readonly inputCount: number; - readonly outputCount: number; +/** Common interface for PSBT types */ +export interface IPsbt { + inputCount(): number; + outputCount(): number; getInputs(): PsbtInputData[]; getOutputs(): PsbtOutputData[]; + version(): number; + lockTime(): number; + unsignedTxId(): string; + addInputAtIndex( + index: number, + txid: string, + vout: number, + value: bigint, + script: Uint8Array, + sequence?: number, + ): number; + addOutputAtIndex(index: number, script: Uint8Array, value: bigint): number; + removeInput(index: number): void; + removeOutput(index: number): void; } -/** Extended introspection with address resolution (no coin parameter needed) */ -export interface IPsbtIntrospectionWithAddress extends IPsbtIntrospection { +/** Extended PSBT with address resolution (no coin parameter needed) */ +export interface IPsbtWithAddress extends IPsbt { getOutputsWithAddress(): PsbtOutputDataWithAddress[]; } diff --git a/packages/wasm-utxo/js/transaction.ts b/packages/wasm-utxo/js/transaction.ts index d1529d9f..435d54aa 100644 --- a/packages/wasm-utxo/js/transaction.ts +++ b/packages/wasm-utxo/js/transaction.ts @@ -41,16 +41,18 @@ export class Transaction implements ITransaction { * @param sequence - Optional sequence number (default: 0xFFFFFFFF) * @returns The index of the newly added input */ + addInputAtIndex(index: number, txid: string, vout: number, sequence?: number): number { + return this._wasm.add_input_at_index(index, txid, vout, sequence); + } + addInput(txid: string, vout: number, sequence?: number): number { return this._wasm.add_input(txid, vout, sequence); } - /** - * Add an output to the transaction - * @param script - Output script (scriptPubKey) - * @param value - Value in satoshis - * @returns The index of the newly added output - */ + addOutputAtIndex(index: number, script: Uint8Array, value: bigint): number { + return this._wasm.add_output_at_index(index, script, value); + } + addOutput(script: Uint8Array, value: bigint): number { return this._wasm.add_output(script, value); } diff --git a/packages/wasm-utxo/package.json b/packages/wasm-utxo/package.json index 1dc42ff3..9edf7116 100644 --- a/packages/wasm-utxo/package.json +++ b/packages/wasm-utxo/package.json @@ -75,9 +75,16 @@ "build:ts": "npm run build:ts-esm && npm run build:ts-cjs", "build:package-json": "echo '{\"type\":\"commonjs\"}' > dist/cjs/package.json", "build": "npm run build:wasm && npm run build:ts && npm run build:package-json", - "check-fmt": "prettier --check . && cargo fmt -- --check", - "lint": "eslint .", - "lint:fix": "eslint . --fix" + "check-fmt": "npm run lint:prettier && npm run lint:rustfmt", + "lint:prettier": "prettier --check .", + "lint:eslint": "eslint .", + "lint:rustfmt": "cargo fmt -- --check", + "lint:clippy": "cargo clippy --all-targets --all-features -- -D warnings", + "lint": "npm run lint:prettier && npm run lint:eslint && npm run lint:rustfmt && npm run lint:clippy", + "lint:prettier:fix": "prettier --write .", + "lint:eslint:fix": "eslint . --fix", + "lint:rustfmt:fix": "cargo fmt", + "lint:fix": "npm run lint:prettier:fix && npm run lint:eslint:fix && npm run lint:rustfmt:fix" }, "devDependencies": { "@bitgo/unspents": "^0.50.13", diff --git a/packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/mod.rs b/packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/mod.rs index 82a4e9da..3e46924e 100644 --- a/packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/mod.rs +++ b/packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/mod.rs @@ -572,28 +572,25 @@ impl BitGoPsbt { /// /// # Returns /// The index of the newly added input - pub fn add_input( + #[allow(clippy::too_many_arguments)] + pub fn add_input_at_index( &mut self, + index: usize, txid: Txid, vout: u32, value: u64, script: miniscript::bitcoin::ScriptBuf, sequence: Option, prev_tx: Option, - ) -> usize { + ) -> Result { use miniscript::bitcoin::{transaction::Sequence, Amount, OutPoint, TxIn, TxOut}; - let psbt = self.psbt_mut(); - - // Create the transaction input let tx_in = TxIn { previous_output: OutPoint { txid, vout }, script_sig: miniscript::bitcoin::ScriptBuf::new(), sequence: Sequence(sequence.unwrap_or(0xFFFFFFFE)), witness: miniscript::bitcoin::Witness::default(), }; - - // Create the PSBT input with witness_utxo populated let psbt_input = miniscript::bitcoin::psbt::Input { witness_utxo: Some(TxOut { value: Amount::from_sat(value), @@ -603,11 +600,21 @@ impl BitGoPsbt { ..Default::default() }; - // Add to the PSBT - psbt.unsigned_tx.input.push(tx_in); - psbt.inputs.push(psbt_input); + crate::psbt_ops::insert_input(self.psbt_mut(), index, tx_in, psbt_input) + } - psbt.inputs.len() - 1 + pub fn add_input( + &mut self, + txid: Txid, + vout: u32, + value: u64, + script: miniscript::bitcoin::ScriptBuf, + sequence: Option, + prev_tx: Option, + ) -> usize { + let index = self.psbt().inputs.len(); + self.add_input_at_index(index, txid, vout, value, script, sequence, prev_tx) + .expect("insert at len should never fail") } /// Add a replay protection input (p2shP2pk) to the PSBT @@ -624,14 +631,15 @@ impl BitGoPsbt { /// /// # Returns /// The index of the newly added input - pub fn add_replay_protection_input( + pub fn add_replay_protection_input_at_index( &mut self, + index: usize, pubkey: miniscript::bitcoin::CompressedPublicKey, txid: Txid, vout: u32, value: u64, options: ReplayProtectionOptions, - ) -> usize { + ) -> Result { use crate::fixed_script_wallet::wallet_scripts::ScriptP2shP2pk; use miniscript::bitcoin::consensus::Decodable; use miniscript::bitcoin::psbt::{Input, PsbtSighashType}; @@ -640,14 +648,11 @@ impl BitGoPsbt { }; let network = self.network(); - let psbt = self.psbt_mut(); - // Create the p2shP2pk script let script = ScriptP2shP2pk::new(pubkey); let output_script = script.output_script(); let redeem_script = script.redeem_script; - // Create the transaction input let tx_in = TxIn { previous_output: OutPoint { txid, vout }, script_sig: miniscript::bitcoin::ScriptBuf::new(), @@ -655,28 +660,23 @@ impl BitGoPsbt { witness: miniscript::bitcoin::Witness::default(), }; - // Determine sighash type: use provided value or default based on network // Networks with SIGHASH_FORKID use SIGHASH_ALL | SIGHASH_FORKID (0x41) - let sighash_type = options.sighash_type.unwrap_or_else(|| { - match network.mainnet() { + let sighash_type = options + .sighash_type + .unwrap_or_else(|| match network.mainnet() { Network::BitcoinCash | Network::Ecash | Network::BitcoinSV - | Network::BitcoinGold => { - PsbtSighashType::from_u32(0x41) // SIGHASH_ALL | SIGHASH_FORKID - } - _ => PsbtSighashType::from_u32(0x01), // SIGHASH_ALL - } - }); + | Network::BitcoinGold => PsbtSighashType::from_u32(0x41), + _ => PsbtSighashType::from_u32(0x01), + }); - // Create the PSBT input let mut psbt_input = Input { redeem_script: Some(redeem_script), sighash_type: Some(sighash_type), ..Default::default() }; - // Set utxo: either non_witness_utxo (full tx) or witness_utxo (output only) if let Some(tx_bytes) = options.prev_tx { let tx = Transaction::consensus_decode(&mut &tx_bytes[..]) .expect("Failed to decode prev_tx"); @@ -688,11 +688,20 @@ impl BitGoPsbt { }); } - // Add to the PSBT - psbt.unsigned_tx.input.push(tx_in); - psbt.inputs.push(psbt_input); + crate::psbt_ops::insert_input(self.psbt_mut(), index, tx_in, psbt_input) + } - psbt.inputs.len() - 1 + pub fn add_replay_protection_input( + &mut self, + pubkey: miniscript::bitcoin::CompressedPublicKey, + txid: Txid, + vout: u32, + value: u64, + options: ReplayProtectionOptions, + ) -> usize { + let index = self.psbt().inputs.len(); + self.add_replay_protection_input_at_index(index, pubkey, txid, vout, value, options) + .expect("insert at len should never fail") } /// Add an output to the PSBT @@ -703,32 +712,48 @@ impl BitGoPsbt { /// /// # Returns /// The index of the newly added output - pub fn add_output(&mut self, script: miniscript::bitcoin::ScriptBuf, value: u64) -> usize { + pub fn add_output_at_index( + &mut self, + index: usize, + script: miniscript::bitcoin::ScriptBuf, + value: u64, + ) -> Result { use miniscript::bitcoin::{Amount, TxOut}; - let psbt = self.psbt_mut(); - - // Create the transaction output let tx_out = TxOut { value: Amount::from_sat(value), script_pubkey: script, }; - // Create the PSBT output - let psbt_output = miniscript::bitcoin::psbt::Output::default(); - - // Add to the PSBT - psbt.unsigned_tx.output.push(tx_out); - psbt.outputs.push(psbt_output); + crate::psbt_ops::insert_output( + self.psbt_mut(), + index, + tx_out, + miniscript::bitcoin::psbt::Output::default(), + ) + } - psbt.outputs.len() - 1 + pub fn add_output(&mut self, script: miniscript::bitcoin::ScriptBuf, value: u64) -> usize { + let index = self.psbt().outputs.len(); + self.add_output_at_index(index, script, value) + .expect("insert at len should never fail") } - pub fn add_output_with_address(&mut self, address: &str, value: u64) -> Result { + pub fn add_output_with_address_at_index( + &mut self, + index: usize, + address: &str, + value: u64, + ) -> Result { let script = crate::address::networks::to_output_script_with_network(address, self.network()) .map_err(|e| e.to_string())?; - Ok(self.add_output(script, value)) + self.add_output_at_index(index, script, value) + } + + pub fn add_output_with_address(&mut self, address: &str, value: u64) -> Result { + let index = self.psbt().outputs.len(); + self.add_output_with_address_at_index(index, address, value) } /// Add a wallet input with full PSBT metadata @@ -746,8 +771,10 @@ impl BitGoPsbt { /// /// # Returns /// The index of the newly added input - pub fn add_wallet_input( + #[allow(clippy::too_many_arguments)] + pub fn add_wallet_input_at_index( &mut self, + index: usize, txid: Txid, vout: u32, value: u64, @@ -767,26 +794,21 @@ impl BitGoPsbt { let psbt = self.psbt_mut(); let chain = script_id.chain; - let index = script_id.index; + let derivation_index = script_id.index; - // Parse chain let chain_enum = Chain::try_from(chain)?; - // Derive wallet keys for this chain/index let derived_keys = wallet_keys - .derive_for_chain_and_index(chain, index) + .derive_for_chain_and_index(chain, derivation_index) .map_err(|e| format!("Failed to derive keys: {}", e))?; let pub_triple = to_pub_triple(&derived_keys); - // Create wallet scripts let script_support = network.output_script_support(); let scripts = WalletScripts::new(&pub_triple, chain_enum, &script_support) .map_err(|e| format!("Failed to create wallet scripts: {}", e))?; - // Get the output script let output_script = scripts.output_script(); - // Create the transaction input let tx_in = TxIn { previous_output: OutPoint { txid, vout }, script_sig: miniscript::bitcoin::ScriptBuf::new(), @@ -794,53 +816,43 @@ impl BitGoPsbt { witness: miniscript::bitcoin::Witness::default(), }; - // Create the PSBT input let mut psbt_input = Input::default(); - // Determine if segwit based on chain type (all types except P2sh are segwit) let is_segwit = chain_enum.script_type != OutputScriptType::P2sh; if let (false, Some(tx_bytes)) = (is_segwit, options.prev_tx) { - // Non-segwit with prev_tx: use non_witness_utxo psbt_input.non_witness_utxo = Some( miniscript::bitcoin::consensus::deserialize(tx_bytes) .map_err(|e| format!("Failed to deserialize previous transaction: {}", e))?, ); } else { - // Segwit or non-segwit without prev_tx: use witness_utxo psbt_input.witness_utxo = Some(TxOut { value: Amount::from_sat(value), script_pubkey: output_script.clone(), }); } - // Set sighash type based on network let sighash_type = get_default_sighash_type(network, chain_enum); psbt_input.sighash_type = Some(sighash_type); - // Populate script-type-specific metadata match &scripts { WalletScripts::P2sh(script) => { - // bip32_derivation for all 3 keys - psbt_input.bip32_derivation = create_bip32_derivation(wallet_keys, chain, index); - // redeem_script + psbt_input.bip32_derivation = + create_bip32_derivation(wallet_keys, chain, derivation_index); psbt_input.redeem_script = Some(script.redeem_script.clone()); } WalletScripts::P2shP2wsh(script) => { - // bip32_derivation for all 3 keys - psbt_input.bip32_derivation = create_bip32_derivation(wallet_keys, chain, index); - // witness_script and redeem_script + psbt_input.bip32_derivation = + create_bip32_derivation(wallet_keys, chain, derivation_index); psbt_input.witness_script = Some(script.witness_script.clone()); psbt_input.redeem_script = Some(script.redeem_script.clone()); } WalletScripts::P2wsh(script) => { - // bip32_derivation for all 3 keys - psbt_input.bip32_derivation = create_bip32_derivation(wallet_keys, chain, index); - // witness_script + psbt_input.bip32_derivation = + create_bip32_derivation(wallet_keys, chain, derivation_index); psbt_input.witness_script = Some(script.witness_script.clone()); } WalletScripts::P2trLegacy(script) | WalletScripts::P2trMusig2(script) => { - // For taproot, sign_path is required let sign_path = options.sign_path.ok_or_else(|| { "sign_path is required for p2tr/p2trMusig2 inputs".to_string() })?; @@ -851,8 +863,6 @@ impl BitGoPsbt { let is_backup_flow = sign_path.signer.is_backup() || sign_path.cosigner.is_backup(); if !is_musig2 || is_backup_flow { - // Script path spending (p2tr or p2trMusig2 with backup) - // Get the leaf script for signer/cosigner pair let signer_keys = [pub_triple[signer_idx], pub_triple[cosigner_idx]]; let leaf_script = crate::fixed_script_wallet::wallet_scripts::build_p2tr_ns_script( @@ -860,7 +870,6 @@ impl BitGoPsbt { ); let leaf_hash = TapLeafHash::from_script(&leaf_script, LeafVersion::TapScript); - // Find the control block for this leaf let control_block = script .spend_info .control_block(&(leaf_script.clone(), LeafVersion::TapScript)) @@ -868,45 +877,36 @@ impl BitGoPsbt { "Could not find control block for leaf script".to_string() })?; - // Set tap_leaf_script psbt_input.tap_scripts.insert( control_block.clone(), (leaf_script.clone(), LeafVersion::TapScript), ); - // Set tap_bip32_derivation for signer and cosigner psbt_input.tap_key_origins = create_tap_bip32_derivation( wallet_keys, chain, - index, + derivation_index, &[signer_idx, cosigner_idx], Some(leaf_hash), ); } else { - // Key path spending (p2trMusig2 with user/bitgo) let internal_key = script.spend_info.internal_key(); let merkle_root = script.spend_info.merkle_root(); - // Set tap_internal_key psbt_input.tap_internal_key = Some(internal_key); - - // Set tap_merkle_root psbt_input.tap_merkle_root = merkle_root; - // Set tap_bip32_derivation for signer and cosigner (no leaf hashes for key path) psbt_input.tap_key_origins = create_tap_bip32_derivation( wallet_keys, chain, - index, + derivation_index, &[signer_idx, cosigner_idx], None, ); - // Set musig2 participant pubkeys (proprietary field) - let user_key = pub_triple[0]; // user is index 0 - let bitgo_key = pub_triple[2]; // bitgo is index 2 + let user_key = pub_triple[0]; + let bitgo_key = pub_triple[2]; - // Create musig2 participants let tap_output_key = script.spend_info.output_key().to_x_only_public_key(); let musig2_participants = Musig2Participants { tap_output_key, @@ -914,18 +914,26 @@ impl BitGoPsbt { participant_pub_keys: [user_key, bitgo_key], }; - // Add to proprietary keys let (key, value) = musig2_participants.to_key_value().to_key_value(); psbt_input.proprietary.insert(key, value); } } } - // Add to PSBT - psbt.unsigned_tx.input.push(tx_in); - psbt.inputs.push(psbt_input); + crate::psbt_ops::insert_input(psbt, index, tx_in, psbt_input) + } - Ok(psbt.inputs.len() - 1) + pub fn add_wallet_input( + &mut self, + txid: Txid, + vout: u32, + value: u64, + wallet_keys: &crate::fixed_script_wallet::RootWalletKeys, + script_id: psbt_wallet_input::ScriptId, + options: WalletInputOptions, + ) -> Result { + let index = self.psbt().inputs.len(); + self.add_wallet_input_at_index(index, txid, vout, value, wallet_keys, script_id, options) } /// Add a wallet output with full PSBT metadata @@ -941,10 +949,12 @@ impl BitGoPsbt { /// /// # Returns /// The index of the newly added output - pub fn add_wallet_output( + #[allow(clippy::too_many_arguments)] + pub fn add_wallet_output_at_index( &mut self, + index: usize, chain: u32, - index: u32, + derivation_index: u32, value: u64, wallet_keys: &crate::fixed_script_wallet::RootWalletKeys, ) -> Result { @@ -959,79 +969,81 @@ impl BitGoPsbt { let network = self.network(); let psbt = self.psbt_mut(); - // Parse chain let chain_enum = Chain::try_from(chain)?; - // Derive wallet keys for this chain/index let derived_keys = wallet_keys - .derive_for_chain_and_index(chain, index) + .derive_for_chain_and_index(chain, derivation_index) .map_err(|e| format!("Failed to derive keys: {}", e))?; let pub_triple = to_pub_triple(&derived_keys); - // Create wallet scripts let script_support = network.output_script_support(); let scripts = WalletScripts::new(&pub_triple, chain_enum, &script_support) .map_err(|e| format!("Failed to create wallet scripts: {}", e))?; - // Get the output script let output_script = scripts.output_script(); - // Create the transaction output let tx_out = TxOut { value: Amount::from_sat(value), script_pubkey: output_script, }; - // Create the PSBT output with metadata let mut psbt_output = Output::default(); - // Populate script-type-specific metadata match &scripts { WalletScripts::P2sh(script) => { - // bip32_derivation for all 3 keys - psbt_output.bip32_derivation = create_bip32_derivation(wallet_keys, chain, index); - // redeem_script + psbt_output.bip32_derivation = + create_bip32_derivation(wallet_keys, chain, derivation_index); psbt_output.redeem_script = Some(script.redeem_script.clone()); } WalletScripts::P2shP2wsh(script) => { - // bip32_derivation for all 3 keys - psbt_output.bip32_derivation = create_bip32_derivation(wallet_keys, chain, index); - // witness_script and redeem_script + psbt_output.bip32_derivation = + create_bip32_derivation(wallet_keys, chain, derivation_index); psbt_output.witness_script = Some(script.witness_script.clone()); psbt_output.redeem_script = Some(script.redeem_script.clone()); } WalletScripts::P2wsh(script) => { - // bip32_derivation for all 3 keys - psbt_output.bip32_derivation = create_bip32_derivation(wallet_keys, chain, index); - // witness_script + psbt_output.bip32_derivation = + create_bip32_derivation(wallet_keys, chain, derivation_index); psbt_output.witness_script = Some(script.witness_script.clone()); } WalletScripts::P2trLegacy(script) | WalletScripts::P2trMusig2(script) => { let is_musig2 = matches!(scripts, WalletScripts::P2trMusig2(_)); - // Set tap_internal_key let internal_key = script.spend_info.internal_key(); psbt_output.tap_internal_key = Some(internal_key); - // Set tap_tree for the output psbt_output.tap_tree = Some(build_tap_tree_for_output(&pub_triple, is_musig2)); - // Set tap_bip32_derivation with correct leaf hashes for each key psbt_output.tap_key_origins = create_tap_bip32_derivation_for_output( wallet_keys, chain, - index, + derivation_index, &pub_triple, is_musig2, ); } } - // Add to PSBT - psbt.unsigned_tx.output.push(tx_out); - psbt.outputs.push(psbt_output); + crate::psbt_ops::insert_output(psbt, index, tx_out, psbt_output) + } + + pub fn add_wallet_output( + &mut self, + chain: u32, + index: u32, + value: u64, + wallet_keys: &crate::fixed_script_wallet::RootWalletKeys, + ) -> Result { + let insert_index = self.psbt().outputs.len(); + self.add_wallet_output_at_index(insert_index, chain, index, value, wallet_keys) + } + + pub fn remove_input(&mut self, index: usize) -> Result<(), String> { + crate::psbt_ops::remove_input(self.psbt_mut(), index) + } - Ok(psbt.outputs.len() - 1) + pub fn remove_output(&mut self, index: usize) -> Result<(), String> { + crate::psbt_ops::remove_output(self.psbt_mut(), index) } pub fn network(&self) -> Network { diff --git a/packages/wasm-utxo/src/lib.rs b/packages/wasm-utxo/src/lib.rs index 046d010b..096a4715 100644 --- a/packages/wasm-utxo/src/lib.rs +++ b/packages/wasm-utxo/src/lib.rs @@ -9,6 +9,7 @@ pub mod inspect; pub mod message; mod networks; pub mod paygo; +pub mod psbt_ops; #[cfg(test)] mod test_utils; pub mod zcash; diff --git a/packages/wasm-utxo/src/psbt_ops.rs b/packages/wasm-utxo/src/psbt_ops.rs new file mode 100644 index 00000000..8259c7a7 --- /dev/null +++ b/packages/wasm-utxo/src/psbt_ops.rs @@ -0,0 +1,58 @@ +use miniscript::bitcoin::{psbt, Psbt, TxIn, TxOut}; + +fn check_bounds(index: usize, len: usize, name: &str) -> Result<(), String> { + if index > len { + return Err(format!( + "{name} index {index} out of bounds (have {len} {name}s)" + )); + } + Ok(()) +} + +pub fn insert_input( + psbt: &mut Psbt, + index: usize, + tx_in: TxIn, + psbt_input: psbt::Input, +) -> Result { + check_bounds(index, psbt.inputs.len(), "input")?; + psbt.unsigned_tx.input.insert(index, tx_in); + psbt.inputs.insert(index, psbt_input); + Ok(index) +} + +pub fn insert_output( + psbt: &mut Psbt, + index: usize, + tx_out: TxOut, + psbt_output: psbt::Output, +) -> Result { + check_bounds(index, psbt.outputs.len(), "output")?; + psbt.unsigned_tx.output.insert(index, tx_out); + psbt.outputs.insert(index, psbt_output); + Ok(index) +} + +pub fn remove_input(psbt: &mut Psbt, index: usize) -> Result<(), String> { + if index >= psbt.inputs.len() { + return Err(format!( + "input index {index} out of bounds (have {} inputs)", + psbt.inputs.len() + )); + } + psbt.unsigned_tx.input.remove(index); + psbt.inputs.remove(index); + Ok(()) +} + +pub fn remove_output(psbt: &mut Psbt, index: usize) -> Result<(), String> { + if index >= psbt.outputs.len() { + return Err(format!( + "output index {index} out of bounds (have {} outputs)", + psbt.outputs.len() + )); + } + psbt.unsigned_tx.output.remove(index); + psbt.outputs.remove(index); + Ok(()) +} diff --git a/packages/wasm-utxo/src/wasm/fixed_script_wallet/mod.rs b/packages/wasm-utxo/src/wasm/fixed_script_wallet/mod.rs index 168d99bd..2e280179 100644 --- a/packages/wasm-utxo/src/wasm/fixed_script_wallet/mod.rs +++ b/packages/wasm-utxo/src/wasm/fixed_script_wallet/mod.rs @@ -376,8 +376,10 @@ impl BitGoPsbt { /// /// # Returns /// The index of the newly added input - pub fn add_input( + #[allow(clippy::too_many_arguments)] + pub fn add_input_at_index( &mut self, + index: usize, txid: &str, vout: u32, value: u64, @@ -392,7 +394,6 @@ impl BitGoPsbt { let txid = Txid::from_str(txid) .map_err(|e| WasmUtxoError::new(&format!("Invalid txid: {}", e)))?; let script = ScriptBuf::from_bytes(script.to_vec()); - let prev_tx = prev_tx .map(|bytes| { Transaction::consensus_decode(&mut bytes.as_slice()) @@ -400,35 +401,53 @@ impl BitGoPsbt { }) .transpose()?; - Ok(self - .psbt - .add_input(txid, vout, value, script, sequence, prev_tx)) + self.psbt + .add_input_at_index(index, txid, vout, value, script, sequence, prev_tx) + .map_err(|e| WasmUtxoError::new(&e)) } - /// Add an output to the PSBT - /// - /// # Arguments - /// * `script` - The output script (scriptPubKey) - /// * `value` - The value in satoshis - /// - /// # Returns - /// The index of the newly added output - pub fn add_output(&mut self, script: &[u8], value: u64) -> Result { - use miniscript::bitcoin::ScriptBuf; + pub fn add_input( + &mut self, + txid: &str, + vout: u32, + value: u64, + script: &[u8], + sequence: Option, + prev_tx: Option>, + ) -> Result { + let index = self.psbt.psbt().inputs.len(); + self.add_input_at_index(index, txid, vout, value, script, sequence, prev_tx) + } + pub fn add_output_at_index( + &mut self, + index: usize, + script: &[u8], + value: u64, + ) -> Result { + use miniscript::bitcoin::ScriptBuf; let script = ScriptBuf::from_bytes(script.to_vec()); + self.psbt + .add_output_at_index(index, script, value) + .map_err(|e| WasmUtxoError::new(&e)) + } - Ok(self.psbt.add_output(script, value)) + pub fn add_output(&mut self, script: &[u8], value: u64) -> Result { + let index = self.psbt.psbt().outputs.len(); + self.add_output_at_index(index, script, value) + } + + pub fn add_output_with_address_at_index( + &mut self, + index: usize, + address: &str, + value: u64, + ) -> Result { + Ok(self + .psbt + .add_output_with_address_at_index(index, address, value)?) } - /// Add an output to the PSBT by address - /// - /// # Arguments - /// * `address` - The destination address - /// * `value` - The value in satoshis - /// - /// # Returns - /// The index of the newly added output pub fn add_output_with_address( &mut self, address: &str, @@ -437,34 +456,28 @@ impl BitGoPsbt { Ok(self.psbt.add_output_with_address(address, value)?) } - /// Add a wallet input with full PSBT metadata - /// - /// This is a higher-level method that adds an input and populates all required - /// PSBT fields (scripts, derivation info, etc.) based on the wallet's chain type. - /// - /// # Arguments - /// * `txid` - The transaction ID (hex string) - /// * `vout` - The output index being spent - /// * `value` - The value in satoshis - /// * `chain` - The chain code (0/1=p2sh, 10/11=p2shP2wsh, 20/21=p2wsh, 30/31=p2tr, 40/41=p2trMusig2) - /// * `index` - The derivation index - /// * `wallet_keys` - The root wallet keys - /// * `signer` - The key that will sign ("user", "backup", or "bitgo") - required for p2tr/p2trMusig2 - /// * `cosigner` - The key that will co-sign - required for p2tr/p2trMusig2 - /// * `sequence` - Optional sequence number (default: 0xFFFFFFFE for RBF) - /// * `prev_tx` - Optional full previous transaction bytes (for non-segwit) - /// - /// # Returns - /// The index of the newly added input + pub fn remove_input(&mut self, index: usize) -> Result<(), WasmUtxoError> { + self.psbt + .remove_input(index) + .map_err(|e| WasmUtxoError::new(&e)) + } + + pub fn remove_output(&mut self, index: usize) -> Result<(), WasmUtxoError> { + self.psbt + .remove_output(index) + .map_err(|e| WasmUtxoError::new(&e)) + } + #[allow(clippy::too_many_arguments)] - pub fn add_wallet_input( + pub fn add_wallet_input_at_index( &mut self, + index: usize, txid: &str, vout: u32, value: u64, wallet_keys: &WasmRootWalletKeys, chain: u32, - index: u32, + derivation_index: u32, signer: Option, cosigner: Option, sequence: Option, @@ -473,6 +486,7 @@ impl BitGoPsbt { use crate::fixed_script_wallet::bitgo_psbt::psbt_wallet_input::{ ScriptId, SignPath, SignerKey, }; + use crate::fixed_script_wallet::bitgo_psbt::WalletInputOptions; use miniscript::bitcoin::Txid; use std::str::FromStr; @@ -480,8 +494,10 @@ impl BitGoPsbt { .map_err(|e| WasmUtxoError::new(&format!("Invalid txid: {}", e)))?; let wallet_keys = wallet_keys.inner(); - - let script_id = ScriptId { chain, index }; + let script_id = ScriptId { + chain, + index: derivation_index, + }; let sign_path = match (signer.as_deref(), cosigner.as_deref()) { (Some(signer_str), Some(cosigner_str)) => { let signer: SignerKey = signer_str @@ -500,10 +516,9 @@ impl BitGoPsbt { } }; - use crate::fixed_script_wallet::bitgo_psbt::WalletInputOptions; - self.psbt - .add_wallet_input( + .add_wallet_input_at_index( + index, txid, vout, value, @@ -518,49 +533,65 @@ impl BitGoPsbt { .map_err(|e| WasmUtxoError::new(&e)) } - /// Add a wallet output with full PSBT metadata - /// - /// This creates a verifiable wallet output (typically for change) with all required - /// PSBT fields (scripts, derivation info) based on the wallet's chain type. - /// - /// # Arguments - /// * `chain` - The chain code (0/1=p2sh, 10/11=p2shP2wsh, 20/21=p2wsh, 30/31=p2tr, 40/41=p2trMusig2) - /// * `index` - The derivation index - /// * `value` - The value in satoshis - /// * `wallet_keys` - The root wallet keys - /// - /// # Returns - /// The index of the newly added output - pub fn add_wallet_output( + #[allow(clippy::too_many_arguments)] + pub fn add_wallet_input( &mut self, + txid: &str, + vout: u32, + value: u64, + wallet_keys: &WasmRootWalletKeys, chain: u32, index: u32, + signer: Option, + cosigner: Option, + sequence: Option, + prev_tx: Option>, + ) -> Result { + let insert_index = self.psbt.psbt().inputs.len(); + self.add_wallet_input_at_index( + insert_index, + txid, + vout, + value, + wallet_keys, + chain, + index, + signer, + cosigner, + sequence, + prev_tx, + ) + } + + pub fn add_wallet_output_at_index( + &mut self, + index: usize, + chain: u32, + derivation_index: u32, value: u64, wallet_keys: &WasmRootWalletKeys, ) -> Result { let wallet_keys = wallet_keys.inner(); - self.psbt - .add_wallet_output(chain, index, value, wallet_keys) + .add_wallet_output_at_index(index, chain, derivation_index, value, wallet_keys) .map_err(|e| WasmUtxoError::new(&e)) } - /// Add a replay protection input to the PSBT - /// - /// Replay protection inputs are P2SH-P2PK inputs used on forked networks to prevent - /// transaction replay attacks. They use a simple pubkey script without wallet derivation. - /// - /// # Arguments - /// * `ecpair` - The ECPair containing the public key for the replay protection input - /// * `txid` - The transaction ID (hex string) of the output being spent - /// * `vout` - The output index being spent - /// * `value` - The value in satoshis - /// * `sequence` - Optional sequence number (default: 0xFFFFFFFE for RBF) - /// - /// # Returns - /// The index of the newly added input - pub fn add_replay_protection_input( + pub fn add_wallet_output( &mut self, + chain: u32, + index: u32, + value: u64, + wallet_keys: &WasmRootWalletKeys, + ) -> Result { + let insert_index = self.psbt.psbt().outputs.len(); + self.add_wallet_output_at_index(insert_index, chain, index, value, wallet_keys) + } + + #[allow(clippy::too_many_arguments)] + pub fn add_replay_protection_input_at_index( + &mut self, + index: usize, ecpair: &WasmECPair, txid: &str, vout: u32, @@ -572,11 +603,9 @@ impl BitGoPsbt { use miniscript::bitcoin::{CompressedPublicKey, Txid}; use std::str::FromStr; - // Parse txid let txid = Txid::from_str(txid) .map_err(|e| WasmUtxoError::new(&format!("Invalid txid: {}", e)))?; - // Get public key from ECPair and convert to CompressedPublicKey let pubkey = ecpair.get_public_key(); let compressed_pubkey = CompressedPublicKey::from_slice(&pubkey.serialize()) .map_err(|e| WasmUtxoError::new(&format!("Failed to convert public key: {}", e)))?; @@ -587,9 +616,31 @@ impl BitGoPsbt { prev_tx: prev_tx.as_deref(), }; - Ok(self - .psbt - .add_replay_protection_input(compressed_pubkey, txid, vout, value, options)) + self.psbt + .add_replay_protection_input_at_index( + index, + compressed_pubkey, + txid, + vout, + value, + options, + ) + .map_err(|e| WasmUtxoError::new(&e)) + } + + pub fn add_replay_protection_input( + &mut self, + ecpair: &WasmECPair, + txid: &str, + vout: u32, + value: u64, + sequence: Option, + prev_tx: Option>, + ) -> Result { + let index = self.psbt.psbt().inputs.len(); + self.add_replay_protection_input_at_index( + index, ecpair, txid, vout, value, sequence, prev_tx, + ) } /// Get the unsigned transaction ID diff --git a/packages/wasm-utxo/src/wasm/psbt.rs b/packages/wasm-utxo/src/wasm/psbt.rs index e2f44bec..862c6259 100644 --- a/packages/wasm-utxo/src/wasm/psbt.rs +++ b/packages/wasm-utxo/src/wasm/psbt.rs @@ -288,9 +288,10 @@ impl WrapPsbt { /// /// # Returns /// The index of the newly added input - #[wasm_bindgen(js_name = addInput)] - pub fn add_input( + #[wasm_bindgen(js_name = addInputAtIndex)] + pub fn add_input_at_index( &mut self, + index: usize, txid: &str, vout: u32, value: u64, @@ -307,7 +308,6 @@ impl WrapPsbt { sequence: Sequence(sequence.unwrap_or(0xFFFFFFFE)), witness: miniscript::bitcoin::Witness::default(), }; - let psbt_input = psbt::Input { witness_utxo: Some(TxOut { value: Amount::from_sat(value), @@ -316,10 +316,20 @@ impl WrapPsbt { ..Default::default() }; - self.0.unsigned_tx.input.push(tx_in); - self.0.inputs.push(psbt_input); + crate::psbt_ops::insert_input(&mut self.0, index, tx_in, psbt_input) + .map_err(|e| JsError::new(&e)) + } - Ok(self.0.inputs.len() - 1) + #[wasm_bindgen(js_name = addInput)] + pub fn add_input( + &mut self, + txid: &str, + vout: u32, + value: u64, + script: &[u8], + sequence: Option, + ) -> Result { + self.add_input_at_index(self.0.inputs.len(), txid, vout, value, script, sequence) } /// Add an output to the PSBT @@ -330,21 +340,37 @@ impl WrapPsbt { /// /// # Returns /// The index of the newly added output - #[wasm_bindgen(js_name = addOutput)] - pub fn add_output(&mut self, script: &[u8], value: u64) -> usize { + #[wasm_bindgen(js_name = addOutputAtIndex)] + pub fn add_output_at_index( + &mut self, + index: usize, + script: &[u8], + value: u64, + ) -> Result { let script = ScriptBuf::from_bytes(script.to_vec()); - let tx_out = TxOut { value: Amount::from_sat(value), script_pubkey: script, }; - let psbt_output = psbt::Output::default(); + crate::psbt_ops::insert_output(&mut self.0, index, tx_out, psbt::Output::default()) + .map_err(|e| JsError::new(&e)) + } + + #[wasm_bindgen(js_name = addOutput)] + pub fn add_output(&mut self, script: &[u8], value: u64) -> usize { + self.add_output_at_index(self.0.outputs.len(), script, value) + .expect("insert at len should never fail") + } - self.0.unsigned_tx.output.push(tx_out); - self.0.outputs.push(psbt_output); + #[wasm_bindgen(js_name = removeInput)] + pub fn remove_input(&mut self, index: usize) -> Result<(), JsError> { + crate::psbt_ops::remove_input(&mut self.0, index).map_err(|e| JsError::new(&e)) + } - self.0.outputs.len() - 1 + #[wasm_bindgen(js_name = removeOutput)] + pub fn remove_output(&mut self, index: usize) -> Result<(), JsError> { + crate::psbt_ops::remove_output(&mut self.0, index).map_err(|e| JsError::new(&e)) } /// Get the unsigned transaction bytes diff --git a/packages/wasm-utxo/src/wasm/transaction.rs b/packages/wasm-utxo/src/wasm/transaction.rs index dbe2516d..2e752d76 100644 --- a/packages/wasm-utxo/src/wasm/transaction.rs +++ b/packages/wasm-utxo/src/wasm/transaction.rs @@ -43,40 +43,72 @@ impl WasmTransaction { /// /// # Returns /// The index of the newly added input - pub fn add_input( + pub fn add_input_at_index( &mut self, + index: usize, txid: &str, vout: u32, sequence: Option, ) -> Result { use miniscript::bitcoin::{transaction::Sequence, OutPoint, ScriptBuf, TxIn, Txid}; use std::str::FromStr; + if index > self.tx.input.len() { + return Err(WasmUtxoError::new(&format!( + "Input index {} out of bounds (have {} inputs)", + index, + self.tx.input.len() + ))); + } let txid = Txid::from_str(txid) .map_err(|e| WasmUtxoError::new(&format!("Invalid txid: {}", e)))?; - self.tx.input.push(TxIn { - previous_output: OutPoint { txid, vout }, - script_sig: ScriptBuf::new(), - sequence: sequence.map(Sequence).unwrap_or(Sequence::MAX), - witness: Default::default(), - }); - Ok(self.tx.input.len() - 1) + self.tx.input.insert( + index, + TxIn { + previous_output: OutPoint { txid, vout }, + script_sig: ScriptBuf::new(), + sequence: sequence.map(Sequence).unwrap_or(Sequence::MAX), + witness: Default::default(), + }, + ); + Ok(index) } - /// Add an output to the transaction - /// - /// # Arguments - /// * `script` - The output script (scriptPubKey) - /// * `value` - The value in satoshis - /// - /// # Returns - /// The index of the newly added output - pub fn add_output(&mut self, script: &[u8], value: u64) -> usize { + pub fn add_input( + &mut self, + txid: &str, + vout: u32, + sequence: Option, + ) -> Result { + self.add_input_at_index(self.tx.input.len(), txid, vout, sequence) + } + + pub fn add_output_at_index( + &mut self, + index: usize, + script: &[u8], + value: u64, + ) -> Result { use miniscript::bitcoin::{Amount, ScriptBuf, TxOut}; - self.tx.output.push(TxOut { - value: Amount::from_sat(value), - script_pubkey: ScriptBuf::from(script.to_vec()), - }); - self.tx.output.len() - 1 + if index > self.tx.output.len() { + return Err(WasmUtxoError::new(&format!( + "Output index {} out of bounds (have {} outputs)", + index, + self.tx.output.len() + ))); + } + self.tx.output.insert( + index, + TxOut { + value: Amount::from_sat(value), + script_pubkey: ScriptBuf::from(script.to_vec()), + }, + ); + Ok(index) + } + + pub fn add_output(&mut self, script: &[u8], value: u64) -> usize { + self.add_output_at_index(self.tx.output.len(), script, value) + .expect("insert at len should never fail") } /// Deserialize a transaction from bytes diff --git a/packages/wasm-utxo/test/bip322/index.ts b/packages/wasm-utxo/test/bip322/index.ts index 9494a6a3..050432d4 100644 --- a/packages/wasm-utxo/test/bip322/index.ts +++ b/packages/wasm-utxo/test/bip322/index.ts @@ -38,8 +38,8 @@ describe("BIP-0322", function () { }); assert.strictEqual(inputIndex, 0, "First input should have index 0"); - assert.strictEqual(psbt.version, 0, "BIP-0322 PSBTs must have version 0"); - assert.strictEqual(psbt.lockTime, 0, "BIP-0322 PSBTs must have lockTime 0"); + assert.strictEqual(psbt.version(), 0, "BIP-0322 PSBTs must have version 0"); + assert.strictEqual(psbt.lockTime(), 0, "BIP-0322 PSBTs must have lockTime 0"); }); it("should add a valid BIP-0322 input for p2wsh", function () { @@ -52,7 +52,7 @@ describe("BIP-0322", function () { }); assert.strictEqual(inputIndex, 0); - assert.strictEqual(psbt.version, 0); + assert.strictEqual(psbt.version(), 0); }); it("should add multiple BIP-0322 inputs", function () { @@ -77,7 +77,7 @@ describe("BIP-0322", function () { assert.strictEqual(idx0, 0); assert.strictEqual(idx1, 1); assert.strictEqual(idx2, 2); - assert.strictEqual(psbt.version, 0); + assert.strictEqual(psbt.version(), 0); }); it("should throw for non-version-0 PSBT", function () { diff --git a/packages/wasm-utxo/test/fixedScript/finalizeExtract.ts b/packages/wasm-utxo/test/fixedScript/finalizeExtract.ts index 23fd9c06..10c532f0 100644 --- a/packages/wasm-utxo/test/fixedScript/finalizeExtract.ts +++ b/packages/wasm-utxo/test/fixedScript/finalizeExtract.ts @@ -35,8 +35,8 @@ describe("finalize and extract transaction", function () { // Verify the deserialized PSBT has the same unsigned txid assert.strictEqual( - deserialized.unsignedTxid(), - fullsignedBitgoPsbt.unsignedTxid(), + deserialized.unsignedTxId(), + fullsignedBitgoPsbt.unsignedTxId(), "Deserialized PSBT should have same unsigned txid after round-trip", ); @@ -46,8 +46,8 @@ describe("finalize and extract transaction", function () { // Verify functional equivalence by deserializing again and checking txid const redeserialized = fixedScriptWallet.BitGoPsbt.fromBytes(reserialized, networkName); assert.strictEqual( - redeserialized.unsignedTxid(), - fullsignedBitgoPsbt.unsignedTxid(), + redeserialized.unsignedTxId(), + fullsignedBitgoPsbt.unsignedTxId(), "PSBT should maintain consistency through multiple serialize/deserialize cycles", ); }); diff --git a/packages/wasm-utxo/test/fixedScript/parseTransactionWithWalletKeys.ts b/packages/wasm-utxo/test/fixedScript/parseTransactionWithWalletKeys.ts index 306dce73..6e78524d 100644 --- a/packages/wasm-utxo/test/fixedScript/parseTransactionWithWalletKeys.ts +++ b/packages/wasm-utxo/test/fixedScript/parseTransactionWithWalletKeys.ts @@ -58,12 +58,12 @@ describe("parseTransactionWithWalletKeys", function () { }); it("should have matching unsigned transaction ID", function () { - const unsignedTxid = bitgoPsbt.unsignedTxid(); + const unsignedTxId = bitgoPsbt.unsignedTxId(); const expectedUnsignedTxid = utxolib.bitgo .createPsbtFromBuffer(fullsignedPsbtBytes, network) .getUnsignedTx() .getId(); - assert.strictEqual(unsignedTxid, expectedUnsignedTxid); + assert.strictEqual(unsignedTxId, expectedUnsignedTxid); }); it("should parse transaction and identify internal/external outputs", function () { diff --git a/packages/wasm-utxo/test/fixedScript/psbtReconstruction.ts b/packages/wasm-utxo/test/fixedScript/psbtReconstruction.ts index a5b29f2f..ef3f2f55 100644 --- a/packages/wasm-utxo/test/fixedScript/psbtReconstruction.ts +++ b/packages/wasm-utxo/test/fixedScript/psbtReconstruction.ts @@ -97,16 +97,16 @@ describe("PSBT reconstruction", function () { rootWalletKeys, { consensusBranchId: ZCASH_SAPLING_BRANCH_ID, - version: zcashPsbt.version, - lockTime: zcashPsbt.lockTime, + version: zcashPsbt.version(), + lockTime: zcashPsbt.lockTime(), versionGroupId: zcashPsbt.versionGroupId, expiryHeight: zcashPsbt.expiryHeight, }, ); } else { reconstructed = BitGoPsbt.createEmpty(networkName, rootWalletKeys, { - version: originalPsbt.version, - lockTime: originalPsbt.lockTime, + version: originalPsbt.version(), + lockTime: originalPsbt.lockTime(), }); } @@ -182,30 +182,34 @@ describe("PSBT reconstruction", function () { // Compare unsigned txids assert.strictEqual( - reconstructed.unsignedTxid(), - originalPsbt.unsignedTxid(), + reconstructed.unsignedTxId(), + originalPsbt.unsignedTxId(), "Reconstructed PSBT should have same unsigned txid as original", ); }); it("should have correct version and lockTime getters", function () { // Version and lockTime should be numbers - assert.strictEqual(typeof originalPsbt.version, "number", "version should be a number"); - assert.strictEqual(typeof originalPsbt.lockTime, "number", "lockTime should be a number"); + assert.strictEqual(typeof originalPsbt.version(), "number", "version should be a number"); + assert.strictEqual(typeof originalPsbt.lockTime(), "number", "lockTime should be a number"); // Version depends on network: Zcash uses version 4 (Sapling) or 5 (NU5), others use 1 or 2 if (network === utxolib.networks.zcash) { assert.ok( - originalPsbt.version === 4 || originalPsbt.version === 5, - `Zcash version should be 4 or 5, got ${originalPsbt.version}`, + originalPsbt.version() === 4 || originalPsbt.version() === 5, + `Zcash version should be 4 or 5, got ${originalPsbt.version()}`, ); } else { assert.ok( - originalPsbt.version === 1 || originalPsbt.version === 2, - `version should be 1 or 2, got ${originalPsbt.version}`, + originalPsbt.version() === 1 || originalPsbt.version() === 2, + `version should be 1 or 2, got ${originalPsbt.version()}`, ); } // LockTime is typically 0 for these fixtures - assert.strictEqual(originalPsbt.lockTime, 0, "lockTime should be 0 for unsigned fixtures"); + assert.strictEqual( + originalPsbt.lockTime(), + 0, + "lockTime should be 0 for unsigned fixtures", + ); }); it("should include sequence in parsed inputs", function () { @@ -253,8 +257,8 @@ describe("PSBT reconstruction", function () { rootWalletKeys, { consensusBranchId: ZCASH_SAPLING_BRANCH_ID, - version: zcashPsbt.version, - lockTime: zcashPsbt.lockTime, + version: zcashPsbt.version(), + lockTime: zcashPsbt.lockTime(), versionGroupId: zcashPsbt.versionGroupId, expiryHeight: zcashPsbt.expiryHeight, }, @@ -263,8 +267,8 @@ describe("PSBT reconstruction", function () { // Create PSBT using block height (preferred approach) const psbtWithHeight = ZcashBitGoPsbt.createEmpty(networkName, rootWalletKeys, { blockHeight: saplingHeight, - version: zcashPsbt.version, - lockTime: zcashPsbt.lockTime, + version: zcashPsbt.version(), + lockTime: zcashPsbt.lockTime(), versionGroupId: zcashPsbt.versionGroupId, expiryHeight: zcashPsbt.expiryHeight, }); @@ -332,8 +336,8 @@ describe("PSBT reconstruction", function () { // Verify both PSBTs produce the same unsigned txid assert.strictEqual( - psbtWithHeight.unsignedTxid(), - psbtWithBranchId.unsignedTxid(), + psbtWithHeight.unsignedTxId(), + psbtWithBranchId.unsignedTxId(), "PSBT created with block height should have same unsigned txid as one created with explicit branch ID", ); @@ -363,10 +367,10 @@ describe("PSBT reconstruction", function () { assert.strictEqual(txid.length, 64, "txid should be 64 characters"); assert.match(txid, /^[0-9a-f]{64}$/, "txid should be lowercase hex"); - // Verify unsignedTxid() also returns valid format - const unsignedTxid = psbt.unsignedTxid(); - assert.strictEqual(unsignedTxid.length, 64, "unsignedTxid should be 64 characters"); - assert.match(unsignedTxid, /^[0-9a-f]{64}$/, "unsignedTxid should be lowercase hex"); + // Verify unsignedTxId() also returns valid format + const unsignedTxId = psbt.unsignedTxId(); + assert.strictEqual(unsignedTxId.length, 64, "unsignedTxId should be 64 characters"); + assert.match(unsignedTxId, /^[0-9a-f]{64}$/, "unsignedTxId should be lowercase hex"); }); }); });