UNPKG

accounts

Version:

Tempo Accounts SDK

277 lines 15 kB
import { Provider as ox_Provider } from 'ox'; import { KeyAuthorization } from 'ox/tempo'; import { hashMessage } from 'viem'; import { Account as TempoAccount } from 'viem/tempo'; import * as z from 'zod/mini'; import * as Account from '../Account.js'; import * as Adapter from '../Adapter.js'; import * as u from '../zod/utils.js'; const secp256k1Schema = z.object({ address: u.address(), keyType: z.literal('secp256k1'), label: z.optional(z.string()), privateKey: u.hex(), }); const p256Schema = z.object({ address: u.address(), keyType: z.literal('p256'), label: z.optional(z.string()), privateKey: u.hex(), }); const webAuthnSchema = z.object({ address: u.address(), credential: z.object({ id: z.string(), publicKey: u.hex(), rpId: z.string(), }), keyType: z.literal('webAuthn'), label: z.optional(z.string()), }); const webAuthnHeadlessSchema = z.object({ address: u.address(), keyType: z.literal('webAuthn_headless'), label: z.optional(z.string()), origin: z.string(), privateKey: u.hex(), rpId: z.string(), }); const webCryptoSchema = z.object({ address: u.address(), keyPair: z.custom(), keyType: z.literal('webCrypto'), label: z.optional(z.string()), }); const functionSignerSchema = z.object({ address: u.address(), keyType: z.union([z.literal('secp256k1'), z.literal('p256'), z.literal('webAuthn')]), label: z.optional(z.string()), sign: z.custom(), }); const signableSchema = z.union([ secp256k1Schema, p256Schema, webAuthnSchema, webAuthnHeadlessSchema, webCryptoSchema, functionSignerSchema, ]); /** * 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, schema: signableSchema }, ({ getAccount, getClient, store }) => { 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 client = getClient(grantOptions?.chainId ? { chainId: Number(grantOptions.chainId) } : undefined); const chainId = grantOptions?.chainId ?? client.chain.id; // TIP-1053 witness binding (see `loadAccounts`): fold the auth // message into the access-key authorization and sign both in the // single create-account ceremony. const witness = personalSign && grantOptions ? hashMessage(personalSign.message) : undefined; const peronsalSign_digest = personalSign && !witness ? hashMessage(personalSign.message) : undefined; const keyAuthorization_unsigned = witness && grantOptions ? await store.accessKeys.prepareAuthorization({ ...grantOptions, chainId, witness }) : undefined; const keyAuthorization_digest = keyAuthorization_unsigned ? KeyAuthorization.getSignPayload(keyAuthorization_unsigned.keyAuthorization) : undefined; const digest = peronsalSign_digest ?? keyAuthorization_digest ?? rest.digest; const { accounts, 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; // Witness path: the ceremony already signed the witness-bearing // key-auth digest, so reuse that signature instead of a 2nd prompt. const keyAuthorization_signed = witness && keyAuthorization_unsigned && signature_ ? await (async () => { const signed = KeyAuthorization.from(keyAuthorization_unsigned.keyAuthorization, { signature: signature_ }); store.accessKeys.add({ account: account.address, authorization: signed, ...(keyAuthorization_unsigned.key ? { handle: keyAuthorization_unsigned.key.handle, publicKey: keyAuthorization_unsigned.key.publicKey, } : {}), ...(keyAuthorization_unsigned.privateKey ? { privateKey: keyAuthorization_unsigned.privateKey } : {}), }); return signed; })() : undefined; const keyAuthorization = await (async () => { if (keyAuthorization_signed) return KeyAuthorization.toRpc(keyAuthorization_signed); if (!grantOptions) return undefined; // Non-witness fallback: sign the key authorization on its own // (a second ceremony when `personalSign` claimed the first). return await store.accessKeys.authorize({ account, chainId: getClient().chain.id, parameters: grantOptions, }); })(); return { accounts, keyAuthorization, signature: signature_, username, ...(personalSign ? { personalSign: { message: personalSign.message, ...(witness && keyAuthorization_signed ? { keyAuthorization: KeyAuthorization.serialize(keyAuthorization_signed) } : {}), }, } : {}), }; }, 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 client = getClient(authorizeAccessKey?.chainId ? { chainId: Number(authorizeAccessKey.chainId) } : undefined); const chainId = authorizeAccessKey?.chainId ?? client.chain.id; // TIP-1053 witness binding: when both a `personalSign` challenge and // an `authorizeAccessKey` are requested, bind the message into the // key authorization's `witness` and sign both in ONE ceremony. The // signed key authorization doubles as the auth proof. Otherwise fall // back to the two-ceremony path below. const witness = personalSign && authorizeAccessKey ? hashMessage(personalSign.message) : undefined; // Only claim the ceremony slot with the `personalSign` digest when // NOT binding via witness — the witness path signs the key-auth // digest (which already commits to the message) instead. const peronsalSign_digest = personalSign && !witness ? hashMessage(personalSign.message) : undefined; const keyAuthorization_unsigned = authorizeAccessKey ? await store.accessKeys.prepareAuthorization({ ...authorizeAccessKey, chainId, ...(witness ? { witness } : {}), }) : undefined; const keyAuthorization_digest = keyAuthorization_unsigned ? KeyAuthorization.getSignPayload(keyAuthorization_unsigned.keyAuthorization) : undefined; // Slot allocation: // 1. `personalSign` digest, if present (non-witness path). // 2. Else unsigned key-auth digest (1-prompt fold for // `authorizeAccessKey`, including the witness path). // 3. Else caller's `rest.digest`. // When BOTH `personalSign` and `authorizeAccessKey` are present on a // non-witness chain, `personalSign` wins the ceremony and the key // authorization gets a follow-up `account.sign` (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, 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 (non-witness), we need a SECOND ceremony to sign the // key-auth digest. // - Else (key-auth digest took the slot — witness path or // `authorizeAccessKey`-only), reuse `signature_`. const keyAuthorization_signed = await (async () => { if (!keyAuthorization_unsigned || !account) return undefined; const signature_keyAuthorization = peronsalSign_digest || !signature_ ? await account.sign({ hash: keyAuthorization_digest }) : signature_; const keyAuthorization = KeyAuthorization.from(keyAuthorization_unsigned.keyAuthorization, { signature: signature_keyAuthorization }); store.accessKeys.add({ account: account.address, authorization: keyAuthorization, ...(keyAuthorization_unsigned.key ? { handle: keyAuthorization_unsigned.key.handle, publicKey: keyAuthorization_unsigned.key.publicKey, } : {}), ...(keyAuthorization_unsigned.privateKey ? { privateKey: keyAuthorization_unsigned.privateKey } : {}), }); return keyAuthorization; })(); const keyAuthorization = keyAuthorization_signed ? KeyAuthorization.toRpc(keyAuthorization_signed) : undefined; return { accounts, keyAuthorization, signature: signature_, username, ...(personalSign ? { personalSign: { message: personalSign.message, // On the witness path the auth proof IS the signed key // authorization; surface it so the verifier can run the // TIP-1053 check. ...(witness && keyAuthorization_signed ? { keyAuthorization: KeyAuthorization.serialize(keyAuthorization_signed) } : {}), }, } : {}), }; }, }, getAccount(options = {}) { return { account: getAccount({ address: options.address, signable: true }) }; }, }; }); } //# sourceMappingURL=local.js.map