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: 11 additions & 5 deletions packages/wasm-utxo/js/fixedScriptWallet/BitGoPsbt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,11 @@ export type ParsedOutput = {
derivationPath: string | null;
};

export type ParsedTransaction<TOutput extends ParsedOutput = ParsedOutput> = {
inputs: ParsedInput[];
export type ParsedTransaction<
TOutput extends ParsedOutput = ParsedOutput,
TInput extends ParsedInput = ParsedInput,
> = {
inputs: TInput[];
outputs: TOutput[];
spendAmount: bigint;
minerFee: bigint;
Expand Down Expand Up @@ -135,7 +138,10 @@ export type HydrationUnspent =
| { chain: number; index: number; value: bigint } // wallet input
| { pubkey: Uint8Array; value: bigint }; // P2SH-P2PK replay protection input

export class BitGoPsbt<TOutput extends ParsedOutput = ParsedOutput>
export class BitGoPsbt<
TOutput extends ParsedOutput = ParsedOutput,
TInput extends ParsedInput = ParsedInput,
>
extends PsbtBase<WasmBitGoPsbt>
implements IPsbtWithAddress
{
Expand Down Expand Up @@ -619,15 +625,15 @@ export class BitGoPsbt<TOutput extends ParsedOutput = ParsedOutput>
parseTransactionWithWalletKeys(
walletKeys: WalletKeysArg,
options: ParseTransactionOptions,
): ParsedTransaction<TOutput> {
): ParsedTransaction<TOutput, TInput> {
const keys = RootWalletKeys.from(walletKeys);
const rp = ReplayProtection.from(options.replayProtection, this._wasm.network());
const pubkeys = options.payGoPubkeys?.map((arg) => ECPair.from(arg).wasm);
return this._wasm.parse_transaction_with_wallet_keys(
keys.wasm,
rp.wasm,
pubkeys,
) as ParsedTransaction<TOutput>;
) as ParsedTransaction<TOutput, TInput>;
}

/**
Expand Down
12 changes: 11 additions & 1 deletion packages/wasm-utxo/js/fixedScriptWallet/ZcashBitGoPsbt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,20 @@ import {
BitGoPsbt,
type CreateEmptyOptions,
type HydrationUnspent,
type ParsedInput,
type ParsedOutput,
} from "./BitGoPsbt.js";
import { ZcashTransaction, type ITransaction } from "../transaction.js";

/** Zcash network names */
export type ZcashNetworkName = "zcash" | "zcashTest" | "zec" | "tzec";

/**
* A transparent Zcash input. Same shape as {@link ParsedInput}; the additive
* `isShielded: false` lets callers discriminate inputs uniformly across the Zcash parse
* results (shielded spends come back as `ZcashShieldedSpend`s instead).
*/
export type ZcashParsedInput = ParsedInput & { isShielded: false };
export type ZcashParsedOutput = ParsedOutput & {
/**
* True for a shielded (Orchard/Ironwood) output. Such an output has no `unsigned_tx` entry of
Expand Down Expand Up @@ -83,7 +90,10 @@ export type CreateEmptyZcashWithConsensusBranchIdOptions = CreateEmptyOptions &
* const psbt = ZcashBitGoPsbt.fromBytes(bytes, "zcash");
* ```
*/
export class ZcashBitGoPsbt extends BitGoPsbt<ZcashParsedOutput> {
export class ZcashBitGoPsbt<
TOutput extends ZcashParsedOutput = ZcashParsedOutput,
TInput extends ZcashParsedInput = ZcashParsedInput,
> extends BitGoPsbt<TOutput, TInput> {
/**
* Create an empty Zcash PSBT with consensus branch ID determined from block height
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,15 @@ import {
IRONWOOD_VERSION_GROUP_ID,
ZcashBitGoPsbt,
type ZcashNetworkName,
type ZcashParsedInput,
type ZcashParsedOutput,
} from "./ZcashBitGoPsbt.js";
import {
type ParsedTransaction,
type ParseOutputsOptions,
type ParseTransactionOptions,
} from "./BitGoPsbt.js";
import { type ZcashShieldedSpend } from "./ZcashIronwoodTypes.js";

/**
* Options for creating an empty Zcash v6 (Ironwood) shielding PSBT.
Expand Down Expand Up @@ -57,6 +65,33 @@ export function zip302NoMemo(): Uint8Array {
return memo;
}

/**
* The parsed shape of a Zcash output through the Ironwood parse: the Zcash output shape plus
* the PCZT action index (`null` for transparent outputs).
*/
export type ZcashIronwoodParsedOutput = ZcashParsedOutput & {
actionIndex: number | null;
};

/**
* The parsed shape of a v6 (Ironwood) transaction. A discriminated union on `inputKind`:
* a shielded-input PSBT has no transparent inputs at all, and a transparent-input PSBT has
* no shielded spends. `valueBalance` is the orchard bundle's value balance (Σv_spend −
* Σv_out), `0` when there is no bundle.
*/
export type ZcashIronwoodParsedTransaction =
| (IronwoodParsedBase & { inputKind: "transparent"; shieldedSpends: [] })
| (IronwoodParsedBase & {
inputKind: "shielded";
inputs: [];
shieldedSpends: ZcashShieldedSpend[];
});

/** The parse fields shared by both `inputKind` arms. */
type IronwoodParsedBase = ParsedTransaction<ZcashIronwoodParsedOutput, ZcashParsedInput> & {
valueBalance: bigint;
};

/**
* A Zcash **v6 (Ironwood / NU6.3)** shielding PSBT.
*
Expand All @@ -82,7 +117,37 @@ export function zip302NoMemo(): Uint8Array {
* const tx = psbt.combineProof(proof);
* ```
*/
export class ZcashIronwoodBitGoPsbt extends ZcashBitGoPsbt {
export class ZcashIronwoodBitGoPsbt extends ZcashBitGoPsbt<
ZcashIronwoodParsedOutput,
ZcashParsedInput
> {
/**
* Parse the transaction through the Zcash parse, which surfaces the shielded side the
* generic parse refuses to drop: per-action output indices, the shielded spends, and the
* bundle's value balance. The plaintext spend info is unverified until the key-free
* verifier lands (plan PR 4).
*/
override parseTransactionWithWalletKeys(
walletKeys: WalletKeysArg,
options: ParseTransactionOptions,
): ZcashIronwoodParsedTransaction {
return super.parseTransactionWithWalletKeys(
walletKeys,
options,
) as ZcashIronwoodParsedTransaction;
}

/**
* Parse outputs through the Zcash parse: every output carries `isShielded` and, for
* shielded outputs, its `actionIndex`.
*/
override parseOutputsWithWalletKeys(
walletKeys: WalletKeysArg,
options?: ParseOutputsOptions,
): ZcashIronwoodParsedOutput[] {
return super.parseOutputsWithWalletKeys(walletKeys, options);
}

/**
* Create an empty Zcash **v6 (Ironwood)** shielding PSBT, with the consensus branch ID
* determined from block height.
Expand Down
92 changes: 92 additions & 0 deletions packages/wasm-utxo/js/fixedScriptWallet/ZcashIronwoodTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/**
* Shared Zcash Ironwood (v6) types for the shielded-spend flow.
*
* These describe what the build service, the client and KMS each see as the PSBT moves
* through the flow in `docs/ironwood-shielded-spend-plan.md` §2. Parse-facing types live in
* `ZcashIronwoodBitGoPsbt.ts`; this module holds the shared vocabulary.
*/

/** One shielded note to spend, as passed to `addShieldedSpends` (plan PR 5). */
export type ZcashShieldedSpendRequest = {
/** Raw 43-byte Orchard recipient (diversifier ‖ pk_d). */
recipient: Uint8Array;
/** Note value in zatoshi. */
value: bigint;
/** The note's rho (the nullifier seed the spend re-derives). */
rho: Uint8Array;
/** The note's random seed. */
rseed: Uint8Array;
/** The note commitment the on-chain action carried. */
cmx: Uint8Array;
/** Merkle witness (position + auth path) proving `cmx` under the bundle's anchor. */
witness: { position: number; authPath: Uint8Array[] };
/** The spend's nullifier, from the note store or from KMS. */
nullifier: Uint8Array;
};

/** One shielded output to create, as passed to `addShieldedSpends` (plan PR 5). */
export type ZcashShieldedOutputRequest = {
/** Raw 43-byte Orchard recipient. */
recipient: Uint8Array;
/** Output value in zatoshi (> 0). */
amount: bigint;
/** ZIP-302 memo; defaults to the no-memo encoding. */
memo?: Uint8Array;
/** Full Unified Address the output was addressed to, preserved verbatim. */
unifiedAddress?: string;
};

/**
* The randomizer state of a shielded spend: `placeholder` while the spend still carries the
* wallet's spend-validating key as its `rk`, `installed` once the signer delivered the
* randomized `(alpha, rk)` pair.
*/
export const IronwoodRandomizerState = ["placeholder", "installed"] as const;
export type IronwoodRandomizerState = (typeof IronwoodRandomizerState)[number];

/** Whether a PSBT's inputs are transparent or shielded — exactly one per PSBT. */
export const ZcashInputKind = ["transparent", "shielded"] as const;
export type ZcashInputKind = (typeof ZcashInputKind)[number];

/** One real shielded spend, as surfaced by the Zcash parse. */
export type ZcashShieldedSpend = {
/** The spend's action index in the orchard bundle. */
actionIndex: number;
/** Note value in zatoshi. */
value: bigint;
/** Raw Orchard recipient (43 bytes). */
recipient: Uint8Array;
/** The nullifier this spend reveals. */
nullifier: Uint8Array;
/** Randomizer state of the spend's `rk` (see {@link IronwoodRandomizerState}). */
randomizer: IronwoodRandomizerState;
/** Whether a spend-auth signature is installed for this spend. */
signed: boolean;
};

/** The client-side verification report of a shielded bundle (plan PR 4). */
export type IronwoodVerificationReport = {
/** The bundle's real spends, as verified. */
spends: ZcashShieldedSpend[];
/** The bundle's real outputs. */
outputs: Array<{ actionIndex: number; value: bigint; recipient: Uint8Array }>;
/** The bundle's value balance (Σv_spend − Σv_out). */
valueBalance: bigint;
/** The anchor the spends' witnesses commit to. */
anchor: Uint8Array;
};

/** Orchestration state of the shielded side of a spend PSBT (plan PR 7). */
export type IronwoodShieldedStatus = {
inputKind: ZcashInputKind;
/** Action indices whose `out_ciphertext` is still pending. */
pendingOutCiphertexts: number[];
/** Action indices whose randomizer `(alpha, rk)` is still pending. */
pendingRandomizers: number[];
/** Action indices whose spend-auth signature is still pending. */
pendingSpendSignatures: number[];
/** Whether a Halo2 proof is installed in the PCZT. */
hasProof: boolean;
/** Whether the PSBT is ready for `combineProof`. */
readyToCombine: boolean;
};
13 changes: 13 additions & 0 deletions packages/wasm-utxo/js/fixedScriptWallet/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export { BitGoKeySubtype, type PsbtKvKey } from "./BitGoKeySubtype.js";
export {
ZcashBitGoPsbt,
type ZcashNetworkName,
type ZcashParsedInput,
type ZcashParsedOutput,
type ZcashTransactionVersion,
type CreateEmptyZcashOptions,
Expand All @@ -53,9 +54,21 @@ export { ZcashPsbt, type ZcashPsbtInstance } from "./ZcashPsbt.js";
export {
ZcashIronwoodBitGoPsbt,
zip302NoMemo,
type ZcashIronwoodParsedOutput,
type ZcashIronwoodParsedTransaction,
type CreateEmptyIronwoodOptions,
type CreateEmptyIronwoodWithConsensusBranchIdOptions,
} from "./ZcashIronwoodBitGoPsbt.js";
// Shared Zcash Ironwood (shielded-spend flow) types
export type {
ZcashShieldedSpendRequest,
ZcashShieldedOutputRequest,
IronwoodRandomizerState,
ZcashInputKind,
ZcashShieldedSpend,
IronwoodVerificationReport,
IronwoodShieldedStatus,
} from "./ZcashIronwoodTypes.js";

// Zcash ZIP-316 Unified Address
export { ZcashUnifiedAddress } from "./ZcashUnifiedAddress.js";
Expand Down
13 changes: 13 additions & 0 deletions packages/wasm-utxo/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ macro_rules! impl_wasm_error_code {
pub enum WasmUtxoError {
StringError(String),
Parse(ParseTransactionError),
ZcashParse(crate::fixed_script_wallet::bitgo_psbt::zcash::parse::ZcashParseTransactionError),
UnifiedAddress(crate::zcash::unified_address::UnifiedAddressError),
ZcashV6(crate::zcash::v6::ZcashV6Error),
Ironwood(crate::zcash::ironwood_build::IronwoodBuildError),
Expand All @@ -39,6 +40,7 @@ impl fmt::Display for WasmUtxoError {
match self {
WasmUtxoError::StringError(s) => write!(f, "{}", s),
WasmUtxoError::Parse(e) => write!(f, "{}", e),
WasmUtxoError::ZcashParse(e) => write!(f, "{}", e),
WasmUtxoError::UnifiedAddress(e) => write!(f, "{}", e),
WasmUtxoError::ZcashV6(e) => write!(f, "{}", e),
WasmUtxoError::Ironwood(e) => write!(f, "{}", e),
Expand All @@ -54,6 +56,7 @@ impl WasmErrorCode for WasmUtxoError {
match self {
WasmUtxoError::StringError(_) => "WasmUtxoError.StringError".to_string(),
WasmUtxoError::Parse(e) => e.code(),
WasmUtxoError::ZcashParse(e) => e.code(),
WasmUtxoError::UnifiedAddress(e) => e.code(),
WasmUtxoError::ZcashV6(e) => e.code(),
WasmUtxoError::Ironwood(e) => e.code(),
Expand Down Expand Up @@ -100,6 +103,16 @@ impl From<ParseTransactionError> for WasmUtxoError {
}
}

impl From<crate::fixed_script_wallet::bitgo_psbt::zcash::parse::ZcashParseTransactionError>
for WasmUtxoError
{
fn from(
err: crate::fixed_script_wallet::bitgo_psbt::zcash::parse::ZcashParseTransactionError,
) -> Self {
WasmUtxoError::ZcashParse(err)
}
}

impl From<crate::zcash::unified_address::UnifiedAddressError> for WasmUtxoError {
fn from(err: crate::zcash::unified_address::UnifiedAddressError) -> Self {
WasmUtxoError::UnifiedAddress(err)
Expand Down
Loading
Loading