UNPKG

accounts

Version:

Tempo Accounts SDK

902 lines 59.6 kB
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