|
1 | 1 | /** |
2 | 2 | * @prettier |
3 | 3 | */ |
4 | | -import { |
5 | | - address as wasmAddress, |
6 | | - fixedScriptWallet, |
7 | | - hasPsbtMagic, |
8 | | - isWasmUtxoError, |
9 | | - zcashAddress as wasmZcashAddress, |
10 | | -} from '@bitgo/wasm-utxo'; |
| 4 | +import { fixedScriptWallet, hasPsbtMagic, isWasmUtxoError, zcashAddress as wasmZcashAddress } from '@bitgo/wasm-utxo'; |
11 | 5 | import { BitGoBase, ExtraPrebuildParamsOptions, Wallet } from '@bitgo/sdk-core'; |
12 | 6 |
|
13 | | -import { AbstractUtxoCoin } from '../../abstractUtxoCoin'; |
| 7 | +import { AbstractUtxoCoin, UnifiedRecipientPreferenceTxParams } from '../../abstractUtxoCoin'; |
14 | 8 | import { stringToBufferTryFormats } from '../../transaction/decode'; |
15 | 9 | import { UtxoCoinName } from '../../names'; |
16 | 10 |
|
@@ -77,15 +71,81 @@ export class Zec extends AbstractUtxoCoin { |
77 | 71 | /** |
78 | 72 | * Resolve `address` to an output script. For a Unified Address, `unifiedRecipientPreference === |
79 | 73 | * 'shielded'` resolves to the raw 43-byte Orchard/Ironwood receiver (a shielded output, no |
80 | | - * scriptPubKey) instead of the default transparent scriptPubKey. Non-Unified addresses and any |
81 | | - * other `unifiedRecipientPreference` value are unaffected and resolve exactly as the base |
82 | | - * implementation would. |
| 74 | + * scriptPubKey); any other value resolves the Unified Address's transparent receiver (a plain |
| 75 | + * transparent address decodes exactly as the base implementation would). A Unified Address |
| 76 | + * without a transparent receiver cannot resolve transparently and throws. |
83 | 77 | */ |
84 | 78 | override resolveOutputScript(address: string, unifiedRecipientPreference?: string): Uint8Array { |
85 | 79 | if (unifiedRecipientPreference === 'shielded') { |
86 | 80 | return wasmZcashAddress.toShieldedReceiverWithCoin(address, this.name); |
87 | 81 | } |
88 | | - return wasmAddress.toOutputScriptWithCoin(address, this.name); |
| 82 | + return wasmZcashAddress.toTransparentReceiverWithCoin(address, this.name); |
| 83 | + } |
| 84 | + |
| 85 | + /** |
| 86 | + * Infer the Unified-Address recipient preference from the recipients when the caller did not |
| 87 | + * pass one — mirroring wallet-platform's utxo-core `buildTransaction` (`inferIsShielded` + |
| 88 | + * `classifyRecipientShieldedness`): a Unified Address carrying only an Orchard receiver can |
| 89 | + * only be spent shielded, one carrying only a transparent receiver only transparently, one |
| 90 | + * carrying both is ambiguous, and a mix of shielded and transparent recipients is rejected. |
| 91 | + */ |
| 92 | + override getUnifiedRecipientPreference(txParams: UnifiedRecipientPreferenceTxParams): string | undefined { |
| 93 | + const preference = txParams.unifiedRecipientPreference; |
| 94 | + if (preference !== undefined) { |
| 95 | + // Indexer parity (utxo-core buildTransaction): a shielded build requires every recipient |
| 96 | + // to be shielded-capable — a plain transparent address mixed in is rejected rather than |
| 97 | + // silently routed through the transparent builder. |
| 98 | + if (preference === 'shielded') { |
| 99 | + for (const recipient of txParams.recipients ?? []) { |
| 100 | + if (!this.isShieldedCapable(recipient.address)) { |
| 101 | + throw new Error('Mixed shielded and transparent recipients are not supported'); |
| 102 | + } |
| 103 | + } |
| 104 | + } |
| 105 | + return preference; |
| 106 | + } |
| 107 | + const shieldedness = (txParams.recipients ?? []).map((recipient) => { |
| 108 | + if (recipient.address === undefined) { |
| 109 | + // Raw script and OP_RETURN recipients are inherently transparent. |
| 110 | + return 'transparent' as const; |
| 111 | + } |
| 112 | + const unified = tryParseUnifiedAddress(recipient.address, this.name as 'zec' | 'tzec'); |
| 113 | + if (!unified) { |
| 114 | + // Not a unified address: the ordinary transparent address-decoding path handles it. |
| 115 | + return 'transparent' as const; |
| 116 | + } |
| 117 | + if (unified.hasOrchardReceiver && unified.hasTransparentReceiver) { |
| 118 | + throw new Error( |
| 119 | + `Unified address ${recipient.address} carries both transparent and Orchard receivers; specify unifiedRecipientPreference: "shielded" or "transparent"` |
| 120 | + ); |
| 121 | + } |
| 122 | + if (unified.hasTransparentReceiver) { |
| 123 | + return 'transparent' as const; |
| 124 | + } |
| 125 | + if (unified.hasOrchardReceiver) { |
| 126 | + return 'shielded' as const; |
| 127 | + } |
| 128 | + throw new Error(`Unified address ${recipient.address} carries no transparent or Orchard receiver`); |
| 129 | + }); |
| 130 | + const hasShielded = shieldedness.includes('shielded'); |
| 131 | + const hasTransparent = shieldedness.includes('transparent'); |
| 132 | + if (hasShielded && hasTransparent) { |
| 133 | + throw new Error('Mixed shielded and transparent recipients are not supported'); |
| 134 | + } |
| 135 | + return hasShielded ? 'shielded' : undefined; |
| 136 | + } |
| 137 | + |
| 138 | + /** |
| 139 | + * Whether `address` can be spent through the shielded (Orchard PCZT) path: a Unified Address |
| 140 | + * carrying an Orchard/Ironwood receiver. Raw scripts, plain transparent addresses, and |
| 141 | + * transparent-only Unified Addresses cannot. |
| 142 | + */ |
| 143 | + private isShieldedCapable(address?: string): boolean { |
| 144 | + if (address === undefined) { |
| 145 | + return false; |
| 146 | + } |
| 147 | + const unified = tryParseUnifiedAddress(address, this.name as 'zec' | 'tzec'); |
| 148 | + return unified !== undefined && unified.hasOrchardReceiver; |
89 | 149 | } |
90 | 150 |
|
91 | 151 | /** |
|
0 commit comments