Skip to content

Commit f3c0e76

Browse files
OttoAllmendingerllm-git
andcommitted
refactor: add dynamic dispatch and Zcash hydration support
Extends BitGoPsbt.fromHalfSignedLegacyTransaction() with: - Dynamic dispatch to detect transaction type (Bitcoin/Dash/Zcash) - Early rejection of Zcash with helpful error message - ITransaction parameter overload to avoid re-parsing Adds ZcashBitGoPsbt.fromHalfSignedLegacyTransaction(): - Block height mode (recommended): auto-determines consensus_branch_id - Explicit consensus_branch_id mode for advanced usage - Full Sapling field restoration - Comprehensive round-trip tests Rust layer implements: - from_half_signed_legacy_transaction_zcash() with block height - from_half_signed_legacy_transaction_zcash_with_consensus_branch_id() - Shared logic for Zcash wire format decoding Transaction classes add supportsCoin() for dispatch routing. WASM bindings expose both Zcash variants with unified unspent parsing. Co-authored-by: llm-git <llm-git@ttll.de>
1 parent 2e80e26 commit f3c0e76

9 files changed

Lines changed: 688 additions & 70 deletions

File tree

packages/wasm-utxo/js/fixedScriptWallet/BitGoPsbt.ts

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -196,18 +196,46 @@ export class BitGoPsbt extends PsbtBase<WasmBitGoPsbt> implements IPsbtWithAddre
196196
* with proper wallet metadata (bip32Derivation, scripts, witnessUtxo).
197197
* Only supports p2sh, p2shP2wsh, and p2wsh inputs (not taproot).
198198
*
199-
* @param txBytes - The serialized half-signed legacy transaction
199+
* Uses dynamic dispatch to handle different transaction types:
200+
* - Bitcoin-like coins (BTC, LTC, DOGE, etc.): Fully supported
201+
* - Dash (DASH): Supported (special transaction format)
202+
* - Zcash (ZEC): Not supported (incompatible overwintered format; use ZcashBitGoPsbt.createEmpty instead)
203+
*
204+
* @param txBytesOrTx - Either serialized transaction bytes or a decoded ITransaction instance
200205
* @param network - Network name
201206
* @param walletKeys - The wallet's root keys
202207
* @param unspents - Chain, index, and value for each input
208+
* @param options - Additional options (e.g., blockHeight for Zcash consensus branch ID)
209+
* @throws Error if transaction is Zcash (use ZcashBitGoPsbt.createEmpty instead)
203210
*/
204211
static fromHalfSignedLegacyTransaction(
205-
txBytes: Uint8Array,
212+
txBytesOrTx: Uint8Array | ITransaction,
206213
network: NetworkName,
207214
walletKeys: WalletKeysArg,
208215
unspents: HydrationUnspent[],
216+
_options?: unknown,
209217
): BitGoPsbt {
210218
const keys = RootWalletKeys.from(walletKeys);
219+
220+
// Decode transaction bytes using dynamic dispatch if needed
221+
let tx: ITransaction;
222+
let txBytes: Uint8Array;
223+
if (txBytesOrTx instanceof Uint8Array) {
224+
txBytes = txBytesOrTx;
225+
// Dynamic dispatch: determine coin type and decode with correct class
226+
const coinName = typeof network === "string" ? (network as CoinName) : network;
227+
tx = Transaction.fromBytes(txBytes, coinName);
228+
} else {
229+
tx = txBytesOrTx;
230+
txBytes = tx.toBytes();
231+
}
232+
233+
// Validate that this is not a Zcash transaction
234+
if (tx instanceof ZcashTransaction) {
235+
throw new Error('Use ZcashBitGoPsbt.fromHalfSignedLegacyTransaction() for Zcash transactions');
236+
}
237+
238+
// For Bitcoin-like and Dash transactions, pass to WASM layer
211239
const wasm = WasmBitGoPsbt.from_half_signed_legacy_transaction(
212240
txBytes,
213241
network,

packages/wasm-utxo/js/fixedScriptWallet/ZcashBitGoPsbt.ts

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { BitGoPsbt as WasmBitGoPsbt } from "../wasm/wasm_utxo.js";
22
import { type WalletKeysArg, RootWalletKeys } from "./RootWalletKeys.js";
3-
import { BitGoPsbt, type CreateEmptyOptions } from "./BitGoPsbt.js";
4-
import { ZcashTransaction } from "../transaction.js";
3+
import { BitGoPsbt, type CreateEmptyOptions, type HydrationUnspent } from "./BitGoPsbt.js";
4+
import { ZcashTransaction, type ITransaction } from "../transaction.js";
55

66
/** Zcash network names */
77
export type ZcashNetworkName = "zcash" | "zcashTest" | "zec" | "tzec";
@@ -144,6 +144,77 @@ export class ZcashBitGoPsbt extends BitGoPsbt {
144144
return new ZcashBitGoPsbt(wasm);
145145
}
146146

147+
/**
148+
* Reconstruct a Zcash PSBT from a half-signed legacy transaction
149+
*
150+
* This is the inverse of `getHalfSignedLegacyFormat()` for Zcash. It decodes the Zcash wire
151+
* format (which includes version_group_id, expiry_height, and sapling fields), extracts
152+
* partial signatures, and reconstructs a proper Zcash PSBT with consensus metadata.
153+
*
154+
* Supports two modes for determining consensus_branch_id:
155+
* - **Recommended**: Pass `blockHeight` to auto-determine consensus_branch_id via network upgrade activation heights
156+
* - **Advanced**: Pass `consensusBranchId` directly if you already know it (e.g., 0xC2D6D0B4 for NU5)
157+
*
158+
* @param txBytesOrTx - Either serialized Zcash transaction bytes or a decoded ITransaction instance
159+
* @param network - Zcash network name ("zcash", "zcashTest", "zec", "tzec")
160+
* @param walletKeys - The wallet's root keys
161+
* @param unspents - Chain, index, and value for each input
162+
* @param options - Either `{ blockHeight: number }` or `{ consensusBranchId: number }`
163+
* @returns A ZcashBitGoPsbt instance
164+
*
165+
* @example
166+
* ```typescript
167+
* // Round-trip with block height (recommended)
168+
* const legacyBytes = psbt.getHalfSignedLegacyFormat();
169+
* const reconstructed = ZcashBitGoPsbt.fromHalfSignedLegacyTransaction(
170+
* legacyBytes,
171+
* "zec",
172+
* walletKeys,
173+
* unspents,
174+
* { blockHeight: 1687105 } // NU5 activation height
175+
* );
176+
*
177+
* // Or with explicit consensus branch ID
178+
* const reconstructed = ZcashBitGoPsbt.fromHalfSignedLegacyTransaction(
179+
* legacyBytes,
180+
* "zec",
181+
* walletKeys,
182+
* unspents,
183+
* { consensusBranchId: 0xC2D6D0B4 } // NU5 branch ID
184+
* );
185+
* ```
186+
*/
187+
static fromHalfSignedLegacyTransaction(
188+
txBytesOrTx: Uint8Array | ITransaction,
189+
network: ZcashNetworkName,
190+
walletKeys: WalletKeysArg,
191+
unspents: HydrationUnspent[],
192+
options: { blockHeight: number } | { consensusBranchId: number },
193+
): ZcashBitGoPsbt {
194+
const keys = RootWalletKeys.from(walletKeys);
195+
const txBytes = txBytesOrTx instanceof Uint8Array ? txBytesOrTx : txBytesOrTx.toBytes();
196+
197+
if ("blockHeight" in options) {
198+
const wasm = WasmBitGoPsbt.from_half_signed_legacy_transaction_zcash(
199+
txBytes,
200+
network,
201+
keys.wasm,
202+
unspents,
203+
options.blockHeight,
204+
);
205+
return new ZcashBitGoPsbt(wasm);
206+
} else {
207+
const wasm = WasmBitGoPsbt.from_half_signed_legacy_transaction_zcash_with_branch_id(
208+
txBytes,
209+
network,
210+
keys.wasm,
211+
unspents,
212+
options.consensusBranchId,
213+
);
214+
return new ZcashBitGoPsbt(wasm);
215+
}
216+
}
217+
147218
// --- Zcash-specific getters ---
148219

149220
/**

packages/wasm-utxo/js/transaction.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,33 @@ export class Transaction extends TransactionBase<WasmTransaction> {
3030
super(wasm);
3131
}
3232

33+
/**
34+
* Check if a coin is supported by this transaction class.
35+
* Bitcoin-like transactions support all coins except Zcash and Dash.
36+
*/
37+
static supportsCoin(coin: CoinName): boolean {
38+
return !ZcashTransaction.supportsCoin(coin) && !DashTransaction.supportsCoin(coin);
39+
}
40+
3341
/**
3442
* Create an empty transaction (version 1, locktime 0)
3543
*/
3644
static create(): Transaction {
3745
return new Transaction(WasmTransaction.create());
3846
}
3947

40-
static fromBytes(bytes: Uint8Array): Transaction {
48+
static fromBytes(bytes: Uint8Array): Transaction;
49+
static fromBytes(bytes: Uint8Array, coin: "zec" | "tzec"): ZcashTransaction;
50+
static fromBytes(bytes: Uint8Array, coin: "dash" | "tdash"): DashTransaction;
51+
static fromBytes(bytes: Uint8Array, coin: CoinName): Transaction | ZcashTransaction | DashTransaction;
52+
static fromBytes(
53+
bytes: Uint8Array,
54+
coin?: CoinName,
55+
): Transaction | ZcashTransaction | DashTransaction {
56+
if (coin !== undefined) {
57+
if (ZcashTransaction.supportsCoin(coin)) return ZcashTransaction.fromBytes(bytes);
58+
if (DashTransaction.supportsCoin(coin)) return DashTransaction.fromBytes(bytes);
59+
}
4160
return new Transaction(WasmTransaction.from_bytes(bytes));
4261
}
4362

@@ -96,6 +115,14 @@ export class ZcashTransaction extends TransactionBase<WasmZcashTransaction> {
96115
super(wasm);
97116
}
98117

118+
/**
119+
* Check if a coin is supported by this transaction class.
120+
* Zcash transactions support Zcash mainnet and testnet.
121+
*/
122+
static supportsCoin(coin: CoinName): boolean {
123+
return coin === "zec" || coin === "tzec";
124+
}
125+
99126
static fromBytes(bytes: Uint8Array): ZcashTransaction {
100127
return new ZcashTransaction(WasmZcashTransaction.from_bytes(bytes));
101128
}
@@ -121,6 +148,14 @@ export class DashTransaction extends TransactionBase<WasmDashTransaction> {
121148
super(wasm);
122149
}
123150

151+
/**
152+
* Check if a coin is supported by this transaction class.
153+
* Dash transactions support Dash mainnet and testnet.
154+
*/
155+
static supportsCoin(coin: CoinName): boolean {
156+
return coin === "dash" || coin === "tdash";
157+
}
158+
124159
static fromBytes(bytes: Uint8Array): DashTransaction {
125160
return new DashTransaction(WasmDashTransaction.from_bytes(bytes));
126161
}
@@ -135,3 +170,4 @@ export class DashTransaction extends TransactionBase<WasmDashTransaction> {
135170
return this._wasm;
136171
}
137172
}
173+

0 commit comments

Comments
 (0)