History
wallet.history() returns one activity list for the account across every chain you
registered a source for, newest first. Each row has a title a person can read (“Received
USDC”, “Staked ETH”, “Swapped USDC → ETH”), the amounts that left and arrived, a fee, and an
explorer link.
Three layers make it:
- Sources read the chain: one per chain id, from an explorer or the RPC.
- Recognizers turn a transaction into a row. Every registered protocol module recognizes its own transactions; a generic fallback covers sends, receives, approvals and unknown calls.
- The intent journal records what your app meant when it broadcast, so a swap shows as one pending row before any explorer has it.
Snippets come from examples/history, which our CI typechecks and tests.
npm install @anywalletkit/kit @anywalletkit/history-evmimport { Wallet, createMemoryIntentJournal, type FeedItem, type HistoryFeedPage, type Intent, type IntentJournal } from '@anywalletkit/kit'import type { ActivityAsset, ChainTx } from '@anywalletkit/core'import { createEvmHistory } from '@anywalletkit/history-evm'import LidoProtocolEvm from '@anywalletkit/protocol-staking-lido-evm'import { WalletManagerEvm } from '@anywalletkit/wallet-evm'import { WalletManagerSolana, createSolanaHistory } from '@anywalletkit/wallet-solana'import { WalletManagerTon, createTonHistory } from '@anywalletkit/wallet-ton'import { WalletManagerTron, createTronHistory } from '@anywalletkit/wallet-tron'Set up
Section titled “Set up”/** Keys for explorer APIs: on the client, point the URLs at your proxy instead. */export interface HistoryKeys { etherscan?: string; blockscoutPro?: string; tonapi?: string; trongrid?: string }
export function walletWithHistory(seedPhrase: string, keys: HistoryKeys = {}): Wallet { const wallet = new Wallet(seedPhrase, { activity: { // A label for contract calls no module recognizes. known: { 'eip155:1': { '0x66a9893cc07d91d95644aedd05d03f95e1dba8af': 'Uniswap Universal Router' } }, onError: (report) => console.warn('history', report.stage, report.chain ?? report.module, report.error), }, }) // The accounts: history reads account `index` on every chain that has a source. // One registration per EVM network, each with its own RPC (see Chain ids: one RPC per EVM network). wallet.registerWallet('eip155:1', WalletManagerEvm, { provider: 'https://ethereum-rpc.publicnode.com', chainId: 1 }) wallet.registerWallet('eip155:8453', WalletManagerEvm, { provider: 'https://base-rpc.publicnode.com', chainId: 8453 }) wallet.registerWallet('solana', WalletManagerSolana, { provider: 'https://api.mainnet-beta.solana.com' }) wallet.registerWallet('ton', WalletManagerTon, { tonClient: { url: 'https://toncenter.com/api/v2/jsonRPC' } }) wallet.registerWallet('tron', WalletManagerTron, { provider: 'https://api.trongrid.io' })
// One source per chain. const evmKeys = { ...(keys.etherscan ? { etherscanApiKey: keys.etherscan } : {}), ...(keys.blockscoutPro ? { blockscoutProApiKey: keys.blockscoutPro } : {}) } wallet.registerHistorySource(createEvmHistory({ chainId: 1, ...evmKeys })) wallet.registerHistorySource(createEvmHistory({ chainId: 8453, ...evmKeys })) wallet.registerHistorySource(createSolanaHistory({ rpcUrl: 'https://api.mainnet-beta.solana.com' })) wallet.registerHistorySource(createTonHistory(keys.tonapi ? { apiKey: keys.tonapi } : {})) wallet.registerHistorySource(createTronHistory(keys.trongrid ? { apiKey: keys.trongrid } : {}))
// Every registered protocol labels its own transactions: a Lido stake reads "Staked ETH". wallet.registerProtocol('lido', LidoProtocolEvm, { chain: 'eip155:1' }) return wallet}| Chain | Source | Keys |
|---|---|---|
| EVM (one per chain id) | createEvmHistory from history-evm: Etherscan v2, Blockscout PRO, Alchemy; keyless Blockscout and Routescan as fallbacks |
Etherscan v2 needs a key; a free Etherscan key plus a free Blockscout PRO key cover most networks |
| Solana | createSolanaHistory: plain RPC, no indexer |
Your RPC; public endpoints rate-limit fast |
| TON | createTonHistory: tonapi events, toncenter v3 fallback |
Keyless tonapi is about 1 request/s |
| Tron | createTronHistory: TronGrid |
Keyless is 1 request/s |
| Bitcoin, NEAR, Zcash | createBtcHistory, createNearHistory, createZcashHistory from the chain packages |
See Build a wallet → History |
Explorer keys are secrets. On a mobile app or an extension, point the source at your own proxy
(etherscanUrl and similar options) and keep the key on the server.
Pages, refresh and errors
Section titled “Pages, refresh and errors”export async function firstPage(wallet: Wallet): Promise<HistoryFeedPage> { const page = await wallet.history({ limit: 20 }) // account 0, every chain with a source // A source that failed is listed in page.errors and retried on the next call; the rest still loads. return page}
export async function nextPage(wallet: Wallet, cursor: string): Promise<HistoryFeedPage> { return wallet.history({ limit: 20, cursor })}
export async function refresh(wallet: Wallet, lastSeen: number): Promise<FeedItem[]> { const { items } = await wallet.history({ since: lastSeen }) // only what is newer return items}cursorpacks every source’s position into one string. Rows stay in time order across chains: a slow chain’s older rows are never shown after newer ones.since(unix seconds) returns only newer rows: use it for pull-to-refresh.errorslists what failed in this call: a source that is down, a recognizer that threw. The other chains still load, and the failed source keeps its place for the next call. Show a quiet “some activity may be missing” rather than an error screen.- Spam is hidden by default: zero-value transfers and airdrops the source flags. Pass
includeSpam: truefor a “hidden” tab.
Pending rows: the intent journal
Section titled “Pending rows: the intent journal”/** Call right after broadcasting a swap: the row shows at once, with what the user meant. */export async function recordSwap(journal: IntentJournal, swap: { id: string; from: string; sourceHash: string; out: ActivityAsset; in: ActivityAsset}): Promise<void> { const intent: Intent = { id: swap.id, kind: 'swap', createdAt: Math.floor(Date.now() / 1000), chain: 'eip155:1', address: swap.from, hashes: [{ chain: 'eip155:1', hash: swap.sourceHash }], // source leg first expect: { assetsOut: [swap.out], assetsIn: [swap.in] }, subtitle: 'LI.FI', status: 'pending', } await journal.put(intent)}
export function attachJournal(wallet: Wallet): IntentJournal { const journal = createMemoryIntentJournal() // or your own { list, put, remove } over SQLite / MMKV wallet.setIntentJournal(journal) return journal}The chain alone can’t say “swapped USDC → ETH via LI.FI”: it shows a USDC transfer to a router. It can’t pair a bridge’s two legs on two chains, and nothing is there until the explorer indexes it. Record an intent right after broadcast:
- Source leg first in
hashes. When the swap’s status reports the destination hash,putthe intent again with it appended. expectholds the quote amounts, shown while the row is pending.- An intent that never gets its row is marked failed after
pendingTtl(24 hours by default). - Persist the journal (SQLite, MMKV, a file): a pending swap must survive an app restart.
Earn modules and approvals don’t need intents: they recognize their own transactions.
Rendering a row
Section titled “Rendering a row”/** One row of the activity list. Amounts are base units: format with the asset's decimals. */export function row(item: FeedItem): { title: string; subtitle: string; amount: string; pending: boolean } { const fmt = (a: ActivityAsset, sign: string) => `${sign}${units(a.amount, a.decimals ?? 0)} ${a.symbol ?? ''}`.trim() const amount = [...item.assetsOut.map((a) => fmt(a, '−')), ...item.assetsIn.map((a) => fmt(a, '+'))].join(' ') return { title: item.title, subtitle: item.subtitle ?? '', amount, pending: item.status === 'pending' }}
function units(amount: bigint, decimals: number): string { const s = amount.toString().padStart(decimals + 1, '0') const whole = s.slice(0, s.length - decimals) const frac = s.slice(s.length - decimals).replace(/0+$/, '') return frac ? `${whole}.${frac}` : whole}| Field | Meaning |
|---|---|
kind |
send, receive, self, swap, bridge, stake, unstake, claim, supply, withdraw, borrow, repay, approve, revoke, wrap, unwrap, contract, unknown |
title, subtitle |
Readable title, and the venue or counterparty |
assetsOut, assetsIn |
What left and what arrived, in base units with symbol and decimals |
status |
confirmed, failed, pending |
counterparty, related |
The other party; the other leg of a cross-chain operation |
approval |
On approve / revoke rows: token, spender, amount (null = unlimited) |
fee, explorerUrl |
Network fee in the native coin; a link for the details |
Address poisoning: scammers send tiny transfers from an address that looks like one the user uses, hoping the user copies it from history. Never offer “copy address” on an incoming row without a warning, and don’t use history as the address book.
One transaction
Section titled “One transaction”/** A detail screen for one transaction, through the same recognizers as the list. */export async function detail(wallet: Wallet, tx: ChainTx): Promise<FeedItem> { return wallet.describeTransaction(tx)}describeTransaction runs one transaction through the same recognizers and journal: use it for
a detail screen, or for a transaction your app just got from a push notification.