Skip to content

refactor(wallets): migrate chain wallet providers to class + Zustand stores#1999

Open
babkenmes wants to merge 55 commits into
dev-monorepofrom
dev-evm-wallet-class-refactor
Open

refactor(wallets): migrate chain wallet providers to class + Zustand stores#1999
babkenmes wants to merge 55 commits into
dev-monorepofrom
dev-evm-wallet-class-refactor

Conversation

@babkenmes

Copy link
Copy Markdown
Collaborator

Summary

  • Refactor every chain wallet package (EVM, Starknet, SVM, Fuel, Bitcoin, TRON, TON, Paradex, Immutable Passport) from hook-based connections to a class + Zustand store architecture, exposing connections via external vanilla stores
  • Replace the hook-based WalletProvider contract with external stores; integrate all providers in the bridge app through getDefaultProviders
  • Add wallet icon resolution utilities (knownConnectorIcons, resolveWalletIcon) and connect-modal/connector store plumbing in the widget
  • Allow the EVM wallet to accept an externally-provided wagmi Config
  • Prune unused dependencies across wallet packages and the bridge app, slimming the lockfile significantly

Test plan

  • Connect/disconnect wallets for each chain (EVM, Starknet, Solana, Fuel, Bitcoin, TRON, TON, Paradex, Immutable Passport)
  • Verify wallet icons render correctly in the wallet modal and connected-wallets list
  • Confirm Starknet connectors only show when the extension is actually installed
  • Run a withdrawal flow end-to-end (ManualWithdraw)
  • pnpm install resolves cleanly with the pruned lockfile; pnpm build succeeds

Note

The tonconnect-manifest.json in this branch points at a trycloudflare.com tunnel URL for local testing — revert to the production URL before merging.

🤖 Generated with Claude Code

babkenmes and others added 25 commits May 21, 2026 15:11
… store

Eliminates React-hook-based state and provider trees in the EVM wallet
package. State now lives in a module-scoped Zustand store and orchestration
in a class singleton, both of which survive the lazy-load remount cycles
that were forcing hotfix workarounds in the old hook-heavy code.

- Replaces the 561-line useEVMConnection.ts hook with a class
  (EvmConnectionService) plus a thin React adapter (service/useEvmConnection.ts).
- Adds Zustand store (service/evmStore.ts) and vanilla wagmi watchers
  (service/syncWagmi.ts) that mirror connections/account/connectors.
- Hoists the wagmi Config into a module-scoped singleton (service/getEvmConfig.ts)
  with reconnect() called on init, replacing <WagmiProvider reconnectOnMount>.
- Deletes ActiveEvmAccountProvider, WagmiProvider, QueryClientProvider,
  WagmiWrapper, QueryWrapper from the EVM tree. EVMProvider/index.tsx is
  now an EVMHydrator that pushes init params and renders children directly.
- Extracts pure helpers to service/: resolveWallet, resolveSupportedNetworks,
  networkBuckets, connectorsHelpers (dedupePreferInjected, splitRegistryConnectors,
  wagmiDisplayUriSource, attemptGetAccount).
- Converts useEVMTransfer hook into a vanilla createEvmTransfer factory.
- Rewrites EVMRpcHealthCheckProvider to read from the store instead of useAccount.
- Replaces useWalletClient in evmUtils/ethers.ts with getWalletClient over the
  config singleton.
- Migrates paradex useParadexConnection's useConfig() to getEvmConfig() so app
  boot doesn't crash without WagmiProvider in tree. Remaining paradex hooks
  (useAccount, useWalletClient) only fire during withdrawal flow and are a
  documented follow-up.
- Preserves all connector orchestration bug-fix sediment verbatim (synchronous
  display URI subscription, attemptGetAccount retry loop, pending-metadata
  ordering, dedupePreferInjected).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The bridge app now mounts only the EVM provider by default instead of
the full multi-chain bundle from getDefaultProviders().

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Restores parity with the pre-refactor context-fallback behavior so host
apps that already own a wagmi Config (and optionally wrap the widget in
their own <WagmiProvider>) can have the EVM package adopt that config
instead of creating a parallel one.

- getEvmConfig.ts: add provideExternalEvmConfig(cfg) + _external flag.
  Adopting an external config skips the internal createConfig() and
  skips reconnect() (host owns that lifecycle). Re-providing a different
  config after one is already in use warns and no-ops.
- EVMHydrator: accept optional wagmiConfig prop (explicit path) and fall
  back to useContext(WagmiContext) for ambient detection (implicit path).
  Resolution order: existing registered config → external (prop/ambient)
  → internal createConfig() with widget-built connectors.
- createEVMProvider: expose wagmiConfig?: Config on EVMProviderConfig and
  forward it to EVMProviderWrapper.
- Re-export provideExternalEvmConfig and isExternalEvmConfig.

attachWagmiSync works against any wagmi Config, so EvmConnectionService,
useEthersSigner, and rpcHealthCheckProvider need no changes — they still
read through getEvmConfig() / useEvmStore.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… store

Mirrors the EVM wallet-provider refactor (ab23f35) for TON: orchestration
moves into TonConnectionService, wallet state is observed once via
attachTonSync and stored in a Zustand store, and useTonConnection becomes a
thin React adapter. Old useTONConnection.ts stays as a re-export shim.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…d store

Mirrors the EVM/TON refactors for Tron: orchestration moves into
TronConnectionService, wallet state is synced from the @tronweb3/* react
hooks into a Zustand store via a TronSync hydrator, and useTronConnection
becomes a thin React adapter. Old useTronConnection.ts stays as a re-export
shim.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tand store

Mirrors the EVM refactor for Bitcoin: orchestration moves into
BitcoinConnectionService, wagmi-style state from @bigmi/client is mirrored
into a Zustand store via attachBitcoinSync, and useBitcoinConnection becomes
a thin React adapter. Old useBitcoinConnection.ts stays as a re-export shim;
unused useAccount/useSyncExternalStoreWithTracked utils are removed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…d store

Mirrors the EVM/TON/Tron/Bitcoin refactors for Fuel: orchestration moves into
FuelConnectionService, @fuels/react state (connectors + fuel instance) is
mirrored into a local Zustand store from a thin React hook, and the hook
becomes a thin adapter that wires global useWalletStore deps via configure().
Old useFuelConnection.ts stays as a re-export shim.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… store

Mirrors the EVM refactor for Solana: orchestration moves into
SvmConnectionService, @solana/wallet-adapter-react state is mirrored into a
local Zustand store via an SvmSync hydrator, and useSvmConnection becomes a
thin React adapter. Old useSVMConnection.tsx stays as a re-export shim.

Also adds @types/react to devDependencies on packages that now use zustand,
so pnpm doesn't pick @types/react@18 transitively (which surfaces a React
Context type mismatch in EVM); and adds an explicit ReactElement return type
to TronProvider for the same reason.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…stand store

Mirrors the EVM refactor for Starknet: orchestration moves into
StarknetConnectionService, @starknet-react/core state (connectors +
disconnectAsync) is mirrored into the existing Zustand store via a
StarknetSync hydrator, and useStarknetConnection becomes a thin React
adapter. resolveStarknetWallet moves into the service. Old
useStarknetConnection.ts and starknetWalletStore.ts stay as re-export shims.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ice class

Mirrors the EVM refactor for Paradex: connect/switch/requestAdditional
orchestration moves into ParadexConnectionService. The hook now wires inner
EVM + Starknet providers and the global useWalletStore paradexAccounts deps
into the class via configure(). Old useParadexConnection.ts stays as a
re-export shim.

Paradex doesn't get its own Zustand store — its persisted state already
lives in the widget's global useWalletStore.paradexAccounts, and inner-
provider connector state lives in the EVM/Starknet stores; the service just
composes them.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…stand store

Mirrors the EVM refactor for ImtblPassport. ImtblPassport has no
WalletConnectionProvider hook of its own (connection happens through the EVM
provider via EIP-6963), so the refactor is scoped to the Passport SDK
singleton: ImtblPassportService owns init + connectEvm + loginCallback, and
the ready flag + instance live in a Zustand store. The old module-level
passportInstance export is removed; consumers should use
imtblPassportService.getInstance() or useImtblPassportStore() instead.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the EVM-only default in WidgetWrapper with getDefaultProviders from
@layerswap/wallets, which bundles EVM, Starknet, Fuel, Paradex, Bitcoin, TON,
SVM, Tron, and (when configured) Immutable Passport. WalletConnect, TON, and
Immutable Passport configs come from the existing NEXT_PUBLIC_* env vars;
the TON manifest URL points at the manifest already shipped under
/public/tonconnect-manifest.json. Callers can still pass walletProviders
explicitly to override.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Removes @walletconnect/universal-provider, @walletconnect/types,
@walletconnect/utils, and bs58 — none have any import in src/. Only
@walletconnect/ethereum-provider is still used (in
connectors/resolveConnectors/walletConnect.ts as a type import).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Removed when useAccount/useSyncExternalStoreWithTracked utils were deleted in
the class-based refactor (commit 14db284). No imports remain.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… tweetnacl

None have any import in src/. Likely orphans from earlier Fuel implementations
that no longer apply.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ypto-utils and bignumber.js

No imports in src/. Paradex SDK and starknet handle the crypto/big-number
needs that these were presumably brought in for.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@ton/crypto has no imports in src/ — only @ton/core and @ton/ton are used.
The devDep `ton` (legacy v13) was superseded by @ton/ton (v15) and is also
unimported.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
No axios imports in src/. @imtbl/sdk handles its own HTTP needs internally.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
TronProvider only dynamically imports adapters for MetaMask, TronLink, OKX,
BitKeep, Bybit, and Gate. The foxwallet, imtoken, ledger, tokenpocket, and
trust adapters are declared but never imported anywhere in src/, so they're
just bloating install size.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previous commit (46da1af) dropped @ton/crypto because nothing in
wallet-ton/src imports it directly. But @ton/ton declares
@ton/crypto as a peerDep, so a clean install of the bridge app fails
with "Module not found: Can't resolve '@ton/crypto'" once the
transitively-hoisted copy is gone.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…act/types

None are imported in apps/bridge source. The other "looks unused" deps
(@bigmi/*, wagmi/@wagmi/*, viem, zustand, @tanstack/react-query) are
peerDeps of the wallet packages and must stay.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ternal stores

Reshapes the widget's wallet-provider contract so wallet packages no longer
have to be React hooks. The driver is to make @layerswap/wallet-evm fully
React-free; the contract change cascades to every wallet package.

Widget contract changes
- WalletProvider.walletConnectionProvider (hook) → createConnection: (props)
  => WalletConnectionStore (vanilla subscribe/getSnapshot/updateProps/destroy)
- WalletProvider gains optional `init: (ctx) => () => void` for one-shot
  lifecycle; `wrapper` stays for packages that need an upstream React tree
  (TonConnectUI, StarknetReact, etc.)
- RpcHealthCheckProvider.useRpcHealthCheck (hook) → createStore() external
  store with combined snapshot + imperative ops
- Wallet.icon: (props) => JSX.Element → string | undefined (URL or data URI);
  new WalletIconView falls back to AddressIcon when undefined
- BaseWalletProviderConfig.customHook → customConnection

Widget internals
- New vanilla zustand-style stores: getAdditionalConnectorsStore(ns, projectId)
  and connectModalStore. useAdditionalConnectors / useConnectModal become thin
  useSyncExternalStore adapters
- New createReactHookConnectionAdapter(useFn) helper bridges legacy hooks to
  the external-store contract via a hidden <Hydrator> component
- getKnownConnectorIconBase64(id) / resolveWalletIconString expose string-only
  icon lookup so wallet packages produce Wallet.icon strings without
  importing React

EVM package (now fully React-free)
- Source has zero `from 'react'`, zero JSX, zero .tsx files
- react / react-dom / @types/react removed from peer + dev deps
- useEvmConnection hook → createEvmConnection(props) external store with
  input-memoized snapshots and ref-counted internal subscriptions
- EVMHydrator component → initEvmProvider(opts) one-shot function
- useChainConfigs hook → getEvmChainsConfig(networks) pure function
- useEVMConnectors hook → buildEVMConnectors(...) pure function
- useEthersSigner hook → getEthersSigner({ chainId }) promise
- EVMRpcHealthCheckProvider.useRpcHealthCheck → createStore() vanilla store
- Deprecated EVMProvider constant + useEVMConnection shim removed

Other wallet packages (contract-only migration)
- bitcoin, fuel, starknet, svm, ton, tron, paradex, imtblPassport: each ships
  a thin xxxConnectionAdapter built with createReactHookConnectionAdapter
  whose Hydrator is mounted inside the package's existing wrapper. The
  factories now return createConnection instead of walletConnectionProvider
- Paradex no longer recreates EVM/Starknet provider snapshots itself — reads
  from useWalletProviders() instead
- Deprecated XxxProvider constants removed across all packages

Strict-mode safety
- The EVM connection store ref-counts its upstream subscriptions
  (useEvmStore, additionalConnectorsStore, connectModalStore) so React 18's
  double-mount cleanup doesn't permanently tear down internal listeners

Apps
- widget-playground: useCustomEvm wrapped via createReactHookConnectionAdapter;
  LayerswapWidgetCustomEvm mounts the hydrator inside its WagmiProvider tree
- examples/nextjs-dynamic: same shape for useCustomStarknet
- bridge: getDefaultProviders shape unchanged externally

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…factories across chain packages

Implements the external-store WalletProvider contract from b5ee460 across
bitcoin, fuel, imtblPassport, paradex, starknet, svm, ton, and tron. Drops
React XxxProvider components and useXxxConnection hooks in favor of
createXxxConnection vanilla factories backed by Zustand stores, renames
useXxxTransfer to createXxxTransfer, and updates the widget consumer to
resolve providers through the new registry.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Connectors were always marked type:'injected' with extensionNotFound
keyed off installLink presence, so configured wallets showed "Installed"
yet routed to the not-detected screen, and Web Wallet / Controller were
mislabeled as installed. Now gate both on the connector's available()
check plus configured-extension status.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented May 27, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
layerswapapp Error Error Jun 26, 2026 10:42am

Request Review

babkenmes and others added 5 commits June 1, 2026 20:08
- ConnectorsList: make the connect-click readiness gate and the connector
  chip's isProviderReady stub-aware, so descriptor-backed providers (e.g.
  Immutable Passport) load on click instead of showing "still initializing"
- Fuel resolveFuelWallet: fix chainId operator precedence — `||` bound
  tighter than the ternary, so a truthy network.chainId always yielded 0;
  use `?? (testnet ? 0 : 9889)` so a real chainId is preserved
- Fuel connect: stop wrapping non-Error rejections in `new Error(e as string)`
  (the SDK rejects with `false` → "Error: false"); rethrow Errors as-is and
  stringify other values

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- walletProviders: collapse double useSettingsState() into one subscription
- ConnectWalletButton: key the hover-prefetch ref by provider id so a
  stub→real provider transition re-arms prefetch instead of staying latched
- FuelConnectionService: extract duplicated connector-icon construction into
  a shared resolveConnectorImageIcon helper

Skipped review #5 (cache-invalidation comment already present) and #7 (the
proposed loadById race fix would leak inflight entries on load failure and
mutate a ref inside a setState updater — deferred for a safer approach).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wallet Standard adapters (e.g. MetaMask/Backpack on Solana) can expose
`icon` as raw inline SVG markup instead of the spec-required `data:` URI.
Handed straight to an <img src>, the browser treated the markup as a
relative URL and fired a bogus request to `/<svg ...>`.

Normalize raw SVG/XML markup into a base64 `data:` URI at three layers:
- knownConnectorIcons: new `normalizeIconSrc`, wired into resolveWalletIconString
- resolveWalletIcon: normalize resolveWalletConnectorIcon / walletIconResolver
- ImageWithFallback: render-layer safety net so no producer can leak markup

Route the SVM connector-list path through the resolver instead of passing
`wallet.icon` through raw.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Subscribe svmAdapterManager to the Wallet Standard registry so wallets
that only expose themselves as Standard Wallets (e.g. MetaMask's Solana
support) are wrapped as adapters and surface in the Solana connector
list. Standard adapters win on name collisions; selection restore and
auto-connect retry as wallets register late.

Compute the connector `isMultiChain` flag live from the current connector
set instead of freezing it into an early snapshot, so it flips to true as
late-loading ecosystems (Solana/Tron) populate. Wait (bounded) for
providers to settle before committing a multi-ecosystem wallet to a
single-ecosystem connect, so the ecosystem picker isn't skipped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace boolean init guards with keyed, re-entrant lifecycle management
across wallet packages so reconnects and network switches rebuild cleanly:

- bitcoin: key config singleton by name|node_url; guard async connector
  resolution against dispose; re-key sync on config identity
- fuel: split resolveConnectedWallets (query) from syncConnectedWallets
  (command) to remove side effects from the resolver
- starknet: dispose hydration lifecycle on store destroy
- svm: destroy internally-owned standard adapters on unregister/dispose;
  disconnect other active/connected adapters (not the target) on switch
- ton: wire AbortController + connect-modal cancellation into connect
- widget: memoize themeData, fix settings-fetch effect deps and cancellation

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
yasha-meursault and others added 2 commits June 9, 2026 15:35
…splaying correctly on mobile devices in safari
…-icon-fix

Wallet icon dimensions are broken in safari browser (monorepo), resolves BAB-240
…ontext

Security & correctness:
- EVM: validate active account against connector's tracked accounts list
- Starknet: validate node_url is a well-formed https/wss URL before RpcProvider
- Starknet: move hydration re-entrancy guard to top of attemptHydration
- Paradex: validate persisted paradexAddress format; drop tampered entries
- TON: wrap Address.parse in try/catch, return undefined on malformed input
- Replace silent `// TODO` + console.log catches with console.error across
  EVM, Bitcoin, SVM, TRON, TON, Fuel, Starknet services

Architecture & React:
- Fuel: use scoped fuelStore for connectedWallets instead of global useWalletStore
- resolverContext: move setProviders side effect out of useMemo into useEffect;
  memoize provider arrays; back isInitialized with real post-commit state
- createConnectionStoreFromHook: reset store to EMPTY_PROVIDER on unmount/destroy
- LayerswapProvider: stabilize loadById/loadAll via ref to stop context cascade
- WidgetWrapper: hoist walletConnectConfigs to module scope
- Memoize createXXXProvider calls in playground + nextjs examples

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

3 participants