UNPKG

@wagmi/core

Version:

VanillaJS library for Ethereum

388 lines 16.2 kB
import { createStore as createMipd, } from 'mipd'; import { createClient, } from 'viem'; import { persist, subscribeWithSelector } from 'zustand/middleware'; import { createStore } from 'zustand/vanilla'; import { injected } from './connectors/injected.js'; import { createEmitter } from './createEmitter.js'; import { createStorage, getDefaultStorage, } from './createStorage.js'; import { ChainNotConfiguredError } from './errors/config.js'; import { uid } from './utils/uid.js'; import { version } from './version.js'; export function createConfig(parameters) { const { multiInjectedProviderDiscovery = true, storage = createStorage({ storage: getDefaultStorage(), }), syncConnectedChain = true, ssr = false, ...rest } = parameters; ///////////////////////////////////////////////////////////////////////////////////////////////// // Set up connectors, clients, etc. ///////////////////////////////////////////////////////////////////////////////////////////////// const mipd = typeof window !== 'undefined' && multiInjectedProviderDiscovery ? createMipd() : undefined; const chains = createStore(() => rest.chains); const connectors = createStore(() => { const collection = []; const rdnsSet = new Set(); for (const connectorFns of rest.connectors ?? []) { const connector = setup(connectorFns); collection.push(connector); if (!ssr && connector.rdns) { const rdnsValues = typeof connector.rdns === 'string' ? [connector.rdns] : connector.rdns; for (const rdns of rdnsValues) { rdnsSet.add(rdns); } } } if (!ssr && mipd) { const providers = mipd.getProviders(); for (const provider of providers) { if (rdnsSet.has(provider.info.rdns)) continue; collection.push(setup(providerDetailToConnector(provider))); } } return collection; }); function setup(connectorFn) { // Set up emitter with uid and add to connector so they are "linked" together. const emitter = createEmitter(uid()); const connector = { ...connectorFn({ emitter, chains: chains.getState(), storage, transports: rest.transports, }), emitter, uid: emitter.uid, }; // Start listening for `connect` events on connector setup // This allows connectors to "connect" themselves without user interaction (e.g. MetaMask's "Manually connect to current site") emitter.on('connect', connect); connector.setup?.(); return connector; } function providerDetailToConnector(providerDetail) { const { info } = providerDetail; const provider = providerDetail.provider; return injected({ target: { ...info, id: info.rdns, provider } }); } const clients = new Map(); function getClient(config = {}) { const chainId = config.chainId ?? store.getState().chainId; const chain = chains.getState().find((x) => x.id === chainId); // chainId specified and not configured if (config.chainId && !chain) throw new ChainNotConfiguredError(); { const client = clients.get(store.getState().chainId); if (client && !chain) return client; if (!chain) throw new ChainNotConfiguredError(); } // If a memoized client exists for a chain id, use that. { const client = clients.get(chainId); if (client) return client; } let client; if (rest.client) client = rest.client({ chain }); else { const chainId = chain.id; const chainIds = chains.getState().map((x) => x.id); // Grab all properties off `rest` and resolve for use in `createClient` const properties = {}; const entries = Object.entries(rest); for (const [key, value] of entries) { if (key === 'chains' || key === 'client' || key === 'connectors' || key === 'transports') continue; if (typeof value === 'object') { // check if value is chainId-specific since some values can be objects // e.g. { batch: { multicall: { batchSize: 1024 } } } if (chainId in value) properties[key] = value[chainId]; else { // check if value is chainId-specific, but does not have value for current chainId const hasChainSpecificValue = chainIds.some((x) => x in value); if (hasChainSpecificValue) continue; properties[key] = value; } } else properties[key] = value; } client = createClient({ ...properties, chain, batch: properties.batch ?? { multicall: true }, transport: (parameters) => rest.transports[chainId]({ ...parameters, connectors }), }); } clients.set(chainId, client); return client; } ///////////////////////////////////////////////////////////////////////////////////////////////// // Create store ///////////////////////////////////////////////////////////////////////////////////////////////// function getInitialState() { return { chainId: chains.getState()[0].id, connections: new Map(), current: null, status: 'disconnected', }; } let currentVersion; const prefix = '0.0.0-canary-'; if (version.startsWith(prefix)) currentVersion = Number.parseInt(version.replace(prefix, '')); // use package major version to version store else currentVersion = Number.parseInt(version.split('.')[0] ?? '0'); const store = createStore(subscribeWithSelector( // only use persist middleware if storage exists storage ? persist(getInitialState, { migrate(persistedState, version) { if (version === currentVersion) return persistedState; const initialState = getInitialState(); const chainId = validatePersistedChainId(persistedState, initialState.chainId); return { ...initialState, chainId }; }, name: 'store', partialize(state) { // Only persist "critical" store properties to preserve storage size. return { connections: { __type: 'Map', value: Array.from(state.connections.entries()).map(([key, connection]) => { const { id, name, type, uid } = connection.connector; const connector = { id, name, type, uid }; return [key, { ...connection, connector }]; }), }, chainId: state.chainId, current: state.current, }; }, merge(persistedState, currentState) { // `status` should not be persisted as it messes with reconnection if (typeof persistedState === 'object' && persistedState && 'status' in persistedState) delete persistedState.status; // Make sure persisted `chainId` is valid const chainId = validatePersistedChainId(persistedState, currentState.chainId); return { ...currentState, ...persistedState, chainId, }; }, skipHydration: ssr, storage: storage, version: currentVersion, }) : getInitialState)); store.setState(getInitialState()); function validatePersistedChainId(persistedState, defaultChainId) { return persistedState && typeof persistedState === 'object' && 'chainId' in persistedState && typeof persistedState.chainId === 'number' && chains.getState().some((x) => x.id === persistedState.chainId) ? persistedState.chainId : defaultChainId; } ///////////////////////////////////////////////////////////////////////////////////////////////// // Subscribe to changes ///////////////////////////////////////////////////////////////////////////////////////////////// // Update default chain when connector chain changes if (syncConnectedChain) store.subscribe(({ connections, current }) => current ? connections.get(current)?.chainId : undefined, (chainId) => { // If chain is not configured, then don't switch over to it. const isChainConfigured = chains .getState() .some((x) => x.id === chainId); if (!isChainConfigured) return; return store.setState((x) => ({ ...x, chainId: chainId ?? x.chainId, })); }); // EIP-6963 subscribe for new wallet providers mipd?.subscribe((providerDetails) => { const connectorIdSet = new Set(); const connectorRdnsSet = new Set(); for (const connector of connectors.getState()) { connectorIdSet.add(connector.id); if (connector.rdns) { const rdnsValues = typeof connector.rdns === 'string' ? [connector.rdns] : connector.rdns; for (const rdns of rdnsValues) { connectorRdnsSet.add(rdns); } } } const newConnectors = []; for (const providerDetail of providerDetails) { if (connectorRdnsSet.has(providerDetail.info.rdns)) continue; const connector = setup(providerDetailToConnector(providerDetail)); if (connectorIdSet.has(connector.id)) continue; newConnectors.push(connector); } if (storage && !store.persist.hasHydrated()) return; connectors.setState((x) => [...x, ...newConnectors], true); }); ///////////////////////////////////////////////////////////////////////////////////////////////// // Emitter listeners ///////////////////////////////////////////////////////////////////////////////////////////////// function change(data) { store.setState((x) => { const connection = x.connections.get(data.uid); if (!connection) return x; return { ...x, connections: new Map(x.connections).set(data.uid, { accounts: data.accounts ?? connection.accounts, chainId: data.chainId ?? connection.chainId, connector: connection.connector, }), }; }); } function connect(data) { // Disable handling if reconnecting/connecting if (store.getState().status === 'connecting' || store.getState().status === 'reconnecting') return; store.setState((x) => { const connector = connectors.getState().find((x) => x.uid === data.uid); if (!connector) return x; if (connector.emitter.listenerCount('connect')) connector.emitter.off('connect', change); if (!connector.emitter.listenerCount('change')) connector.emitter.on('change', change); if (!connector.emitter.listenerCount('disconnect')) connector.emitter.on('disconnect', disconnect); return { ...x, connections: new Map(x.connections).set(data.uid, { accounts: data.accounts, chainId: data.chainId, connector: connector, }), current: data.uid, status: 'connected', }; }); } function disconnect(data) { store.setState((x) => { const connection = x.connections.get(data.uid); if (connection) { const connector = connection.connector; if (connector.emitter.listenerCount('change')) connection.connector.emitter.off('change', change); if (connector.emitter.listenerCount('disconnect')) connection.connector.emitter.off('disconnect', disconnect); if (!connector.emitter.listenerCount('connect')) connection.connector.emitter.on('connect', connect); } x.connections.delete(data.uid); if (x.connections.size === 0) return { ...x, connections: new Map(), current: null, status: 'disconnected', }; const nextConnection = x.connections.values().next().value; return { ...x, connections: new Map(x.connections), current: nextConnection.connector.uid, }; }); } return { get chains() { return chains.getState(); }, get connectors() { return connectors.getState(); }, storage, getClient, get state() { return store.getState(); }, setState(value) { let newState; if (typeof value === 'function') newState = value(store.getState()); else newState = value; // Reset state if it got set to something not matching the base state const initialState = getInitialState(); if (typeof newState !== 'object') newState = initialState; const isCorrupt = Object.keys(initialState).some((x) => !(x in newState)); if (isCorrupt) newState = initialState; store.setState(newState, true); }, subscribe(selector, listener, options) { return store.subscribe(selector, listener, options ? { ...options, fireImmediately: options.emitImmediately, // Workaround cast since Zustand does not support `'exactOptionalPropertyTypes'` } : undefined); }, _internal: { mipd, store, ssr: Boolean(ssr), syncConnectedChain, transports: rest.transports, chains: { setState(value) { const nextChains = (typeof value === 'function' ? value(chains.getState()) : value); if (nextChains.length === 0) return; return chains.setState(nextChains, true); }, subscribe(listener) { return chains.subscribe(listener); }, }, connectors: { providerDetailToConnector, setup: setup, setState(value) { return connectors.setState(typeof value === 'function' ? value(connectors.getState()) : value, true); }, subscribe(listener) { return connectors.subscribe(listener); }, }, events: { change, connect, disconnect }, }, }; } //# sourceMappingURL=createConfig.js.map