UNPKG

accounts

Version:

Tempo Accounts SDK

487 lines 23 kB
import { Address as core_Address, Bytes, Hex, Provider as ox_Provider, PublicKey, RpcResponse, Secp256k1, } from 'ox'; import { KeyAuthorization } from 'ox/tempo'; import { hashMessage, hashTypedData, isAddressEqual } from 'viem'; import { prepareTransactionRequest } from 'viem/actions'; import { Account as TempoAccount } from 'viem/tempo'; import * as AccessKey from '../AccessKey.js'; import * as Adapter from '../Adapter.js'; import * as Store from '../Store.js'; const turnkeySessionErrorCodes = new Set([ 'API_KEY_EXPIRED', 'NO_SESSION_FOUND', 'REQUEST_NOT_AUTHORIZED', 'SESSION_EXPIRED', 'SIGNATURE_INVALID', 'SIGNATURE_MISSING', 'UNAUTHENTICATED', 'UNAUTHORIZED', ]); /** * Creates a Turnkey adapter backed by `@turnkey/core` client sessions and Ethereum wallet accounts. * * The adapter owns silent reconnect, session-expiry cleanup, and provider signing actions. * Apps provide the UI-bearing login or sign-up flow through `loadAccounts`. The adapter * fetches Ethereum wallet accounts from Turnkey after the flow completes. Provide * `createAccount` only when registration needs a distinct Turnkey flow. * * @example * ```ts * import { TurnkeyClient, generateWalletAccountsFromAddressFormat } from '@turnkey/core' * import { Provider, turnkey } from 'accounts' * * const provider = Provider.create({ * adapter: turnkey({ * client: new TurnkeyClient({ organizationId, authProxyConfigId }), * createAccount: async ({ client, parameters }) => { * await client.signUpWithPasskey({ * passkeyDisplayName: parameters.name, * createSubOrgParams: { * userName: parameters.name, * customWallet: { * walletName: 'FooBar', * walletAccounts: generateWalletAccountsFromAddressFormat({ * addresses: ['ADDRESS_FORMAT_ETHEREUM'], * }), * }, * }, * }) * }, * loadAccounts: async ({ client }) => { * await client.loginWithPasskey() * }, * }), * }) * ``` */ export function turnkey(options) { const { icon, name = 'Turnkey', rdns = 'com.turnkey', sessionSkewMs = 10_000 } = options; return Adapter.define({ icon, name, rdns }, ({ getAccount, getClient, store }) => { let turnkeyClient_promise; let expiry_timeout; let restore_promise; let walletAccounts = []; async function getTurnkeyClient() { turnkeyClient_promise ??= (async () => { const { client } = options; await client.init?.(); return client; })(); return await turnkeyClient_promise; } function toStoreAccount(account, label) { return { address: core_Address.from(account.address), ...(label ? { label } : {}), }; } function toTempoAccount(account) { const publicKey = toPublicKey(account); assertAddress(account, publicKey); const sign = async (parameters) => await signPayload({ payload: parameters.hash, turnkeyClient: await getTurnkeyClient(), walletAccount: account, }); return TempoAccount.from({ keyType: 'secp256k1', publicKey, sign, }); } function toPublicKey(account) { const publicKey = account.publicKey.startsWith('0x') ? account.publicKey : `0x${account.publicKey}`; Hex.assert(publicKey, { strict: true }); return PublicKey.from(Secp256k1.noble.ProjectivePoint.fromHex(Bytes.fromHex(publicKey))); } function assertAddress(account, publicKey) { const address = core_Address.from(account.address); const address_publicKey = core_Address.fromPublicKey(publicKey); if (isAddressEqual(address, address_publicKey)) return; throw new RpcResponse.InternalError({ message: `Turnkey account publicKey does not match address "${address}".`, }); } async function fetchWalletAccounts() { const turnkeyClient = await getTurnkeyClient(); return (await turnkeyClient.fetchWallets()).flatMap((wallet) => wallet.accounts.filter((account) => account.addressFormat === 'ADDRESS_FORMAT_ETHEREUM')); } function selectWalletAccounts(accounts, addresses) { if (!addresses) return accounts; return addresses.map((address) => { const address_ = core_Address.from(address); const account = accounts.find((account) => isAddressEqual(core_Address.from(account.address), address_)); if (account) return account; throw new RpcResponse.InternalError({ message: `Turnkey callback returned address "${address_}" that was not found in fetched wallet accounts.`, }); }); } function clear() { if (expiry_timeout) clearTimeout(expiry_timeout); expiry_timeout = undefined; restore_promise = undefined; walletAccounts = []; store.setState({ accessKeys: [], accounts: [], activeAccount: 0 }); } function scheduleExpiry(session) { if (expiry_timeout) clearTimeout(expiry_timeout); expiry_timeout = undefined; const delay = Math.max(session.expiry * 1000 - Date.now() - sessionSkewMs, 0); expiry_timeout = setTimeout(() => clear(), delay); } async function getValidSession() { const turnkeyClient = await getTurnkeyClient(); const session = await turnkeyClient.getSession(); if (!session || session.expiry * 1000 - sessionSkewMs <= Date.now()) { clear(); return undefined; } scheduleExpiry(session); return session; } async function restore() { await Store.waitForHydration(store); if (walletAccounts.length > 0) return; if (restore_promise) return await restore_promise; restore_promise = (async () => { const state = store.getState(); const persisted = state.accounts; if (persisted.length === 0) return; const session = await getValidSession(); if (!session) return; const restored = await fetchWalletAccounts(); walletAccounts = persisted .map((account) => restored.find((walletAccount) => isAddressEqual(core_Address.from(walletAccount.address), account.address))) .filter((account) => !!account); if (walletAccounts.length === 0) return; store.setState({ accounts: walletAccounts.map((account) => toStoreAccount(account)), activeAccount: Math.min(state.activeAccount, walletAccounts.length - 1), }); })(); try { await restore_promise; } finally { restore_promise = undefined; } } async function requireSession() { const session = await getValidSession(); if (!session) throw new ox_Provider.DisconnectedError({ message: 'Turnkey session expired.' }); } async function accountForSigning(address) { await restore(); await requireSession(); const address_ = address ?? store.getState().accounts[store.getState().activeAccount]?.address; if (!address_) throw new ox_Provider.DisconnectedError({ message: 'No accounts connected.' }); const account = walletAccounts.find((account) => isAddressEqual(core_Address.from(account.address), address_)); if (account) return account; if (walletAccounts.length === 0) throw new ox_Provider.DisconnectedError({ message: 'No Turnkey account connected.', }); throw new ox_Provider.UnauthorizedError({ message: `Account "${address_}" not found.` }); } function signatureToHex(value) { const v = value.v.startsWith('0x') ? value.v : Hex.fromNumber(Number(value.v)); return Hex.concat(value.r, value.s, Hex.padLeft(v, 1)); } async function signPayload(parameters) { const { payload, turnkeyClient, walletAccount } = parameters; const result = await turnkeyClient.httpClient .signRawPayload({ encoding: 'PAYLOAD_ENCODING_HEXADECIMAL', hashFunction: 'HASH_FUNCTION_NO_OP', payload, signWith: walletAccount.address, }) .catch((error) => { if (!isSessionError(error)) throw error; clear(); throw new ox_Provider.DisconnectedError({ message: 'Turnkey session expired.' }); }); return signatureToHex(result); } async function withAccessKey(options, fn) { const account = (() => { try { return getAccount({ ...options, signable: true }); } catch { return undefined; } })(); if (!account || account.source !== 'accessKey') return undefined; const keyAuthorization = AccessKey.getPending(account, { store }); try { const result = await fn(account, keyAuthorization ?? undefined); return { account, result }; } catch (error) { AccessKey.invalidate(account, error, { store }); return undefined; } } async function signTransaction(parameters) { const account = toTempoAccount(await accountForSigning(parameters.from)); const { feePayer, ...rest } = parameters; const viemClient = getClient({ chainId: parameters.chainId, feePayer: feePayer === true ? undefined : feePayer, }); const prepared = await prepareTransactionRequest(viemClient, { account, ...rest, ...(feePayer ? { feePayer: true } : {}), type: 'tempo', }); return await account.signTransaction(prepared); } function isSessionError(error) { const code = getTurnkeyErrorCode(error); return !!code && turnkeySessionErrorCodes.has(code); } function getTurnkeyErrorCode(error) { if (!isObject(error)) return undefined; if (typeof error.code === 'string') return error.code; if (Array.isArray(error.details)) { for (const detail of error.details) { if (!isObject(detail)) continue; if (typeof detail.turnkeyErrorCode === 'string') return detail.turnkeyErrorCode; } } return getTurnkeyErrorCode(error.cause); } function isObject(value) { return typeof value === 'object' && value !== null; } void restore(); return { cleanup() { if (expiry_timeout) clearTimeout(expiry_timeout); }, actions: { async createAccount(parameters) { const { authorizeAccessKey, personalSign } = parameters; if (personalSign && parameters.digest) throw new ox_Provider.ProviderRpcError(-32602, '`digest` and `personalSign` cannot both be set on `wallet_connect`.'); const turnkeyClient = await getTurnkeyClient(); const addresses = options.createAccount ? await options.createAccount({ client: turnkeyClient, parameters }) : await options.loadAccounts({ client: turnkeyClient, parameters: { authorizeAccessKey, digest: parameters.digest, ...(personalSign ? { personalSign } : {}), }, }); await requireSession(); walletAccounts = selectWalletAccounts(await fetchWalletAccounts(), addresses); restore_promise = undefined; const digest = personalSign ? hashMessage(personalSign.message) : parameters.digest; const account = walletAccounts[0]; const keyAuthorization = authorizeAccessKey ? account ? await AccessKey.authorize({ account: toTempoAccount(account), chainId: getClient().chain.id, parameters: authorizeAccessKey, store, }) : undefined : undefined; return { accounts: walletAccounts.map((account, index) => toStoreAccount(account, index === 0 ? parameters.name : undefined)), ...(personalSign ? { personalSign: { message: personalSign.message } } : {}), ...(keyAuthorization ? { keyAuthorization } : {}), signature: digest && account ? await signPayload({ payload: digest, turnkeyClient, walletAccount: account, }) : undefined, }; }, async loadAccounts(parameters) { const { authorizeAccessKey, personalSign } = parameters ?? {}; if (personalSign && parameters?.digest) throw new ox_Provider.ProviderRpcError(-32602, '`digest` and `personalSign` cannot both be set on `wallet_connect`.'); const turnkeyClient = await getTurnkeyClient(); const addresses = await options.loadAccounts({ client: turnkeyClient, parameters }); await requireSession(); walletAccounts = selectWalletAccounts(await fetchWalletAccounts(), addresses); restore_promise = undefined; const digest = personalSign ? hashMessage(personalSign.message) : parameters?.digest; const account = walletAccounts[0]; const keyAuthorization = authorizeAccessKey && account ? await AccessKey.authorize({ account: toTempoAccount(account), chainId: getClient().chain.id, parameters: authorizeAccessKey, store, }) : undefined; return { accounts: walletAccounts.map((account) => toStoreAccount(account)), ...(personalSign ? { personalSign: { message: personalSign.message } } : {}), ...(keyAuthorization ? { keyAuthorization } : {}), signature: digest && account ? await signPayload({ payload: digest, turnkeyClient, walletAccount: account, }) : undefined, }; }, async authorizeAccessKey(parameters) { const account = await accountForSigning(undefined); const keyAuthorization = await AccessKey.authorize({ account: toTempoAccount(account), chainId: getClient().chain.id, parameters, store, }); return { keyAuthorization, rootAddress: core_Address.from(account.address) }; }, async signPersonalMessage(parameters) { const turnkeyClient = await getTurnkeyClient(); const account = await accountForSigning(parameters.address); return await signPayload({ payload: hashMessage({ raw: parameters.data }), turnkeyClient, walletAccount: account, }); }, async signTransaction(parameters) { const result = await withAccessKey({ address: parameters.from, calls: parameters.calls, chainId: parameters.chainId }, async (account, keyAuthorization) => { const { feePayer, ...rest } = parameters; const viemClient = getClient({ chainId: parameters.chainId, feePayer: feePayer === true ? undefined : feePayer, }); const prepared = await prepareTransactionRequest(viemClient, { account, ...rest, ...(feePayer ? { feePayer: true } : {}), keyAuthorization, type: 'tempo', }); return await account.signTransaction(prepared); }); if (result !== undefined) return result.result; return await signTransaction(parameters); }, async signTypedData(parameters) { const turnkeyClient = await getTurnkeyClient(); const account = await accountForSigning(parameters.address); const typedData = JSON.parse(parameters.data); return await signPayload({ payload: hashTypedData(typedData), turnkeyClient, walletAccount: account, }); }, async sendTransaction(parameters) { const result = await withAccessKey({ address: parameters.from, calls: parameters.calls, chainId: parameters.chainId }, async (account, keyAuthorization) => { const { feePayer, ...rest } = parameters; const viemClient = getClient({ chainId: parameters.chainId, feePayer: feePayer === true ? undefined : feePayer, }); const prepared = await prepareTransactionRequest(viemClient, { account, ...rest, ...(feePayer ? { feePayer: true } : {}), keyAuthorization, type: 'tempo', }); const signed = await account.signTransaction(prepared); return await viemClient.request({ method: 'eth_sendRawTransaction', params: [signed], }); }); if (result !== undefined) { AccessKey.removePending(result.account, { store }); return result.result; } const signed = await signTransaction(parameters); const viemClient = getClient({ chainId: parameters.chainId, feePayer: parameters.feePayer === true ? undefined : parameters.feePayer, }); return await viemClient.request({ method: 'eth_sendRawTransaction', params: [signed], }); }, async sendTransactionSync(parameters) { const result = await withAccessKey({ address: parameters.from, calls: parameters.calls, chainId: parameters.chainId }, async (account, keyAuthorization) => { const { feePayer, ...rest } = parameters; const viemClient = getClient({ chainId: parameters.chainId, feePayer: feePayer === true ? undefined : feePayer, }); const prepared = await prepareTransactionRequest(viemClient, { account, ...rest, ...(feePayer ? { feePayer: true } : {}), keyAuthorization, type: 'tempo', }); const signed = await account.signTransaction(prepared); return await viemClient.request({ method: 'eth_sendRawTransactionSync', params: [signed], }); }); if (result !== undefined) { AccessKey.removePending(result.account, { store }); return result.result; } const signed = await signTransaction(parameters); const viemClient = getClient({ chainId: parameters.chainId, feePayer: parameters.feePayer === true ? undefined : parameters.feePayer, }); return await viemClient.request({ method: 'eth_sendRawTransactionSync', params: [signed], }); }, async disconnect() { await (await getTurnkeyClient()).logout(); clear(); }, }, }; }); } //# sourceMappingURL=turnkey.js.map