accounts
Version:
Tempo Accounts SDK
401 lines • 19.5 kB
JavaScript
import { Address as core_Address, Base64, Hex, P256, Provider as core_Provider, PublicKey, RpcResponse, } from 'ox';
import { KeyAuthorization } from 'ox/tempo';
import { Actions, Account as TempoAccount, Secp256k1 } from 'viem/tempo';
import * as AccessKey from '../core/AccessKey.js';
import * as Adapter from '../core/Adapter.js';
import * as AccessKeyTransaction from '../core/internal/AccessKeyTransaction.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.add({
account: address,
authorization: keyAuthorization,
privateKey: entry.key,
store,
});
else
AccessKey.remove({
account: address,
accessKey: keyAuthorization.address,
chainId: Number(keyAuthorization.chainId),
store,
});
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.add({
account: address,
authorization: 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 prepareManagedTransaction(client, parameters, options = {}) {
const state = store.getState();
const address = parameters.from ?? state.accounts[state.activeAccount]?.address;
if (!address)
throw new core_Provider.DisconnectedError({ message: 'No active account.' });
const managedKey = await loadManagedKey(address);
if (managedKey && !(await isManagedKeyAuthorized(address, managedKey)))
await reauthorizeManagedKey(address, managedKey);
const transaction = await AccessKeyTransaction.create({
address,
calls: options.calls,
chainId: options.chainId ?? state.chainId,
client,
store,
});
if (!transaction)
throw new core_Provider.UnauthorizedError({
message: `Account "${address}" cannot sign with an access key.`,
});
return await transaction.prepare(parameters);
}
async function loadManagedAccount(address) {
await loadManagedKey(address);
return getAccount({ address, signable: true });
}
async function authorize(request) {
const { host, redirectUri, open = defaultOpen } = options;
const { account, authorizeAccessKey, method, personalSign, showDeposit } = 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) })) }
: {}),
...(personalSign ? { personalSign } : {}),
pubKey: publicKey,
...(showDeposit !== undefined ? { showDeposit } : {}),
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;
const signature = params.get('signature');
const personalSignMessage = params.get('personalSignMessage');
if (managedKey)
await saveManagedKey(accountAddress, managedKey, signed);
return {
accountAddress: accountAddress,
keyAuthorization: signed,
...(personalSignMessage ? { personalSign: { message: personalSignMessage } } : {}),
...(signature ? { signature } : {}),
};
}
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',
...(parameters.showDeposit !== undefined
? { showDeposit: parameters.showDeposit }
: {}),
});
if (!account)
store.setState({
accounts: [{ address: result.accountAddress }],
activeAccount: 0,
});
return {
keyAuthorization: KeyAuthorization.toRpc(result.keyAuthorization),
rootAddress: result.accountAddress,
};
},
async createAccount(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',
...(parameters?.personalSign ? { personalSign: parameters.personalSign } : {}),
...(parameters?.showDeposit !== undefined
? { showDeposit: parameters.showDeposit }
: {}),
});
return {
accounts: [
{
address: result.accountAddress,
capabilities: {},
},
],
keyAuthorization: KeyAuthorization.toRpc(result.keyAuthorization),
...(result.personalSign ? { personalSign: result.personalSign } : {}),
...(result.signature ? { signature: result.signature } : {}),
};
},
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',
...(parameters?.personalSign ? { personalSign: parameters.personalSign } : {}),
...(parameters?.showDeposit !== undefined
? { showDeposit: parameters.showDeposit }
: {}),
});
return {
accounts: [
{
address: result.accountAddress,
capabilities: {},
},
],
keyAuthorization: KeyAuthorization.toRpc(result.keyAuthorization),
...(result.personalSign ? { personalSign: result.personalSign } : {}),
...(result.signature ? { signature: result.signature } : {}),
};
},
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 prepared = await prepareManagedTransaction(client, {
...rest,
...(feePayer ? { feePayer: true } : {}),
}, {
calls: parameters.calls,
chainId: parameters.chainId,
});
return await prepared.send();
},
async sendTransactionSync(parameters) {
const { feePayer, ...rest } = parameters;
const client = getClient(typeof feePayer === 'string' ? { feePayer } : {});
const prepared = await prepareManagedTransaction(client, {
...rest,
...(feePayer ? { feePayer: true } : {}),
}, {
calls: parameters.calls,
chainId: parameters.chainId,
});
return await prepared.sendSync();
},
async signPersonalMessage({ address, data }) {
const account = await loadManagedAccount(address);
return await account.signMessage({ message: { raw: data } });
},
async signTransaction(parameters) {
const { feePayer, ...rest } = parameters;
const client = getClient(typeof feePayer === 'string' ? { feePayer } : {});
const prepared = await prepareManagedTransaction(client, {
...rest,
...(feePayer ? { feePayer: true } : {}),
}, {
calls: parameters.calls,
chainId: parameters.chainId,
});
return await prepared.sign();
},
async signTypedData({ address, data }) {
const account = await loadManagedAccount(address);
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) {
const url = new URL('/remote/auth/mobile', host);
url.searchParams.set('pubKey', params.pubKey);
if (params.keyType)
url.searchParams.set('keyType', params.keyType);
url.searchParams.set('chainId', `0x${params.chainId.toString(16)}`);
if (typeof params.expiry !== 'undefined')
url.searchParams.set('expiry', `0x${params.expiry.toString(16)}`);
if (params.limits)
url.searchParams.set('limits', JSON.stringify(params.limits));
if (params.personalSign)
url.searchParams.set('personalSign', JSON.stringify(params.personalSign));
if (params.showDeposit === true)
url.searchParams.set('showDeposit', 'true');
else if (params.showDeposit)
url.searchParams.set('showDeposit', JSON.stringify(params.showDeposit));
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