@wagmi/core
Version:
VanillaJS library for Ethereum
388 lines • 16.2 kB
JavaScript
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