@bigmi/client
Version:
Reactive primitives for Bitcoin apps.
369 lines • 14.6 kB
JavaScript
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