Scope: canonical blockchain classes, aliases, and address validation.
src/core/chain.tsholdsChainand the shared family abstractionssrc/core/errors.tsholds theChainsErrorhierarchy. Never throw a rawErrorsrc/core/registry.tsis the constructor registrysrc/core/resolve.tsowns the aliases andgetChain; canonical keys and display names are matched against the registry, so the alias table holds only real aliases, never a key as its own entrysrc/core/identify.tspartitions the registry by an address: matching validators and unchecked chainssrc/core/text.tsholds the guards caller text passes through before any surface prints itsrc/core/base58.tsdecodes base58 for chains that check the bytes behind an address. The alphabet is an argument, Bitcoin's by default and the XRP Ledger's forxrplsrc/core/base58check.tsdecodes Base58Check on top ofbase58.tsand refuses a checksum that does not hold. Bitcoin's legacy form, Litecoin, Pepecoin, TRON and the XRP Ledger read through itsrc/core/sha256.tsis SHA-256 written out, because the core imports nothing at runtime andassertAddresscannot await the Web Crypto digestsrc/core/bech32.tsreads Bech32 digits and packs them into bytes. The human-readable part and the digit bound are arguments, because BIP-173's 90-character cap is Bitcoin's rule and Cardano writes past itsrc/core/segwit.tschecks BIP-173/350 SegWit addresses on top ofbech32.tsfor the chains that took Bitcoin's witness program rules. The human-readable part is an argument,bcfor Bitcoin andltcfor Litecoinsrc/chains/*.tsis one concrete blockchain class per filesrc/chains/index.tsholdsbuiltins, the ordered list the registry is seeded from. A chain file that is not in it is not in the registrysrc/index.tsis the public APIsrc/cli.tsplussrc/commands/*.tsis the citty CLI:info,resolve,validate,identify,list,mcpsrc/tool-operations.tsholds the tool executors shared by MCP, Pi and OMP. No surface reimplements an operationsrc/mcp.tsexportscreateMcpServer(),src/commands/mcp.tsruns it over stdiopackages/{pi,omp}/extensions/chains.tsare the agent tools. The OMP file is a full copy, never a re-exportsrc/version.tsis the version stringtest/unit/chains.test.tscovers hierarchy, registry, metadata, and validationtest/unit/mcp.test.tsdrives the MCP server over an in-memory transportdocs/is the Docus site behind chains.agntn.dev, with its ownAGENTS.md. It imports the built package fromfile:.., sopnpm buildcomes first
Constructor registry. Concrete blockchain classes own their metadata and behavior, the registry owns constructors and hands back instances.
- ESM-only. The core imports nothing at runtime, the CLI adds
cittyandconsola, the MCP server adds@modelcontextprotocol/sdk, the extensions needtypeboxand@earendil-works/pi-coding-agent - Build with
obuild, entriessrc/index.ts,src/cli.ts,src/mcp.tsandsrc/tool-operations.ts.obuild0.4 accepts onlycwd,entriesandhooks, everything else is silently ignored sideEffectsnamesdist/cli.mjsand nothing else. That holds only while no module registers itself on import: put aregister()call back at the top of a chain file and the class reaches the registry through a bare import, which a tree-shaker is free to drop. New chains go inbuiltins- Extensions load
dist/tool-operations.mjs, sopnpm buildhas to run beforetsc -p tsconfig.extensions.json - The OMP loader must keep both dynamic imports literal (
import("../../../dist/tool-operations.mjs")orimport("../../../src/tool-operations.ts")). Animport(url.href)built from a runtime value loses bare-dependency resolution in the compiled OMP binary. Pi may keep the existsSync form. - MCP is built on the low-level
Server, deprecated in the SDK, becauseMcpServer.registerTooltakes Standard Schema only andtypebox1.x is not one. The alternative is a second definition of every parameter - An MCP client reads
contentand neverdetails, so tool text has to carry whatever the next call needs - Caller text reaches a tool's
contentor the CLI's own output throughquoted()orstripControlCharacters()insrc/core/text.ts, never raw: a newline in an address writes its own line of the answer.detailskeeps the value unchanged, so a surface that renders it owes its own escaping - Lint and format with
oxlintplusoxfmt(pnpm run fmt) - Test with vitest (
pnpm run test) verbatimModuleSyntax: true, so type imports useimport type- Canonical chain key is a lowercase
ChainKeythat names the chain rather than its ticker:ethereum, noteth. A short name is still a name, sobsc,zksyncandarbitrumstay; ticker spellings belong in the alias table - Metadata that encodes the same fact twice gets a cross-field test, not just a type.
chainIdand theeip155:reference incaip2are checked against each other intest/unit/chains.test.ts; Linea shipped a testnet id against a mainnet CAIP-2 until that test existed - An address validator built only from a character-length window is wrong. Decode when the format is base58 with a known byte length, verify the checksum when the format is Base58Check, and follow the spec's case rules for bech32. Octra is the exception: its address is a fixed 44 characters cut out of base58, not encoded from a payload, so the width is the whole format and decoding rejects real contract addresses
- Use contextual class names:
EVM extends Chain,Ethereum extends EVM. Do not repeatChainin subclass names
Key generation and HD key/address derivation belong in @agntn/keys, not in the chain classes. RPC calls belong in @agntn/nodes. Wallet storage, account management, and transaction building remain outside this package's scope.