accounts
Version:
Tempo Accounts SDK
380 lines • 18.9 kB
JavaScript
import { Address, Provider as ox_Provider, RpcRequest as ox_RpcRequest } from 'ox';
import { KeyAuthorization } from 'ox/tempo';
import { prepareTransactionRequest } from 'viem/actions';
import { Account as TempoAccount } from 'viem/tempo';
import { z } from 'zod/mini';
import * as AccessKey from '../AccessKey.js';
import * as Adapter from '../Adapter.js';
import * as Dialog from '../Dialog.js';
import * as Schema from '../Schema.js';
import * as Rpc from '../zod/rpc.js';
/**
* Creates a dialog adapter that delegates signing to a remote embed app
* via an iframe or popup dialog.
*
* @example
* ```ts
* import { dialog, Provider } from 'accounts'
*
* const provider = Provider.create({
* adapter: dialog(),
* })
* ```
*/
export function dialog(options = {}) {
const { dialog = Dialog.isInsecureContext() ? Dialog.popup() : Dialog.iframe(),
// TODO: use the new host
// host = 'https://wallet-next.tempo.xyz/remote',
host = 'https://wallet.tempo.xyz/embed', icon = 'data:image/svg+xml,<svg width="269" height="269" viewBox="0 0 269 269" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="269" height="269" fill="black"/><path d="M123.273 190.794H93.445L121.09 105.318H85.7334L93.445 80.2642H191.95L184.238 105.318H150.773L123.273 190.794Z" fill="white"/></svg>', name = 'Tempo Wallet', rdns = 'xyz.tempo', theme, } = options;
if (typeof window !== 'undefined' && !window.isSecureContext)
console.warn('[accounts] Detected insecure context (HTTP).', `\n\nThe Tempo Wallet iframe dialog is not supported on HTTP origins (${window.location.origin})`, 'due to lack of WebAuthn passkey support in non-secure contexts.');
return Adapter.define({ icon, name, rdns }, ({ getAccount, getClient, store }) => {
const listeners = new Set();
const requestStore = ox_RpcRequest.createStore();
/** Wait for a queued request to be resolved via the store. */
function waitForQueuedRequest(requestId) {
return new Promise((resolve, reject) => {
const listener = (requestQueue) => {
const queued = requestQueue.find((x) => x.request.id === requestId);
// Request removed and queue empty — cancelled or dialog closed.
if (!queued && requestQueue.length === 0) {
listeners.delete(listener);
reject(new ox_Provider.UserRejectedRequestError());
return;
}
// Request not found but queue has other requests — wait.
if (!queued)
return;
// Request found but not yet resolved — wait.
if (queued.status !== 'success' && queued.status !== 'error')
return;
listeners.delete(listener);
if (queued.status === 'success')
resolve(queued.result);
else
reject(ox_Provider.parseError(queued.error));
// Remove the resolved request from the queue.
store.setState((x) => ({
...x,
requestQueue: x.requestQueue.filter((x) => x.request.id !== requestId),
}));
};
listeners.add(listener);
// Notify immediately with current state so the store subscription
// picks up the request that was just added (setState fires
// synchronously before this listener is registered).
listener(store.getState().requestQueue);
});
}
/**
* An ox provider that queues RPC requests in the store. The store
* subscription syncs the pending queue to the dialog via `syncRequests`.
*/
const provider = ox_Provider.from({
async request(r) {
const request = requestStore.prepare(r);
store.setState((x) => ({
...x,
requestQueue: [...x.requestQueue, { request, status: 'pending' }],
}));
return waitForQueuedRequest(request.id);
},
}, { schema: Schema.ox });
/**
* Prepares a local key pair when `authorizeAccessKey` is requested without
* an external publicKey/address, and returns the params to inject into the
* RPC request so the dialog signs the authorization.
*/
async function generateAccessKey(options) {
if (!options)
return undefined;
if (options.publicKey || options.address)
return undefined;
const { accessKey, keyPair } = await AccessKey.generate();
return {
accessKey,
keyPair,
request: {
...options,
publicKey: accessKey.publicKey,
keyType: 'p256',
},
};
}
/**
* After the dialog returns a signed key authorization, saves the local
* key pair + key authorization into the store.
*/
function saveAccessKey(address, keyAuth, keyPair) {
const keyAuthorization = KeyAuthorization.fromRpc(keyAuth);
AccessKey.save({ address, keyAuthorization, keyPair, store });
}
/**
* Tries to execute `fn` with the local access key. Returns `undefined`
* when no access key exists so the caller can fall through to the dialog.
* On stale-key errors, removes the key and also returns `undefined`.
* On recoverable transaction errors, keeps the key and falls through to
* the dialog so the user can fund, approve, or retry.
*/
async function withAccessKey(options, fn) {
if (!options.from || typeof options.chainId === 'undefined')
return undefined;
const account = AccessKey.selectAccount({
address: options.from,
calls: options.calls,
chainId: options.chainId,
store,
});
if (!account)
return undefined;
const keyAuthorization = AccessKey.getPending(account, { store });
try {
const result = await fn(account, keyAuthorization ?? undefined);
return { account, result };
}
catch (err) {
if (AccessKey.invalidate(account, err, { store }))
console.warn('[accounts] access key invalidated, falling through to dialog:', err);
else
console.warn('[accounts] access key sign failed, falling through to dialog:', err);
return undefined;
}
}
const dialogInstance = dialog({ host, store, theme });
// Sync store → dialog: whenever the request queue changes, notify
// listeners and sync pending requests to the dialog.
const unsubscribe = store.subscribe((x) => x.requestQueue, (requestQueue) => {
for (const listener of listeners)
listener(requestQueue);
const pending = requestQueue.filter((x) => x.status === 'pending');
dialogInstance?.syncRequests(pending);
if (pending.length === 0)
dialogInstance?.close();
});
return {
cleanup() {
unsubscribe();
dialogInstance?.destroy();
},
forwardsAuth: true,
actions: {
async createAccount(parameters, request) {
const accessKey = await generateAccessKey(parameters.authorizeAccessKey);
const { accounts } = await provider.request({
...request,
params: [
{
...request.params?.[0],
capabilities: {
...request.params?.[0]?.capabilities,
...(accessKey
? {
authorizeAccessKey: z.encode(Rpc.wallet_connect.authorizeAccessKey, accessKey.request),
}
: {}),
},
},
],
});
const address = accounts[0]?.address;
const keyAuthorization = accounts[0]?.capabilities.keyAuthorization;
if (accessKey && address && keyAuthorization)
saveAccessKey(address, keyAuthorization, accessKey.keyPair);
return {
accounts: accounts.map((a) => ({ address: a.address })),
...(keyAuthorization ? { keyAuthorization } : {}),
...(accounts[0]?.capabilities.signature
? { signature: accounts[0].capabilities.signature }
: {}),
...(accounts[0]?.capabilities.personalSign
? { personalSign: accounts[0].capabilities.personalSign }
: {}),
};
},
async loadAccounts(parameters, request) {
const accessKey = await generateAccessKey(parameters?.authorizeAccessKey);
const { accounts } = await provider.request({
...request,
params: [
{
...request.params?.[0],
capabilities: {
...request.params?.[0]?.capabilities,
...(accessKey
? {
authorizeAccessKey: z.encode(Rpc.wallet_connect.authorizeAccessKey, accessKey.request),
}
: {}),
},
},
],
});
const address = accounts[0]?.address;
const keyAuthorization = accounts[0]?.capabilities.keyAuthorization;
if (accessKey && address && keyAuthorization)
saveAccessKey(address, keyAuthorization, accessKey.keyPair);
return {
accounts: accounts.map((a) => ({ address: a.address })),
...(keyAuthorization ? { keyAuthorization } : {}),
...(accounts[0]?.capabilities.signature
? { signature: accounts[0].capabilities.signature }
: {}),
...(accounts[0]?.capabilities.personalSign
? { personalSign: accounts[0].capabilities.personalSign }
: {}),
};
},
async signPersonalMessage(_params, request) {
return await provider.request(request);
},
async signTransaction(parameters, request) {
const result = await withAccessKey(parameters, async (account, keyAuthorization) => {
const { feePayer, ...rest } = parameters;
const client = getClient({
chainId: parameters.chainId,
feePayer: (() => {
if (feePayer === false)
return false;
if (typeof feePayer === 'string')
return feePayer;
return undefined;
})(),
});
const prepared = await prepareTransactionRequest(client, {
account,
...rest,
...(typeof feePayer !== 'undefined' ? { feePayer: !!feePayer } : {}),
keyAuthorization,
type: 'tempo',
});
return await account.signTransaction(prepared);
});
if (result !== undefined)
return result.result;
return await provider.request({
...request,
params: [z.encode(Rpc.transactionRequest, parameters)],
});
},
async signTypedData(_params, request) {
return await provider.request(request);
},
async sendTransaction(parameters, request) {
const result = await withAccessKey(parameters, async (account, keyAuthorization) => {
const { feePayer, ...rest } = parameters;
const client = getClient({
chainId: parameters.chainId,
feePayer: (() => {
if (feePayer === false)
return false;
if (typeof feePayer === 'string')
return feePayer;
return undefined;
})(),
});
const prepared = await prepareTransactionRequest(client, {
account,
...rest,
...(typeof feePayer !== 'undefined' ? { feePayer: !!feePayer } : {}),
keyAuthorization,
type: 'tempo',
});
const signed = await account.signTransaction(prepared);
return await client.request({
method: 'eth_sendRawTransaction',
params: [signed],
});
});
if (result !== undefined) {
AccessKey.removePending(result.account, { store });
return result.result;
}
return await provider.request({
...request,
params: [z.encode(Rpc.transactionRequest, parameters)],
});
},
async sendTransactionSync(parameters, request) {
const result = await withAccessKey(parameters, async (account, keyAuthorization) => {
const { feePayer, ...rest } = parameters;
const client = getClient({
chainId: parameters.chainId,
feePayer: (() => {
if (feePayer === false)
return false;
if (typeof feePayer === 'string')
return feePayer;
return undefined;
})(),
});
const prepared = await prepareTransactionRequest(client, {
account,
...rest,
...(typeof feePayer !== 'undefined' ? { feePayer: !!feePayer } : {}),
keyAuthorization,
type: 'tempo',
});
const signed = await account.signTransaction(prepared);
return await client.request({
method: 'eth_sendRawTransactionSync',
params: [signed],
});
});
if (result !== undefined) {
AccessKey.removePending(result.account, { store });
return result.result;
}
return await provider.request({
...request,
params: [z.encode(Rpc.transactionRequest, parameters)],
});
},
async authorizeAccessKey(parameters, request) {
const accessKey = await generateAccessKey(parameters);
const result = await provider.request({
...request,
params: [
z.encode(Rpc.wallet_connect.authorizeAccessKey, accessKey ? accessKey.request : parameters),
],
});
if (accessKey) {
const account = getAccount({ accessKey: false, signable: false });
saveAccessKey(account.address, result.keyAuthorization, accessKey.keyPair);
}
return result;
},
async revokeAccessKey(_params, request) {
await provider.request(request);
},
async deposit(_params, request) {
return await provider.request(request);
},
async transfer(params, request) {
return await provider.request({
...request,
params: [z.encode(Rpc.wallet_transfer.parameters, params)],
});
},
async swap(_params, request) {
return await provider.request(request);
},
async depositZone(params, request) {
return await provider.request({
...request,
params: [z.encode(Rpc.wallet_depositZone.parameters, params)],
});
},
async withdrawZone(params, request) {
return await provider.request({
...request,
params: [z.encode(Rpc.wallet_withdrawZone.parameters, params)],
});
},
async disconnect() {
store.setState({ accessKeys: [], accounts: [], activeAccount: 0 });
},
},
};
});
}
//# sourceMappingURL=dialog.js.map