Skip to content

Instantly share code, notes, and snippets.

@kelsos
Created February 18, 2026 10:17
Show Gist options
  • Select an option

  • Save kelsos/d624a8c7f60164b8ab3e82890649acdf to your computer and use it in GitHub Desktop.

Select an option

Save kelsos/d624a8c7f60164b8ab3e82890649acdf to your computer and use it in GitHub Desktop.
Detailed implementation plan: Lazy-load WalletConnect and local bridge backends

Lazy-Load Wallet Backends (WalletConnect + Local Bridge)

Context

The useWalletStore Pinia store eagerly imports both wallet backends at module level:

  • WalletConnect (use-wallet-connect.ts): imports @reown/appkit + ethers — ~2 MB
  • Local Bridge (use-injected-wallet.ts): imports ethers + cross-imports supportedNetworks from WC — ~300 KB
  • wallet-constants.ts: imports formatUnits from ethers — pulls ethers into a constants file

The store is on the startup path via use-logout.ts and account.ts, both of which only use disconnect. This means ~2.5 MB of wallet libraries load on every page visit, even though most users never use the wallet feature during a session.

Goal: Make the store a lightweight facade. Neither WalletConnect nor the local bridge should load until the user explicitly connects a wallet.


Dependency Analysis

Heavy (must be lazy-loaded):

  • use-wallet-connect.ts@reown/appkit-adapter-ethers, @reown/appkit/networks, @reown/appkit/vue, ethers
  • use-injected-wallet.tsethers (BrowserProvider, getAddress), supportedNetworks from WC module

Lightweight (stay eager in the store):

  • useUnifiedProviders — no ethers, EIP-6963 provider discovery
  • useWalletProxy — no ethers, Electron IPC bridge
  • useTransactionManager — only type imports from ethers
  • useTradeApi — API calls only
  • transaction-helpers.ts — pure validation, no ethers
  • useWalletHelper — chain ID conversions (currently pulls in WC for EIP155 string — fix below)

Changes

1. wallet-constants.ts — Remove ethers import

File: frontend/app/src/modules/onchain/wallet-constants.ts

  • Add EIP155 constant (move from use-wallet-connect.ts):

    export const EIP155 = 'eip155';
  • Add SUPPORTED_WALLET_CHAIN_IDS (mirrors supportedNetworks chain IDs — mainnet, base, arbitrum, optimism, bsc, gnosis, polygon, scroll):

    export const SUPPORTED_WALLET_CHAIN_IDS = [1, 8453, 42161, 10, 56, 100, 137, 534352] as const;
  • Replace formatUnits from ethers with a native formatWei helper. The function only formats wei (18 decimals) to a decimal string — pure bigint arithmetic:

    function formatWei(value: bigint): string {
      const DIVISOR = 10n ** 18n;
      const integerPart = value / DIVISOR;
      const remainder = value % DIVISOR;
      if (remainder === 0n)
        return integerPart.toString();
      const fractionalPart = remainder.toString().padStart(18, '0').replace(/0+$/, '');
      return `${integerPart}.${fractionalPart}`;
    }

    Update calculateGasFee to use formatWei instead of formatUnits. Remove the ethers import.

2. use-wallet-helper.ts — Fix EIP155 import

File: frontend/app/src/modules/onchain/use-wallet-helper.ts

  • Change: import { EIP155 } from './wallet-connect/use-wallet-connect'
  • To: import { EIP155 } from './wallet-constants'

This breaks the transitive dependency on the WC module.

3. use-wallet-connect.ts — Remove EIP155 export

File: frontend/app/src/modules/onchain/wallet-connect/use-wallet-connect.ts

  • Remove export const EIP155 = 'eip155' (now in wallet-constants.ts)
  • Import EIP155 from ../wallet-constants for internal use
  • supportedNetworks stays exported (consumed via dynamic import)

4. use-injected-wallet.ts — Remove static WC import

File: frontend/app/src/modules/onchain/wallet-bridge/use-injected-wallet.ts

  • Remove: import { supportedNetworks } from '../wallet-connect/use-wallet-connect' (line 8)
  • In switchNetwork() error-4902 handler (line 256), use dynamic import:
    if (error.code === 4902) {
      const { supportedNetworks } = await import('../wallet-connect/use-wallet-connect');
      const network = supportedNetworks.find(item => BigInt(item.id) === chainId);
      // ... rest unchanged
    }
    This path only fires when a chain needs to be added to the wallet — a rare edge case.

After this change, use-injected-wallet.ts only imports ethers as its heavy dependency. It no longer pulls in @reown/appkit.

5. use-wallet-store.ts — Facade with lazy backends (core change)

File: frontend/app/src/modules/onchain/use-wallet-store.ts

Remove static imports

- import { useInjectedWallet } from './wallet-bridge/use-injected-wallet';
- import { supportedNetworks, useWalletConnect } from './wallet-connect/use-wallet-connect';
+ import { SUPPORTED_WALLET_CHAIN_IDS } from './wallet-constants';

Remove eager initialization

- const walletConnect = useWalletConnect();
- const injectedWallet = useInjectedWallet();

Add lazy backend loaders

// Lazy backend instances — loaded on first use
type WalletConnectInstance = ReturnType<typeof import('./wallet-connect/use-wallet-connect').useWalletConnect>;
type InjectedWalletInstance = ReturnType<typeof import('./wallet-bridge/use-injected-wallet').useInjectedWallet>;

let walletConnectInstance: WalletConnectInstance | undefined;
let injectedWalletInstance: InjectedWalletInstance | undefined;

// Local ref to mirror injectedWallet.isConnecting (since injected wallet may not be loaded)
const isConnecting = ref<boolean>(false);

async function getWalletConnect(): Promise<WalletConnectInstance> {
  if (!walletConnectInstance) {
    const { useWalletConnect } = await import('./wallet-connect/use-wallet-connect');
    walletConnectInstance = useWalletConnect();
    // Set up state sync watcher (moved from eager watcher)
    watch(
      [walletConnectInstance.connected, walletConnectInstance.connectedAddress,
       walletConnectInstance.connectedChainId, walletConnectInstance.supportedChainIds],
      () => {
        if (get(walletMode) === WALLET_MODES.WALLET_CONNECT)
          syncWalletState();
      },
    );
  }
  return walletConnectInstance;
}

async function getInjectedWallet(): Promise<InjectedWalletInstance> {
  if (!injectedWalletInstance) {
    const { useInjectedWallet } = await import('./wallet-bridge/use-injected-wallet');
    injectedWalletInstance = useInjectedWallet();
    // Mirror isConnecting into local ref
    watch(injectedWalletInstance.isConnecting, (v) => { set(isConnecting, v); });
    // Set up state sync watcher (moved from eager watcher)
    watch(
      [injectedWalletInstance.connected, injectedWalletInstance.connectedAddress,
       injectedWalletInstance.connectedChainId],
      () => {
        if (get(walletMode) === WALLET_MODES.LOCAL_BRIDGE)
          syncWalletState();
      },
    );
  }
  return injectedWalletInstance;
}

Update syncWalletState() — nil-safe

const syncWalletState = (): void => {
  if (get(walletMode) === WALLET_MODES.WALLET_CONNECT) {
    if (!walletConnectInstance) return; // Not loaded yet — nothing to sync
    set(connected, get(walletConnectInstance.connected));
    set(connectedAddress, get(walletConnectInstance.connectedAddress));
    set(connectedChainId, get(walletConnectInstance.connectedChainId));
    set(supportedChainIds, get(walletConnectInstance.supportedChainIds));
  }
  else {
    if (!injectedWalletInstance) return; // Not loaded yet — nothing to sync
    set(connected, get(injectedWalletInstance.connected));
    set(connectedAddress, get(injectedWalletInstance.connectedAddress));
    set(connectedChainId, get(injectedWalletInstance.connectedChainId));
    set(supportedChainIds, []);
  }
};

Update supportedChainsIdForConnectedAccount

// Before: supportedNetworks.map(network => Number(network.id))
// After:
const supportedChainsIdForConnectedAccount = computed<number[]>(() => {
  const chainIds = get(supportedChainIds);
  if (chainIds.length === 0 || get(walletMode) === WALLET_MODES.LOCAL_BRIDGE) {
    return [...SUPPORTED_WALLET_CHAIN_IDS];
  }
  return chainIds.map(item => getChainIdFromNamespace(item));
});

Update getBrowserProvider() — sync, asserts backend loaded

const getBrowserProvider = (): BrowserProvider => {
  if (get(walletMode) === WALLET_MODES.LOCAL_BRIDGE) {
    assert(injectedWalletInstance, 'Injected wallet not initialized');
    return injectedWalletInstance.getBrowserProvider();
  }
  assert(walletConnectInstance, 'WalletConnect not initialized');
  return walletConnectInstance.getBrowserProvider();
};

This is safe: getBrowserProvider is only called after connect() succeeds, which always loads the backend first.

Update connect() — loads backend on demand

const connect = async (): Promise<void> => {
  if (get(walletMode) === WALLET_MODES.LOCAL_BRIDGE) {
    try {
      if (get(isPackaged)) {
        await walletProxy.setupProxy();
      }
      const providerSelected = await unifiedProviders.checkIfSelectedProvider();
      const iw = await getInjectedWallet(); // <-- lazy load
      if (!providerSelected) {
        await unifiedProviders.detectProviders();
        const providers = get(unifiedProviders.availableProviders);
        if (providers.length === 0) {
          throw new Error(WALLET_ERRORS.NO_PROVIDERS);
        }
        else if (providers.length === 1) {
          await unifiedProviders.selectProvider(providers[0].info.uuid);
          await iw.connectToSelectedProvider();
        }
        else {
          set(unifiedProviders.showProviderSelection, true);
        }
      }
      else {
        await iw.connectToSelectedProvider();
      }
    }
    catch (error) {
      logger.error(WALLET_ERRORS.CONNECTION_FAILED, error);
      throw error;
    }
  }
  else {
    const wc = await getWalletConnect(); // <-- lazy load
    await wc.connect();
  }
};

Update disconnect() — nil-safe

const disconnect = async (): Promise<void> => {
  set(isDisconnecting, true);
  try {
    if (get(walletMode) === WALLET_MODES.LOCAL_BRIDGE) {
      if (injectedWalletInstance) {
        await injectedWalletInstance.disconnect();
      }
      unifiedProviders.clearProvider();
    }
    else {
      if (walletConnectInstance) {
        await walletConnectInstance.disconnect();
      }
    }
    resetState();
  }
  finally {
    set(isDisconnecting, false);
  }
};

If the backend was never loaded (user never connected), disconnect() is a no-op — correct behavior.

Update switchNetwork()

const switchNetwork = async (chainId: bigint): Promise<void> => {
  if (get(walletMode) === WALLET_MODES.LOCAL_BRIDGE) {
    const iw = await getInjectedWallet();
    await iw.switchNetwork(chainId);
  }
  else {
    const wc = await getWalletConnect();
    await wc.switchNetwork(chainId);
  }
};

Update sendTransaction() — WC check

// In sendTransaction, replace:
//   await walletConnect.checkWalletConnection();
// With:
if (get(walletMode) === WALLET_MODES.WALLET_CONNECT) {
  const wc = await getWalletConnect();
  await wc.checkWalletConnection();
}

Update preparing return value

// Before: preparing: logicOr(preparing, injectedWallet.isConnecting)
// After:
preparing: logicOr(preparing, isConnecting),

Remove eager watchers

Remove the two watcher blocks at lines 268-279 (WC state sync and injected state sync). These are now set up lazily inside getWalletConnect() and getInjectedWallet().

Keep the watch(walletMode, ...) watcher (lines 259-265) — it calls disconnect() and syncWalletState() which are both nil-safe now.


Files Modified (summary)

File Change
wallet-constants.ts Add EIP155, SUPPORTED_WALLET_CHAIN_IDS, formatWei; remove ethers import
use-wallet-helper.ts Import EIP155 from wallet-constants instead of WC module
use-wallet-connect.ts Remove EIP155 export, import from wallet-constants
use-injected-wallet.ts Dynamic import for supportedNetworks in error-4902 path
use-wallet-store.ts Lazy backends, nil-safe sync/disconnect, local isConnecting ref

No changes needed to account.ts or use-logout.ts — they only use disconnect, and the store itself is now lightweight.


Verification

  1. Build: cd frontend && pnpm run build — verify wallet-connect/ethers chunks are no longer in the initial dependency graph (they become async chunks)
  2. Type check: cd frontend && pnpm run typecheck
  3. Unit tests: cd frontend && pnpm run test:unit
  4. Manual dev test (cd frontend && pnpm run dev:web):
    • App loads without wallet chunks in Network tab
    • Navigate to send/trade page → wallet chunks load on demand
    • WalletConnect flow: connect, switch network, send tx, disconnect
    • Local bridge flow: connect, switch network, send tx, disconnect
    • Logout works without errors (disconnect is nil-safe)
    • Mode switching between WC and local bridge works
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment