Skip to content

feat(widget): make ZEC a destination-only chain and check receivers per destination chain - #884

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

chybisov merged 52 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 2 of 2, the widget half of Zcash as a destination-only chain.

SDK and rebase: rebased onto main (#896, no merge commits). main already uses @lifi/sdk 4.11.0 with its matching providers and bigmi 0.10.5 / 0.9.3; this PR adds @lifi/sdk-provider-zcash ^4.0.0, released from lifinance/sdk#492 through lifinance/sdk#508. pnpm why @lifi/sdk → 1 version. Upgrade the @lifi/widget-provider-* packages together with @lifi/widget: an older Bitcoin provider hands the chain ID to bigmi as a network and rejects every Bitcoin receiver.

Bundle size budgets: main (#896) already raised the Sui budget for SDK 4.11. This PR resets every budget to the size measured on this branch plus 20%, rounded up to a whole kB (LiFiWidget 322 kB, Solana 39 kB, Bitcoin 107 kB, Tron 877 kB, Stellar 213 kB, full integration 2292 kB; the rest are unchanged), and adds a check for the Zcash provider (6.6 kB, budget 8 kB), which the full integration check now also imports. Without it the report flags @lifi/widget-provider-zcash as a published package with no size check.

What was wrong

ZEC carries chainType: UTXO, like Bitcoin, and every receiver check in the widget compared chain types only. Once the API lists ZEC:

  • A Zcash address could not be entered at all, and a Bitcoin address passed as a ZEC receiver.
  • BTC → ZEC never asked for a receiver, so the quote went out with the Bitcoin fromAddress as the ZEC receiver.
  • useRoutes gated the quote on getChainTypeFromAddress(toAddress) === toChain.chainType, so a valid Zcash receiver never produced a quote.
  • A connected Bitcoin wallet was auto-filled, offered, seeded, kept and priced (balances, gas refuel) as the ZEC receiver, and a selected Bitcoin receiver survived a switch of the destination to ZEC.
  • ZEC appeared as a source chain and source token, could arrive as fromChain from the config or the URL, and the reverse and swap buttons could move it to the source side.

What changed

New package @lifi/widget-provider-zcash

  • ZcashProvider(config?) fills a new ZcashContext with the SDK's ZcashProvider (from @lifi/sdk-provider-zcash). No wallets, no connectors; config.sdkProvider overrides the SDK provider.
  • Opt-in, like every other ecosystem:
    providers: [EthereumProvider(), BitcoinProvider(), ZcashProvider()]
    With external wallet management the widget ignores providers, so render the component ZcashProvider() returns around the widget, next to the other ecosystem providers.

@lifi/widget-provider

  • One question, asked of the provider that serves the destination. useAddressForChain() → isAddressForChain(address, chain) finds the provider with the SDK's findProvider (the one that lists the chain in chainIds, otherwise the provider of its chain type) and asks isAddress(address, chain.id). Every call site that knows the destination uses it instead of comparing chain types: useRoutes, useToAddressRequirements (plus isValidReceiver for the list pages), useToAddressReset, useToAddressAutoPopulate, useAccountsBalancesData, useGasRefuel, the send-to-wallet pages and checkout's recipient hooks.
  • The provider map keyed by chain type is gone: the helpers work on the ordered useSDKProviders() list, which places Zcash right after Bitcoin, and keep the explicit detection order between ecosystems.
  • Chainless detection also reports the chain when the matching provider serves one chain (getChainFromAddress), so a Zcash address entered with any destination is recognised as ZEC.
  • New exports: ZcashContext, useZcashContext, and the AddressChain, ChainRef and IsAddressForChain types.

@lifi/widget

  • ZEC only when it can be served. Below the provider components, SDKProviderInitializer re-provides the widget config with every destination-only chain that no provider lists denied (withServedDestinationChains) and with such a preset toChain/toToken dropped (withoutUnservedToChain); the URL toChain is dropped the same way. Without ZcashProvider(), ZEC appears in no chain list, no token list and no preset.
  • Destination-only chains, once. WidgetProvider resolves the integrator config through withDestinationOnlyChains, which adds ZEC to chains.from.deny, and drops a destination-only fromChain/fromToken. The widget's existing allow/deny filters then keep ZEC out of the source chain list, the source token list, the opposite-chain sync and the from-side chain order with no special case. The URL fromChain is dropped the same way, and the reverse and cards-layout swap buttons hide when the destination is not allowed as a source (isDestinationAllowedAsSource) — for ZEC and for any chain an integrator denies as a source.
  • Receiver requirement. A route also needs a receiver when the source signer — an account valid on the source chain — cannot receive on the destination chain. BTC → ZEC now behaves like EVM → SVM.
  • Validation. useAddressValidation takes chain (strict: the route's receiver) or no chain (bookmarks keep accepting any ecosystem). Existing messages are unchanged; an unrecognised ZEC receiver gets a new message naming the accepted formats (t1/t3, or u1 with an Orchard receiver) and the refused ones (Sapling zs1, TEX) under a plain key, so every locale falls back to English until translated.
  • Unified receivers. The SDK's ZcashProvider accepts a unified u1 address that carries an Orchard receiver — exactly what the API pays since EXBE-597 (NEAR Intents pays it to a shielded balance) — so the widget takes it with no other change: detection, the receiver checks, bookmarks and the Zcash icon all go through the provider. The address inputs now take up to 512 characters (addressInputMaxLength): a payable unified address has at most 213, and the longest official test vector 509.
  • Icons. A valid receiver stores the chainId its provider reports, so a Zcash bookmark shows the Zcash icon wherever it was saved. Existing bookmarks need no migration.
  • Quotes before a receiver is set. With no receiver the API delivers to the signer when both chains share a chain type, so BTC → ZEC with a connected Bitcoin wallet found no route until a receiver was set (li.quest: 0 routes with fromAddress=bc1…, "recipient is not valid"; 1 route without it). useRoutes now quotes as if disconnected (quoteFromAddress) while the signer would be taken as the receiver (isSignerInvalidReceiver); once a receiver is set it quotes with the signer again.
  • No balance reads on ZEC. useTokenBalance reads no balance on a destination-only chain. No provider can read one, and a balance without a block number was retried until the backoff gave up, then again on every refetch; the refuel check started this with a ZEC receiver.
  • Receiver reset on a token pick. A token pick that moves the destination chain (for example from "All networks") now resets a receiver the new chain cannot take, as the chain select already does — a t1 receiver no longer stays after a switch from ZEC to BTC. Its own patch changeset, since it applies to every chain.
  • Required by any SDK bump: since @lifi/sdk@4.8.1 the SDK entrypoint exports name and version, which made export type * from '@lifi/sdk' ambiguous with the widget's own (TS2308). The widget now exports its name and version explicitly — same public API.
  • The playground registers ZcashProvider(), and the playground e2e workflows also run for packages/widget-provider-zcash.

Why was it implemented this way?

  • Zcash is its own provider. It shares only the address shape with Bitcoin; its transactions, signatures, partial-transaction format and fees differ, so source-side Zcash (PMT-55) needs its own provider anyway. The new widget package is where a Zcash wallet context will go.
  • Ask the provider that serves the destination, don't compare chain types. One helper answers the exact question for every ecosystem: an EVM address still passes on every EVM chain, a Bitcoin address fails on ZEC, a Zcash address fails on BTC. Nothing trusts a stored chainType for validity any more.
  • One lookup rule, shared with the SDK. The widget uses the SDK's findProvider, so the two cannot drift apart.
  • Hide what cannot be served. Without a provider for ZEC, every ZEC receiver would be refused, so the chain is not offered at all. The rule lives where the providers are known, and the existing allow/deny filters apply it everywhere.
  • Reuse the config's allow/deny instead of new guards. "ZEC is never a source" is one line in the resolved config; every source filter the widget already has applies it.

Alternatives considered:

  • Keep ZEC as a branch of the Bitcoin provider. It worked for address checks, but couples two chains that share nothing else.
  • Build the Zcash SDK provider into @lifi/widget-provider. Zero integrator work, but an opt-in package matches the other ecosystems and gives PMT-55 a home.
  • A guard at every place that can pick a source. The first version did this; review found seven such places and more kept appearing. The config deny covers them all.

Compatibility

  • An integrator who does not add ZcashProvider() sees no change, and no ZEC.
  • The widget passes the chain ID to each provider's isAddress, so upgrade the @lifi/widget-provider-* packages together with @lifi/widget: an older @lifi/widget-provider-bitcoin refuses every Bitcoin receiver.
  • @lifi/widget-embedded (widget-light) does not register ZcashProvider() yet, so widget-light shows no ZEC.

Release note

@lifi/widget-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 release.

The package starts at 4.0.0-0, so its minor changeset releases it as 4.0.0, in line with the other providers (a base of 4.0.0 would become 4.0.1, because every @lifi/widget-provider bump also patch-bumps it). Publish the manual bootstrap as 0.0.0, as was done for @lifi/sdk-provider-zcash, so it cannot collide with the 4.0.0 release; deprecate 0.0.0 afterwards.

Visual showcase (Screenshots or Videos)

Checks 1–8 ran before li.quest listed ZEC, in the playground against a local proxy that adds a ZEC chain and token:

  1. ZEC is in the "to" chain list and not in the "from" chain list; with ZEC as the destination the reverse button is hidden, and the "from" token search offers only wrapped ZEC on other chains.
  2. With ZEC as the destination, "Send to wallet" refuses bc1… ("Wallet address doesn't match the selected destination chain Zcash") and, before unified support, a truncated u1… address; it accepts t1…; the receiver shows the Zcash icon.
  3. Switching the destination to BTC clears the Zcash receiver; a t1 bookmark saved while BTC is the destination keeps the Zcash icon and is disabled as a BTC receiver.
  4. The widget sends one /v1/chains request at start-up.
  5. With ZcashProvider() removed from the config, ZEC appears in no chain list and no "to" token list.

Unified receivers, checked headless (Playwright) in the playground against the same proxy and the SDK preview 0.0.0-preview-b7870f68, with ZEC as the destination:

  1. An Orchard-only u1… (106 characters) is accepted.
  2. A transparent + Sapling + Orchard u1… (213 characters) fits the address input in full and is accepted.
  3. A transparent + Sapling u1… without an Orchard receiver is refused with the Zcash message.

Checks 9–12 ran headless (Playwright) against li.quest before the rebase, which now lists ZEC and returns a NEAR route for USDC → ZEC. They were repeated with the released SDK: 9, 11 and 12 pass again; in 10 the route requests carried the receivers, but li.quest answered HTTP 429 (its keyless rate limit), so no route came back in that run:

  1. The t1, Orchard-only u1 and transparent + Sapling + Orchard u1 receivers are accepted, a u1 without Orchard gets the Zcash message, and bc1… is refused as the wrong chain.
  2. The route request carries the t1 or u1 receiver, and the API returns a route.
  3. A fromChain=20000000000005 link is ignored; the "from" network and token searches find nothing on Zcash, the "to" searches do.
  4. With a t1 receiver, picking BTC on Bitcoin from "All networks" on the "to" side resets the receiver.

Not changed here: a toAddress link is still validated without the destination chain, so toAddress=bc1… with ZEC as the destination is kept as the receiver and the quote waits — the same as any ecosystem mismatch before this PR.

Tests

  • Unit specs: chainTypeFromAddress (provider list, detection order, chainFromAddress, isAddressForChain with and without a Zcash provider), useAddressForChain, useChainTypeFromAddress, ZcashProvider (the new package), useAddressValidation (strict and chainless modes and their messages), useToAddressRequirements (BTC → ZEC, a fallback account of another ecosystem, no source chain, EVM → EVM, EVM → SVM, STL → STL), useToAddressReset, canQuoteWithToAddress, isSignerInvalidReceiver, useTokenBalance (no read on ZEC), useTokenSelect (receiver reset), useToAddressAutoPopulate, useAccountsBalancesData, useChains, filterAllowedTokens, getDefaultValuesFromQueryString, withDestinationOnlyChains, withServedDestinationChains, withoutUnservedToChain, isUnservedDestinationChain, isDestinationAllowedAsSource, checkout's useDefaultWalletRecipient and useResolvedCheckoutRecipient.
  • pnpm build, pnpm check, pnpm check:types, pnpm check:circular-deps, pnpm knip:check and pnpm test all exit 0.
  • Reviewed in four /code-review rounds after the provider split (five before it) until no code finding remained.

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 Widget API or adds new features that require documentation, I have updated the documentation in the public-docs repository. — Not yet: @lifi/widget-provider-zcash, ZcashProvider() and the @lifi/widget-provider exports need docs after the release.

@changeset-bot

changeset-bot Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 75824f2

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

This PR includes changesets to release 32 packages
Name Type
@lifi/widget Minor
@lifi/widget-provider Minor
@lifi/widget-checkout Patch
@lifi/wallet-management Patch
@lifi/widget-provider-bitcoin Patch
@lifi/widget-provider-ethereum Patch
@lifi/widget-provider-solana Patch
@lifi/widget-provider-stellar Patch
@lifi/widget-provider-sui Patch
@lifi/widget-provider-tron Patch
@lifi/widget-provider-zcash Minor
connectkit Patch
deposit-flow Patch
dynamic Patch
nextjs Patch
nextjs15 Patch
nft-checkout Patch
nuxt-app Patch
privy-ethers-example Patch
privy Patch
rainbowkit Patch
react-router Patch
remix Patch
reown Patch
svelte Patch
tanstack-router-example Patch
vite-project Patch
vue Patch
zustand-widget-config Patch
@lifi/widget-provider-mesh Patch
@lifi/widget-provider-transak Patch
vite-iframe 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

@github-actions

github-actions Bot commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

✅ E2E Dev Smoke — passing

Check Result
Dev server start (pnpm dev) ✅ started
Smoke tests ✅ passed

4 passed · 0 failed · 0 skipped · 21s

View run

@github-actions

github-actions Bot commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

E2E Playground results

passed  164 passed

Details

stats  164 tests across 11 suites
duration  2 minutes, 21 seconds
commit  75824f2

📥 Download full HTML report (open the run → Artifacts → playwright-report)

@github-actions

github-actions Bot commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

E2E Examples — all passed

All examples passed in the latest run.

@chybisov
chybisov force-pushed the feature/jumemb-113-zcash-add-support-on-widgetsdk branch from b6c676a to acf07f7 Compare October 7, 2026 10:48
@chybisov
chybisov marked this pull request as ready for review October 7, 2026 11:05
@github-actions

github-actions Bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

📦 Bundle size

✅ No significant changes. Every check is within budget.

@lifi/widget

Import Base PR Change Budget used
⚪ LiFiWidget (without translations) 267.3 kB 267.8 kB 🔺 +0.51 kB (+0.2%) ▰▰▰▰▰▰▰▰▱▱ 83%
⚪ English translations (always loaded) 7.66 kB 7.73 kB 🔺 +0.07 kB (+1.0%) ▰▰▰▰▰▰▰▰▱▱ 77%
⚪ /shared (all exports) 171.0 kB 171.5 kB 🔺 +0.53 kB (+0.3%) ▰▰▰▰▰▰▰▰▱▱ 83%
⚪ /queries (all exports) 2.12 kB 2.12 kB — ▰▰▰▰▰▰▰▱▱▱ 71%
⚪ /skeleton (WidgetSkeleton) 43.8 kB 43.8 kB — ▰▰▰▰▰▰▰▰▱▱ 83%

@lifi/wallet-management

Import Base PR Change Budget used
⚪ all exports (without translations) 72.8 kB 72.8 kB — ▰▰▰▰▰▰▰▰▱▱ 83%

@lifi/widget-light

Import Base PR Change Budget used
⚪ LiFiWidgetLight 1.21 kB 1.21 kB — ▰▰▰▰▰▰▱▱▱▱ 60%

Providers

Import Base PR Change Budget used
⚪ ethereum 97.4 kB 97.4 kB — ▰▰▰▰▰▰▰▰▱▱ 83%
⚪ solana 31.7 kB 31.7 kB — ▰▰▰▰▰▰▰▰▱▱ 81%
⚪ bitcoin 89.1 kB 89.1 kB — ▰▰▰▰▰▰▰▰▱▱ 83%
⚪ sui 92.7 kB 92.7 kB — ▰▰▰▰▰▰▰▰▱▱ 83%
⚪ tron (incl. WalletConnect loaded on demand) 730.2 kB 730.2 kB — ▰▰▰▰▰▰▰▰▱▱ 83%
⚪ stellar 177.0 kB 177.0 kB — ▰▰▰▰▰▰▰▰▱▱ 83%
⚪ zcash — 6.59 kB 🆕 new ▰▰▰▰▰▰▰▰▱▱ 82%

Full integration

Import Base PR Change Budget used
⚪ LiFiWidget + all providers + their peers (incl. on-demand code) 1907.9 kB 1909.2 kB 🔺 +1.37 kB (+0.1%) ▰▰▰▰▰▰▰▰▱▱ 83%
How to read this
  • Sizes are minified + brotli, measured with size-limit the way an app bundles the import: tree-shaken, with all dependencies included except the ones in the check's ignore list (packages the app already has, such as peers).
  • Code behind a dynamic import() is counted too, unless the check's name says otherwise.
  • 🟠 / 🟢 mark a change of at least 512 B and at least 1% or 5.00 kB. 🔴 means the check is over its budget.
  • Budgets live in .size-limit.json. The base commit is measured with the same checks as the PR.

75824f2 merged into 10d954a · workflow run

…hain

useAccount falls back to an account of another ecosystem when none matches
the source chain, and that account never proves the source signer cannot
receive at the destination.
…olved config

WidgetProvider adds every destination-only chain to chains.from.deny once, so
the existing allow/deny filters keep ZEC out of the source chain list, the
source token list, the opposite-chain sync in useTokenSelect and the from-side
chain order, instead of a special case in each.
The config memo runs on every parent render, so a fresh chains object each
time re-ran the chain-order effect and refiltered every token list.
…k makes redundant

A signer that is valid on its own source chain proves the shared provider
exists, so the extra provider check can never change the answer.
…never reaches

Checkout allows EVM chains only, so ZEC can never be its destination.
…ved config

FormStore and ChainOrderStore both read the config fromChain, so dropping it
where the config is resolved gives both the same answer.
Found in a follow-up review:
- useTokenBalance reads no balance on a destination-only chain. No
  provider can read one, and a balance without a block number was
  retried until the backoff gave up, then again on every refetch
  (the refuel check with a ZEC receiver started it).
- useRoutes quotes as if disconnected while the signer would be taken
  as the receiver: with no receiver the API delivers to the signer when
  both chains share a chain type, so BTC -> ZEC with a connected Bitcoin
  wallet found no route until a receiver was set.
- useTokenSelect resets a receiver the new destination cannot take when
  a token pick moves the destination chain, as the chain select does.
Start the package at 4.0.0-0, so the pending minor changeset releases
it as 4.0.0 in line with the other providers instead of 0.1.0. A base
of 4.0.0 would not work: every widget-provider bump also patch-bumps
this package, which would release 4.0.1.
Replace the SDK preview pins with the released versions: @lifi/sdk 4.11.0,
@lifi/sdk-provider-zcash 4.0.0, @lifi/sdk-provider-bitcoin 4.1.0 and the
other providers' matching patch releases.
@chybisov
chybisov force-pushed the feature/jumemb-113-zcash-add-support-on-widgetsdk branch from 8a744dc to ef5329c Compare October 7, 2026 13:00
Every budget is the measured size plus 20% again, measured on this
branch on top of the SDK 4.11 bump, and a check covers the Zcash
provider, which the full integration check also imports. Without it the
report flags @lifi/widget-provider-zcash as a published package with no
size check.
@chybisov
chybisov force-pushed the feature/jumemb-113-zcash-add-support-on-widgetsdk branch from ef5329c to 75824f2 Compare October 7, 2026 13:01
@chybisov
chybisov merged commit 97e59c5 into main Oct 7, 2026
35 checks passed
@chybisov
chybisov deleted the feature/jumemb-113-zcash-add-support-on-widgetsdk branch October 7, 2026 13:18
@github-actions github-actions Bot mentioned this pull request Oct 7, 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.

1 participant