Skip to content
docs

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:

  1. Sources read the chain: one per chain id, from an explorer or the RPC.
  2. 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.
  3. 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.

Terminal window
npm install @anywalletkit/kit @anywalletkit/history-evm
import { 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'
/** 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.

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
}
  • cursor packs 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.
  • errors lists 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: true for a “hidden” tab.
/** 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, put the intent again with it appended.
  • expect holds 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.

/** 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.

/** 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.