UNPKG

accounts

Version:

Tempo Accounts SDK

373 lines 18 kB
import { Address as core_Address, Base64, Hex, P256, Provider as core_Provider, PublicKey, RpcResponse, } from 'ox'; import { KeyAuthorization } from 'ox/tempo'; import { prepareTransactionRequest } from 'viem/actions'; import { Actions, Account as TempoAccount, Secp256k1 } from 'viem/tempo'; import * as AccessKey from '../core/AccessKey.js'; import * as Adapter from '../core/Adapter.js'; /** * Creates a React Native adapter that authorizes access keys via the system browser. * * Authentication opens a browser session and completes via a redirect callback * that returns the signed key authorization. */ export function reactNative(options) { const { name = 'Tempo Mobile', rdns = 'xyz.tempo.mobile' } = options; return Adapter.define({ name, rdns }, ({ getAccount, getClient, store }) => { async function loadManagedKey(address, parameters = {}) { const { keyType } = parameters; const { secureStorage } = options; if (!secureStorage) return undefined; const { chainId } = store.getState(); const storageKeys = keyType ? [managedKeyStorageKey(address, chainId, keyType)] : [ managedKeyStorageKey(address, chainId, 'secp256k1'), managedKeyStorageKey(address, chainId, 'p256'), managedKeyStorageKey(address, chainId), ]; let entry = null; for (const storageKey of storageKeys) { entry = await secureStorage.getItem(storageKey); if (entry) break; } if (!entry) return undefined; const account = entry.keyType === 'p256' ? TempoAccount.fromP256(entry.key, { access: address }) : TempoAccount.fromSecp256k1(entry.key, { access: address }); const keyAddress = core_Address.fromPublicKey(PublicKey.from(account.publicKey)); const deserialized = KeyAuthorization.deserialize(entry.keyAuthorization); if (!deserialized.signature) throw new Error('Managed access key is missing a signature.'); const keyAuthorization = deserialized; if (keyAuthorization.address.toLowerCase() === keyAddress.toLowerCase()) AccessKey.save({ address, keyAuthorization, privateKey: entry.key, store, }); else store.setState((state) => ({ accessKeys: state.accessKeys.filter((accessKey) => accessKey.address.toLowerCase() !== keyAuthorization.address.toLowerCase()), })); return { account, expiry: entry.expiry, key: entry.key, keyAddress, keyType: entry.keyType, publicKey: account.publicKey, storedAuthorization: keyAuthorization, }; } async function resolveManagedKey(resolveOptions = {}) { const { address, keyType } = resolveOptions; const requestedKeyType = keyType === 'p256' || keyType === 'secp256k1' ? keyType : undefined; const entry = address ? await loadManagedKey(address, requestedKeyType ? { keyType: requestedKeyType } : {}) : undefined; if (entry) return { account: entry.account, key: entry.key, keyAddress: entry.keyAddress, keyType: entry.keyType, publicKey: entry.publicKey, }; const nextKeyType = requestedKeyType === 'p256' ? 'p256' : 'secp256k1'; const key = nextKeyType === 'p256' ? P256.randomPrivateKey() : Secp256k1.randomPrivateKey(); const account = nextKeyType === 'p256' ? TempoAccount.fromP256(key, address ? { access: address } : undefined) : TempoAccount.fromSecp256k1(key, address ? { access: address } : undefined); return { account, key, keyAddress: core_Address.fromPublicKey(PublicKey.from(account.publicKey)), keyType: nextKeyType, publicKey: account.publicKey, }; } async function saveManagedKey(address, managedKey, keyAuthorization) { if (!managedKey) return; AccessKey.save({ address, keyAuthorization, privateKey: managedKey.key, store, }); const { secureStorage } = options; if (!secureStorage) return; const { chainId } = store.getState(); const storageKey = managedKeyStorageKey(address, chainId, managedKey.keyType); const entry = { chainId, expiry: keyAuthorization.expiry ?? 0, key: managedKey.key, keyAddress: managedKey.keyAddress, keyAuthorization: KeyAuthorization.serialize(keyAuthorization), keyType: managedKey.keyType, walletAddress: address, }; await secureStorage.setItem(storageKey, entry); } async function isManagedKeyAuthorized(address, managedKey) { try { const metadata = await Actions.accessKey.getMetadata(getClient(), { account: address, accessKey: managedKey.keyAddress, }); return (metadata.address.toLowerCase() === managedKey.keyAddress.toLowerCase() && !metadata.isRevoked); } catch { return false; } } async function reauthorizeManagedKey(address, managedKey) { const result = await authorize({ account: address, authorizeAccessKey: { expiry: managedKey.expiry, keyType: managedKey.keyType, ...(managedKey.storedAuthorization.limits ? { limits: managedKey.storedAuthorization.limits.map((limit) => ({ ...limit })) } : {}), publicKey: managedKey.publicKey, }, method: 'wallet_authorizeAccessKey', }); await saveManagedKey(address, managedKey, result.keyAuthorization); return result.keyAuthorization; } async function withManagedAccessKey(fn) { const rootAddress = store.getState().accounts[store.getState().activeAccount]?.address; const managedKey = rootAddress ? await loadManagedKey(rootAddress) : undefined; const account = managedKey?.account ?? getAccount({ signable: true }); let keyAuthorization = AccessKey.getPending(account, { store }); if (rootAddress && managedKey && !keyAuthorization) if (!(await isManagedKeyAuthorized(rootAddress, managedKey))) keyAuthorization = await reauthorizeManagedKey(rootAddress, managedKey); try { return await fn(account, keyAuthorization ?? undefined); } catch (error) { AccessKey.remove(account, { store }); throw error; } } async function authorize(request) { const { host, redirectUri, open = defaultOpen } = options; const { account, authorizeAccessKey, method } = request; const managedKey = authorizeAccessKey && !authorizeAccessKey.publicKey && !authorizeAccessKey.address ? await resolveManagedKey({ ...(account ? { address: account } : {}), ...(authorizeAccessKey.keyType ? { keyType: authorizeAccessKey.keyType } : {}), }) : undefined; const publicKey = authorizeAccessKey?.publicKey ?? managedKey?.publicKey; const keyType = authorizeAccessKey?.keyType ?? managedKey?.keyType; if (!publicKey) throw new RpcResponse.InvalidParamsError({ message: method === 'wallet_connect' ? '`wallet_connect` on the React Native adapter requires `capabilities.authorizeAccessKey`.' : '`wallet_authorizeAccessKey` on the React Native adapter requires key parameters.', }); const state = Base64.fromBytes(Hex.toBytes(Hex.random(16)), { pad: false, url: true }); const authUrl = buildAuthUrl(host, { callback: redirectUri, chainId: store.getState().chainId, ...(typeof authorizeAccessKey?.expiry !== 'undefined' ? { expiry: authorizeAccessKey.expiry } : {}), ...(keyType ? { keyType } : {}), ...(authorizeAccessKey?.limits ? { limits: authorizeAccessKey.limits.map((l) => ({ ...l, limit: String(l.limit) })) } : {}), pubKey: publicKey, state, }); const callbackUrl = await open(authUrl, redirectUri); if (!callbackUrl) throw new AuthCancelledError(); const params = new URL(callbackUrl).searchParams; const returnedState = params.get('state'); if (returnedState !== state) throw new StateMismatchError(); const accountAddress = params.get('accountAddress'); if (!accountAddress) throw new Error('Missing accountAddress in callback.'); const keyAuthorizationHex = params.get('keyAuthorization'); if (!keyAuthorizationHex) throw new Error('Missing keyAuthorization in callback.'); const keyAuthorization = KeyAuthorization.deserialize(keyAuthorizationHex); if (!keyAuthorization.signature) throw new Error('Key authorization in callback is missing a signature.'); const signed = keyAuthorization; if (managedKey) await saveManagedKey(accountAddress, managedKey, signed); return { accountAddress: accountAddress, keyAuthorization: signed, }; } return { actions: { async authorizeAccessKey(parameters) { const { accounts, activeAccount } = store.getState(); const account = accounts[activeAccount]?.address; const result = await authorize({ ...(account ? { account } : {}), authorizeAccessKey: parameters, method: 'wallet_authorizeAccessKey', }); if (!account) store.setState({ accounts: [{ address: result.accountAddress }], activeAccount: 0, }); return { keyAuthorization: KeyAuthorization.toRpc(result.keyAuthorization), rootAddress: result.accountAddress, }; }, async createAccount(params, request) { return this.loadAccounts(params, request); }, async loadAccounts(parameters) { if (parameters?.digest) throw unsupported('`wallet_connect` digest signing not supported by React Native adapter.'); const result = await authorize({ authorizeAccessKey: parameters?.authorizeAccessKey, method: 'wallet_connect', }); return { accounts: [ { address: result.accountAddress, capabilities: {}, }, ], keyAuthorization: KeyAuthorization.toRpc(result.keyAuthorization), }; }, async revokeAccessKey() { throw unsupported('`wallet_revokeAccessKey` not supported by React Native adapter.'); }, async sendTransaction(parameters) { const { feePayer, ...rest } = parameters; const client = getClient(typeof feePayer === 'string' ? { feePayer } : {}); const { account, prepared } = await withManagedAccessKey(async (account, keyAuthorization) => ({ account, prepared: await prepareTransactionRequest(client, { account, ...rest, ...(feePayer ? { feePayer: true } : {}), ...(keyAuthorization ? { 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(typeof feePayer === 'string' ? { feePayer } : {}); const { account, prepared } = await withManagedAccessKey(async (account, keyAuthorization) => ({ account, prepared: await prepareTransactionRequest(client, { account, ...rest, ...(feePayer ? { feePayer: true } : {}), ...(keyAuthorization ? { 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; }, async signPersonalMessage({ address, data }) { await loadManagedKey(address); const account = getAccount({ address, signable: true }); return await account.signMessage({ message: { raw: data } }); }, async signTransaction(parameters) { const { feePayer, ...rest } = parameters; const client = getClient(typeof feePayer === 'string' ? { feePayer } : {}); const { account, prepared } = await withManagedAccessKey(async (account, keyAuthorization) => ({ account, prepared: await prepareTransactionRequest(client, { account, ...rest, ...(feePayer ? { feePayer: true } : {}), ...(keyAuthorization ? { keyAuthorization } : {}), type: 'tempo', }), })); return await account.signTransaction(prepared); }, async signTypedData({ address, data }) { await loadManagedKey(address); const account = getAccount({ address, signable: true }); return await account.signTypedData(JSON.parse(data)); }, }, }; }); } class AuthCancelledError extends Error { constructor() { super('Authentication was cancelled by the user.'); this.name = 'AuthCancelledError'; } } class StateMismatchError extends Error { constructor() { super('State parameter mismatch — possible CSRF attack.'); this.name = 'StateMismatchError'; } } async function defaultOpen(url, redirectUri) { const { openAuthSessionAsync } = await import('expo-web-browser'); const result = await openAuthSessionAsync(url, redirectUri); if (result.type !== 'success') return null; return result.url; } function buildAuthUrl(host, params) { // TODO: use the new host // const url = new URL('/remote/auth/mobile', host) const url = new URL('/mobile-auth', host); url.searchParams.set('pubKey', params.pubKey); if (params.keyType) url.searchParams.set('keyType', params.keyType); url.searchParams.set('chainId', String(params.chainId)); if (typeof params.expiry !== 'undefined') url.searchParams.set('expiry', String(params.expiry)); if (params.limits) url.searchParams.set('limits', JSON.stringify(params.limits)); url.searchParams.set('callback', params.callback); url.searchParams.set('state', params.state); return url.toString(); } function managedKeyStorageKey(address, chainId, keyType) { return `managedKey.${address.toLowerCase()}.${chainId}${keyType ? `.${keyType}` : ''}`; } function unsupported(message) { return new core_Provider.UnsupportedMethodError({ message }); } //# sourceMappingURL=adapter.js.map