Minimal library for Ethereum transactions, addresses and smart contracts.
- 🔓 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 keys, 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 APIs (chainlink, uniswap) expect instance of Web3Provider. The call stack would look like this:
Chainlink=>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 { Chainlink, Web3Provider } from 'micro-eth-signer/net.js';
const prov = new Web3Provider(jsonrpc(fetch, 'http://localhost:8545'));
const link = new Chainlink(prov);
async function main() {
const btc = await link.coinPrice('BTC');
const bat = await link.tokenPrice('BAT');
console.log({ btc, bat }); // BTC 19188.68870991, BAT 0.39728989 in USD
}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/bls.js implements EIP-2333, EIP-2334, and EIP-2335
for Ethereum consensus validator keys.
Online demo: eip2333-tool
npm install @scure/bip39for mnemonic-to-seed helpers
import { mnemonicToSeedSync } from '@scure/bip39';
import { createDerivedEIP2334Keystores } from 'micro-eth-signer/advanced/bls.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
);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)