accounts
Version:
Tempo Accounts SDK
902 lines • 59.6 kB
JavaScript
import { announceProvider } from 'mipd';
import { Mppx, tempo as mppx_tempo } from 'mppx/client';
import { Address, Hash, Hex, Json, Provider as ox_Provider, RpcResponse } from 'ox';
import { KeyAuthorization } from 'ox/tempo';
import { http, parseUnits } from 'viem';
import { tempo, tempoDevnet, tempoModerato } from 'viem/chains';
import { parseSiweMessage } from 'viem/siwe';
import { Actions } from 'viem/tempo';
import * as z from 'zod/mini';
import * as AccessKey from './AccessKey.js';
import * as Account from './Account.js';
import { dialog } from './adapters/dialog.js';
import * as Client from './Client.js';
import { withDedupe } from './internal/withDedupe.js';
import * as Schema from './Schema.js';
import * as Storage from './Storage.js';
import * as Store from './Store.js';
import * as Tokenlist from './Tokenlist.js';
import * as Request from './zod/request.js';
import * as Rpc from './zod/rpc.js';
const announced = new Set();
/**
* Creates an EIP-1193 provider with a pluggable adapter.
*
* @example
* ```ts
* import { Provider } from 'accounts'
*
* const provider = Provider.create()
* ```
*/
export function create(options = {}) {
const { adapter = dialog(), chains = [tempo, tempoModerato, tempoDevnet], maxAccounts, persistCredentials, relay, testnet, storage = typeof window !== 'undefined' ? Storage.idb() : Storage.memory(), } = options;
// Build per-chain transports from `relay` (if set), then layer caller-provided
// `transports` on top so explicit per-chain overrides win.
const transports = (() => {
if (!relay && !options.transports)
return undefined;
const base = relay
? Object.fromEntries(chains.map((c) => [c.id, http(`${relay.replace(/\/$/, '')}/${c.id}`)]))
: {};
return { ...base, ...options.transports };
})();
const feePayerConfig = (() => {
if (!options.feePayer)
return undefined;
if (typeof options.feePayer === 'string')
return { precedence: 'fee-payer-first', url: options.feePayer };
return {
precedence: options.feePayer.precedence ?? 'fee-payer-first',
url: options.feePayer.url,
};
})();
const defaultChain = testnet
? (chains.find((c) => c.testnet) ?? chains[chains.length - 1])
: chains[0];
const store = Store.create({
chainId: defaultChain.id,
maxAccounts,
persistCredentials,
storage,
});
const getAccount = (options = {}) => Account.find({ ...options, store });
// Lazy reference — assigned after the provider is created so the client
// transport can route provider methods (wallet_connect, etc.) through it.
let providerRef;
function getClient(options = {}) {
const { chainId, feePayer } = options;
return Client.fromChainId(chainId, {
chains,
feePayer: (() => {
if (feePayer === false)
return false;
if (feePayer)
return { url: feePayer, precedence: feePayerConfig?.precedence };
return undefined;
})(),
store,
transports,
});
}
const instance = adapter({ getAccount, getClient, storage, store });
const { actions } = instance;
const emitter = ox_Provider.createEmitter();
// Emit EIP-1193 events on state changes.
store.subscribe((state) => state.accounts.map((a) => a.address).join(), () => emitter.emit('accountsChanged', store.getState().accounts.map((a) => a.address)));
store.subscribe((state) => state.chainId, (chainId) => emitter.emit('chainChanged', Hex.fromNumber(chainId)));
store.subscribe((state) => state.accounts.length > 0, (connected) => {
if (connected)
emitter.emit('connect', { chainId: Hex.fromNumber(store.getState().chainId) });
else
emitter.emit('disconnect', new ox_Provider.DisconnectedError());
});
/** Throws `DisconnectedError` if no accounts are connected. */
function assertConnected() {
if (store.getState().accounts.length === 0)
throw new ox_Provider.DisconnectedError({ message: 'No accounts connected.' });
}
/** Returns connected account addresses with the active account first. */
function getAccountAddresses() {
const { accounts, activeAccount } = store.getState();
if (accounts.length === 0)
return [];
const active = accounts[activeAccount]?.address;
const activeIdx = accounts.findIndex((a) => a.address === active);
const sorted = [...accounts];
if (activeIdx >= 0) {
const [account] = sorted.splice(activeIdx, 1);
return [account.address, ...sorted.map((a) => a.address)];
}
return sorted.map((a) => a.address);
}
/** Returns accounts to persist. When `persistAccounts` is set, merges new accounts with existing ones. */
function resolveAccounts(accounts) {
if (!instance.persistAccounts)
return accounts;
const merged = [...accounts];
for (const a of store.getState().accounts)
if (!merged.some((m) => m.address.toLowerCase() === a.address.toLowerCase()))
merged.push(a);
return merged;
}
/** Resolves the `feePayer` field from a transaction request into an absolute URL string or `undefined`. */
function resolveFeePayer(feePayer) {
if (feePayer === false)
return false;
const url = (() => {
if (typeof feePayer === 'string')
return feePayer;
return feePayerConfig?.url;
})();
if (!url)
return undefined;
if (url.startsWith('http://') || url.startsWith('https://'))
return url;
if (typeof window !== 'undefined')
return new URL(url, window.location.origin).href;
return url;
}
const provider = Object.assign(ox_Provider.from({
...emitter,
async request({ method, params }) {
await Store.waitForHydration(store);
const shouldDedupe = [
'eth_accounts',
'eth_chainId',
'eth_requestAccounts',
'wallet_connect',
'wallet_getBalances',
'wallet_getCapabilities',
].includes(method);
return withDedupe(async () => {
// Validate known methods. Unknown methods fall through to the RPC proxy.
let request;
try {
request = Request.validate(Schema.Request, { method, params });
}
catch (e) {
if (!(e instanceof ox_Provider.UnsupportedMethodError))
throw e;
// Proxy unknown methods to the RPC node.
return await Client.fromChainId(undefined, { chains, store, transports }).request({
method: method,
params: params,
});
}
const result = await (async () => {
switch (request.method) {
case 'eth_accounts':
return getAccountAddresses();
case 'eth_chainId':
return Hex.fromNumber(store.getState().chainId);
case 'eth_requestAccounts': {
const existing = getAccountAddresses();
if (existing.length > 0)
return existing;
const { accounts } = await actions.loadAccounts(undefined, {
method: 'wallet_connect',
params: undefined,
});
store.setState({ accounts: resolveAccounts(accounts), activeAccount: 0 });
return accounts.map((a) => a.address);
}
case 'eth_sendTransaction': {
assertConnected();
const [decoded] = request._decoded.params;
const { to, data, ...rest } = decoded;
const calls = decoded.calls ?? (to ? [{ to, data, value: decoded.value }] : undefined);
const state = store.getState();
return (await actions.sendTransaction({
...rest,
chainId: decoded.chainId ?? state.chainId,
from: decoded.from ?? state.accounts[state.activeAccount]?.address,
...(calls ? { calls } : {}),
feePayer: resolveFeePayer(decoded.feePayer),
}, request));
}
case 'eth_fillTransaction': {
const [decoded] = request._decoded.params;
const parameters = { ...decoded };
const chainId = parameters.chainId;
const feePayer = resolveFeePayer(parameters.feePayer);
const fill = (params) => {
const client = getClient({ chainId, feePayer });
const fillRequest = {
...params,
chainId: params.chainId ?? client.chain?.id,
...(feePayer ? { feePayer: true } : {}),
};
const formatter = client.chain?.formatters?.transactionRequest;
const formatted = formatter && !fillRequest.keyAuthorization
? formatter.format({ ...fillRequest }, 'fillTransaction')
: fillRequest;
return client.request({
method: 'eth_fillTransaction',
params: [formatted],
});
};
// Inject pending keyAuthorization so the node accounts for
// key authorization gas during estimation.
if (!parameters.keyAuthorization) {
const account = (() => {
try {
const calls = parameters.calls ??
(parameters.to
? [
{
data: parameters.data,
to: parameters.to,
},
]
: undefined);
return getAccount({
address: parameters.from,
calls,
chainId: parameters.chainId ?? store.getState().chainId,
signable: true,
});
}
catch {
return undefined;
}
})();
if (account?.source === 'accessKey') {
const keyAuth = AccessKey.getPending(account, { store });
if (keyAuth) {
try {
const result = await fill({
...parameters,
keyAuthorization: {
address: keyAuth.address,
...KeyAuthorization.toRpc(keyAuth),
},
});
return result;
}
catch (error) {
AccessKey.invalidate(account, error, { store });
return await fill(parameters);
}
}
}
}
return await fill(parameters);
}
case 'eth_signTransaction': {
assertConnected();
const [decoded] = request._decoded.params;
const { to, data, ...rest } = decoded;
const calls = decoded.calls ?? (to ? [{ to, data, value: decoded.value }] : undefined);
const state = store.getState();
return (await actions.signTransaction({
...rest,
chainId: decoded.chainId ?? state.chainId,
from: decoded.from ?? state.accounts[state.activeAccount]?.address,
...(calls ? { calls } : {}),
feePayer: resolveFeePayer(decoded.feePayer),
}, request));
}
case 'eth_sendTransactionSync': {
assertConnected();
const [decoded] = request._decoded.params;
const { to, data, ...rest } = decoded;
const calls = decoded.calls ?? (to ? [{ to, data, value: decoded.value }] : undefined);
const state = store.getState();
return (await actions.sendTransactionSync({
...rest,
chainId: decoded.chainId ?? state.chainId,
from: decoded.from ?? state.accounts[state.activeAccount]?.address,
...(calls ? { calls } : {}),
feePayer: resolveFeePayer(decoded.feePayer),
}, request));
}
case 'eth_signTypedData_v4': {
assertConnected();
const [address, data] = request._decoded.params;
return (await actions.signTypedData({
address,
data,
}, request));
}
case 'personal_sign': {
assertConnected();
const [data, address] = request._decoded.params;
return (await actions.signPersonalMessage({
address,
data,
}, request));
}
case 'wallet_sendCalls': {
try {
assertConnected();
const decoded = request._decoded.params?.[0];
const { calls = [], capabilities, chainId, from } = decoded ?? {};
const sync = capabilities?.sync;
const feePayer = resolveFeePayer(capabilities?.feePayer ?? (feePayerConfig ? true : undefined));
const state = store.getState();
const txRequest = {
calls,
chainId,
from: from ?? state.accounts[state.activeAccount]?.address,
...(feePayer ? { feePayer } : {}),
};
if (!sync) {
const hash = await actions.sendTransaction(txRequest, {
method: 'eth_sendTransaction',
params: [z.encode(Rpc.transactionRequest, txRequest)],
});
const chainId = Hex.fromNumber(store.getState().chainId);
const id = Hex.concat(hash, Hex.padLeft(chainId, 32), sendCallsMagic);
return { capabilities: { sync }, id };
}
const receipt = await actions.sendTransactionSync(txRequest, {
method: 'eth_sendTransactionSync',
params: [z.encode(Rpc.transactionRequest, txRequest)],
});
const hash = receipt.transactionHash;
const chainIdHex = Hex.fromNumber(store.getState().chainId);
const id = Hex.concat(hash, Hex.padLeft(chainIdHex, 32), sendCallsMagic);
return {
atomic: true,
capabilities: { sync },
chainId: chainIdHex,
id,
receipts: [receipt],
status: receipt.status === '0x1' ? 200 : 500,
version: '2.0.0',
};
}
catch (error) {
throw withDetails(error);
}
}
case 'wallet_getBalances': {
const decoded = request._decoded.params?.[0];
const { accounts, activeAccount } = store.getState();
const account = decoded?.account ?? accounts[activeAccount]?.address;
if (!account)
throw new ox_Provider.DisconnectedError({
message: 'No accounts connected.',
});
const tokens = decoded?.tokens;
// TODO: hook up to indexer
if (!tokens || tokens.length === 0)
throw new RpcResponse.InvalidParamsError({
message: '`tokens` is required.',
});
const client = Client.fromChainId(decoded?.chainId, {
chains,
store,
transports,
});
return (await Promise.all(tokens.map(async (token) => {
const [balance, metadata] = await Promise.all([
Actions.token.getBalance(client, { account, token }),
Actions.token.getMetadata(client, { token }),
]);
const value = Number(balance) / 10 ** metadata.decimals;
const display = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(value);
return {
address: token,
balance: Hex.fromNumber(balance),
decimals: metadata.decimals,
display,
name: metadata.name,
symbol: metadata.symbol,
};
})));
}
case 'wallet_getCallsStatus': {
const [id] = request._decoded.params ?? [];
if (!id)
throw new Error('`id` not found');
if (!id.endsWith(sendCallsMagic.slice(2)))
throw new Error('`id` not supported');
Hex.assert(id);
const hash = Hex.slice(id, 0, 32);
const chainId = Hex.fromNumber(Number(Hex.slice(id, 32, 64)));
const client = Client.fromChainId(Number(chainId), {
chains,
store,
transports,
});
const receipt = await client.request({
method: 'eth_getTransactionReceipt',
params: [hash],
});
return {
atomic: true,
chainId,
id,
receipts: receipt ? [receipt] : [],
status: (() => {
if (!receipt)
return 100; // pending
if (receipt.status === '0x1')
return 200; // success
return 500; // failed
})(),
version: '2.0.0',
};
}
case 'wallet_getCapabilities': {
const decoded = request._decoded.params;
const address = decoded?.[0];
const chainIds = decoded?.[1];
if (address) {
const { accounts } = store.getState();
if (!accounts.some((a) => a.address.toLowerCase() === address.toLowerCase()))
throw new ox_Provider.UnauthorizedError({
message: `Address ${address} is not connected.`,
});
}
const filtered = chainIds
? chains.filter((c) => chainIds.includes(Hex.fromNumber(c.id)))
: chains;
const result = {};
for (const chain of filtered)
result[Hex.fromNumber(chain.id)] = {
accessKeys: { status: 'supported' },
atomic: { status: 'supported' },
...(feePayerConfig ? { feePayer: { status: 'supported' } } : {}),
};
return result;
}
case 'wallet_connect': {
const chainId = request._decoded.params?.[0]?.chainId;
if (chainId)
store.setState((x) => ({ ...x, chainId }));
const capabilities = request._decoded.params?.[0]?.capabilities;
const authorizeAccessKey = capabilities?.authorizeAccessKey ?? options.authorizeAccessKey?.();
// Server Authentication: pre-resolve `auth` URLs against
// this dapp-side Provider's `window.location.origin`. The
// wallet host (different origin in dialog mode) cannot
// reconstruct the dapp's origin, so forwarding the raw
// relative URLs would resolve to the wrong host. We then
// fetch the challenge BEFORE the ceremony so we can fold
// its message into the existing `personalSign` capability.
// Forwarding adapters (dialog) skip orchestration — the
// wallet host's Provider runs it instead.
const auth_input = capabilities?.auth ?? options.auth;
const auth_request = auth_input
? absolutizeAuth(auth_input)
: undefined;
if (auth_request && capabilities?.personalSign)
throw new RpcResponse.InvalidParamsError({
message: '`auth` and `personalSign` cannot both be set on `wallet_connect`.',
});
// Patch the raw request so forwarding adapters carry the
// absolutized auth URLs downstream.
if (auth_request)
request = {
...request,
params: [
{
...request.params?.[0],
capabilities: {
...request.params?.[0]?.capabilities,
auth: auth_request,
},
},
],
};
const auth = auth_request && !instance.forwardsAuth
? await fetchAuthChallenge(auth_request, chainId ?? store.getState().chainId ?? 0)
: undefined;
const personalSign_request = auth
? { message: auth.message }
: capabilities?.personalSign;
const { keyAuthorization, accounts, email, personalSign, signature, username } = await (async () => {
if (capabilities?.method === 'register') {
// If a stored account already has this label, sign in
// with its credential instead of creating a new one.
const existing = capabilities.name
? store
.getState()
.accounts.find((a) => 'credential' in a &&
a.label?.toLowerCase() === capabilities.name.toLowerCase())
: undefined;
if (existing && 'credential' in existing)
return await actions.loadAccounts({
credentialId: existing.credential?.id,
digest: capabilities.digest,
authorizeAccessKey,
...(personalSign_request
? { personalSign: personalSign_request }
: {}),
}, request);
return await actions.createAccount({
digest: capabilities.digest,
authorizeAccessKey,
name: capabilities.name ?? 'default',
userId: capabilities.userId ?? Hex.random(16),
...(personalSign_request
? { personalSign: personalSign_request }
: {}),
}, request);
}
return await actions.loadAccounts({
credentialId: capabilities?.credentialId,
digest: capabilities?.digest,
authorizeAccessKey,
selectAccount: capabilities?.selectAccount,
...(personalSign_request ? { personalSign: personalSign_request } : {}),
}, request);
})();
store.setState({
accounts: resolveAccounts(accounts),
activeAccount: 0,
// Persist absolutized auth URLs so a later
// `wallet_disconnect` can hit logout even when the
// URL was passed per-call. Always overwrite (never
// merge) so a connect WITHOUT auth clears stale
// state from a prior connect — otherwise a later
// disconnect could POST to a logout URL the
// current page never opted into.
auth: auth_request && typeof auth_request === 'object' ? auth_request : undefined,
});
const accountAddress = accounts[0]?.address;
// Server Authentication verify: POST the signed SIWE message
// to the verify endpoint. Skipped when the auth capability
// omits `verify` — typical when the wallet host strips it
// so the dapp-origin Provider does the verify call (and
// receives the session cookie on the dapp's origin).
//
// The signed message comes from one of two places:
// - terminal Provider (wallet host): `auth.message` we just fetched.
// - forwarding Provider (dapp): `personalSign.message` echoed back
// by the wallet host's Provider.
const verifyUrl = auth_request && typeof auth_request === 'object'
? auth_request.verify
: undefined;
const verifyMessage = auth?.message ?? personalSign?.message;
const auth_result = auth_request && verifyUrl && verifyMessage && signature && accountAddress
? await verifyAuthMessage(auth_request, {
address: accountAddress,
message: verifyMessage,
signature,
})
: undefined;
return {
accounts: accounts.map((a) => ({
address: a.address,
capabilities: a.address === accountAddress
? {
...(keyAuthorization
? {
keyAuthorization: {
...keyAuthorization,
address: keyAuthorization.keyId,
},
}
: {}),
...(signature && (!auth_request || auth_result)
? { signature }
: {}),
...(email !== undefined ? { email } : {}),
...(username !== undefined ? { username } : {}),
...(auth_result ? { auth: auth_result } : {}),
...(personalSign
? { personalSign: { message: personalSign.message } }
: {}),
}
: {},
})),
};
}
case 'wallet_disconnect': {
// Best-effort logout. Source of the URL, in order:
// 1. Last-connected `auth` URLs persisted in the store
// (handles per-call `auth` passed via wallet_connect).
// 2. Provider.create({ auth }) option fallback.
// Swallows all errors — disconnect must succeed even
// when the session is already gone or the server is
// unreachable.
const logoutUrl = (() => {
const stored = store.getState().auth;
if (stored?.logout)
return stored.logout;
if (!options.auth)
return undefined;
try {
const absolute = absolutizeAuth(options.auth);
return typeof absolute === 'object' ? absolute.logout : undefined;
}
catch {
return undefined;
}
})();
if (logoutUrl)
await fetch(logoutUrl, {
method: 'POST',
credentials: 'include',
}).catch(() => { });
await actions.disconnect?.();
store.setState({
accessKeys: [],
accounts: [],
activeAccount: 0,
auth: undefined,
});
return;
}
case 'wallet_authorizeAccessKey': {
if (!actions.authorizeAccessKey)
throw new ox_Provider.UnsupportedMethodError({
message: '`authorizeAccessKey` not supported by adapter.',
});
const decoded = request._decoded.params[0];
const result = await actions.authorizeAccessKey(decoded, request);
return {
keyAuthorization: {
...result.keyAuthorization,
address: result.keyAuthorization.keyId,
},
rootAddress: result.rootAddress,
};
}
case 'wallet_revokeAccessKey': {
assertConnected();
if (!actions.revokeAccessKey)
throw new ox_Provider.UnsupportedMethodError({
message: '`revokeAccessKey` not supported by adapter.',
});
const [decoded] = request._decoded.params;
await actions.revokeAccessKey({
...decoded,
}, request);
return;
}
case 'wallet_deposit': {
if (!actions.deposit)
throw new ox_Provider.UnsupportedMethodError({
message: '`deposit` not supported by adapter.',
});
return (await actions.deposit(request._decoded.params[0], request));
}
case 'wallet_transfer': {
assertConnected();
// Default to the editable variant when params are
// omitted — Read-only mode requires `amount`,
// `to`, and `token`, so an empty call only makes
// sense as "open the wallet send UI".
const decoded = request._decoded.params?.[0] ?? { editable: true };
// Editable variant: forward to the wallet host UI.
if (decoded.editable === true) {
if (!actions.transfer)
throw new ox_Provider.UnsupportedMethodError({
message: '`transfer` not supported by adapter.',
});
const parameters = {
...decoded,
...(typeof decoded.feePayer !== 'undefined'
? { feePayer: resolveFeePayer(decoded.feePayer) }
: {}),
};
return (await actions.transfer(parameters, request));
}
// Programmatic variant (default): skip the wallet UI,
// build the TIP-20 `transfer` call inline, and route
// through `eth_sendTransactionSync` (which uses an
// access key when one matches, falling back to the
// dialog otherwise).
const { amount, feePayer, from, memo, to, token } = decoded;
const state = store.getState();
const chainId = decoded.chainId ?? state.chainId;
const resolvedFeePayer = resolveFeePayer(feePayer);
const client = getClient({
chainId,
feePayer: typeof resolvedFeePayer === 'string' ? resolvedFeePayer : undefined,
});
const { address: tokenAddress, decimals } = await (async () => {
if (Address.validate(token)) {
const metadata = await Actions.token.getMetadata(client, {
token,
});
return { address: token, decimals: metadata.decimals };
}
const resolved = await Tokenlist.resolveSymbol({
chainId: client.chain.id,
symbol: token,
});
if (!resolved)
throw new ox_Provider.ProviderRpcError(-32602, `Unknown token symbol "${token}".`);
return { address: resolved.address, decimals: resolved.decimals };
})();
const amountUnits = parseUnits(amount, decimals);
// The signer is the active account (or its access
// key). `from` here is the TIP-20 source for
// `transferFrom` semantics, so we only forward it
// when the caller explicitly set it to a different
// address — otherwise `Actions.token.transfer.call`
// emits `transferFrom` (different selector) instead
// of plain `transfer`, breaking access-key scope
// matching.
const signerAddress = state.accounts[state.activeAccount]?.address;
const sourceFrom = from && signerAddress && from.toLowerCase() !== signerAddress.toLowerCase()
? from
: undefined;
const call = Actions.token.transfer.call({
amount: amountUnits,
...(sourceFrom ? { from: sourceFrom } : {}),
memo: memo ? Hex.fromString(memo) : undefined,
to,
token: tokenAddress,
});
const txRequest = {
calls: [call],
chainId,
from: signerAddress,
...(resolvedFeePayer !== undefined ? { feePayer: resolvedFeePayer } : {}),
};
const receipt = await actions.sendTransactionSync(txRequest, {
method: 'eth_sendTransactionSync',
params: [z.encode(Rpc.transactionRequest, txRequest)],
});
return {
chainId: Hex.fromNumber(chainId),
receipt,
};
}
case 'wallet_swap': {
assertConnected();
if (!actions.swap)
throw new ox_Provider.UnsupportedMethodError({
message: '`swap` not supported by adapter.',
});
return (await actions.swap((request._decoded.params?.[0] ?? {}), request));
}
case 'wallet_depositZone': {
assertConnected();
if (!actions.depositZone)
throw new ox_Provider.UnsupportedMethodError({
message: '`depositZone` not supported by adapter.',
});
const decoded = request._decoded.params?.[0] ?? {};
const parameters = {
...decoded,
...(typeof decoded.feePayer !== 'undefined'
? { feePayer: resolveFeePayer(decoded.feePayer) }
: {}),
};
return (await actions.depositZone(parameters, request));
}
case 'wallet_withdrawZone': {
assertConnected();
if (!actions.withdrawZone)
throw new ox_Provider.UnsupportedMethodError({
message: '`withdrawZone` not supported by adapter.',
});
return (await actions.withdrawZone((request._decoded.params?.[0] ?? {}), request));
}
case 'wallet_switchEthereumChain': {
const { chainId } = request._decoded.params[0];
if (!chains.some((c) => c.id === chainId))
throw new ox_Provider.UnsupportedChainIdError({
message: `Chain ${chainId} not configured.`,
});
await actions.switchChain?.({ chainId });
store.setState({ chainId });
return;
}
}
})();
return result;
}, {
enabled: shouldDedupe,
id: Json.stringify({ method, params }),
});
},
}, { schema: Schema.ox }), {
chains,
getAccount,
async getAccessKeyStatus(options = {}) {
const state = store.getState();
const address = options.address ?? state.accounts[state.activeAccount]?.address;
if (!address)
return AccessKey.status.missing;
const chainId = options.chainId ?? state.chainId;
return await AccessKey.getStatus({
...options,
address,
chainId,
client: provider.getClient({ chainId }),
store,
});
},
getClient(options = {}) {
const { chainId, feePayer } = options;
return Client.fromChainId(chainId, {
chains,
feePayer,
provider: providerRef,
store,
transports,
});
},
store,
});
if (typeof window !== 'undefined' && typeof CustomEvent !== 'undefined') {
const rdns = adapter.rdns ?? `com.${(adapter.name ?? 'Injected Wallet').toLowerCase().replace(/\s+/g, '')}`;
if (!announced.has(rdns)) {
announced.add(rdns);
announceProvider({
info: {
icon: adapter.icon ?? defaultIcon,
name: adapter.name ?? 'Injected Wallet',
rdns,
uuid: crypto.randomUUID(),
},
provider,
});
}
}
const mpp = (() => {
if (options.mpp === false)
return undefined;
if (typeof options.mpp === 'object')
return options.mpp;
return {};
})();
if (mpp) {
const { mode = 'push', polyfill: polyfill_option, ...methodOptions } = mpp;
// Skip polyfill on runtimes where `globalThis.fetch` is read-only (e.g.
// Cloudflare Workers). Caller can also explicitly opt out via `mpp.polyfill`.
const polyfill = polyfill_option ?? isFetchWritable();
const getClient = ({ chainId }) => {
const client = provider.getClient({ chainId });
const account = provider.getAccount();
return Object.assign(client, { account });
};
const mppx = Mppx.create({
methods: [
mppx_tempo({ ...methodOptions, getClient, mode }),
mppx_tempo.subscription({ getClient }),
],
polyfill,
});
mppx.onPaymentResponse(({ challenge, method }) => {
if (method.name !== 'tempo' || method.intent !== 'charge')
return;
const amount = challenge.request.amount;
if (typeof amount !== 'string' &&
typeof amount !== 'number' &&
typeof amount !== 'bigint' &&
typeof amount !== 'boolean')
return;
if (BigInt(amount) === 0n)
return;
const account = provider.getAccount();
if ('source' in account && account.source === 'accessKey')
AccessKey.removePending(account, { store });
});
}
providerRef = provider;
return provider;
}
const defaultIcon = 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1"><rect width="1" height="1"/></svg>';
const sendCallsMagic = Hash.keccak256(Hex.fromString('TEMPO_5792'));
function withDetails(error) {
if (error instanceof Error) {
const details = error.details;
if (typeof details === 'string')
return error;
Object.assign(error, { details: error.message });
return error;
}
const next = new Error(String(error));
Object.assign(next, { details: next.message });
return next;
}
/**
* Returns `true` if `globalThis.fetch` can be reassigned. Some runtimes
* (notably Cloudflare Workers) expose a non-writable, non-configurable
* `fetch` that throws when `Mppx.create({ polyfill: true })` tries to
* replace it.
*
* Tries an actual no-op self-reassignment because some runtimes report a
* writ