UNPKG

@bigmi/client

Version:

Reactive primitives for Bitcoin apps.

369 lines 14.6 kB
import { ChainNotConfiguredError, createClient, uid, version, } from '@bigmi/core'; import { persist, subscribeWithSelector } from 'zustand/middleware'; import { createStore } from 'zustand/vanilla'; import { createEmitter } from './createEmitter.js'; import { createStorage, getDefaultStorage } from './createStorage.js'; export function createConfig(parameters) { const { multiInjectedProviderDiscovery = true, storage = createStorage({ key: 'bigmi', storage: getDefaultStorage(), }), syncConnectedChain = true, ssr = false, ...rest } = parameters; ///////////////////////////////////////////////////////////////////////////////////////////////// // Set up connectors, clients, etc. ///////////////////////////////////////////////////////////////////////////////////////////////// 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); } } } 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; } 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, 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) { persistedState.status = undefined; } // 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, })); }); } ///////////////////////////////////////////////////////////////////////////////////////////////// // 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: { 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: { 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