@anywalletkit/tokens
v0.2.1
Published
Tokens users add themselves, at their own risk: read a token's decimals, symbol, name and holder-relevant facts from the chain (EVM contracts, Solana mints incl. Token-2022 extensions), a risk assessment that flags look-alikes of known tokens and issuer p
Maintainers
Readme
@anywalletkit/tokens
Tokens users add themselves, by address and at their own risk: read the token from the chain, warn about what matters before it is saved, keep the list, and swap it through the engines that accept tokens outside the catalog. EVM contracts and Solana mints (Token-2022 included).
Install
npm install @anywalletkit/tokens
# or: pnpm add @anywalletkit/tokens · yarn add @anywalletkit/tokensOn npm: @anywalletkit/tokens, published from the release workflow with provenance.
Pure TypeScript; depends on @anywalletkit/swap2-assets (the catalog it checks against) and takes @anywalletkit/core types. Runs under Hermes.
import { inspectToken, TokensTable, swapSource } from '@anywalletkit/tokens'
const found = await inspectToken(account, '0x6982508145454Ce325dDbE47a25d4ec3d2311933') // an EVM or Solana account
if (found.listed) {
// already in the catalog: nothing to add
} else {
found.assessment // { level: 'caution' | 'danger', risks: [{ code, level, message }] } — show before saving
const tokens = new TokensTable(myJsonStorage) // AsyncStorage / chrome.storage / memoryJsonStorage()
await tokens.load()
await tokens.add(found.token) // after the user accepted the warnings
}
// Swap a user token: the engine must opt in
const swaps = new RouterSwap2Protocol(account, { fromChain: 'eip155:1', providers, allowUnlistedTokens: true })
await swaps.quoteSwap2Routes({ ...swapSource(token), toToken: 'USDC', fromTokenAmount })What it checks
inspectToken calls the account's getTokenInfo(address) (wallet-evm, wallet-solana): decimals, symbol, name, supply and holder-relevant flags. A wallet, an NFT or a plain contract throws NotATokenError. assessToken(info, chain) then lists every warning, worst first:
- danger:
impersonation(the symbol reads as a catalog token of the same chain, or as a native coin or major stablecoin anywhere, after folding Cyrillic / Greek look-alikes,$→ S,₮→ T, ASCIIl/1/|→ I and0→ O,rn→ m,cl→ d,vv→ w, accents, fullwidth and invisible characters),odd-characterswhen the symbol has letters that pass for Latin ones (Cyrillic, Greek, Armenian, Cherokee, Canadian syllabics, small capitals, letterlike, enclosed and math letters) or characters that hide in text (zero-width, bidi),permanent-delegate,transfer-hook,non-transferable; - caution:
unlisted(always),upgradeable,mint-authority,freeze-authority,transfer-fee,default-frozen,pausable,odd-characters(emoji, CJK, accents, a space inside the symbol, odd characters in the name),no-symbol,zero-supply,unusual-decimals.
A user token's symbol and name are what the token says about itself: display them next to the address, never identify the token by them. Both are trimmed and cut (tokenText: symbol TOKEN_SYMBOL_MAX = 32, name TOKEN_NAME_MAX = 128 code points) before they are stored or assessed, so what is shown is what was checked.
TokensTable
One JSON value in the app's storage. load() once (a failed read throws and leaves the table unloaded instead of empty), then list(chain?) / get(chain, address) synchronously; add / remove queue behind each other and a failed save leaves memory as stored. Catalog tokens, malformed entries and more than maxTokens (200) are refused; stored entries that do not validate are dropped on load. Solana is keyed by the catalog's solana:mainnet, whichever id the account reports.
Testing
pnpm test: assessment rules (look-alikes, flags, levels), the table (persistence, refusals, failed reads and saves),inspectToken.pnpm verify:live: mainnet reads (PEPE, USDC, MKR, an NFT, BONK, JUP, a wallet) and quotes of tokens outside the catalog through the real router and providers — PEPE ⇄ USDC on Ethereum, PEPE → USDC on Base, BONK ⇄ USDC on Solana — refused withoutallowUnlistedTokens. Nothing is signed.
Apache-2.0
