Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions packages/wasm-utxo/js/fixedScriptWallet/BitGoPsbt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -931,6 +931,22 @@ export class BitGoPsbt extends PsbtBase<WasmBitGoPsbt> implements IPsbtWithAddre
this._wasm.combine_musig2_nonces(sourcePsbt.wasm);
}

/**
* Merge all input fields from a raw PSBT (bytes) into this PSBT.
*
* The source bytes are parsed with the underlying bitcoin PSBT deserializer,
* bypassing network-specific validation (e.g. ZCash consensusBranchId), so the
* source may be a stripped PSBT that lacks those fields.
*
* Copies per input: partial_sigs, tap_key_sig, tap_script_sigs, proprietary.
*
* @param otherPsbtBytes - Raw bytes of the PSBT to merge signatures from
* @throws Error if PSBT parsing fails or input counts don't match
*/
combineInputs(otherPsbtBytes: Uint8Array): void {
this._wasm.combine_inputs(otherPsbtBytes);
}

/**
* Finalize all inputs in the PSBT
*
Expand Down
46 changes: 46 additions & 0 deletions packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1300,6 +1300,52 @@ impl BitGoPsbt {
Ok(())
}

/// Merge all input fields from a raw PSBT (given as bytes) into this PSBT.
///
/// For Zcash PSBTs the source is parsed with the ZEC-aware deserializer (skipping
/// the ZecConsensusBranchId check so that stripped HSM responses are accepted).
/// For all other coins the bitcoin PSBT deserializer is used.
///
/// Copies per input: partial_sigs, tap_key_sig, tap_script_sigs, proprietary.
pub fn combine_inputs(&mut self, other_bytes: &[u8]) -> Result<(), String> {
let raw: Psbt = match self {
BitGoPsbt::Zcash(_, network) => {
ZcashBitGoPsbt::deserialize_stripped(other_bytes, *network)
.map(|z| z.psbt)
.map_err(|e| format!("Failed to parse PSBT: {}", e))?
}
_ => Psbt::deserialize(other_bytes)
.map_err(|e| format!("Failed to parse PSBT: {}", e))?,
};

let dest = self.psbt_mut();

if raw.inputs.len() != dest.inputs.len() {
return Err(format!(
"PSBT input count mismatch: source has {}, destination has {}",
raw.inputs.len(),
dest.inputs.len()
));
}

for (src_in, dest_in) in raw.inputs.iter().zip(dest.inputs.iter_mut()) {
for (k, v) in &src_in.partial_sigs {
dest_in.partial_sigs.insert(*k, *v);
}
if let Some(sig) = src_in.tap_key_sig {
dest_in.tap_key_sig = Some(sig);
}
for (k, v) in &src_in.tap_script_sigs {
dest_in.tap_script_sigs.insert(*k, *v);
}
for (k, v) in &src_in.proprietary {
dest_in.proprietary.insert(k.clone(), v.clone());
}
}

Ok(())
}

/// Serialize the PSBT to bytes, using network-specific logic
pub fn serialize(&self) -> Result<Vec<u8>, SerializeError> {
match self {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,10 +204,21 @@ impl ZcashBitGoPsbt {
Ok(hash.to_byte_array())
}

/// Deserialize a Zcash PSBT from bytes without requiring the ZecConsensusBranchId
/// proprietary key. Used when combining with a stripped HSM response that may not
/// carry the branch ID (the key is only needed for sighash, not for merging sigs).
pub(crate) fn deserialize_stripped(
bytes: &[u8],
network: crate::Network,
) -> Result<Self, super::DeserializeError> {
Self::decode_with_zcash_tx(bytes, network, false)
}

/// Deserialize the PSBT by converting the Zcash transaction to Bitcoin format first
fn decode_with_zcash_tx(
bytes: &[u8],
network: crate::Network,
require_branch_id: bool,
) -> Result<Self, super::DeserializeError> {
let mut r = bytes;

Expand Down Expand Up @@ -342,7 +353,7 @@ impl ZcashBitGoPsbt {
let psbt = Psbt::deserialize(&modified_psbt)?;

// Consensus branch ID must be set in the PSBT proprietary map
if super::propkv::get_zec_consensus_branch_id(&psbt).is_none() {
if require_branch_id && super::propkv::get_zec_consensus_branch_id(&psbt).is_none() {
return Err(super::DeserializeError::Network(
"Missing ZecConsensusBranchId in PSBT proprietary map".to_string(),
));
Expand All @@ -366,7 +377,7 @@ impl ZcashBitGoPsbt {
bytes: &[u8],
network: crate::Network,
) -> Result<Self, super::DeserializeError> {
Self::decode_with_zcash_tx(bytes, network)
Self::decode_with_zcash_tx(bytes, network, true)
}

/// Convert to a standard Bitcoin PSBT (losing Zcash-specific fields)
Expand Down
15 changes: 14 additions & 1 deletion packages/wasm-utxo/src/wasm/fixed_script_wallet/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -971,7 +971,6 @@ impl BitGoPsbt {
_ => None,
}
}

pub fn get_outputs_with_address(&self) -> Result<JsValue, WasmUtxoError> {
crate::wasm::psbt::get_outputs_with_address_from_psbt(self.psbt.psbt(), self.psbt.network())
}
Expand Down Expand Up @@ -1816,6 +1815,20 @@ impl BitGoPsbt {
.map_err(|e| WasmUtxoError::new(&format!("Failed to combine PSBTs: {}", e)))
}

/// Merge all input fields from a raw PSBT (given as bytes) into this PSBT.
///
/// The source bytes are parsed with the underlying bitcoin PSBT deserializer,
/// bypassing any network-specific validation, so the source may be a partial
/// PSBT that lacks fields like ZecConsensusBranchId.
///
/// # Errors
/// Returns error if PSBT parsing fails or input counts don't match
pub fn combine_inputs(&mut self, other_bytes: &[u8]) -> Result<(), WasmUtxoError> {
self.psbt
.combine_inputs(other_bytes)
.map_err(|e| WasmUtxoError::new(&format!("Failed to combine inputs: {}", e)))
}

/// Finalize all inputs in the PSBT
///
/// This method attempts to finalize all inputs in the PSBT, computing the final
Expand Down
Loading
Loading