Skip to content

feat(sdk): validate an address against its destination chain, starting with Zcash - #492

Merged
chybisov merged 24 commits into
mainfrom
feature/jumemb-113-zcash-add-support-on-widgetsdk
Oct 6, 2026
Merged

chybisov merged 24 commits into
mainfrom
feature/jumemb-113-zcash-add-support-on-widgetsdk

Conversation

@chybisov

@chybisov chybisov commented Sep 23, 2026 •

Copy link
Copy Markdown
Member

Which Linear task is linked to this PR?

JUMEMB-113 — part 1 of 2, the SDK half of Zcash as a destination-only chain. The widget half (lifinance/widget#884) consumes these packages, so it waits for this release.

What was wrong

ZEC (ChainId.ZEC) carries chainType: UTXO, which Bitcoin already owns, and the SDK held one provider per chain type. So:

  • BitcoinProvider.isAddress is bigmi's isUTXOAddress. It rejects every Zcash address and accepts Bitcoin addresses, so a Zcash receiver cannot be validated and nothing can tell a Bitcoin address from a Zcash one.
  • A separate Zcash provider was impossible: setProviders keyed its map by provider.type, so a second UTXO provider replaced BitcoinProvider.
  • getBitcoinBalance always reads Bitcoin (ChainId.BTC is hard-coded) and copies that amount onto whatever tokens it is given, so a Bitcoin wallet asked for a ZEC balance reports its BTC balance as ZEC.

What changed

@lifi/sdk (minor, additive)

  • SDKProvider.isAddress(address, chainId?) takes an optional chain ID. Without it, every provider answers exactly as before.
  • SDKProvider.chainIds? declares the chains a provider serves. A provider without it serves every chain of its type that no other provider lists.
  • findProvider(providers, chainType, chainId?) (new export) holds that rule: the provider that lists the chain, otherwise the provider of the type that lists no chains.
  • client.getProvider(type, chainId?) uses it; getProvider(type) still returns the generic provider, so getProvider(ChainType.UTXO) is still BitcoinProvider.
  • setProviders replaces a provider only when both its type and its chainIds match, so setProviders([BitcoinProvider()]) keeps a registered Zcash provider.

@lifi/sdk-provider-zcash (new package)

  • ZcashProvider() — type: UTXO, chainIds: [ChainId.ZEC], no options.
  • isAddress accepts what the API pays:
    • a mainnet transparent address — t1 (P2PKH) or t3 (P2SH) — after a Base58Check decode with checksum and a 2-byte version check (0x1CB8 / 0x1CBD);
    • a unified address (u1, lowercase) that carries an Orchard receiver: Bech32m (prefix u), the inverse F4Jumble (ZIP 316, personalized BLAKE2b), the 16-byte padding, then a receiver list of P2PKH, P2SH, Sapling and Orchard receivers only, each at its fixed length, in ascending typecode order, with at most one transparent receiver.
    • Sapling, TEX and testnet addresses, a unified address without an Orchard receiver and one with any other receiver type are refused.
  • getBalance leaves every amount unknown, resolveAddress resolves nothing, and getStepExecutor throws "ZEC is a destination-only chain."

@lifi/sdk-provider-bitcoin (minor)

  • isAddress: no chain or ChainId.BTC → bigmi, as before; any other UTXO chain, ZEC included, → false.
  • getBitcoinBalance gives the Bitcoin balance and block number to BTC tokens only. Other tokens come back with no amount, and a list without a BTC token asks no node.
  • isBitcoinProvider also requires setOptions, so it no longer matches another UTXO provider such as ZcashProvider.

@lifi/sdk-provider-ethereum

  • isAddress wraps viem in a one-argument function, so a chain ID never reaches viem's options parameter. No behaviour change, so no changeset; the @lifi/sdk minor bump re-releases it.

Why was it implemented this way?

  • Zcash is its own provider. It shares only the address shape with Bitcoin. Its transaction format, signature digest (ZIP 244), partial-transaction format (PCZT) and fee rule (ZIP 317) differ, so source-side Zcash (PMT-55) needs its own provider anyway. The package is where that step executor and a balance source will go.
  • An optional chainIds, not a registry keyed by chain. Every existing and third-party provider declares only type; they keep their meaning with no change.
  • One rule in one exported function. getProvider and the widget both use findProvider, so the two cannot drift apart.
  • Order keeps old lookups working. Registered after Bitcoin, the Zcash provider leaves providers.find((p) => p.type === ChainType.UTXO) returning Bitcoin.
  • A chain ID never reaches a library function. bigmi's second parameter is network: isUTXOAddress(bitcoinAddress, ChainId.BTC) returns false for a valid address. viem's is options. With the new signature, tsc reports TS2322 for both bare aliases, so both providers now wrap them.
  • Unknown UTXO chains are refused. If BCH, LTC or DGE are listed later, a Bitcoin address must not pass as their receiver. A refusal is safe; a wrong acceptance can lose funds.
  • Exactly what the API pays. Since lifinance/lifi-backend#9013 (EXBE-597, production 2.87.0) the API routes a unified ZEC receiver with an Orchard receiver to NEAR Intents, and refuses every other non-transparent form: without an Orchard receiver a bridge would pay the transparent one. The client check mirrors that decoder rule for rule; a review compared both on all official vectors and about 28,000 random receiver lists with identical answers. A receiver list is read one byte per typecode and length: every accepted typecode and length fits one CompactSize byte, so a wider CompactSize can only encode a refused typecode.
  • Checksums, not a pattern. A shape-only regex accepts a mistyped address, and a mistyped receiver loses the funds.
  • @scure/base with @noble/hashes 1.x. Both have named exports that work in the CommonJS and ESM builds (@noble/hashes 2.x is ESM-only). bitcoinjs-lib's fromBase58Check expects a 1-byte version, and Zcash uses 2. No maintained TypeScript Zcash client exists that would replace these few lines.
  • No balances yet. The widget shows balances only for connected wallets, no Zcash wallet exists, and no free public Zcash endpoint serves getaddressbalance.

Alternatives considered:

  • Keep ZEC as a branch of BitcoinProvider.isAddress. It worked for address checks, but couples two chains that share nothing else.
  • A UTXO router provider in sdk-provider-bitcoin. No core change, but it couples the Bitcoin and Zcash packages and adds an indirection layer.
  • Key every provider by chain. Breaks every third-party provider.

Compatibility

Who After this PR
Integrator with the existing providers No change: getProvider(type), setProviders and execution behave as before.
Code that runs providers.find((p) => p.type === ChainType.UTXO) Still gets Bitcoin when Zcash is registered after it. filter returns both.
isBitcoinProvider(p) true only for a UTXO provider with setOptions.

Release note

@lifi/sdk-provider-zcash is a new npm package. npm trusted publishing cannot publish a package that does not exist yet, so an npm org owner must publish its first version by hand and register .github/workflows/publish.yaml as its trusted publisher before the preview or the release can publish it.

Visual showcase (Screenshots or Videos)

Not applicable — no UI.

Tests

  • findProvider.unit.spec.ts: a listed chain wins; an unlisted chain and a missing chain ID fall back to the generic provider; the list order does not matter; no match gives undefined.
  • createClient.unit.spec.ts: a chain-specific provider sits beside the generic one; setProviders replaces only the provider with the same chains, in any order, and an empty chain list stays apart from the generic provider; getProvider with and without a chain ID.
  • ZcashProvider.unit.spec.ts and zcashAddress.unit.spec.ts: official vectors — ZIP 320 (t1, and its TEX form), ZIP 214 (t3), zcash-test-vectors (unified and Sapling) — plus an Orchard unified address from Zashi, a corrupted checksum, testnet forms, Bitcoin and EVM addresses; other chain IDs refused; unknown balances; no step executor.
  • unifiedAddress.unit.spec.ts: the inverse F4Jumble against the official f4jumble.json rows, and all 60 rows of unified_address.json — accepted exactly when a row has an Orchard receiver and no unknown typecode — plus a corrupted checksum, the uppercase form, a testnet (utest) address, Sapling, TEX, truncated and non-address input. The vectors live in a *.mock.ts file, so they ship neither in dist nor in the tarball.
  • BitcoinProvider: without a chain and with ChainId.BTC the answers are unchanged; ZEC, LTC, BCH and DGE refuse everything; isBitcoinProvider refuses another UTXO provider.
  • Ethereum, Solana, Sui, Tron and Stellar: isAddress(a, chainId) equals isAddress(a) on their own chains.
  • getBitcoinBalance.unit.spec.ts: non-BTC lists make no RPC call; mixed lists in both orders give the balance to BTC only.
  • getBitcoinBalance.int.spec.ts: its only case asked the Bitcoin balance action for USDC and USDT on Polygon, and passed on main only because the BTC balance was relabelled as theirs. It now reads BTC on Bitcoin.
  • The CommonJS and ESM builds of @lifi/sdk-provider-zcash both accept a t1 and an Orchard u1 address and refuse a Bitcoin address. A u1 check costs about 30 µs; other input is unchanged at about 5 µs.
  • pnpm test:unit, pnpm check, pnpm check:types, pnpm check:circular-deps and pnpm knip:check all exit 0.
  • Reviewed in three /code-review rounds until no code finding remained. The rounds fixed the setProviders scope key (a generic provider keys by its bare type, a chain-specific one by type plus sorted chain IDs) and restored getBitcoinBalance to set no amount when the balance request fails.

Checklist before requesting a review

  • I have performed a self-review and testing of my code.
  • This pull request is focused and addresses a single problem.
  • If this PR modifies the SDK API or adds new features that require documentation, I have updated the documentation in the public-docs repository. — Not yet: the new @lifi/sdk-provider-zcash package and SDKProvider.chainIds need a docs page after the release.

@changeset-bot

changeset-bot Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: b7870f6

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 8 packages
Name Type
@lifi/sdk-provider-bitcoin Minor
@lifi/sdk Minor
@lifi/sdk-provider-zcash Minor
@lifi/sdk-provider-ethereum Patch
@lifi/sdk-provider-solana Patch
@lifi/sdk-provider-stellar Patch
@lifi/sdk-provider-sui Patch
@lifi/sdk-provider-tron Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@chybisov chybisov added the release-preview Publish a 0.0.0-preview-<sha> build of this PR to npm label Sep 23, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📦 Preview published under the preview dist-tag.

Install the exact version(s) — @preview moves with the newest preview across PRs:

npm i @lifi/sdk-provider-bitcoin@0.0.0-preview-267433d4
npm i @lifi/sdk-provider-ethereum@0.0.0-preview-267433d4
npm i @lifi/sdk-provider-solana@0.0.0-preview-267433d4
npm i @lifi/sdk-provider-stellar@0.0.0-preview-267433d4
npm i @lifi/sdk-provider-sui@0.0.0-preview-267433d4
npm i @lifi/sdk-provider-tron@0.0.0-preview-267433d4
npm i @lifi/sdk@0.0.0-preview-267433d4

@github-actions github-actions Bot removed the release-preview Publish a 0.0.0-preview-<sha> build of this PR to npm label Sep 23, 2026
@chybisov chybisov added the release-preview Publish a 0.0.0-preview-<sha> build of this PR to npm label Sep 23, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📦 Preview published under the preview dist-tag.

Install the exact version(s) — @preview moves with the newest preview across PRs:

npm i @lifi/sdk-provider-bitcoin@0.0.0-preview-dd8065ff
npm i @lifi/sdk-provider-ethereum@0.0.0-preview-dd8065ff
npm i @lifi/sdk-provider-solana@0.0.0-preview-dd8065ff
npm i @lifi/sdk-provider-stellar@0.0.0-preview-dd8065ff
npm i @lifi/sdk-provider-sui@0.0.0-preview-dd8065ff
npm i @lifi/sdk-provider-tron@0.0.0-preview-dd8065ff
npm i @lifi/sdk@0.0.0-preview-dd8065ff

@github-actions github-actions Bot removed the release-preview Publish a 0.0.0-preview-<sha> build of this PR to npm label Sep 23, 2026
@chybisov
chybisov force-pushed the feature/jumemb-113-zcash-add-support-on-widgetsdk branch from dd8065f to fbb46fa Compare September 30, 2026 12:08
… builds

bs58check exports only a default, which the CommonJS build resolved to the
whole exports object, so isAddress(t1, ChainId.ZEC) threw for CommonJS users.
@scure/base and @noble/hashes ship named exports for both builds.
The @lifi/sdk minor bump already re-releases the Ethereum provider.
@chybisov
chybisov force-pushed the feature/jumemb-113-zcash-add-support-on-widgetsdk branch from 1d9fdb4 to 1b774ee Compare October 5, 2026 11:58
@chybisov chybisov added the release-preview Publish a 0.0.0-preview-<sha> build of this PR to npm label Oct 5, 2026
@github-actions

github-actions Bot commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

📦 Preview published under the preview dist-tag.

Install the exact version(s) — @preview moves with the newest preview across PRs:

npm i @lifi/sdk-provider-bitcoin@0.0.0-preview-1b774eec
npm i @lifi/sdk-provider-ethereum@0.0.0-preview-1b774eec
npm i @lifi/sdk-provider-solana@0.0.0-preview-1b774eec
npm i @lifi/sdk-provider-stellar@0.0.0-preview-1b774eec
npm i @lifi/sdk-provider-sui@0.0.0-preview-1b774eec
npm i @lifi/sdk-provider-tron@0.0.0-preview-1b774eec
npm i @lifi/sdk-provider-zcash@0.0.0-preview-1b774eec
npm i @lifi/sdk@0.0.0-preview-1b774eec

@github-actions github-actions Bot removed the release-preview Publish a 0.0.0-preview-<sha> build of this PR to npm label Oct 5, 2026
@chybisov chybisov added the release-preview Publish a 0.0.0-preview-<sha> build of this PR to npm label Oct 6, 2026
@github-actions

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

📦 Preview published under the preview dist-tag.

Install the exact version(s) — @preview moves with the newest preview across PRs:

npm i @lifi/sdk-provider-bitcoin@0.0.0-preview-b7870f68
npm i @lifi/sdk-provider-ethereum@0.0.0-preview-b7870f68
npm i @lifi/sdk-provider-solana@0.0.0-preview-b7870f68
npm i @lifi/sdk-provider-stellar@0.0.0-preview-b7870f68
npm i @lifi/sdk-provider-sui@0.0.0-preview-b7870f68
npm i @lifi/sdk-provider-tron@0.0.0-preview-b7870f68
npm i @lifi/sdk-provider-zcash@0.0.0-preview-b7870f68
npm i @lifi/sdk@0.0.0-preview-b7870f68

@github-actions github-actions Bot removed the release-preview Publish a 0.0.0-preview-<sha> build of this PR to npm label Oct 6, 2026
@chybisov
chybisov merged commit 48af3ae into main Oct 6, 2026
7 checks passed
@chybisov
chybisov deleted the feature/jumemb-113-zcash-add-support-on-widgetsdk branch October 6, 2026 15:52
@github-actions github-actions Bot mentioned this pull request Oct 6, 2026
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