๐Ÿ“„ src/web3/vue/bootstrap.ts
D-OPEN SOVEREIGN

Documentation & Insights

Single init entry-point (spec ยง13.1). Replaces bootstrapCrypto() +
registry.load() + the manual per-wallet discovery loop previously spread
across main.ts / boot-secondary.ts.
1import { EVM_CHAINS, type EvmChainConfig } from '../network/chains';
2import { MetaMaskConnector } from '../wallet/MetaMaskConnector';
3import { PrivateKeyConnector } from '../wallet/PrivateKeyConnector';
4import { registerConnector, getRegisteredConnectors } from '../wallet/registry';
5import type { WalletConnector } from '../client/types';
6import type { Web3SessionAccount } from '../session/types';
7import { sessionStore } from '../session/store';
8import { syncAccount } from '../session/sync';
9import { addressLoader } from '../registry/loader';
10
11export interface BootstrapOptions {
12  environment?: 'production' | 'staging';
13  withTest?: boolean;
14  chainFilter?: number[];
15}
16
17export interface ChainConnectors {
18  chain: EvmChainConfig;
19  installed: WalletConnector[];
20  installable: WalletConnector[];
21}
22
23export interface BootstrapResult {
24  primaryAccount: Web3SessionAccount | null;
25
26  chains: EvmChainConfig[];
27  connectorsByChain: Record<number, ChainConnectors>;
28
29  installedConnectors: WalletConnector[];
30  installableConnectors: WalletConnector[];
31
32  ready: Promise<void>;
33}
34
35interface ChainBootstrapState {
36  chain: EvmChainConfig;
37  connectors: WalletConnector[];
38  ready: Promise<void>;
39}
40
41// Module-level accumulator โ€” bootstrapWebSDK() is idempotent (spec ยง13.1 / ยง11 Q9):
42// chains already bootstrapped are skipped entirely (no re-instantiation of
43// connectors, no re-sync); only newly-requested chains are initialized. The
44// returned BootstrapResult always reflects the full accumulated state.
45const bootstrapState = new Map<number, ChainBootstrapState>();
46let connectorsInitialized = false;
47let sessionLoaded = false;
48
49function detectEnvironment(): 'production' | 'staging' {
50  try {
51    // import.meta.env is bundler-injected (Vite); absent under vitest/node.
52    return (import.meta as any)?.env?.MODE === 'staging' ? 'staging' : 'production';
53  } catch {
54    return 'production';
55  }
56}
57
58function ensureConnectors(): WalletConnector[] {
59  if (!connectorsInitialized) {
60    registerConnector(new MetaMaskConnector());
61    registerConnector(new PrivateKeyConnector());
62    connectorsInitialized = true;
63  }
64  return getRegisteredConnectors();
65}
66
67function selectChainIds(options?: BootstrapOptions): number[] {
68  if (options?.chainFilter?.length) return options.chainFilter;
69  const withTest = options?.withTest ?? true;
70  return Object.values(EVM_CHAINS)
71    .filter((c) => withTest || c.main)
72    .map((c) => c.chainId);
73}
74
75async function bootstrapChain(
76  chainId: number,
77  environment: 'production' | 'staging',
78  connectors: WalletConnector[]
79): Promise<void> {
80  if (bootstrapState.has(chainId)) return;
81
82  const chain = EVM_CHAINS[chainId];
83  if (!chain) return;
84
85  const chainConnectors = connectors.filter((c) => c.isAvailable());
86
87  const ready = (async () => {
88    for (const connector of chainConnectors) {
89      try {
90        const addresses = await connector.getAccounts();
91        for (const address of addresses) {
92          await syncAccount(address, chainId, connector.type, environment);
93        }
94      } catch (e) {
95        console.warn(`[bootstrapWebSDK] account discovery failed for ${connector.type} on chain ${chainId}`, e);
96      }
97    }
98  })();
99
100  bootstrapState.set(chainId, { chain, connectors: chainConnectors, ready });
101  await ready;
102}
103
104function buildResult(): BootstrapResult {
105  const chains: EvmChainConfig[] = [];
106  const connectorsByChain: Record<number, ChainConnectors> = {};
107  const installedConnectors = new Set<WalletConnector>();
108  const installableConnectors = new Set<WalletConnector>();
109
110  for (const [chainId, state] of bootstrapState.entries()) {
111    chains.push(state.chain);
112    const installed = state.connectors.filter((c) => c.isAvailable());
113    const installable = state.connectors.filter((c) => !c.isAvailable());
114    connectorsByChain[chainId] = { chain: state.chain, installed, installable };
115    installed.forEach((c) => installedConnectors.add(c));
116    installable.forEach((c) => installableConnectors.add(c));
117  }
118
119  const primaryAccount = sessionStore.getState().known.find((a) => a.hasAccount) ?? null;
120
121  return {
122    primaryAccount,
123    chains,
124    connectorsByChain,
125    installedConnectors: Array.from(installedConnectors),
126    installableConnectors: Array.from(installableConnectors),
127    ready: Promise.all(Array.from(bootstrapState.values()).map((s) => s.ready)).then(() => undefined),
128  };
129}
130
131/**
132 * Single init entry-point (spec ยง13.1). Replaces bootstrapCrypto() +
133 * registry.load() + the manual per-wallet discovery loop previously spread
134 * across main.ts / boot-secondary.ts.
135 */
136export async function bootstrapWebSDK(options?: BootstrapOptions): Promise<BootstrapResult> {
137  const environment = options?.environment ?? detectEnvironment();
138  const connectors = ensureConnectors();
139
140  // Warm the registry manifest (addresses + ABIs) up front so the first
141  // EvmClient constructed after boot doesn't hit a cold fetch. loadManifest()
142  // memoizes per environment, so repeat bootstrapWebSDK() calls are no-ops here.
143  addressLoader.loadManifest(environment).catch((e) =>
144    console.warn(`[bootstrapWebSDK] registry manifest warm-up failed for ${environment}:`, e)
145  );
146
147  if (!sessionLoaded) {
148    sessionStore.load();
149    sessionLoaded = true;
150  }
151
152  const chainIds = selectChainIds(options);
153  const newChainIds = chainIds.filter((id) => !bootstrapState.has(id));
154
155  await Promise.all(newChainIds.map((id) => bootstrapChain(id, environment, connectors)));
156
157  return buildResult();
158}
159