๐ 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