UNPKG

accounts

Version:

Tempo Accounts SDK

275 lines 14.7 kB
import { Provider as ox_Provider } from 'ox'; import { KeyAuthorization } from 'ox/tempo'; import { BaseError, hashMessage } from 'viem'; import { prepareTransactionRequest } from 'viem/actions'; import { Account as TempoAccount, Actions } from 'viem/tempo'; import * as AccessKey from '../AccessKey.js'; import * as Account from '../Account.js'; import * as Adapter from '../Adapter.js'; /** * Creates a local adapter where the app manages keys and signing in-process. * * @example * ```ts * import { local, Provider } from 'accounts' * * const Provider = Provider.create({ * adapter: local({ * loadAccounts: async () => ({ * accounts: [{ address: '0x...' }], * }), * }), * }) * ``` */ export function local(options) { const { createAccount, icon, loadAccounts, name, rdns } = options; return Adapter.define({ icon, name, rdns }, ({ getAccount, getClient, store }) => { async function withAccessKey(options, fn) { const account = getAccount({ ...options, signable: true }); const keyAuthorization = AccessKey.getPending(account, { store }); try { return await fn(account, keyAuthorization ?? undefined); } catch (error) { if (account.source !== 'accessKey') throw error; AccessKey.invalidate(account, error, { store }); const root = getAccount({ accessKey: false, address: options.address, signable: true }); return await fn(root, undefined); } } return { actions: { async createAccount(parameters) { if (!createAccount) throw new ox_Provider.UnsupportedMethodError({ message: '`createAccount` not configured on adapter.', }); const { authorizeAccessKey: grantOptions, personalSign, ...rest } = parameters; // `personalSign` claims the ceremony's challenge slot. It conflicts // with a caller-supplied `digest` because both target the single // WebAuthn challenge in the create-account ceremony. if (personalSign && rest.digest) throw new ox_Provider.ProviderRpcError(-32602, '`digest` and `personalSign` cannot both be set on `wallet_connect`.'); const peronsalSign_digest = personalSign ? hashMessage(personalSign.message) : undefined; const digest = peronsalSign_digest ?? rest.digest; const { accounts, email, signature, username } = await createAccount({ ...rest, digest, }); // Hydrate the first account for signing. Must be done here (not via // the store) because accounts aren't merged into the store until // Provider.ts processes the return value. const account = Account.hydrate(accounts[0], { signable: true }); // If the caller requested a digest signature but the adapter didn't // produce one (e.g. secp256k1 adapters), sign it ourselves. const signature_ = digest && !signature ? await account.sign({ hash: digest }) : signature; const keyAuthorization = await (async () => { if (!grantOptions) return undefined; return await AccessKey.authorize({ account, chainId: getClient().chain.id, parameters: grantOptions, store, }); })(); return { accounts, email, keyAuthorization, signature: signature_, username, ...(personalSign ? { personalSign: { message: personalSign.message } } : {}), }; }, async authorizeAccessKey(parameters) { const account = getAccount({ accessKey: false, signable: true }); const keyAuthorization = await AccessKey.authorize({ account, chainId: getClient().chain.id, parameters, store, }); return { keyAuthorization, rootAddress: account.address }; }, async loadAccounts(parameters) { const { authorizeAccessKey, personalSign, ...rest } = parameters ?? {}; // `personalSign` claims the ceremony's challenge slot. It conflicts // with a caller-supplied `digest` because both target the single // WebAuthn challenge in the load-accounts ceremony. if (personalSign && rest.digest) throw new ox_Provider.ProviderRpcError(-32602, '`digest` and `personalSign` cannot both be set on `wallet_connect`.'); const peronsalSign_digest = personalSign ? hashMessage(personalSign.message) : undefined; const keyAuthorization_unsigned = authorizeAccessKey ? await AccessKey.prepareAuthorization({ ...authorizeAccessKey, chainId: authorizeAccessKey.chainId ?? getClient().chain.id, }) : undefined; const keyAuthorization_digest = keyAuthorization_unsigned ? KeyAuthorization.getSignPayload(keyAuthorization_unsigned.keyAuthorization) : undefined; // Slot allocation: // 1. `personalSign` digest, if present. // 2. Else key-auth digest (existing 1-prompt fold for `authorizeAccessKey`). // 3. Else caller's `rest.digest`. // When BOTH `personalSign` and `authorizeAccessKey` are present, // `personalSign` wins the load-accounts ceremony and the key // authorization gets its own follow-up `account.sign` ceremony // (2 prompts total). const digest = peronsalSign_digest ?? keyAuthorization_digest ?? rest.digest; // Pass the prepared digest (or the caller's) into loadAccounts so // the ceremony can sign it in a single biometric prompt. const { accounts, email, signature, username } = await loadAccounts({ ...rest, digest }); // Hydrate here (not from the store) — same reason as createAccount. // Guard against empty accounts (e.g. user cancelled the ceremony). const account = accounts[0] ? Account.hydrate(accounts[0], { signable: true }) : undefined; // Fall back to local signing if the adapter didn't return a signature. let signature_ = signature; if (digest && !signature_ && account) signature_ = await account.sign({ hash: digest }); // Key auth signing path: // - If `personalSign` took the ceremony slot AND `authorizeAccessKey` // is set, we need a SECOND ceremony to sign the key-auth digest. // - Else (key-auth digest took the slot), reuse `signature_`. const keyAuthorization = await (async () => { if (!keyAuthorization_unsigned || !account) return undefined; const signature_keyAuthorization = peronsalSign_digest || !signature_ ? await account.sign({ hash: keyAuthorization_digest }) : signature_; return AccessKey.saveAuthorization({ address: account.address, prepared: keyAuthorization_unsigned, signature: signature_keyAuthorization, store, }); })(); return { accounts, email, keyAuthorization, signature: signature_, username, ...(personalSign ? { personalSign: { message: personalSign.message } } : {}), }; }, async revokeAccessKey(parameters) { const account = getAccount({ accessKey: false, signable: true }); const client = getClient(); try { await Actions.accessKey.revoke(client, { account, accessKey: parameters.accessKeyAddress, }); } catch (error) { const isKeyNotFound = error instanceof BaseError && !!error.walk((e) => e.data?.errorName === 'KeyNotFound'); if (!isKeyNotFound) throw error; } store.setState((state) => ({ accessKeys: state.accessKeys.filter((a) => a.address?.toLowerCase() !== parameters.accessKeyAddress.toLowerCase()), })); }, async signPersonalMessage({ data, address }) { const account = getAccount({ address, signable: true }); return await account.signMessage({ message: { raw: data } }); }, async signTransaction(parameters) { 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 { account, prepared } = await withAccessKey({ address: parameters.from, calls: parameters.calls, chainId: parameters.chainId }, async (account, keyAuthorization) => ({ account, prepared: await prepareTransactionRequest(client, { account, ...rest, ...(feePayer ? { feePayer: true } : {}), keyAuthorization, type: 'tempo', }), })); return await account.signTransaction(prepared); }, async signTypedData({ data, address }) { const account = getAccount({ address, signable: true }); const parsed = JSON.parse(data); return await account.signTypedData(parsed); }, async sendTransaction(parameters) { 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 { account, prepared } = await withAccessKey({ address: parameters.from, calls: parameters.calls, chainId: parameters.chainId }, async (account, keyAuthorization) => ({ account, prepared: await prepareTransactionRequest(client, { account, ...rest, ...(feePayer ? { feePayer: true } : {}), keyAuthorization, type: 'tempo', }), })); const signed = await account.signTransaction(prepared); const result = await client.request({ method: 'eth_sendRawTransaction', params: [signed], }); AccessKey.removePending(account, { store }); return result; }, async sendTransactionSync(parameters) { 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 { account, prepared } = await withAccessKey({ address: parameters.from, calls: parameters.calls, chainId: parameters.chainId }, async (account, keyAuthorization) => ({ account, prepared: await prepareTransactionRequest(client, { account, ...rest, ...(feePayer ? { feePayer: true } : {}), keyAuthorization, type: 'tempo', }), })); const signed = await account.signTransaction(prepared); const result = await client.request({ method: 'eth_sendRawTransactionSync', params: [signed], }); AccessKey.removePending(account, { store }); return result; }, }, }; }); } //# sourceMappingURL=local.js.map