Skip to content

feat(abstract-utxo): add zec shielded psbt decode and recipient resolution support - #9642

Open
veetragjain wants to merge 1 commit into
masterfrom
veetragjain/cshld-1639-resolve-zec-unified-address-recipients-for-shielding
Open

feat(abstract-utxo): add zec shielded psbt decode and recipient resolution support#9642
veetragjain wants to merge 1 commit into
masterfrom
veetragjain/cshld-1639-resolve-zec-unified-address-recipients-for-shielding

Conversation

@veetragjain

@veetragjain veetragjain commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Ticket: CSHLD-1640

@linear-code

linear-code Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

CSHLD-1639

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

⚠️ Unit tests are failing on Node 26.x (Current release line, non-blocking). This is not an LTS version yet, so it does not block merge, but it signals an incompatibility to fix before Node 26.x becomes LTS.

View run

@veetragjain
veetragjain force-pushed the veetragjain/cshld-1639-resolve-zec-unified-address-recipients-for-shielding branch 2 times, most recently from c0b5ca0 to d3a9a4f Compare September 4, 2026 19:50
@veetragjain veetragjain changed the title feat(abstract-utxo): support shielding transaction build and decode feat(abstract-utxo): add zec shielded psbt decode and recipient resolution support Sep 4, 2026
@veetragjain
veetragjain marked this pull request as ready for review September 7, 2026 10:00
@veetragjain
veetragjain requested review from a team as code owners September 7, 2026 10:00
Comment on lines +63 to +68
/**
* Custom change wallet xpubs, when the transaction spends to a custom change wallet. Outputs
* matching these keys are classified as change, not recipients — matching how
* `explainPsbtWasm` treats them.
*/
customChangeXpubs?: Triple<string>;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we don't need to support custom change wallets, let's leave this unimplemented and fail hard if a custom change wallet is configured

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

try {
return fixedScriptWallet.ZcashUnifiedAddress.parse(address, network);
} catch (e) {
return undefined;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should prefer failing hard instead

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Failing hard now

address: string,
param?: { anyFormat?: boolean; allowLightning?: boolean } | boolean
): boolean {
const unifiedAddress = tryParseUnifiedAddress(address, this.name as 'zec' | 'tzec');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this seems to be the only site where we use the undefined returning func, I'd rather inline the catch here

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

catching the error now

// `ZcashBitGoPsbt.fromBytes` signals v6 (Ironwood) bytes with a plain Error (not a
// WasmUtxoError) telling the caller to use `ZcashIronwoodBitGoPsbt.fromBytes` instead —
// see its doc comment. Fall back for that message as well as wasm-layer errors.
if (isWasmUtxoError(e) || (e instanceof Error && e.message.includes('v6 (Ironwood)'))) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a brittle way to identify errors. We have a more structured system for categorizing errors in wasm-utxo already, please check if we can already use it. If not, submit a change for wasm-utxo so we have a proper error code here instead and let's fix it in a follow-up.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using the new ZcashBitGoPsbt.fromBytes which automatically detects the version and returns the right psbt instance

@veetragjain
veetragjain force-pushed the veetragjain/cshld-1639-resolve-zec-unified-address-recipients-for-shielding branch from d3a9a4f to 14d690e Compare September 9, 2026 06:32
@veetragjain
veetragjain force-pushed the veetragjain/cshld-1639-resolve-zec-unified-address-recipients-for-shielding branch from 14d690e to 70ffbd0 Compare September 9, 2026 07:22
import { resolvePsbtRecipients, PsbtRecipient } from './recipients';

export class Zec extends AbstractUtxoCoin {
readonly name: UtxoCoinName = 'zec';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
readonly name: UtxoCoinName = 'zec';
readonly name: 'zec' | 'tzec' = 'zec';

this should get rid of the as 'zec' | 'tzec' casts, please try

Comment on lines +89 to +90
// Raw script inherently transparent.
return 'transparent' as const;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok

Comment on lines +96 to +97
// Not a unified address: the ordinary transparent address-decoding path handles it.
return 'transparent' as const;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't agree with that

ZcashUnifiedAddress.parse can fail for many different reasons, I don't think it's fair to assume transparent as a good default here

we should try to parse it as a transparent address first instead, return transparent on success, and then try to parse it as a Unified address and propagate the error if it is nothing recognizable

* only be spent shielded, one carrying only a transparent receiver only transparently, one
* carrying both is ambiguous, and a mix of shielded and transparent recipients is rejected.
*/
getUnifiedRecipientPreference(txParams: {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see any tests

move this to a standalone func to recipients.ts and add better tests for it

you can also clean up the signature

getUnifiedRecipientPreference(
name: 'zec' | 'tzec',
recipients: { address: string | undefined; }[] // amount isn't actually used
)

Comment on lines +42 to +43
/* Zcash-only: how to resolve a Unified Address recipient ('shielded' or transparent) */
unifiedRecipientPreference: t.string,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use a union of literals here

* Orchard/Ironwood receiver (a shielded output); any other value (or omission) resolves it to
* its transparent receiver.
*/
unifiedRecipientPreference?: string;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use 'shielded' | 'transparent' here

* whose address space needs additional context to resolve — e.g. Zcash Unified Addresses,
* which resolve differently depending on `unifiedRecipientPreference`.
*/
resolveOutputScript(address: string, unifiedRecipientPreference?: string): Uint8Array {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

weak string type

the optional argument here is only useful for zcash and leaks into general AbstractUtxo, we should look for a better solution

export function fromExtendedAddressFormatToScript(
extendedAddress: string,
coinName: UtxoCoinName,
resolveScript?: (address: string, coinName: UtxoCoinName) => Uint8Array

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this looks a bit problematic, let's find a better solution here.

let's split up the PR and discuss these changes later

do a separate PR that adds

getUnifiedRecipientPreference(
name: 'zec' | 'tzec',
recipients: { address: string | undefined; }[] // amount isn't actually used
)

only

as discussed here

https://github.com/BitGo/BitGoJS/pull/9642/changes#r3965967895

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants