micro-eth-signer

repository·main·Indexed 42 days ago

https://github.com/paulmillr/micro-eth-signer

A minimal, privacy-focused Ethereum library for offline cryptographic operations. It provides core signing capabilities for transactions (including EIP-1559, EIP-4844, and EIP-7702), EIP-191 and EIP-712 messages, and authorizations. Features include type-safe ABI parsing via createContract, support for Clear Signing via ERC-7730 descriptor maps, and management of consensus layer (EIP-2333, EIP-2334, EIP-2335) and legacy Web3 v3 keystores.

Tokens
36.9K
Snippets
118
Records
138
Agent score
54%

What's inside micro-eth-signer

  1. Implement Clear Signing with ERC-7730 descriptor maps

    main

    Micro-eth-signer supports Clear Signing via ERC-7730 descriptor maps. These maps allow raw transaction data to be decoded into human-readable 'intents'.

    Descriptor Map Options

    • CLEARSIG_REPO: The base repository containing generic ERC interfaces, curated contracts (Uniswap, etc.), and a built-in token registry.
    • addTokens(repo, tokens): Extends a repository with custom token metadata (address, symbol, decimals). This also automatically binds ERC-2612 permit interfaces to those tokens.
    • CLEARSIG_REPO_FULL: A large (approx. 500KB) repository containing every descriptor from the upstream registry. Import this via the specific subpath micro-eth-signer/clearsig/repo-full.js only when needed.
    import { CLEARSIG_REPO, addTokens } from 'micro-eth-signer/abi.js';
    import { CLEARSIG_REPO_FULL } from 'micro-eth-signer/clearsig/repo-full.js';
    
    const base = CLEARSIG_REPO;
    
    const mine = addTokens(CLEARSIG_REPO, {
      '0x00000000000000000000000000000000000000123': {
        abi: 'ERC20',
        symbol: 'MTK',
        decimals: 18,
      },
    });
    
    const full = { ...CLEARSIG_REPO, ...CLEARSIG_REPO_FULL };
  2. Initialize RpcClient with micro-ftch

    main

    To use micro-eth-signer network features, you must provide an RpcClient. It is recommended to use micro-ftch to wrap the standard fetch API, as it provides features like batching, logging, and concurrency control.

    To enable JSON-RPC batching (which significantly improves latency for methods like tokenBalances), pass a batchSize in the jsonrpc options.

    Conventions:

    • Timestamps: All timestamps (e.g., BlockInfo.timestamp) are in Unix seconds.
    • Numbers: Block numbers and indexes are number; balances, fees, and gas are bigint (wei/token units).
    • Addresses: Returned as reported by the node (lowercase in logs, checksummed elsewhere). Use addr.addChecksum from the core module to normalize.
    • Errors: Methods throw on failure. Web3Error includes method, rpcCode, and isRevert. In batch methods (like tokenInfo), individual contract errors are returned as { contract, error } objects instead of throwing.
    import { jsonrpc } from 'micro-ftch';
    import { RpcClient } from 'micro-eth-signer/net.js';
    
    const RPC_URL = 'http://localhost:8545';
    // Basic initialization
    const prov = new RpcClient(jsonrpc(fetch, RPC_URL));
    
    // Initialization with batching for performance
    const provBatched = new RpcClient(jsonrpc(fetch, RPC_URL, { batchSize: 20 }));
    
    // Initialization with custom Origin header
    const provWithOrigin = new RpcClient(jsonrpc(fetch, 'https://nodes.mewapi.io/rpc/eth', { Origin: 'https://www.myetherwallet.com' }));
  3. Install micro-eth-signer

    main

    You can install micro-eth-signer using npm or JSR. It supports all major platforms and runtimes. Note that React Native users may need to install a polyfill for getRandomValues (e.g., react-native-get-random-values).

    npm install micro-eth-signer
    
    # Or using JSR
    jsr add jsr:@paulmillr/micro-eth-signer
  4. Send transactions with prepare, sign, and broadcast

    main

    The transaction lifecycle involves preparing fields (nonce, gas, etc.), signing the transaction, broadcasting it, and waiting for inclusion.

    prov.prepare supports two population policies:

    1. viem v3-compatible (Default): Attempts eth_fillTransaction. If unsupported, it falls back to manual population using a pending nonce, estimateGas(), and viem-style fee calculation (1.2 * latest base fee + eth_maxPriorityFeePerGas).
    2. ethers_v6-compatible: Pass 'ethers_v6' as the second argument. It uses 2 * latest base fee + priority fee, a 1 gwei priority fallback, and unscaled legacy gas prices.

    waitForReceipt accepts an options object: { confirmations, timeoutMs, pollIntervalMs, signal }.

    import { Transaction } from 'micro-eth-signer';
    
    async function main(privateKey: string, prov: RpcClient) {
      // 1. Prepare fields (nonce, gas, etc.)
      const fields = await prov.prepare({
        from: '0xd8da6bf26964af9d7eed9e03e53415d37aa96045',
        to: '0x7aa96045d8da6bf26964af9d7eed9e03e53415d3',
        value: 10n ** 18n, // 1 eth in wei
      });
    
      // 2. Create and sign the transaction
      const tx = Transaction.prepare(fields).signBy(privateKey);
    
      // 3. Broadcast to the network
      const txHash = await prov.broadcast(tx);
    
      // 4. Wait for inclusion
      const receipt = await prov.waitForReceipt(txHash, { confirmations: 2 });
      console.log(receipt.status === 1 ? 'confirmed' : 'reverted');
    }
  5. Understand the SSZCoder interface

    main

    An SSZCoder<T> is a specialized coder used for Simple Serialize (SSZ) encoding and Merkleization. It extends a standard micro-packed coder with additional metadata required for the Beacon Chain consensus mechanism.

    Key properties include:

    • default: The default value used when building composite structures.
    • info: Contains metadata like the human-readable type (e.g., uint64, container).
    • composite: A boolean indicating if Merkleization differs from plain byte serialization.
    • chunkCount: The number of 32-byte chunks occupied by the fixed part of the value.
    • chunks(value: T): A function that splits the value into 32-byte Merkle chunks.
    • merkleRoot(value: T): Computes the SSZ Merkle root of the value.
    • _isProgressiveCompat(other: SSZCoder<any>): An internal method for checking compatibility with progressive SSZ structures.
  6. Handle evolving BeaconState via Progressive and Profiled Coders

    main

    Because the Ethereum consensus state evolves with different forks (Capella, Electra, Fulu, etc.), the library uses ProgressiveContainerCoder and profile to manage field additions without breaking compatibility.

    • ProgressiveBeaconState: A comprehensive coder that includes fields added in Electra (like pending_deposits, pending_partial_withdrawals, and pending_consolidations) and Fulu (proposer_lookahead).
    • BeaconStateElectra: A profiled version of the state that includes specific Electra fields.
    • BeaconStateFulu: A profiled version of the state that includes Fulu-specific fields like proposer_lookahead.

    Developers should use the specific profiled coder that matches the fork context of the data they are processing.

  7. Understand the ClearSigResult structure

    main

    The ClearSigResult is the output produced after a descriptor has been applied to a transaction or typed-data input. It contains both the raw intent and structured, human-readable fields.

    • intent: The base ERC-7730 intent string.
    • interpolatedIntent: The intent string with values replaced by formatted text (if the descriptor supports interpolation).
    • structuredIntent: An array of string literals and ClearSigField objects, allowing UIs to render specific parts of the intent with custom styling.
    • fields: A map of label-keyed ClearSigField objects, useful for custom wallet layouts that need to display specific values (like amount or recipient) outside of the main intent string.
  8. Discover and verify NFT holdings

    main

    The library uses a two-step process to manage NFT inventory: discovery via history and verification via on-chain state.

    1. Discover candidates with nftCandidates()

    Pass transaction history rows to nftCandidates() to identify ERC-721 and ERC-1155 contracts the address has interacted with. This function returns NftCandidate[] objects containing contract addresses, ABIs, and a map of token IDs seen in transfers.

    2. Verify ownership with nftHoldings()

    Pass the candidates from the previous step to nftHoldings() to perform on-chain checks (e.g., ownerOf or balanceOf). This returns the actual current inventory.

    Constraints & Limits:

    • Limits: To prevent excessive RPC calls, the process is limited to 256 contracts and 4096 distinct contract/ID pairs. If your inventory exceeds these, you must paginate.
    • Errors: Zero-balance IDs are automatically dropped. If a contract check fails, it is returned as a TokenError entry in the results.
    // 1. Discover
    const candidates = nftCandidates(historyRows);
    
    // 2. Verify
    const holdings = await nftHoldings(prov, address, candidates, {
      concurrency: 5,
      signal: abortController.signal
    });
  9. Create and sign Ethereum transactions

    main

    The Transaction class allows you to prepare and sign various transaction types, including legacy, EIP-2930, EIP-1559, EIP-4844, and EIP-7702.

    Key features:

    • Preparation: Use Transaction.prepare() with fields like to, value, maxFeePerGas, and nonce.
    • Signing: Use .signBy(privateKey) to sign.
    • Hedged Signatures: You can enable or disable hedged signatures (which add extra noise/security) by passing a boolean as the second argument to .signBy(privateKey, boolean). The default is true.
    • Max Amount: Use .setMaxAmount(amount) to calculate a transaction that sends the maximum possible amount while reserving the worst-case fee.
    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
      nonce: 0n,
    });
    
    // Sign with hedged signatures (default)
    const signedTx = tx.signBy(random.privateKey, true);
    
    // Sign without hedged signatures
    const signedTxNoHedge = tx.signBy(random.privateKey, false);
    
    // Send max amount while reserving worst-case fee
    const CURRENT_BALANCE = '1.7182050000017';
    const txSendingMaxAmount = tx.setMaxAmount(weieth.decode(CURRENT_BALANCE));
  10. Swap tokens using UniswapV3

    main

    The UniswapV3 class allows you to find swap routes and generate transaction data for token swaps.

    • Slippage & TTL: You can specify slippagePercent and ttl (time-to-live in seconds) in the swap options.
    • Token Lookup: Use tokenFromSymbol from micro-eth-signer/abi.js to resolve symbols to addresses.
    import { tokenFromSymbol } from 'micro-eth-signer/abi.js';
    import { UniswapV3 } from 'micro-eth-signer/net/uniswap.js';
    
    const USDT = tokenFromSymbol('USDT');
    const BAT = tokenFromSymbol('BAT');
    
    async function main() {
      const u3 = new UniswapV3(prov);
      const fromAddress = '0xd8da6bf26964af9d7eed9e03e53415d37aa96045';
      const toAddress = '0xd8da6bf26964af9d7eed9e03e53415d37aa96045';
    
      // Swap 12.12 USDT to BAT with 0.5% slippage and 30 min expiration
      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);
    }
  11. Sign and verify EIP-191 messages

    main

    EIP-191 is a standard for signing plain text messages. Use the eip191Signer object to handle these operations.

    • Signing: eip191Signer.sign(message, privateKey)
    • Verification: eip191Signer.verify(signature, message, address)
    • Hashing: eip191Signer.getHash(message) returns the digest that gets signed.
    • Signature Parsing: Use parseSignature(signature) to extract { r, s, yParity } and serializeSignature({ r, s, yParity }) to rebuild a 65-byte hex signature.
    import { eip191Signer, parseSignature, serializeSignature } from 'micro-eth-signer';
    
    const message = 'Hello, Ethereum!';
    const privateKey = '0x4c0883a69102937d6231471b5dbb6204fe512961708279f1d7b1b8e7e8b1b1e1';
    
    // Sign
    const signature = eip191Signer.sign(message, privateKey);
    
    // Verify
    const address = '0xYourEthereumAddress';
    const isValid = eip191Signer.verify(signature, message, address);
    
    // Parse and Rebuild
    const { r, s, yParity } = parseSignature(signature);
    const rebuiltSignature = serializeSignature({ r, s, yParity });