Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

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

Minimal library for Ethereum transactions, addresses and smart contracts.
https://github.com/paulmillr/micro-eth-signer

address eip1559 eip4844 eip7702 eth ethereum ethers signature signer signing transaction validate viem web3

Last synced: 25 days ago
JSON representation

Minimal library for Ethereum transactions, addresses and smart contracts.

Awesome Lists containing this project

README

        

# micro-eth-signer

Minimal library for Ethereum transactions, addresses and smart contracts.

- 🔓 Secure: 3 deps, audited [noble](https://paulmillr.com/noble/) cryptography, no network code
- ðŸ”ŧ Tree-shakeable: unused code is excluded from your builds
- 🔍 Reliable: 150MB of test vectors from EIPs, ethers and viem
- ✍ïļ Transactions: create, sign, decode with human-friendly hints
- 🆎 Execute contracts in JS: includes Chainlink and Uniswap APIs
- ðŸĶš Typescript-friendly: ABI parsing, RLP, SSZ, KZG proof generation & verification
- 🌍 Archive-node-friendly: fetch balances & history
- ðŸŠķ Lightweight: 1200 lines for core functionality

Check out article [ZSTs, ABIs, stolen keys and broken legs](https://github.com/paulmillr/micro-eth-signer/discussions/20) about caveats of secure ABI parsing found during development of the library.

_Check out all web3 utility libraries:_ [ETH](https://github.com/paulmillr/micro-eth-signer), [BTC](https://github.com/paulmillr/scure-btc-signer), [SOL](https://github.com/paulmillr/micro-sol-signer)

## Usage

> npm install micro-eth-signer

We support all major platforms and runtimes.
For [Deno](https://deno.land), ensure to use [npm specifier](https://deno.land/[email protected]/node/npm_specifiers).
For React Native, you may need a [polyfill for getRandomValues](https://github.com/LinusU/react-native-get-random-values).
If you don't like NPM, a standalone [eth-signer.js](https://github.com/paulmillr/micro-eth-signer/releases) is also available.

- [Create random wallet](#create-random-wallet)
- [Transactions: create, sign](#create-and-sign-transactions)
- [Addresses: create, checksum](#create-and-checksum-addresses)
- [Network and smart contracts](#network-and-smart-contracts)
- [Init network](#init-network)
- [Fetch balances and history from an archive node](#fetch-balances-and-history-from-an-archive-node)
- [Fetch Chainlink oracle prices](#fetch-chainlink-oracle-prices)
- [Resolve ENS address](#resolve-ens-address)
- [Swap tokens with Uniswap](#swap-tokens-with-uniswap)
- Parsing
- [Human-readable transaction hints](#human-readable-transaction-hints)
- [Human-readable event hints](#human-readable-event-hints)
- [ABI type inference](#abi-type-inference)
- [RLP parsing](#rlp-parsing)
- [SSZ parsing](#ssz-parsing)
- Utilities
- [KZG EIP-4844 proofs](#kzg-eip-4844-proofs)
- [Send whole account balance](#send-whole-account-balance)
- [Sign and verify messages with EIP-191, EIP-712](#sign-and-verify-messages)
- [Security](#security)
- [Performance](#performance)
- [License](#license)

### Create random wallet

```ts
import { addr } from 'micro-eth-signer';
const random = addr.random(); // Secure: uses CSPRNG
console.log(random.privateKey, random.address);
// '0x17ed046e6c4c21df770547fad9a157fd17b48b35fe9984f2ff1e3c6a62700bae'
// '0x26d930712fd2f612a107A70fd0Ad79b777cD87f6'
```

### Transactions: create, sign

```ts
import { Transaction, weigwei, weieth } from 'micro-eth-signer';
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);
```

We support legacy, EIP2930, EIP1559, EIP4844 and EIP7702 transactions.

### Addresses: create, checksum

```ts
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)
);
```

### Network and smart contracts

A common problem in web3 libraries is how complex they are to audit with regards to network calls.

In eth-signer, all network calls are done with user-provided function, conforming to built-in `fetch()`:

1. This makes library network-free, which simplifies auditability
2. User fully controls all network requests

It's recommended to use [micro-ftch](https://github.com/paulmillr/micro-ftch),
which works on top of fetch and implements killswitch, logging, concurrency limits and other features.

#### Init network

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:

```js
// Requests are made with fetch(), a built-in method
import { jsonrpc } from 'micro-ftch';
import { Web3Provider } from 'micro-eth-signer/net';
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' })
);
```

#### Fetch balances and history from an archive node

```ts
const addr = '0xd8da6bf26964af9d7eed9e03e53415d37aa96045';
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; // {baseFeePerGas, hash, timestamp...}
// height(): Promise;
// internalTransactions(address: string, opts?: TraceOpts): Promise;
// ethLogsSingle(topics: Topics, opts: LogOpts): Promise;
// ethLogs(topics: Topics, opts?: LogOpts): Promise;
// 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;
// transfers(address: string, opts?: TraceOpts & LogOpts): Promise;
// allowances(address: string, opts?: LogOpts): Promise;
// tokenBalances(address: string, tokens: string[]): Promise>;
```

Basic data can be fetched from any node.

Historical balances, transactions and others can only be fetched from an archive node, such as Erigon or Reth.

#### Fetch Chainlink oracle prices

```ts
import { Chainlink } from 'micro-eth-signer/net';
const link = new Chainlink(prov);
const btc = await link.coinPrice('BTC');
const bat = await link.tokenPrice('BAT');
console.log({ btc, bat }); // BTC 19188.68870991, BAT 0.39728989 in USD
```

#### Resolve ENS address

```ts
import { ENS } from 'micro-eth-signer/net';
const ens = new ENS(prov);
const vitalikAddr = await ens.nameToAddress('vitalik.eth');
```

#### Swap tokens with Uniswap

> 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.

```ts
import { tokenFromSymbol } from 'micro-eth-signer/abi';
import { UniswapV3 } from 'micro-eth-signer/net'; // or UniswapV2

const USDT = tokenFromSymbol('USDT');
const BAT = tokenFromSymbol('BAT');
const u3 = new UniswapV3(prov); // or new UniswapV2(provider)
const fromAddress = '0xd8da6bf26964af9d7eed9e03e53415d37aa96045';
const toAddress = '0xd8da6bf26964af9d7eed9e03e53415d37aa96045';
const swap = await u3.swap(USDT, BAT, '12.12', { slippagePercent: 0.5, ttl: 30 * 60 });
const swapData = await swap.tx(fromAddress, toAddress);
console.log(swapData.amount, swapData.expectedAmount, swapData.allowance);
```

### Parsers

#### Human-readable transaction hints

The transaction sent ERC-20 USDT token between addresses. The library produces a following hint:

> Transfer 22588 USDT to 0xdac17f958d2ee523a2206206994597c13d831ec7

```ts
import { decodeTx } from 'micro-eth-signer/abi';

const tx =
'0xf8a901851d1a94a20082c12a94dac17f958d2ee523a2206206994597c13d831ec780b844a9059cbb000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7000000000000000000000000000000000000000000000000000000054259870025a066fcb560b50e577f6dc8c8b2e3019f760da78b4c04021382ba490c572a303a42a0078f5af8ac7e11caba9b7dc7a64f7bdc3b4ce1a6ab0a1246771d7cc3524a7200';
// Decode tx information
deepStrictEqual(decodeTx(tx), {
name: 'transfer',
signature: 'transfer(address,uint256)',
value: {
to: '0xdac17f958d2ee523a2206206994597c13d831ec7',
value: 22588000000n,
},
hint: 'Transfer 22588 USDT to 0xdac17f958d2ee523a2206206994597c13d831ec7',
});
```

Or if you have already decoded tx:

```ts
import { decodeData } from 'micro-eth-signer/abi';

const to = '0x7a250d5630b4cf539739df2c5dacb4c659f2488d';
const data =
'7ff36ab5000000000000000000000000000000000000000000000000ab54a98ceb1f0ad30000000000000000000000000000000000000000000000000000000000000080000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa96045000000000000000000000000000000000000000000000000000000006fd9c6ea0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000106d3c66d22d2dd0446df23d7f5960752994d600';
const value = 100000000000000000n;

deepStrictEqual(decodeData(to, data, value, { customContracts }), {
name: 'swapExactETHForTokens',
signature: 'swapExactETHForTokens(uint256,address[],address,uint256)',
value: {
amountOutMin: 12345678901234567891n,
path: [
'0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2',
'0x106d3c66d22d2dd0446df23d7f5960752994d600',
],
to: '0xd8da6bf26964af9d7eed9e03e53415d37aa96045',
deadline: 1876543210n,
},
});

// With custom tokens/contracts
const customContracts = {
'0x106d3c66d22d2dd0446df23d7f5960752994d600': { abi: 'ERC20', symbol: 'LABRA', decimals: 9 },
};
deepStrictEqual(decodeData(to, data, value, { customContracts }), {
name: 'swapExactETHForTokens',
signature: 'swapExactETHForTokens(uint256,address[],address,uint256)',
value: {
amountOutMin: 12345678901234567891n,
path: [
'0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2',
'0x106d3c66d22d2dd0446df23d7f5960752994d600',
],
to: '0xd8da6bf26964af9d7eed9e03e53415d37aa96045',
deadline: 1876543210n,
},
hint: 'Swap 0.1 ETH for at least 12345678901.234567891 LABRA. Expires at Tue, 19 Jun 2029 06:00:10 GMT',
});
```

#### Human-readable event hints

Decoding the event produces the following hint:

> Allow 0xe592427a0aece92de3edee1f18e0157c05861564 spending up to 1000 BAT from 0xd8da6bf26964af9d7eed9e03e53415d37aa96045

```ts
import { decodeEvent } from 'micro-eth-signer/abi';

const to = '0x0d8775f648430679a709e98d2b0cb6250d2887ef';
const topics = [
'0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925',
'0x000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa96045',
'0x000000000000000000000000e592427a0aece92de3edee1f18e0157c05861564',
];
const data = '0x00000000000000000000000000000000000000000000003635c9adc5dea00000';
const einfo = decodeEvent(to, topics, data);
console.log(einfo);
```

#### ABI type inference

The ABI is type-safe when `as const` is specified:

```ts
import { createContract } from 'micro-eth-signer/abi';
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);
// Would create following typescript type:
{
getReserves: {
encodeInput: () => Uint8Array;
decodeOutput: (b: Uint8Array) => {
reserve0: bigint;
reserve1: bigint;
blockTimestampLast: bigint;
};
}
}
```

We're parsing values as:

```js
// 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`](./src/net/ens.ts) for type-safe contract execution example.

#### RLP parsing

We implement RLP (Recursive Length Prefix) in 100 lines of code, using [packed](https://github.com/paulmillr/micro-packed).

Check out [rlp.test.js](https://github.com/paulmillr/micro-eth-signer/blob/main/test/rlp.test.js) for example usage.

```ts
import { RLP } from 'micro-eth-signer/rlp';
RLP.decode(RLP.encode('dog'));
```

#### SSZ parsing

We implement SSZ (simple serialize) in 1500 lines of code, using [packed](https://github.com/paulmillr/micro-packed).
Stable containers from [EIP-7495](https://eips.ethereum.org/EIPS/eip-7495) are supported.

Check out [ssz.test.js](https://github.com/paulmillr/micro-eth-signer/blob/main/test/ssz.test.js) for example usage.

```ts
import * as ssz from 'micro-eth-signer/ssz';
```

### Sign and verify messages

#### EIP-191

```ts
import * as typed from 'micro-eth-signer/typed-data';

// Example message
const message = "Hello, Ethereum!";
const privateKey = "0x4c0883a69102937d6231471b5dbb6204fe512961708279f1d7b1b8e7e8b1b1e1";

// Sign the message
const signature = typed.personal.sign(message, privateKey);
console.log("Signature:", signature);

// Verify the signature
const address = "0xYourEthereumAddress";
const isValid = typed.personal.verify(signature, message, address);
console.log("Is valid:", isValid);
```

#### EIP-712

```ts
import { signTyped, verifyTyped, recoverPublicKeyTyped, EIP712Domain, TypedData } from './typed-data';
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: 1,
verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC',
salt: '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 = {
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 = "0xYourEthereumAddress";
const isValid = verifyTyped(signature, typedData, address);

// Recover the public key
const publicKey = recoverPublicKeyTyped(signature, typedData);
```

### Utilities

#### KZG EIP-4844 proofs

```ts
import { KZG } from 'micro-eth-signer/kzg';
// 400kb, 4-sec init
import { trustedSetup } from '@paulmillr/trusted-setups';
// 800kb, instant init
import { trustedSetup as fastSetup } from '@paulmillr/trusted-setups/fast.js';

const kzg = new KZG(trustedSetup);

// Example blob and scalar
const blob = '0x1234567890abcdef'; // Add actual blob data
const z = '0x1'; // Add actual scalar

// Compute and verify proof
const [proof, y] = kzg.computeProof(blob, z);
console.log('Proof:', proof);
console.log('Y:', y);
const commitment = '0x1234567890abcdef'; // Add actual commitment
const z = '0x1'; // Add actual scalar
// const y = '0x2'; // Add actual y value
const proof = '0x3'; // Add actual proof
const isValid = kzg.verifyProof(commitment, z, y, proof);
console.log('Is valid:', isValid);

// Compute and verify blob proof
const blob = '0x1234567890abcdef'; // Add actual blob data
const commitment = '0x1'; // Add actual commitment
const proof = kzg.computeBlobProof(blob, commitment);
console.log('Blob proof:', proof);
const isValidB = kzg.verifyBlobProof(blob, commitment, proof);
```

#### Send whole account balance

```ts
import { addr, Transaction, weigwei, weieth } from 'micro-eth-signer';
const privKey = '0x6b911fd37cdf5c81d4c0adb1ab7fa822ed253ab0ad9aa18d77257c88b29b718e';
const senderAddr = addr.fromPrivateKey(privKey);
const unsignedTx = Transaction.prepare({
to: '0xdf90dea0e0bf5ca6d2a7f0cb86874ba6714f463e',
maxFeePerGas: weigwei.decode('100'), // 100 gwei in wei
value: weieth.decode('1.1'), // 1.1 eth in wei
nonce: 0n,
});

const CURRENT_BALANCE = '1.7182050000017'; // eth
const txSendingWholeBalance = unsignedTx.setWholeAmount(weieth.decode(CURRENT_BALANCE));
```

Creates transaction which sends whole account balance. Does two things:

1. `amount = accountBalance - maxFeePerGas * gasLimit`
2. `maxPriorityFeePerGas = 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_.

## Security

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 contract
- `createContract(abi, net)` would create contract that calls network using `net`, 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

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

## Performance

Transaction signature matches `noble-curves` `sign()` speed,
which means over 4000 times per second on macs.

The first call of `sign` will take 20ms+ due to noble-curves secp256k1 `utils.precompute`.

To run benchmarks, execute `npm run bench`.

## Contributing

Make sure to use recursive cloning for the [eth-vectors](https://github.com/paulmillr/eth-vectors) submodule:

git clone --recursive https://github.com/paulmillr/micro-eth-signer.git

## License

MIT License

Copyright (c) 2021 Paul Miller (https://paulmillr.com)