Guide

Working with EVM Chains

Ethereum and Base share one implementation, one address, and one signing scheme.

One base class

Ethereum and Base extend AbstractEVMBlockchain. Keys, addresses, validation, and message signing all live in the base, so a new EVM chain is two lines:

import { AbstractEVMBlockchain, BIP44 } from "@agntn/keys";

export default class Ethereum extends AbstractEVMBlockchain {
  override readonly name = "ethereum";
  override readonly bip44 = BIP44.ETHEREUM;
}

Base looks identical apart from the name. It even keeps coin type 60, because that is what every wallet does for L2s.

Same key, same address everywhere

import { useBlockchain } from "@agntn/keys";
import Ethereum from "@agntn/keys/blockchains/ethereum";
import Base from "@agntn/keys/blockchains/base";

const ethereumChain = useBlockchain(new Ethereum());
const baseChain = useBlockchain(new Base());

const privateKey = ethereumChain.generateKeyPrivate();

const ethAddress = ethereumChain.getAddress(ethereumChain.getKeyPublic(privateKey));
const baseAddress = baseChain.getAddress(baseChain.getKeyPublic(privateKey));

ethAddress === baseAddress; // true

This is a feature of the EVM, not of the library. Address is a function of the public key alone, so one wallet serves every EVM chain. It also means sending to the wrong chain lands the funds at the right address on the wrong network, which is a much worse afternoon than a typo.

The address

Take the uncompressed public key without the leading 04, Keccak-256 it, keep the last 20 bytes, add 0x. Then EIP-55:

"0x7e5f4552091a69125d5dfcb7b8c2659029395bdf"; // valid, no checksum
"0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf"; // same address, checksummed

The checksum hides in the letter case. Hash the lowercase hex, and every character whose hash nibble is 8 or more gets uppercased. getAddress always returns the checksummed form.

Validation

ethereumChain.validateAddress("0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf"); // true
ethereumChain.validateAddress("0x7e5f4552091a69125d5dfcb7b8c2659029395bdf"); // true, all lowercase
ethereumChain.validateAddress("0x7e5F4552091A69125d5DfCb7b8C2659029395Bdf"); // false, mixed case, wrong checksum

All lowercase and all uppercase pass, because those carry no checksum and plenty of tooling still emits them. Mixed case has to match EIP-55 exactly.

Signing

signMessage uses the personal_sign preamble, "\x19Ethereum Signed Message:\n" + length + message, hashed with Keccak-256 and signed with secp256k1. That is what MetaMask and every EVM verifier expect, so signatures made here verify elsewhere and the other way round.

Adding another EVM chain

Drop a file into src/blockchains/ and register it in the lazy loader:

// src/blockchains/polygon.ts
import { AbstractEVMBlockchain } from "../utils/evm.ts";
import { BIP44 } from "../utils/bip44/index.ts";

export default class Polygon extends AbstractEVMBlockchain {
  override readonly name = "polygon";
  override readonly bip44 = BIP44.ETHEREUM;
}
// src/_blockchains.ts
polygon: lazy(() => import("./blockchains/polygon.ts")),

Nothing else. The base class already knows how to do everything the chain needs.

@agntn/keys·MIT license· Keys never leave the browser.