accounts
Version:
Tempo Accounts SDK
315 lines • 14.5 kB
JavaScript
import { Provider as ox_Provider } from 'ox';
import { KeyAuthorization, SignatureEnvelope } from 'ox/tempo';
import { hashMessage } from 'viem';
import { prepareTransactionRequest } from 'viem/actions';
import { Account as TempoAccount, Actions } from 'viem/tempo';
import * as z from 'zod/mini';
import * as AccessKey from '../AccessKey.js';
import * as Account from '../Account.js';
import * as Adapter from '../Adapter.js';
import * as AccessKeyTransaction from '../internal/AccessKeyTransaction.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 }) => {
async function prepareTransaction(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 request = {
...rest,
...(feePayer ? { feePayer: true } : {}),
};
const state = store.getState();
const address = parameters.from ?? state.accounts[state.activeAccount]?.address;
const transaction = address
? await AccessKeyTransaction.create({
address,
calls: parameters.calls,
chainId: parameters.chainId ?? state.chainId,
client,
store,
})
: undefined;
if (transaction) {
try {
return await transaction.prepare(request);
}
catch { }
}
const account = getAccount({
address: parameters.from,
signable: true,
});
const prepared = await prepareTransactionRequest(client, {
account,
...request,
type: 'tempo',
});
async function sign() {
return await account.signTransaction(prepared);
}
return {
request: prepared,
sign,
async send() {
const signed = await sign();
return (await client.request({
method: 'eth_sendRawTransaction',
params: [signed],
}));
},
async sendSync() {
const signed = await sign();
return (await client.request({
method: 'eth_sendRawTransactionSync',
params: [signed],
}));
},
};
}
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({ 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 unsigned 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_;
const keyAuthorization = KeyAuthorization.from(keyAuthorization_unsigned.keyAuthorization, {
signature: SignatureEnvelope.from(signature_keyAuthorization),
});
AccessKey.add({
account: account.address,
authorization: keyAuthorization,
...(keyAuthorization_unsigned.keyPair
? { keyPair: keyAuthorization_unsigned.keyPair }
: {}),
store,
});
return KeyAuthorization.toRpc(keyAuthorization);
})();
return {
accounts,
email,
keyAuthorization,
signature: signature_,
username,
...(personalSign ? { personalSign: { message: personalSign.message } } : {}),
};
},
async revokeAccessKey(parameters) {
const account = getAccount({ signable: true });
const client = getClient();
try {
await Actions.accessKey.revoke(client, {
account,
accessKey: parameters.accessKeyAddress,
});
}
catch (error) {
if (!AccessKey.isUnavailableError(error))
throw error;
}
AccessKey.remove({
accessKey: parameters.accessKeyAddress,
account: account.address,
chainId: client.chain.id,
store,
});
},
async signPersonalMessage({ data, address }) {
const account = getAccount({ address, signable: true });
return await account.signMessage({ message: { raw: data } });
},
async signTransaction(parameters) {
const prepared = await prepareTransaction(parameters);
return await prepared.sign();
},
async signTypedData({ data, address }) {
const account = getAccount({ address, signable: true });
const parsed = JSON.parse(data);
return await account.signTypedData(parsed);
},
async sendTransaction(parameters) {
const prepared = await prepareTransaction(parameters);
return await prepared.send();
},
async sendTransactionSync(parameters) {
const prepared = await prepareTransaction(parameters);
return await prepared.sendSync();
},
},
};
});
}
//# sourceMappingURL=local.js.map