@anywalletkit/kit

中文文档
0.8.1发布于 2026/11/07Apache-2.0TypeScriptESM + CJS构建来源已验证近 30 天下载 2459

The Any Wallet Kit orchestrator: one seed or signer, registered chain wallets and protocol modules, accounts by CAIP-2 chain, policies that run before anything is broadcast.

原始使用文档
文档目录4 节
  1. Install
  2. Activity history
  3. Generic fallback
  4. Intent journal — the app-side contract

@anywalletkit/kit

npm

The orchestrator. Chain packages and protocol modules are registered; the kit hands out accounts by CAIP-2 chain id and protocols by name, all fed by one seed phrase (or its 64-byte seed) or one external Signer, all wrapped by the same policies. The phrase is turned into seed bytes in the constructor and never kept; no seed or key is ever an own property of a Wallet, manager, account or signer, so logs and serializers show no secret.

Install

npm install @anywalletkit/kit
# or: pnpm add @anywalletkit/kit  ·  yarn add @anywalletkit/kit

On npm: @anywalletkit/kit, published from the release workflow with provenance.

import { Wallet } from '@anywalletkit/kit'
import WalletManagerEvm from '@anywalletkit/wallet-evm'
import WalletManagerSolana from '@anywalletkit/wallet-solana'

const wallet = new Wallet(seedPhrase, {
  policies: {
    maxFee: { 'eip155:1': 10n ** 16n },                 // 0.01 ETH
    allowlist: { 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp': [TREASURY] },
    confirm: async (intent) => ui.confirm(intent),      // last word before broadcast
  },
})
wallet.registerWallet('eip155', WalletManagerEvm, { provider: ['https://rpc-a', 'https://rpc-b'] })
wallet.registerWallet('solana', WalletManagerSolana, { provider: SOL_RPC })
wallet.registerProtocol('lido', LidoProtocolEvm, { chain: 'eip155:1' })

const eth = await wallet.getAccount('eip155:1')          // policies applied
await eth.transfer({ recipient: '0x…', amount: 10n ** 15n })
const lido = await wallet.getProtocol<LidoProtocolEvm>('lido')
wallet.dispose()                                          // zeroizes the seed and every chain's keys; later calls throw

Policies run before anything is signed or broadcast, in this order: fee cap (quotes first, where the account can quote), allowlist, blocklist, confirm. maxFee (native base units) is checked on the quote of sendTransaction, signTransaction and transfer, then handed to the account as WriteOptions.maxFee so it holds for what is signed; approve (EVM), sendSerialized (Solana), signRaw (Tron), revokeDelegation and swap2's sendSerializedTransaction hook get it the same way, checked by the account on the transaction it signs. Under a cap, a write that pays a fee but has nowhere to take one (signBuilt, signCompiled, signTransactionMessage) is refused; off-chain signatures are not capped. Every write surface of an account is covered — sendTransaction, transfer, signTransaction (a module that signs and broadcasts on its own cannot skip them), sign, NEAR's signNep413 and TON's signTonProof / signTonConnectData (message signatures: confirm only, nothing moves), the approval surfaces approve, signPermit and EIP-2612 / Permit2 signTypedData (checked against the spender; confirm sees kind: 'approval' with unlimited), EIP-3009 TransferWithAuthorization / ReceiveWithAuthorization, ERC-2771 ForwardRequest, MetaTransaction and the 1inch LOP Order (kind: 'send', judged by whom they pay), any other signTypedData (kind: 'typedData' with destinationUnknown: true: an allowlist refuses it, so does a blocklist without confirm; a blocklisted verifyingContract is refused), and chain-specific ones such as Tron's signRaw, NEAR's signBuilt, Solana's sendSerialized. The rule is deny-by-default: any account method named sign* / send* / approve* / revoke* / transfer* is guarded unless it is listed in UNGUARDED_WRITES, so a new signing method in a chain package cannot bypass the policies by not being named. Reads are never intercepted. Nothing hands out a way around the wrapper: its signer is hidden, an account a method returns (NEAR's withAccountId) comes back under the same policies, and wallet.getManager(chain) hands out guarded accounts (getAccount, getAccountByPath, getNamedAccount) and no seed or signer accessors. The wrappers hide the signer from descriptors and key listings too, refuse writes to the account (_config reads as a frozen copy), and under a maxFee rule refuse a sign* / send* helper the policies cannot quote. The boundary is what the wallet hands out: the Wallet object itself is the app's — give modules and dApp bridges accounts, managers or protocols, never the Wallet. A protocol module always gets the wrapper, with policies or without: without any it is a wrapper with no rules (no limits, but no signer, no internal helpers and no writable settings). Chain accounts keep their signer in a private field, so not even the app's own account (getAccount on a wallet without policies) has a signer property; signerKind says what holds the key.

The lists check every destination a write names (intent.destinations), not only the contract it calls: the recipient or spender inside standard token calls (ERC-20 / 721 / 1155 transfers, approve, increaseAllowance, setApprovalForAll, Permit2 approve, an on-chain EIP-2612 permit, ERC-677 transferAndCall), TRC-20 calls (by parameters and inside raw_data_hex), NEAR ft_transfer(_call) / nft_transfer(_call) / storage_deposit / deleteAccount (also in base64 borsh), TON jetton and NFT transfers (destination and excess receiver), the owner of the token account an SPL transfer pays (read through the account's getTokenAccountOwner, only when a list or confirm applies), and whom a Solana Stake instruction pays or empowers. An allowlist must hold all of them. When part of a write cannot be read, the intent says destinationUnknown: an allowlist refuses it, and so does a blocklist with no confirm hook. transfer is judged by recipient, the field every chain pays. An approval is unlimited at type(uint128).max or above (core's isUnlimitedAllowance), and when its amount cannot be read; a Permit2 batch lists every token in intent.approvals. The checks and the signature use one copy of the arguments, taken before the first check, and confirm gets its own: a getter, a confirm hook or a timer cannot change what is signed after it was judged.

A revoke is kind: 'revoke': an approve / signPermit / EIP-2612 Permit of 0, raw revoke calldata (approve(0x0, tokenId), setApprovalForAll(…, false), Permit2 lockdown, or a batch of only these) and revokeDelegation(). Raw approve(spender, 0) is NOT a revoke: on an ERC-721 it approves spender for token #0, so it is checked as an approval of spender. It skips the allowlist and the spender's blocklist entry (taking an allowance back from a blocklisted drainer must stay possible); the blocklist still applies to the contract called, the fee cap and confirm apply, and confirm sees revokes: [{ token, spender }].

Activity history

history() is the wallet's activity feed: one page across every chain that has a history source, newest first, every row described by the first registered protocol module whose describeActivity recognizes it (registration order), else by the generic fallback. Rows the source flagged as spam are hidden unless asked for.

import { createEvmHistory } from '@anywalletkit/history-evm'

const wallet = new Wallet(seedPhrase, {
  activity: {
    known: { 'eip155:8453': { '0x3fC9…7FAD': 'Uniswap Universal Router' } }, // subtitle for otherwise-anonymous contract calls
    natives: { 'eip155:999999': { symbol: 'XYZ', decimals: 18 } },            // overrides the built-in table / swap2-assets catalog
    onError: (e) => log.warn('activity', e),                                 // a source down, a recognizer that threw, a journal write that failed
    pendingTtl: 24 * 3600,                                                   // seconds a pending intent waits for its row (default)
  },
})
wallet.registerWallet('eip155', WalletManagerEvm, { provider })
wallet.registerProtocol('aave', AaveProtocolEvm, { chain: 'eip155:8453' })   // every registered protocol is a recognizer
wallet.registerHistorySource(createEvmHistory({ chainId: 8453, etherscanApiKey }))
wallet.registerHistorySource(createEvmHistory({ chainId: 1, etherscanApiKey }))

const page = await wallet.history({ limit: 20 })        // account 0 on every chain with a source
const older = await wallet.history({ limit: 20, cursor: page.cursor })
// or explicit addresses / chains:
await wallet.history({ addresses: [{ chain: 'eip155:1', address }], includeSpam: true, since: lastRefresh })

const item = await wallet.describeTransaction(tx)       // one ChainTx through the same pipeline, for a detail screen

history(query) → { items: FeedItem[], cursor?, errors? }

  • query.addresses ({chain, address}[]) or query.index (account index on every chain with a source, default 0); chains restricts; limit (default 20); includeSpam; since (unix seconds, for refresh); cursor from the previous page.
  • items are ActivityItems (id = chain:hash, kind, title, subtitle, assetsOut / assetsIn in base units with symbol and decimals, counterparty, related, spam, fee, explorerUrl) plus intentId when the row belongs to a journal intent and approval (token, spender, amount, null = unlimited) for approve / revoke.
  • cursor packs every source's own cursor into one opaque string; absent when every source is exhausted. Ordering is time desc, then chain, then hash. A source that returned a short page is never overtaken: older rows of other chains wait for its next page, so a row can never appear on a later page than a newer one. Sources with block-window pagination that answer empty pages are followed up to maxEmptyHops (default 3) per call.
  • errors lists what failed this call (stage: 'source' | 'recognizer' | 'journal', with chain / module). A failed source keeps its position in the cursor and is retried on the next call; a recognizer that throws is skipped for that row and the next recognizer / the fallback still runs. Nothing is silently dropped.

Generic fallback

describeGeneric(tx, ctx, opts) is exported and pure. From the owner's point of view:

kindwhentitle / subtitle
sendonly own → others"Sent USDC", recipient label
receiveonly others → own (also airdrops through someone else's contract call)"Received ETH", sender label
selfown → own (two accounts of the wallet)"Self-transfer", moved amount in assetsOut
approve / revokeowner called ERC-20 approve (0x095ea7b3); amount 0 = revoke, ≥ 2^255 = unlimited"Approved USDC for Permit2", approval filled
contractany other call the owner made (amounts attached when something moved)"Contract call", opts.known[chain][to] label or 0x1234…abcd
unknownnothing moved, no call"Transaction"

Failed sends keep the intended amount (native value, or ERC-20 transfer calldata). Symbols and decimals come from the transfer rows, then the swap2-assets catalog, then the native table (natives option → built-in: GRAM on TON, SOL, TRX, BTC, NEAR → catalog natives for EVM chains → namespace default). spam is read from raw.spam / tx.spam (history-evm's rules).

Intent journal — the app-side contract

On-chain rows cannot say "swapped USDC → ETH via 1Click" (the chain shows a USDC send to a deposit address), cannot pair a bridge's two legs, and are not there until the explorer indexes them. The app records what it meant and the kit folds it in:

const journal = createMemoryIntentJournal()        // or your own { list, put, remove } over SQLite / AsyncStorage
wallet.setIntentJournal(journal)

Write one Intent per user action, right after broadcast:

await journal.put({
  id: crypto.randomUUID(),                 // or the provider's id (1Click deposit address, LI.FI route id)
  kind: 'swap',                            // swap | bridge | stake | unstake | claim | supply | withdraw | borrow | repay | send | wrap | unwrap
  createdAt: Math.floor(Date.now() / 1000),
  chain: 'eip155:1', address: from,        // where it started, who signed
  hashes: [{ chain: 'eip155:1', hash: depositHash }],          // source leg first; push the destination leg when the provider reports it
  expect: { assetsOut: [{ token: USDC, amount, symbol: 'USDC', decimals: 6 }], assetsIn: [{ token: null, amount: quoteOut, symbol: 'ETH', decimals: 18 }] },
  protocol: '@anywalletkit/protocol-swap2-near-intents',
  subtitle: 'NEAR Intents',
  status: 'pending',
})

What the app writes, per flow:

  • Swap / bridge (swap2): kind swap (same chain) or bridge, hashes[0] = the source transaction (deposit / router call), expect from the quote. When the provider's status endpoint returns the destination hash, put the intent again with it appended to hashes (a relayer fill that is not the wallet's own tx goes in related). For 1Click, where the user sends to a deposit address and the provider fills later, the deposit tx is hashes[0] and the fill hash is appended once GET /status returns it.
  • Earn (Aave / Morpho / Lido / Tonstakers / Jito …): kind supply / withdraw / stake / unstake / claim, hashes[0] the transaction, protocol the module name, subtitle the market label. Modules recognize these rows on their own; the intent matters only before the explorer indexes the row.
  • Send: optional (kind: 'send'); useful for the pending row and for a contact label in subtitle.
  • Approvals: not needed (the fallback and approvals module recognize them).

What history() does with the journal:

  1. A row whose hash is in an intent inherits kind, protocol, subtitle and the expected assets when the recognizer returned a generic item; a recognizer's verdict is kept. Every matched item carries intentId and related (the other legs).
  2. A pending intent with no row on the first page is looked up with each leg's getTransaction (explorer lag); found → emitted confirmed; not found → emitted at the top as status: 'pending', id: intent:<id>, titled from expect.
  3. Two legs of one intent on the same page collapse into one item (the source leg), assetsIn from the destination row, related pointing at it; the destination row is dropped. Legs that fall on different pages each carry intentId; collapse by it in the app if you keep pages in memory.
  4. Pending longer than pendingTtl (default 24 h) with no row → status: 'failed', persisted. Failed intents keep showing at the top of the first page until the app removes them (dismiss action).
  5. Matched intents are written back as confirmed / failed. The kit never removes intents; prune confirmed ones older than your retention window.

Timestamps are unix seconds like ChainTx.time; amounts are base-unit bigints.

版本与发布记录

@anywalletkit/kit 共发布 14 个版本,首次发布于 2026/10/25;过去 12 个月发布了 14 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.8.1latest2026/11/07今天
0.8.02026/11/035 天前
0.7.12026/10/308 天前
0.7.02026/10/309 天前
0.6.02026/10/2810 天前
0.5.12026/10/2811 天前

关于 @anywalletkit/kit 的常见问题

@anywalletkit/kit 是什么?

The Any Wallet Kit orchestrator: one seed or signer, registered chain wallets and protocol modules, accounts by CAIP-2 chain, policies that run before anything is broadcast.

如何安装 @anywalletkit/kit?

在项目目录运行 npm install @anywalletkit/kit,或使用 pnpm add / yarn add / bun add。

@anywalletkit/kit 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@anywalletkit/kit 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@anywalletkit/kit 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=22。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@anywalletkit/kit 可以商用吗?

@anywalletkit/kit 使用 Apache-2.0 许可。宽松许可:允许商业使用、修改和再分发,并附带明确的专利授权;分发时需保留许可与 NOTICE 文件,修改过的文件要注明改动。以上为概括,不构成法律意见,请以许可原文为准。

@anywalletkit/kit 的下载量有多少?

根据 npm 官方下载统计,@anywalletkit/kit 在 2026/10/06 – 2026/11/04 这 30 天内被下载 2,459 次,日均约 82 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@anywalletkit/kit 还在维护吗?

最新版本 0.8.1 发布于 2026/11/07(今天);至今共发布 14 个版本,其中过去 12 个月发布了 14 个;首次发布于 2026/10/25。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。