accounts
Version:
Tempo Accounts SDK
487 lines • 23 kB
JavaScript
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