@anywalletkit/wallet-evm
v0.5.0
Published
EVM wallet for Any Wallet Kit: BIP-32 m/44'/60'/0'/0/i accounts, EIP-191 / EIP-712 / EIP-1559 signing, ERC-20 transfer and approve, built on viem.
Maintainers
Readme
@anywalletkit/wallet-evm
EVM accounts for Any Wallet Kit, derived at m/44'/60'/0'/0/i — the same
path as MetaMask and ethers, pinned by golden vectors in @anywalletkit/conformance.
Install
npm install @anywalletkit/wallet-evm
# or: pnpm add @anywalletkit/wallet-evm · yarn add @anywalletkit/wallet-evmOn npm: @anywalletkit/wallet-evm, published from the release workflow with provenance.
import WalletManagerEvm from '@anywalletkit/wallet-evm'
const wallet = new WalletManagerEvm(seedPhrase, { provider: ['https://rpc-a', 'https://rpc-b'] })
const account = await wallet.getAccount(0)
await account.transfer({ recipient: '0x…', amount: 10n ** 15n }) // ETH
await account.transfer({ token: USDT, recipient: '0x…', amount: 1_000_000n }) // ERC-20
await account.approve({ token: USDT, spender: ROUTER, amount: 1_000_000n })
const sig = await account.signTypedData({ domain, types, message }) // EIP-712
wallet.dispose() // zeroizes seed + keysOffline signing: omit provider, pass chainId (or put it on the tx) and call
signTransaction with every field set — nothing touches the network.
Token info
getTokenInfo(address) reads an ERC-20 from its contract: { address, standard: 'erc20', decimals,
symbol, name, totalSupply, flags }. bytes32 symbols (MKR) are decoded; text that is not UTF-8 is
''. flags holds upgradeable when the address is an EIP-1967 / EIP-1822 / beacon / ZeppelinOS
proxy. No code, an EIP-7702-delegated account or no decimals() (an NFT, a plain contract) throws
NotATokenError; RPC failures are thrown as they are. @anywalletkit/tokens builds the "add a
token" flow on it.
Safety layer
Every send goes through, in order: static refusals → simulation → fee cap → sign → broadcast.
- What you sign.
describeTransaction(tx, {chainId})anddescribeTypedData(typed, {chainId, account})are pure, offline decoders for a confirmation screen: kind (native-transfer,erc20-transfer,erc20-approve,contract-call,eip7702-delegation, …;permit,permit2,safe-tx,generic), decoded fields, and warnings (approve-unlimited,to-zero-address,chain-mismatch,deadline-passed,owner-mismatch,legacy-fee-fields). - Hard refusals before any signature: a tx or typed-data domain for another chain throws
ChainMismatchError(override typed data withallowCrossChainTypedData); value to the zero address throwsUnsafeTransactionError(override withallowBurn). - Simulation.
simulateTransaction(tx)runseth_call+ gas estimate and returns{ok, gas, returnData}or{ok: false, reason, data}with the decodedError(string)/Panic/ custom-error selector.sendTransaction/transfer/approvesimulate first and throwSimulationErrorinstead of broadcasting a revert;simulate: falseopts out. The estimated gas is reused, so the network is asked once. - Fee cap.
transferMaxFee/transactionMaxFeeare checked on the transaction about to be signed — gas limit × itsgasPrice/maxFeePerGas, fee fields you set yourself included — before every signature.quoteSendTransactionprices the same fields, so a kitmaxFeepolicy sees them too. - Broadcast. A lost answer throws
BroadcastUnknownErrorwith the locally computedhash: watch it, do not send again. With several RPCs,already knownfrom the next one is the same transaction and resolves to its hash.nonce too lowis re-signed with the next nonce only after the account has checked that the rejected hash is not on the network;replacement transaction underpricedis never retried. - EIP-2612 permit.
getPermitInfo(token)readsname/version/nonces/DOMAIN_SEPARATORand reportssupportedonly when the rebuilt EIP-712 domain equals the contract's own;signPermit({token, spender, value, deadline})returns{signature, v, r, s, nonce, deadline, typedData}. DAI's legacypermit(holder, spender, nonce, expiry, allowed)is not supported.
const d = describeTransaction(tx, { chainId: await account.getChainId() })
if (d.warnings.some((w) => w.code === 'approve-unlimited')) ask(d)
const sim = await account.simulateTransaction(tx)
if (!sim.ok) show(sim.reason)