accounts
Version:
Tempo Accounts SDK
229 lines • 9.24 kB
JavaScript
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