Minimal privacy-focused Ethereum library.
- 🔓 Secure: audited noble cryptography, no network code, hedged signatures
- 🔻 Tree-shakeable: unused code is excluded from your builds
- 🔍 Reliable: 800MB of test vectors from EIPs, ethers and viem
- ✍️ Core: Transactions, addresses, messages
- 🦺 Type-safe ABI, RLP, SSZ, KZG, PeerDAS, BLS validator keystore
- 👓 Clear Signing
- 🌍 Archive node connector
- 🪶 32KB (gzipped) for core+deps: 4x smaller than alternatives
Check out all web3 utility libraries: ETH, BTC, SOL
npm install micro-eth-signer
jsr add jsr:@paulmillr/micro-eth-signer
We support all major platforms and runtimes. For React Native, you may need a polyfill for getRandomValues. If you don't like NPM, a standalone eth-signer.js is also available.
import { addr, authorization, Transaction } from 'micro-eth-signer';
import { eip191Signer, recoverPublicKeyTyped, signTyped, verifyTyped } from 'micro-eth-signer';
import { amounts, ethHex, ethHexNoLeadingZero, weieth, weigwei } from 'micro-eth-signer';import { addr } from 'micro-eth-signer';
const random = addr.random(); // Secure: uses CSPRNG
console.log(random.privateKey, random.address);
// '0x17ed046e6c4c21df770547fad9a157fd17b48b35fe9984f2ff1e3c6a62700bae'
// '0x26d930712fd2f612a107A70fd0Ad79b777cD87f6'import { addr, Transaction, weigwei, weieth } from 'micro-eth-signer';
const random = addr.random();
const tx = Transaction.prepare({
to: '0xdf90dea0e0bf5ca6d2a7f0cb86874ba6714f463e',
value: weieth.decode('1.1'), // 1.1eth in wei
maxFeePerGas: weigwei.decode('100'), // 100gwei in wei (priority fee is 1 gwei)
nonce: 0n,
});
// Uses `random` from example above. Alternatively, pass 0x hex string or Uint8Array
const signedTx = tx.signBy(random.privateKey);
console.log('signed tx', signedTx, signedTx.toHex());
console.log('fee', signedTx.fee);
// Hedged signatures, with extra noise / security
const tx2 = tx.signBy(random.privateKey, true); // default, same as above
const tx3 = tx.signBy(random.privateKey, false); // disable
// Send whole account balance. See Security section for caveats
const CURRENT_BALANCE = '1.7182050000017'; // in eth
const txSendingWholeBalance = tx.setWholeAmount(weieth.decode(CURRENT_BALANCE));We support legacy, EIP2930, EIP1559, EIP4844 and EIP7702 transactions.
Signing is done with noble-curves, using RFC 6979. Hedged signatures are also supported - check out the blog post Deterministic signatures are not your friends.
import { addr } from 'micro-eth-signer';
const priv = '0x0687640ee33ef844baba3329db9e16130bd1735cbae3657bd64aed25e9a5c377';
const pub = '030fba7ba5cfbf8b00dd6f3024153fc44ddda93727da58c99326eb0edd08195cdb';
const nonChecksummedAddress = '0x0089d53f703f7e0843953d48133f74ce247184c2';
const checksummedAddress = addr.addChecksum(nonChecksummedAddress);
console.log(
checksummedAddress, // 0x0089d53F703f7E0843953D48133f74cE247184c2
addr.isValid(checksummedAddress), // true
addr.isValid(nonChecksummedAddress), // also true
addr.fromPrivateKey(priv),
addr.fromPublicKey(pub)
);There are two messaging standards: EIP-191 & EIP-712.
import { eip191Signer } from 'micro-eth-signer';
// Example message
const message = 'Hello, Ethereum!';
const privateKey = '0x4c0883a69102937d6231471b5dbb6204fe512961708279f1d7b1b8e7e8b1b1e1';
// Sign the message
const signature = eip191Signer.sign(message, privateKey);
console.log('Signature:', signature);
// Verify the signature
const address = '0xYourEthereumAddress';
const isValid = eip191Signer.verify(signature, message, address);
console.log('Is valid:', isValid);import { addr, ethHex, recoverPublicKeyTyped, signTyped, verifyTyped } from 'micro-eth-signer';
import type { EIP712Domain, TypedData } from 'micro-eth-signer';
const types = {
Person: [
{ name: 'name', type: 'string' },
{ name: 'wallet', type: 'address' },
],
Mail: [
{ name: 'from', type: 'Person' },
{ name: 'to', type: 'Person' },
{ name: 'contents', type: 'string' },
],
};
// Define the domain
const domain: EIP712Domain = {
name: 'Ether Mail',
version: '1',
chainId: 1n,
verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC',
salt: ethHex.decode('0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef'),
};
// Define the message
const message = {
from: {
name: 'Alice',
wallet: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC',
},
to: {
name: 'Bob',
wallet: '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB',
},
contents: 'Hello, Bob!',
};
// Create the typed data
const typedData: TypedData<typeof types, 'Mail'> = {
types,
primaryType: 'Mail',
domain,
message,
};
// Sign the typed data
const privateKey = '0x4c0883a69102937d6231471b5dbb6204fe512961708279f1d7b1b8e7e8b1b1e1';
const signature = signTyped(typedData, privateKey);
console.log('Signature:', signature);
// Verify the signature
const address = addr.fromPrivateKey(privateKey);
const isValid = verifyTyped(signature, typedData, address);
console.log('Is valid:', isValid);
// Recover the public key
const publicKey = recoverPublicKeyTyped(signature, typedData);
console.log('Recovered:', publicKey);
npm install micro-ftch
eth-signer is network-free and makes it easy to audit network-related code:
all requests are done with user-provided function, conforming to built-in fetch().
We recommend using micro-ftch,
which implements kill-switch, logging, batching / concurrency and other features.
Most network APIs expect an instance of Web3Provider.
The call stack would look like this:
Quoter=>Web3Provider=>jsonrpc=>fetch
To initialize Web3Provider, do the following:
// Requests are made with fetch(), a built-in method
import { jsonrpc } from 'micro-ftch';
import { Web3Provider } from 'micro-eth-signer/net.js';
const RPC_URL = 'http://localhost:8545';
const prov = new Web3Provider(jsonrpc(fetch, RPC_URL));
// Example using mewapi RPC
const RPC_URL_2 = 'https://nodes.mewapi.io/rpc/eth';
const prov2 = new Web3Provider(
jsonrpc(fetch, RPC_URL_2, { Origin: 'https://www.myetherwallet.com' })
);Note
Basic data can be fetched from any node.
Uses trace_filter & requires Erigon, others are too slow.
import { jsonrpc } from 'micro-ftch';
import { Web3Provider } from 'micro-eth-signer/net.js';
const prov = new Web3Provider(jsonrpc(fetch, 'http://localhost:8545'));
const addr = '0xd8da6bf26964af9d7eed9e03e53415d37aa96045';
async function main() {
const block = await prov.blockInfo(await prov.height());
console.log('current block', block.number, block.timestamp, block.baseFeePerGas);
console.log('info for addr', addr, await prov.unspent(addr));
}
// Other methods of Web3Provider:
// blockInfo(block: number): Promise<BlockInfo>; // {baseFeePerGas, hash, timestamp...}
// height(): Promise<number>;
// internalTransactions(address: string, opts?: TraceOpts): Promise<any[]>;
// ethLogsSingle(topics: Topics, opts: LogOpts): Promise<Log[]>;
// ethLogs(topics: Topics, opts?: LogOpts): Promise<Log[]>;
// tokenTransfers(address: string, opts?: LogOpts): Promise<[Log[], Log[]]>;
// wethTransfers(address: string, opts?: LogOpts): Promise<[Log[]]>;
// txInfo(txHash: string, opts?: TxInfoOpts): Promise<{
// type: "legacy" | "eip2930" | "eip1559" | "eip4844"; info: any; receipt: any; raw: string | undefined;
// }>;
// tokenInfo(address: string): Promise<TokenInfo | undefined>;
// transfers(address: string, opts?: TraceOpts & LogOpts): Promise<TxTransfers[]>;
// allowances(address: string, opts?: LogOpts): Promise<TxAllowances>;
// tokenBalances(address: string, tokens: string[]): Promise<Record<string, bigint>>;import { jsonrpc } from 'micro-ftch';
import { Quoter, Web3Provider } from 'micro-eth-signer/net.js';
const prov = new Web3Provider(jsonrpc(fetch, 'http://localhost:8545'));
async function main() {
const quoter = new Quoter(prov);
const btc = await quoter.coinPrice('BTC'); // Chainlink is the default provider.
const bat = await quoter.tokenPrice('BAT');
const ethV2 = await quoter.coinPrice('ETH', 'uniswap-v2');
const ethV3 = await quoter.coinPrice('ETH', 'uniswap-v3', { fees: [500, 3000] });
const btc_eur = await quoter.coinPrice('BTC', 'uniswap-v3', { priceIn: 'EUR' });
console.log({ btc, btc_eur, bat, ethV2, ethV3 }); // prices in USD
}Uniswap quote helpers default to USDT prices and can discover pairs or pools from the requested
asset before the first quote. Pass priceIn to use another quote token, or use
rate(amount, provider, params) for raw pair, pool, and vault conversions. priceIn accepts token
addresses, built-in token symbols such as USDC or WBTC, and the EUR/EURC aliases for
mainnet EURC. Uniswap v3 EUR/EURC auto prices route through USDC to avoid thin direct pools.
Call quoter.clearRoutes() to force auto-discovered Uniswap routes to be refreshed.
import { jsonrpc } from 'micro-ftch';
import { ENS, Web3Provider } from 'micro-eth-signer/net.js';
const prov = new Web3Provider(jsonrpc(fetch, 'http://localhost:8545'));
const ens = new ENS(prov);
async function main() {
const vitalikAddr = await ens.nameToAddress('vitalik.eth');
}Btw cool tool, glad you built it!
Uniswap Founder
Swap 12.12 USDT to BAT with uniswap V3 defaults of 0.5% slippage, 30 min expiration.
import { jsonrpc } from 'micro-ftch';
import { tokenFromSymbol } from 'micro-eth-signer/advanced/abi.js';
import { UniswapV3, Web3Provider } from 'micro-eth-signer/net.js'; // or UniswapV2
const prov = new Web3Provider(jsonrpc(fetch, 'http://localhost:8545'));
const USDT = tokenFromSymbol('USDT');
const BAT = tokenFromSymbol('BAT');
const u3 = new UniswapV3(prov); // or new UniswapV2(provider)
const fromAddress = '0xd8da6bf26964af9d7eed9e03e53415d37aa96045';
const toAddress = '0xd8da6bf26964af9d7eed9e03e53415d37aa96045';
async function main() {
const swap = await u3.swap(USDT, BAT, '12.12', { slippagePercent: 0.5, ttl: 30 * 60 });
if (!swap) throw new Error('No swap route found');
const swapData = await swap.tx(fromAddress, toAddress);
console.log(swapData.amount, swapData.expectedAmount, swapData.allowance);
}The ABI is type-safe when as const is specified:
import { createContract } from 'micro-eth-signer/advanced/abi.js';
const PAIR_CONTRACT = [
{
type: 'function',
name: 'getReserves',
outputs: [
{ name: 'reserve0', type: 'uint112' },
{ name: 'reserve1', type: 'uint112' },
{ name: 'blockTimestampLast', type: 'uint32' },
],
},
] as const;
const contract = createContract(PAIR_CONTRACT);
type Contract = typeof contract;
// Contract type:
// {
// getReserves: {
// encodeInput: () => Uint8Array;
// decodeOutput: (b: Uint8Array) => {
// reserve0: bigint;
// reserve1: bigint;
// blockTimestampLast: bigint;
// };
// };
// }We're parsing values as:
// no inputs
{} -> encodeInput();
// single input
{inputs: [{type: 'uint'}]} -> encodeInput(bigint);
// all inputs named
{inputs: [{type: 'uint', name: 'lol'}, {type: 'address', name: 'wut'}]} -> encodeInput({lol: bigint, wut: string})
// at least one input is unnamed
{inputs: [{type: 'uint', name: 'lol'}, {type: 'address'}]} -> encodeInput([bigint, string])
// Same applies for output!
There are following limitations:
- Fixed size arrays can have 999 elements at max: string[], string[1], ..., string[999]
- Fixed size 2d arrays can have 39 elements at max: string[][], string[][1], ..., string[39][39]
- Which is enough for almost all cases
- ABI must be described as constant value:
[...] as const - We're not able to handle contracts with method overload (same function names with different args) — the code will still work, but not types
Check out src/net/ens.ts for type-safe contract execution example.
The library supports Clear Signing through
ERC-7730 descriptor maps via decodeTx, decodeData, and eip712. The
previous transaction-display strings are now ERC-7730 descriptors in OURS.
CLEARSIG_REPO is the batteries-included descriptor map: the generic ERC
interfaces (erc20/erc721/erc4626/...), curated and legacy contracts (uniswap
v2/v3, kyber, the metamask swap router, weth), and the built-in token registry
already bound to them - including an ERC-2612 permit binding per token.
import { CLEARSIG_REPO, addTokens } from 'micro-eth-signer/advanced/abi.js';
import { CLEARSIG_REPO_FULL } from 'micro-eth-signer/advanced/clearsig-repo-full.js';
// basic: generic ERCs + curated contracts + built-in tokens
const base = CLEARSIG_REPO;
// your own tokens: binds erc20/erc721 interfaces + an ERC-2612 permit per token
const mine = addTokens(CLEARSIG_REPO, {
'0x0000000000000000000000000000000000000123': {
abi: 'ERC20',
symbol: 'MTK',
decimals: 18,
},
}); // chainId is optional, defaults to mainnet (1)
// full: every descriptor from the upstream registry on top.
// CLEARSIG_REPO_FULL is about 500KB of generated source; the normal ABI facade
// does not re-export it, so import this separate subpath only when needed.
const full = { ...CLEARSIG_REPO, ...CLEARSIG_REPO_FULL };decodeTx decodes a raw transaction through the built-in ABI and clear-signing
registries; matched transactions carry a clearSig promise with the rendered
intent and fields. Use decodeData(to, data, value, opts) when you already
have RPC calldata fields instead of full transaction hex.
Both default to CLEARSIG_REPO when you omit clearSig; pass { clearSig } to
override it - with addTokens(...) output or your own descriptor map. Each call
returns the exact decoded call (carrying clearSig), an array of ABI-shape
guesses when no exact contract matches (these never carry clearSig), or
undefined for unknown selectors and contract creation - so guard with
out && !Array.isArray(out) before reading clearSig.
/// <reference types="node" />
import { deepStrictEqual } from 'node:assert';
import { decodeTx } from 'micro-eth-signer/advanced/abi.js';
const tx =
'0xf8a901851d1a94a20082c12a94dac17f958d2ee523a2206206994597c13d831ec780b844a9059cbb000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7000000000000000000000000000000000000000000000000000000054259870025a066fcb560b50e577f6dc8c8b2e3019f760da78b4c04021382ba490c572a303a42a0078f5af8ac7e11caba9b7dc7a64f7bdc3b4ce1a6ab0a1246771d7cc3524a7200';
const decoded = decodeTx(tx);
if (!decoded || Array.isArray(decoded)) throw new Error('expected exact ABI match');
const clear = await decoded.clearSig;
deepStrictEqual(decoded.value, {
to: '0xdac17f958d2ee523a2206206994597c13d831ec7',
value: 22588000000n,
});
deepStrictEqual(clear, {
intent: 'Send',
interpolatedIntent: 'Transfer 22588 USDT to 0xdac17f958d2ee523a2206206994597c13d831ec7',
structuredIntent: [
'Transfer ',
{ value: '22588 USDT', format: 'tokenAmount', rawValue: 22588000000n },
' to ',
{
value: '0xdac17f958d2ee523a2206206994597c13d831ec7',
format: 'addressName',
rawValue: '0xdac17f958d2ee523a2206206994597c13d831ec7',
},
],
fields: {
Amount: { value: '22588 USDT', format: 'tokenAmount', rawValue: 22588000000n },
To: {
value: '0xdac17f958d2ee523a2206206994597c13d831ec7',
format: 'addressName',
rawValue: '0xdac17f958d2ee523a2206206994597c13d831ec7',
},
},
});So, a user will see Transfer 22588 USDT to 0xdac17f958d2ee523a2206206994597c13d831ec7 instead of
0xf8a901851d1a94a20082c12a94dac17f958d2ee523a2206206994597c13d831ec780b844a9059cbb000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7000000000000000000000000000000000000000000000000000000054259870025a066fcb560b50e577f6dc8c8b2e3019f760da78b4c04021382ba490c572a303a42a0078f5af8ac7e11caba9b7dc7a64f7bdc3b4ce1a6ab0a1246771d7cc3524a7200.
The result drives the signing screen: render intent as the headline and
fields (label -> { value, format, rawValue }) as the detail rows. When the
descriptor defines an interpolatedIntent, the result also carries
interpolatedIntent (a ready-to-print sentence) and structuredIntent (that
sentence split into literal strings and formatted field objects, for inline
highlighting); both are absent otherwise - the EIP-712 permit further down
renders with only intent and fields, so treat those two keys as optional.
Unsigned transactions decode through the same decodeTx - here with a custom
token bound via addTokens:
/// <reference types="node" />
import { deepStrictEqual } from 'node:assert';
import { Transaction } from 'micro-eth-signer';
import { CLEARSIG_REPO, addTokens, decodeData, decodeTx } from 'micro-eth-signer/advanced/abi.js';
const to = '0x7a250d5630b4cf539739df2c5dacb4c659f2488d';
const data =
'7ff36ab5000000000000000000000000000000000000000000000000ab54a98ceb1f0ad30000000000000000000000000000000000000000000000000000000000000080000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa96045000000000000000000000000000000000000000000000000000000006fd9c6ea0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000106d3c66d22d2dd0446df23d7f5960752994d600';
const value = 100000000000000000n;
// With custom tokens/contracts
const customContracts = {
'0x106d3c66d22d2dd0446df23d7f5960752994d600': { abi: 'ERC20', symbol: 'LABRA', decimals: 9 },
} as const;
const decodedData = decodeData(to, data, value, { customContracts })!;
if (Array.isArray(decodedData) || !decodedData.clearSig)
throw new Error('expected exact ABI match');
const { clearSig: _decodedClearSig, ...decodedCall } = decodedData;
deepStrictEqual(decodedCall, {
name: 'swapExactETHForTokens',
signature: 'swapExactETHForTokens(uint256,address[],address,uint256)',
value: {
amountOutMin: 12345678901234567891n,
path: [
'0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2',
'0x106d3c66d22d2dd0446df23d7f5960752994d600',
],
to: '0xd8da6bf26964af9d7eed9e03e53415d37aa96045',
deadline: 1876543210n,
},
});
const customClearSig = addTokens(CLEARSIG_REPO, customContracts);
const unsigned = Transaction.prepare({
to,
value,
data,
nonce: 0n,
maxFeePerGas: 2000000000n,
gasLimit: 250000n,
}).toHex(false);
const decodedSwap = decodeTx(unsigned, { clearSig: customClearSig });
// Arrays are ABI shape guesses used when no exact contract match is available.
if (!decodedSwap || Array.isArray(decodedSwap)) throw new Error('expected exact ABI match');
deepStrictEqual(await decodedSwap.clearSig, {
intent: 'Swap',
interpolatedIntent:
'Swap 0.1 ETH for at least 12345678901.234567891 LABRA. Expires at Tue, 19 Jun 2029 06:00:10 GMT',
structuredIntent: [
'Swap ',
{ value: '0.1 ETH', format: 'amount', rawValue: 100000000000000000n },
' for at least ',
{
value: '12345678901.234567891 LABRA',
format: 'tokenAmount',
rawValue: 12345678901234567891n,
},
'. Expires at ',
{ value: 'Tue, 19 Jun 2029 06:00:10 GMT', format: 'date', rawValue: 1876543210n },
],
fields: {
'Amount to Send': {
value: '0.1 ETH',
format: 'amount',
rawValue: 100000000000000000n,
},
'Minimum to Receive': {
value: '12345678901.234567891 LABRA',
format: 'tokenAmount',
rawValue: 12345678901234567891n,
},
Beneficiary: {
value: '0xd8da6bf26964af9d7eed9e03e53415d37aa96045',
format: 'addressName',
rawValue: '0xd8da6bf26964af9d7eed9e03e53415d37aa96045',
},
Deadline: {
value: 'Tue, 19 Jun 2029 06:00:10 GMT',
format: 'date',
rawValue: 1876543210n,
},
},
});ERC-7730 does not describe plain value transfers (data: '0x'), so decodeTx
produces no clearSig for them; word those in the wallet itself (e.g.
"Send 0.5 ETH to ...") instead of showing an unknown-transaction fallback.
Web3Provider.discoverTx wires decodeTx to archive-node callbacks. The
clear-signing renderer stays no-network by default; this path adds trusted
token metadata, names, NFT metadata, block timestamps, and factory proofs when a
provider is available.
import { Web3Provider } from 'micro-eth-signer/net.js';
async function reviewTx(prov: InstanceType<typeof Web3Provider>, txHex: string) {
const decoded = await prov.discoverTx(txHex);
if (!decoded || Array.isArray(decoded)) throw new Error('expected exact ABI match');
return decoded.clearSig;
}Resolvers are independent of the network path: any ClearSigOpt callback -
resolveAddress, resolveToken, resolveNft, resolveBlock, resolveChain -
can be passed to decodeTx/eip712 alongside clearSig, e.g.
{ clearSig: CLEARSIG_REPO, resolveAddress: async ({ address }) => book[address] }.
resolveAddress is intentionally left out of discoverTx's bundle - what counts
as a trusted name is wallet policy. To teach the renderer about a non-token
contract, merge your own ERC-7730 descriptor files into the map
({ ...CLEARSIG_REPO, ...myDescriptors }); descriptor maps are plain
Record<string, ClearSigDef>.
eip712 defaults to CLEARSIG_REPO like decodeTx. Signature requests render
through the same repository. addTokens gives every ERC-20 an ERC-2612 permit
binding (the upstream permit descriptor ships
without deployments, so out of the box it matches nothing), and the bound
token metadata makes amounts render offline:
/// <reference types="node" />
import { deepStrictEqual } from 'node:assert';
import { eip712 } from 'micro-eth-signer/advanced/abi.js';
import type { ClearSigTypedInput } from 'micro-eth-signer/advanced/abi.js';
const typed = {
types: {
Permit: [
{ name: 'owner', type: 'address' },
{ name: 'spender', type: 'address' },
{ name: 'value', type: 'uint256' },
{ name: 'nonce', type: 'uint256' },
{ name: 'deadline', type: 'uint256' },
],
},
primaryType: 'Permit',
domain: {
name: 'USD Coin',
version: '2',
chainId: 1,
verifyingContract: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
},
message: {
owner: '0xd8da6bf26964af9d7eed9e03e53415d37aa96045',
spender: '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45',
value: 25000000n,
nonce: 0n,
deadline: 1893456000n,
},
} as const;
const clear = (await eip712(typed as unknown as ClearSigTypedInput))!;
deepStrictEqual(clear, {
intent: 'Authorize spending of tokens',
fields: {
Spender: {
value: '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45',
format: 'raw',
rawValue: '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45',
},
'Max spending amount': {
value: '25 USDC',
format: 'tokenAmount',
rawValue: 25000000n,
},
'Valid until': {
value: 'Tue, 01 Jan 2030 00:00:00 GMT',
format: 'date',
rawValue: 1893456000n,
},
},
});Receipt logs are post-transaction facts, not ERC-7730 signing prompts. Minimal event hints still exist for decoded token events:
/// <reference types="node" />
import { deepStrictEqual } from 'node:assert';
import { decodeEvent } from 'micro-eth-signer/advanced/abi.js';
const to = '0x0d8775f648430679a709e98d2b0cb6250d2887ef';
const topics = [
'0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925',
'0x000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa96045',
'0x000000000000000000000000e592427a0aece92de3edee1f18e0157c05861564',
];
const data = '0x00000000000000000000000000000000000000000000003635c9adc5dea00000';
const event = decodeEvent(to, topics, data)!;
// Arrays are ABI topic guesses used when no exact contract match is available.
if (Array.isArray(event)) throw new Error('expected exact event match');
deepStrictEqual(event, {
name: 'Approval',
signature: 'Approval(address,address,uint256)',
value: {
value: 1000000000000000000000n,
owner: '0xd8da6bf26964af9d7eed9e03e53415d37aa96045',
spender: '0xe592427a0aece92de3edee1f18e0157c05861564',
},
hint: 'Allow 0xe592427a0aece92de3edee1f18e0157c05861564 spending up to 1000 BAT from 0xd8da6bf26964af9d7eed9e03e53415d37aa96045',
});micro-eth-signer/advanced/keystore.js implements various keystores:
- The module exposes EIP-2333, EIP-2334, and EIP-2335 for Ethereum consensus validator keys
- It also implements legacy v3 / sale keystores.
- bip32 is not included, but can be easily combined with scure-bip32
- bip39 can be used from scure-bip39
Public helpers:
hkdfModR,deriveMaster,deriveChild,deriveSeedTree: low-level EIP-2333 / EIP-2334 BLS key derivation.deriveEIP2334Key,deriveEIP2334SigningKey: derive validator withdrawal or signing keys and paths.EIP2335Keystore,decryptEIP2335Keystore: encrypt and decrypt EIP-2335 consensus-layer keystore objects.createDerivedEIP2334Keystores: export multiple EIP-2335 keystores from one seed and password.privToLegacyKeystore,privFromLegacyKeystore: export and import execution-layer Web3 v3 keystores.privFromLegacySaleKeystore: import Ethereum legacy sale wallet files.
Online demo: eip2333-tool
npm install @scure/bip39for mnemonic-to-seed helpers
import { mnemonicToSeedSync } from '@scure/bip39';
import {
createDerivedEIP2334Keystores,
decryptEIP2335Keystore,
} from 'micro-eth-signer/advanced/keystore.js';
const password = 'my_password';
const mnemonic = 'letter advice cage absurd amount doctor acoustic avoid letter advice cage above';
const keyType = 'signing'; // or 'withdrawal'
const indexes = [0, 1, 2, 3]; // create 4 keys
const keystores = createDerivedEIP2334Keystores(
password,
'scrypt',
mnemonicToSeedSync(mnemonic, ''),
keyType,
indexes
);
const firstPrivateKey = decryptEIP2335Keystore(keystores[0], password);Legacy Web3 v3 keystores protect execution-layer secp256k1 account keys:
import { addr } from 'micro-eth-signer';
import {
privFromLegacyKeystore,
privToLegacyKeystore,
} from 'micro-eth-signer/advanced/keystore.js';
const account = addr.random();
const legacyStore = await privToLegacyKeystore(account.privateKey, 'my_password');
const recoveredPrivateKey = await privFromLegacyKeystore(legacyStore, 'my_password');packed allows us to implement RLP in just 100 lines of code, and SSZ in 1500 lines.
SSZ includes EIP-7688 progressive containers.
import { RLP } from 'micro-eth-signer/core/rlp.js';
// More RLP examples in test/rlp.test.ts
RLP.decode(RLP.encode('dog'));import * as ssz from 'micro-eth-signer/advanced/ssz.js';
// More SSZ examples in test/ssz.test.tsAllows to create & verify KZG EIP-4844 proofs. Supports PeerDAS from EIP-7594.
npm install @paulmillr/trusted-setups
import { KZG } from 'micro-eth-signer/advanced/kzg.js';
// 400kb, 4-sec init
import { trustedSetup } from '@paulmillr/trusted-setups/small-kzg.js';
// 800kb, instant init
// import { trustedSetup } from '@paulmillr/trusted-setups/fast-kzg.js';
// PeerDAS EIP-7594
// import { trustedSetup } from '@paulmillr/trusted-setups/small-peerdas.js';
// import { trustedSetup } from '@paulmillr/trusted-setups/fast-peerdas.js';
// More KZG examples in
// https://github.com/ethereumjs/ethereumjs-monorepo
const kzg = new KZG(trustedSetup);
// Example blob and scalar
const blob = new Array(4096).fill(0n);
const commitment = kzg.blobToKzgCommitment(blob);
const z = 1n;
// Compute and verify proof
const [proof, y] = kzg.computeProof(blob, z);
console.log('Commitment:', commitment);
console.log('Proof:', proof);
console.log('Y:', y);
const isValid = kzg.verifyProof(commitment, z, y, proof);
console.log('Is valid:', isValid);
const blobProof = kzg.computeBlobProof(blob, commitment);
console.log('Blob proof:', blobProof);
console.log('Blob proof valid:', kzg.verifyBlobProof(blob, commitment, blobProof));- Commits are signed with PGP keys to prevent forgery. Be sure to verify the commit signatures
- Releases are made transparently through token-less GitHub CI and Trusted Publishing. Be sure to verify the provenance logs for authenticity.
Main points to consider when auditing the library:
- ABI correctness
- All ABI JSON should be compared to some external source
- There are different databases of ABI: one is hosted by Etherscan, when you open contract page
- Network access
- There must be no network calls in the library
- Some functionality requires network: these need external network interface, conforming to
Web3Provider createContract(abi)should create purely offline contractcreateContract(abi, net)would create contract that calls network usingnet, using external interface
- Skipped test vectors
- There is
SKIPPED_ERRORS, which contains list of test vectors from other libs that we skip - They are skipped because we consider them invalid, or so
- If you believe they're skipped for wrong reasons, investigate and report
- There is
The library is cross-tested against other libraries (last update on 25 Feb 2024):
- ethereum-tests v13.1
- ethers 6.11.1
- viem v2.7.13
Check out article ZSTs, ABIs, stolen keys and broken legs about caveats of secure ABI parsing found during development of the library.
Default priority fee is 1 gwei, which matches what other wallets have. However, it's recommended to fetch recommended priority fee from a node.
There is a method setWholeAmount which allows to send whole account balance:
import { Transaction, weigwei, weieth } from 'micro-eth-signer';
const tx = Transaction.prepare({
to: '0xdf90dea0e0bf5ca6d2a7f0cb86874ba6714f463e',
value: weieth.decode('1.1'),
maxFeePerGas: weigwei.decode('100'),
nonce: 0n,
});
const CURRENT_BALANCE = '1.7182050000017'; // in eth
const txSendingWholeBalance = tx.setWholeAmount(weieth.decode(CURRENT_BALANCE));It does two things:
amount = accountBalance - maxFeePerGas * gasLimitmaxPriorityFeePerGas = maxFeePerGas
Every eth block sets a fee for all its transactions, called base fee. maxFeePerGas indicates how much gas user is able to spend in the worst case. If the block's base fee is 5 gwei, while user is able to spend 10 gwei in maxFeePerGas, the transaction would only consume 5 gwei. That means, base fee is unknown before the transaction is included in a block.
By setting priorityFee to maxFee, we make the process deterministic:
maxFee = 10, maxPriority = 10, baseFee = 5 would always spend 10 gwei.
In the end, the balance would become 0.
Warning
Using the method would decrease privacy of a transfer, because payments for services have specific amounts, and not the whole amount.
npm run bench
Note
The first call of sign will take 20ms+ due to noble-curves secp256k1 BASE point precompute.
decodeTxFrom ethers x 1,014 ops/sec @ 985μs/op
decodeTxFrom micro-eth-signer x 1,035 ops/sec @ 966μs/op
decodeTxHash ethers x 15,716 ops/sec @ 63μs/op
decodeTxHash micro-eth-signer x 24,597 ops/sec @ 40μs/op
sign ethers x 5,477 ops/sec @ 182μs/op
sign viem x 6,427 ops/sec @ 155μs/op
sign micro-eth-signer x 5,339 ops/sec @ 187μs/op
# KZG and PeerDAS
init micro-eth-signer 4ms
init kzg-wasm 190ms
# micro-eth-signer
blobToKzgCommitment x 1 ops/sec @ 550ms/op
computeProof x 135 ops/sec @ 7ms/op
computeBlobProof x 1 ops/sec @ 558ms/op ± 1.42% (557ms..559ms)
verifyProof x 539 ops/sec @ 1ms/op
verifyBlogProof x 146 ops/sec @ 6ms/op
verifyBlobProofBatch x 17 ops/sec @ 56ms/op
# compared to pure WASM kzg-wasm
blobToKZGCommitment x 5 ops/sec @ 192ms/op
computeBlobProof x 5 ops/sec @ 197ms/op ± 1.06% (195ms..201ms)
verifyProof x 377 ops/sec @ 2ms/op
verifyBlogProof x 164 ops/sec @ 6ms/op
verifyBlobProofBatch x 23 ops/sec @ 43ms/op
Make sure to use recursive cloning for the eth-vectors submodule:
git clone --recursive https://github.com/paulmillr/micro-eth-signer.git
MIT License
Copyright (c) 2021 Paul Miller (https://paulmillr.com)