UNPKG

accounts

Version:

Tempo Accounts SDK

229 lines 9.24 kB
import { Hex } from 'ox'; import * as Provider from 'ox/Provider'; import * as RpcResponse from 'ox/RpcResponse'; import { createStore } from 'zustand/vanilla'; import * as Schema from '../core/Schema.js'; import * as Rpc from '../core/zod/rpc.js'; /** Creates a remote context for the dialog app. */ export function create(options) { const { messenger, provider, trustedHosts } = options; const ready = typeof window !== 'undefined' && !new URLSearchParams(window.location.search).get('mode'); const store = createStore(() => ({ mode: undefined, origin: undefined, ready, requests: [], })); return { messenger, provider, store, trustedHosts: trustedHosts ?? [], onUserRequest(cb) { return this.onRequests(async (requests, event, { account }) => { // Sync the active account with the host. if (account) { const state = provider.store.getState(); const index = state.accounts.findIndex((a) => a.address.toLowerCase() === account.address.toLowerCase()); if (index < 0) { messenger.send('sync', { valid: false }); for (const r of requests) if (r.status === 'pending') this.reject(r.request); return; } if (index !== state.activeAccount) provider.store.setState({ activeAccount: index }); } const pending = requests.find((r) => r.status === 'pending'); store.setState({ origin: event.origin, ready: false, }); await cb({ account, origin: event.origin, request: pending?.request ?? null, }); if (pending) store.setState({ ready: true }); }); }, onRequests(cb) { return messenger.on('rpc-requests', async (payload, event) => { const { account, chainId, requests } = payload; // Rehydrate persisted state so the iframe picks up accounts // created in a popup (e.g. Safari WebAuthn fallback). await provider.store.persist?.rehydrate(); store.setState({ requests }); if (provider.store.getState().chainId !== chainId) provider.request({ method: 'wallet_switchEthereumChain', params: [{ chainId: Hex.fromNumber(chainId) }], }); cb(requests, event, { account }); }); }, ready(options) { const { accounts, ...readyOptions } = options ?? {}; messenger.ready({ ...readyOptions, trustedHosts }); // Respond to account sync requests from the SDK. if (accounts) messenger.on('sync', ({ addresses }) => { if (!addresses) return; const valid = addresses.some((a) => accounts.some((b) => a.toLowerCase() === b.toLowerCase())); messenger.send('sync', { valid }); }); if (typeof window !== 'undefined') { const params = new URLSearchParams(window.location.search); const mode = params.get('mode'); if (mode) store.setState({ mode }); } }, reject(request, error) { const error_ = error ?? new Provider.UserRejectedRequestError(); messenger.send('rpc-response', Object.assign(RpcResponse.from({ error: { code: error_.code, message: error_.message }, id: request.id, jsonrpc: '2.0', }), { _request: request })); }, rejectAll(error) { store.setState({ ready: false }); const requests = store.getState().requests; for (const queued of requests) this.reject(queued.request, error); }, async respond(request, options = {}) { const { error, onError, selector } = options; const shared = { id: request.id, jsonrpc: '2.0' }; if (error) { messenger.send('rpc-response', Object.assign(RpcResponse.from({ ...shared, error, status: 'error' }), { _request: request, })); return; } try { let result = 'result' in options ? options.result : await provider?.request(request); if (selector) result = selector(result); messenger.send('rpc-response', Object.assign(RpcResponse.from({ ...shared, result }), { _request: request })); return result; } catch (e) { // Browser extensions (e.g. Bitwarden) monkey-patch navigator.credentials // and reject WebAuthn calls in cross-origin iframes. Fall back to popup // so the credential ceremony runs in a top-level browsing context. if (e instanceof Error && e.message.includes('sameOriginWithAncestors')) { messenger.send('switch-mode', { mode: 'popup' }); return; } if (e instanceof Error && onError?.(e)) throw e; const err = e; messenger.send('rpc-response', Object.assign(RpcResponse.from({ ...shared, error: err, status: 'error' }), { _request: request, })); throw err; } }, }; } /** Returns an inert remote context for SSR environments. */ export function noop() { const store = createStore(() => ({ mode: undefined, origin: undefined, ready: false, requests: [], })); const off = () => () => { }; return { messenger: { destroy: () => { }, on: () => () => { }, ready: () => { }, send: () => { }, waitForReady: () => Promise.resolve({}), }, provider: {}, store, trustedHosts: [], onUserRequest: off, onRequests: off, ready: () => { }, reject: () => { }, rejectAll: () => { }, respond: async () => { }, }; } /** * Validates an RPC request from search params. * * Parses against the `Schema.Request` discriminated union, checks the * method matches, and enforces strict parameter schemas (e.g. required * `limits`). On failure, rejects all pending requests via the messenger * and re-throws so the router can handle the error boundary. */ export function validateSearch(remote, search, parameters) { const { method } = parameters; try { const result = Schema.Request.safeParse(search); if (!result.success) throw new RpcResponse.InvalidParamsError({ message: formatZodErrors(method, result.error), }); if (result.data.method !== method) throw new RpcResponse.InvalidParamsError({ message: `Method mismatch: expected "${method}" but got "${result.data.method}".`, }); const strict = Rpc.strictParameters[method]; const params = search.params?.[0]; if (strict && params !== undefined) { const strictResult = strict.safeParse(params); if (!strictResult.success) throw new RpcResponse.InvalidParamsError({ message: formatZodErrors(method, strictResult.error), }); } return { ...search, _decoded: result.data, id: Number(search.id), jsonrpc: '2.0', }; } catch (error) { if (error instanceof RpcResponse.BaseError) void remote.rejectAll(error); throw error; } } function formatZodErrors(method, error) { const issues = flattenIssues(error.issues) .map((i) => ` - ${i.path.map(String).join('.')}: ${i.message}`) .join('\n'); return `Invalid params for "${method}":\n${issues}`; } function flattenIssues(issues) { const result = []; for (const issue of issues) { if (issue.errors?.length) { const best = issue.errors.reduce((a, b) => (a.length <= b.length ? a : b)); for (const nested of flattenIssues(best)) result.push({ path: [...issue.path, ...nested.path], message: nested.message }); } else { let message = issue.message; if (issue.code === 'invalid_type' && issue.expected) message = `Expected ${issue.expected}`; else if (issue.code === 'invalid_value') message = 'Invalid value'; result.push({ path: issue.path, message }); } } return result; } //# sourceMappingURL=Remote.js.map