Skip to content

Commit c1be159

Browse files
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> refactor: change BitGoPsbt.fromHalfSignedLegacyTransaction to accept Transaction directly - Accept Transaction | DashTransaction instead of Uint8Array to avoid round-tripping through bytes - Extract WASM transaction directly and pass to WASM layer without re-serialization - Add Rust overload from_half_signed_legacy_transaction_dash for DashTransaction support - Update TypeScript to route to correct WASM method based on transaction type - Update all tests to parse transactions first before calling fromHalfSignedLegacyTransaction - Simplify the call site in BitGoPsbt - no more instanceof checks for coin names - WasmBitGoPsbt is private so breaking change in signature is allowed - All 13 fromHalfSignedLegacy tests pass Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
1 parent 2e80e26 commit c1be159

9 files changed

Lines changed: 739 additions & 78 deletions

File tree

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

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { type BIP32Arg, BIP32, isBIP32Arg } from "../bip32.js";
1111
import { type ECPairArg, ECPair } from "../ecpair.js";
1212
import type { UtxolibName } from "../utxolibCompat.js";
1313
import type { CoinName } from "../coinName.js";
14+
import { toCoinName } from "../coinName.js";
1415
import type { InputScriptType } from "./scriptType.js";
1516
import {
1617
Transaction,
@@ -196,24 +197,52 @@ export class BitGoPsbt extends PsbtBase<WasmBitGoPsbt> implements IPsbtWithAddre
196197
* with proper wallet metadata (bip32Derivation, scripts, witnessUtxo).
197198
* Only supports p2sh, p2shP2wsh, and p2wsh inputs (not taproot).
198199
*
199-
* @param txBytes - The serialized half-signed legacy transaction
200+
* Supports both Bitcoin-like coins (BTC, LTC, DOGE) and Dash (DASH).
201+
* Zcash is NOT supported; use ZcashBitGoPsbt.fromHalfSignedLegacyTransaction instead.
202+
*
203+
* @param txBytesOrTx - Transaction bytes or decoded transaction instance (Bitcoin-like or Dash)
200204
* @param network - Network name
201205
* @param walletKeys - The wallet's root keys
202206
* @param unspents - Chain, index, and value for each input
207+
* @throws Error if transaction is Zcash (use ZcashBitGoPsbt.fromHalfSignedLegacyTransaction instead)
203208
*/
204209
static fromHalfSignedLegacyTransaction(
205-
txBytes: Uint8Array,
210+
txBytesOrTx: Uint8Array | Transaction | DashTransaction,
206211
network: NetworkName,
207212
walletKeys: WalletKeysArg,
208213
unspents: HydrationUnspent[],
214+
_options?: unknown,
209215
): BitGoPsbt {
210216
const keys = RootWalletKeys.from(walletKeys);
211-
const wasm = WasmBitGoPsbt.from_half_signed_legacy_transaction(
212-
txBytes,
213-
network,
214-
keys.wasm,
215-
unspents,
216-
);
217+
218+
// Parse bytes to Transaction if needed
219+
const tx =
220+
txBytesOrTx instanceof Uint8Array
221+
? Transaction.fromBytes(txBytesOrTx, toCoinName(network))
222+
: txBytesOrTx;
223+
224+
// Validate that this is not a Zcash transaction
225+
if (tx instanceof ZcashTransaction) {
226+
throw new Error('Use ZcashBitGoPsbt.fromHalfSignedLegacyTransaction() for Zcash transactions');
227+
}
228+
229+
// Pass WASM transaction directly to avoid serialization round-trip
230+
let wasm;
231+
if (tx instanceof DashTransaction) {
232+
wasm = WasmBitGoPsbt.from_half_signed_legacy_transaction_dash(
233+
tx.wasm,
234+
network,
235+
keys.wasm,
236+
unspents,
237+
);
238+
} else {
239+
wasm = WasmBitGoPsbt.from_half_signed_legacy_transaction(
240+
tx.wasm,
241+
network,
242+
keys.wasm,
243+
unspents,
244+
);
245+
}
217246
return new BitGoPsbt(wasm);
218247
}
219248

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 ZcashTransaction 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)