accounts
Version:
Tempo Accounts SDK
189 lines (181 loc) • 7.9 kB
text/typescript
import { Hex, Provider as core_Provider } from 'ox'
import { custom } from 'viem'
import { z } from 'zod/mini'
import * as Adapter from '../../Adapter.js'
import type * as Keystore from '../../Keystore.js'
import * as Rpc from '../../zod/rpc.js'
import * as u from '../../zod/utils.js'
/**
* Builds an adapter that forwards wallet RPC to a remote wallet through a
* single request function.
*
* Shared core for adapters whose wallet lives behind a request/response
* session (e.g. Wata mobile web auth or postMessage). Access keys are
* generated locally; every other wallet action round-trips through
* `request`.
*/
export function fromRequest(options: fromRequest.Options): Adapter.Adapter {
const {
actions,
bind,
cleanup,
close,
icon,
keystores,
name,
rdns,
request: requestRemote,
} = options
return Adapter.define(
{ ...(icon ? { icon } : {}), name, rdns, schema: z.object({ address: u.address() }) },
({ getAccount, getClient, store }) => {
// Late-bind the store so the request channel can reconcile local
// connection state (e.g. a wallet `accountsChanged` notification).
bind?.({ store })
const provider = core_Provider.from({
async request(request) {
const { accounts, activeAccount, chainId } = store.getState()
if (request.method === 'eth_chainId') return Hex.fromNumber(chainId)
// Forward the active account + chain as request context so the wallet
// acts against the session the app believes it is using rather than
// whatever the wallet last selected for itself.
const account = accounts[activeAccount]?.address
return await requestRemote({
method: request.method,
params: request.params as readonly unknown[] | undefined,
context: { chainId, ...(account ? { account } : {}) },
})
},
})
// Connect capabilities are read from the first account only: the remote
// wallet connects a single account per exchange.
function toConnectReturn(result: Rpc.wallet_connect.Encoded['returns']) {
const capabilities = result.accounts[0]?.capabilities
return {
accounts: result.accounts.map((account) => ({ address: account.address })),
...(capabilities?.auth ? { auth: capabilities.auth } : {}),
...(capabilities?.identity ? { identity: capabilities.identity } : {}),
...(capabilities?.keyAuthorization
? { keyAuthorization: capabilities.keyAuthorization }
: {}),
...(capabilities?.personalSign ? { personalSign: capabilities.personalSign } : {}),
...(capabilities?.signature ? { signature: capabilities.signature } : {}),
...(capabilities?.username !== undefined ? { username: capabilities.username } : {}),
}
}
return {
forwardsAuth: true,
...(keystores ? { accessKey: { keystores } } : {}),
actions: {
async createAccount(_parameters, request) {
return toConnectReturn(
(await provider.request(request)) as Rpc.wallet_connect.Encoded['returns'],
)
},
async loadAccounts(_parameters, request) {
return toConnectReturn(
(await provider.request(request)) as Rpc.wallet_connect.Encoded['returns'],
)
},
async deposit(_parameters, request) {
return (await provider.request(request)) as Rpc.wallet_deposit.Encoded['returns']
},
async depositZone(parameters, request) {
return (await provider.request({
...request,
params: [z.encode(Rpc.wallet_depositZone.parameters, parameters)] as const,
})) as Rpc.wallet_depositZone.Encoded['returns']
},
// `disconnect` (when `close` is set) only tears down the local
// wallet session — it is never forwarded. No `switchChain` action:
// chain selection is local state the provider handles itself, and
// forwarding it would open a wallet session for nothing.
...(close ? { disconnect: close } : {}),
async swap(_parameters, request) {
return (await provider.request(request)) as Rpc.wallet_swap.Encoded['returns']
},
async transfer(parameters, request) {
return (await provider.request({
...request,
params: [z.encode(Rpc.wallet_transfer.parameters, parameters)] as const,
})) as Rpc.wallet_transfer.Encoded['returns']
},
async withdrawZone(parameters, request) {
return (await provider.request({
...request,
params: [z.encode(Rpc.wallet_withdrawZone.parameters, parameters)] as const,
})) as Rpc.wallet_withdrawZone.Encoded['returns']
},
...actions?.({ getClient, request: requestRemote, store }),
},
...(cleanup ? { cleanup } : close ? { cleanup: () => void close() } : {}),
getAccount(parameters = {}) {
return {
account: {
address: parameters.address ?? getAccount({ signable: false }).address,
type: 'json-rpc' as const,
},
transport: custom(provider),
}
},
}
},
)
}
export declare namespace fromRequest {
/** Options for {@link fromRequest}. */
export type Options = {
/**
* Adapter-specific action overrides layered over the request-forwarding
* defaults. Receives the request channel plus the adapter's client and
* store for actions that need local bookkeeping around an exchange.
*/
actions?:
| ((context: {
getClient: Adapter.SetupFn.Parameters['getClient']
request: Options['request']
store: Adapter.SetupFn.Parameters['store']
}) => Partial<Adapter.Instance['actions']>)
| undefined
/**
* Called once during setup with the adapter's store, for late wiring
* that needs to mutate local state (e.g. reconciling connection state
* from a wallet `accountsChanged` notification on the request channel).
*/
bind?: ((context: { store: Adapter.SetupFn.Parameters['store'] }) => void) | undefined
/**
* Tears down adapter-owned resources (sessions, mounted UI) when the
* provider discards the adapter instance. @default `close`, when set.
*/
cleanup?: (() => void) | undefined
/** Data URI of the provider icon, announced via EIP-6963. */
icon?: Adapter.Meta['icon'] | undefined
/**
* Default keystores backing locally generated access keys. Omit to use
* the SDK default (non-extractable `webCryptoP256`). Pass an explicit
* keystore where the transport's environment needs one — e.g. pure-JS
* P-256 for React Native, which may lack WebCrypto and persists access
* keys through string-based storage.
*/
keystores?: Keystore.Keystores | undefined
/**
* Tears down the adapter's wallet session. Wired to the `disconnect`
* action and instance cleanup; never forwarded to the remote wallet.
* Omit for single-exchange transports with no session to clean up.
*/
close?: (() => Promise<void>) | undefined
/** Provider display name. */
name: string
/** Reverse-DNS provider identifier. */
rdns: string
/** Forwards a single wallet JSON-RPC request to the remote wallet. */
request: (request: {
/** JSON-RPC method name. */
method: string
/** JSON-RPC params. */
params?: readonly unknown[] | undefined
/** Session selectors (active account + chain) for the wallet to act on. */
context?: { account?: string | undefined; chainId?: number | undefined } | undefined
}) => Promise<unknown>
}
}