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