Working with EVM Chains
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.