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
6 changes: 6 additions & 0 deletions packages/wasm-utxo/js/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,12 @@ declare module "./wasm/wasm_utxo.js" {
tapBip32Derivation: PsbtBip32Derivation[];
}

/** A transparent input's previous-output outpoint (display-order txid) returned by getInputOutpoints() */
interface PsbtInputOutpoint {
txid: string;
vout: number;
}

/** A serialized input-map record with a standard PSBT key type. */
interface PsbtKnownInputKeyValue {
type: "known";
Expand Down
5 changes: 5 additions & 0 deletions packages/wasm-utxo/js/psbtBase.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type {
PsbtInputData,
PsbtInputKeyValue,
PsbtInputOutpoint,
PsbtOutputData,
WasmBIP32,
} from "./wasm/wasm_utxo.js";
Expand All @@ -15,6 +16,7 @@ interface WasmPsbtBase {
unsigned_tx_id(): string;
serialize(): Uint8Array;
get_inputs(): unknown;
get_input_outpoints(): unknown;
get_input_key_values(index: number): unknown;
get_outputs(): unknown;
get_global_xpubs(): unknown;
Expand Down Expand Up @@ -59,6 +61,9 @@ export abstract class PsbtBase<W extends WasmPsbtBase> {
getInputs(): PsbtInputData[] {
return this._wasm.get_inputs() as PsbtInputData[];
}
getInputOutpoints(): PsbtInputOutpoint[] {
return this._wasm.get_input_outpoints() as PsbtInputOutpoint[];
}
getInputKeyValues(index: number): PsbtInputKeyValue[] {
return this._wasm.get_input_key_values(index) as PsbtInputKeyValue[];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,59 @@ use orchard::tree::Anchor;
use orchard::Proof;
use rand::rngs::OsRng;

/// `input_outpoints` is version-symmetric: the same mock outpoint spent by a v4 and a v6
/// PSBT yields the identical display-order `{txid, vout}` pair — the wasm counterpart of
/// utxo-lib's raw `txInputs` + `getOutputIdForInput`, so unspent-id extraction (bms's
/// PA-creation path) behaves the same for both versions.
#[test]
fn input_outpoints_are_version_symmetric() {
let wallet_keys = RootWalletKeys::new(get_test_wallet_keys("outpoints"));
let mock_txid = Txid::from_byte_array([0x44u8; 32]);
let nu6_3 = NetworkUpgrade::Nu6_3.testnet_activation_height();

let mut v6 =
BitGoPsbt::new_zcash_v6_at_height(Network::ZcashTestnet, &wallet_keys, nu6_3, None, None)
.unwrap();
let mut v4 = BitGoPsbt::new_zcash_at_height(
Network::ZcashTestnet,
&wallet_keys,
2_000_000,
None,
None,
None,
None,
)
.unwrap();
for psbt in [&mut v6, &mut v4] {
psbt.add_wallet_input(
mock_txid,
7,
200_000_000,
&wallet_keys,
ScriptId { chain: 0, index: 0 },
WalletInputOptions::default(),
)
.unwrap();
}

let BitGoPsbt::Zcash(z6, _) = &v6 else {
panic!("expected Zcash PSBT");
};
let BitGoPsbt::Zcash(z4, _) = &v4 else {
panic!("expected Zcash PSBT");
};
let out6 = crate::wasm::input_outpoints(&z6.psbt);
let out4 = crate::wasm::input_outpoints(&z4.psbt);
assert_eq!(out6, out4, "v4 and v6 outpoints must be identical");
assert_eq!(out6.len(), 1);
assert_eq!(
out6[0].txid,
mock_txid.to_string(),
"txid must be in display (reversed) order, like utxo-lib's getOutputIdForInput"
);
assert_eq!(out6[0].vout, 7);
}

/// A transparent-only v6 (Ironwood) PSBT — no shielded output is ever added — is the shape
/// IMS will build once t->t ZEC sends move to v6 ahead of a shielded-send path existing for
/// them. Verifies the dedicated no-PCZT extractor produces a structurally valid, zebra-chain-
Expand Down
1 change: 1 addition & 0 deletions packages/wasm-utxo/src/wasm/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ pub use inscriptions::InscriptionsNamespace;
pub use message::MessageNamespace;
pub use miniscript::WrapMiniscript;
pub use package_info::WasmUtxoNamespace;
pub use psbt::input_outpoints;
pub use psbt::WrapPsbt;
pub use replay_protection::WasmReplayProtection;
pub use transaction::{WasmTransaction, WasmZcashTransaction};
Expand Down
39 changes: 39 additions & 0 deletions packages/wasm-utxo/src/wasm/psbt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,15 @@ pub struct PsbtInputData {
pub tap_bip32_derivation: Vec<Bip32Derivation>,
}

/// A transparent input's previous-output outpoint, with the txid in display (reversed)
/// order — the wasm counterpart of utxo-lib's raw `txInputs` access: callers that only
/// need the spent outpoints (e.g. unspent-id extraction) behave the same for v4 and v6.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PsbtInputOutpoint {
pub txid: String,
pub vout: u32,
}

/// A PSBT input key classified from its BIP-174 key type.
#[derive(Debug, Clone)]
pub enum PsbtInputKey {
Expand Down Expand Up @@ -180,6 +189,26 @@ pub fn get_inputs_from_psbt(psbt: &Psbt) -> Result<JsValue, WasmUtxoError> {
inputs.try_to_js_value()
}

/// Get every transparent input's previous-output outpoint from the embedded unsigned
/// transaction, in input order — the wasm counterpart of utxo-lib's `txInputs` +
/// `getOutputIdForInput` introspection, so callers extracting unspent ids behave
/// identically for v4 and v6 PSBTs.
pub fn get_input_outpoints_from_psbt(psbt: &Psbt) -> Result<JsValue, WasmUtxoError> {
input_outpoints(psbt).try_to_js_value()
}

/// The outpoints alone, as plain Rust values — separated from the JsValue wrapper so the
/// extraction is unit-testable on non-wasm targets.
pub fn input_outpoints(psbt: &Psbt) -> Vec<PsbtInputOutpoint> {
psbt.unsigned_tx
.input
.iter()
.map(|txin| PsbtInputOutpoint {
txid: txin.previous_output.txid.to_string(),
vout: txin.previous_output.vout,
})
.collect()
}
/// Get every serialized key-value record for one PSBT input.
pub fn get_input_key_values_from_psbt(
psbt: &Psbt,
Expand Down Expand Up @@ -886,6 +915,11 @@ macro_rules! impl_wasm_psbt_ops {
) -> Result<::wasm_bindgen::JsValue, $crate::error::WasmUtxoError> {
self.wasm_get_inputs()
}
pub fn get_input_outpoints(
&self,
) -> Result<::wasm_bindgen::JsValue, $crate::error::WasmUtxoError> {
self.wasm_get_input_outpoints()
}
pub fn get_input_key_values(
&self,
index: usize,
Expand Down Expand Up @@ -1000,6 +1034,11 @@ macro_rules! impl_wasm_psbt_ops {
) -> Result<::wasm_bindgen::JsValue, $crate::error::WasmUtxoError> {
self.$field.wasm_get_inputs()
}
pub fn get_input_outpoints(
&self,
) -> Result<::wasm_bindgen::JsValue, $crate::error::WasmUtxoError> {
self.$field.wasm_get_input_outpoints()
}
pub fn get_input_key_values(
&self,
index: usize,
Expand Down
3 changes: 3 additions & 0 deletions packages/wasm-utxo/src/wasm/psbt_ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ pub(crate) trait WasmPsbtOps: PsbtAccess {
fn wasm_get_inputs(&self) -> Result<JsValue, WasmUtxoError> {
crate::wasm::psbt::get_inputs_from_psbt(self.psbt())
}
fn wasm_get_input_outpoints(&self) -> Result<JsValue, WasmUtxoError> {
crate::wasm::psbt::get_input_outpoints_from_psbt(self.psbt())
}

fn wasm_get_input_key_values(&self, index: usize) -> Result<JsValue, WasmUtxoError> {
crate::wasm::psbt::get_input_key_values_from_psbt(self.psbt(), index)
Expand Down
9 changes: 9 additions & 0 deletions packages/wasm-utxo/src/wasm/try_into_js_value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -526,6 +526,15 @@ impl TryIntoJsValue for crate::wasm::psbt::PsbtInputData {
}
}

impl TryIntoJsValue for crate::wasm::psbt::PsbtInputOutpoint {
fn try_to_js_value(&self) -> Result<JsValue, WasmUtxoError> {
js_obj!(
"txid" => self.txid.clone(),
"vout" => self.vout
)
}
}

impl TryIntoJsValue for crate::wasm::psbt::PsbtInputKeyValue {
fn try_to_js_value(&self) -> Result<JsValue, WasmUtxoError> {
match &self.key {
Expand Down
19 changes: 19 additions & 0 deletions packages/wasm-utxo/test/fixedScript/zcashIronwoodPsbt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1298,3 +1298,22 @@ describe("ZcashIronwoodBitGoPsbt v6 (Ironwood)", function () {
});
});
});

describe("getInputOutpoints", () => {
it("is version-symmetric: a v4 and a v6 PSBT spending the same outpoint return identical {txid, vout} in display order", () => {
const keys = getWalletKeysForSeed("outpoints");
// asymmetric txid so the display-order assertion cannot pass by accident
const mockTxid = `a1${"00".repeat(30)}b2`;
const v6 = ZcashIronwoodBitGoPsbt.createEmpty("tzec", keys, {
blockHeight: NU6_3_TESTNET_HEIGHT + 1,
});
const v4 = ZcashBitGoPsbt.createEmpty("tzec", keys, { blockHeight: 2_000_000 });
for (const psbt of [v6, v4]) {
psbt.addWalletInput({ txid: mockTxid, vout: 7, value: 200_000_000n }, keys, {
scriptId: SCRIPT_ID,
});
}
assert.deepEqual(v6.getInputOutpoints(), v4.getInputOutpoints());
assert.deepEqual(v6.getInputOutpoints(), [{ txid: mockTxid, vout: 7 }]);
});
});
Loading