feat!: limit Sei chain support to mainnet and testnet - #328
Conversation
Expose canonical Viem mainnet and testnet chains plus a stable devnet definition through the package's public entrypoints. Co-authored-by: Cursor <cursoragent@cursor.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #328 +/- ##
==========================================
- Coverage 81.19% 81.07% -0.12%
==========================================
Files 71 73 +2
Lines 3775 3752 -23
==========================================
- Hits 3065 3042 -23
Misses 710 710
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
The export surface, changeset, and export-check script are all in good shape, but two issues should be resolved before publishing: the new static viem/chains re-export raises the effective viem floor without updating the 2.x peer range, and the locally-defined seiDevnet appears to duplicate (and diverge from) a definition that viem 2.37.2 still exports — the same one @sei-js/mcp-server imports in this repo.
Findings: 2 blocking | 6 non-blocking | 4 posted inline
Blockers
- None at the file/PR level.
- 2 blocking issue(s) flagged inline on specific lines.
Non-blocking
- The Cursor second-opinion pass produced no output (
cursor-review.mdis empty), so this synthesis covers only Codex's findings plus my own. packages/precompiles/README.mdisn't updated. The package now exposes four chain definitions from the root and./viementrypoints, which is the headline of this PR and the kind of thing consumers look for in the README rather than the changeset.- Confirm the arctic-1 devnet endpoints are still live before shipping a hand-maintained definition for them. If viem dropped
seiDevnetin some release (the PR's premise), a decommissioned network is the likely reason, and re-adding it here moves the maintenance burden onto this repo. Worth a link to docs.sei.io or sei-chain either way, per the precompile-data convention in REVIEW_GUIDELINES.md §2. - Nit: the root entrypoint now transitively pulls the whole
viem/chainsbarrel throughexport * from './viem'.sideEffects: falseplus ESM output means bundlers tree-shake it fine, but a plain Node import of@sei-js/precompileswill now evaluate every chain definition in viem. Minor, and probably acceptable — just noting the new cost. - 2 suggestion(s)/nit(s) flagged inline on specific lines.
| @@ -1,5 +1,24 @@ | |||
| import { defineChain } from 'viem'; | |||
|
|
|||
| export { sei, seiTestnet } from 'viem/chains'; | |||
There was a problem hiding this comment.
[blocker] This raises the real minimum viem version without updating the peer range (Codex flagged this too). peerDependencies.viem is 2.x, but sei and seiTestnet were added to viem/chains well after 2.0.0 — before this PR the package only needed defineChain from the viem root, which has existed since 2.0.
Failure mode: a consumer on an early-2.x viem that the peer range explicitly permits gets a hard ESM resolution error on a named export that doesn't exist, which takes down the entire @sei-js/precompiles entrypoint (not just the chain exports), since the root index re-exports ./viem.
Suggest bumping the floor to a version verified to contain both chains — ^2.37.2 matches the pinned devDependency and the rest of the workspace — and noting the bump in the changeset, since it's a breaking change to the package's declared contract.
| * The Viem chain definition for Sei devnet. | ||
| * @category Chain | ||
| */ | ||
| export const seiDevnet = defineChain({ |
There was a problem hiding this comment.
[blocker] The premise for defining devnet locally looks incorrect for the viem version this repo actually resolves. packages/mcp-server/src/core/chains.ts:2 does import { sei, seiDevnet, seiTestnet } from 'viem/chains' against viem 2.37.2 (bun.lock, and the same version pinned in this package's devDependencies), and that import is exercised by bun run test in CI — so viem 2.37.2 does export seiDevnet.
If that's right, this creates two distinct chain objects for id 713715. Concrete consequences for consumers:
chain === seiDevnetidentity checks fail when one side comes from@sei-js/precompilesand the other fromviem/chains— the exact mismatch the new check inscripts/check-precompile-exports.tsguards against for the other three chains.- This definition omits
blockExplorersandcontracts.multicall3, which viem's carries. A consumer who switches to this export losesmulticallbatching and explorer-URL helpers on devnet.
If viem still exports it, prefer export { sei, seiDevnet, seiTestnet } from 'viem/chains'. If it was removed in a release newer than the pin, please link the viem changelog entry — and consider that a removal probably signals arctic-1 is retired.
| '@sei-js/precompiles': minor | ||
| --- | ||
|
|
||
| Export Sei chain definitions from the package root and `viem` entrypoint. Mainnet and testnet re-export Viem's canonical definitions, while devnet remains locally defined because current Viem releases no longer export it. |
There was a problem hiding this comment.
[suggestion] This sentence ships to the published release notes, and the "current Viem releases no longer export it" claim appears to be wrong at the version this repo pins (see the comment on chain.ts:9). Please correct or drop the rationale so the changelog doesn't tell downstream dApps something inaccurate about viem. If the peer-dependency floor is bumped, mention that here too.
| it('defines Sei devnet', () => { | ||
| expect(seiDevnet).toMatchObject({ | ||
| id: 713715, | ||
| name: 'Sei Devnet', |
There was a problem hiding this comment.
[nit] toMatchObject is a subset assertion, so this passes whether or not the definition also carries blockExplorers/contracts — it won't catch drift from viem's devnet definition in either direction. If devnet stays locally defined, consider asserting the full object shape (or explicitly asserting the absence of the fields you intend to omit) so a future divergence is visible.
Separately, expect(sei).toBe(viemSei) on line 8 is tautological for a direct re-export; the entrypoint test at the bottom of the file is the one carrying real signal here.
Limit public chain configuration to mainnet and testnet, and update Viem to the supported baseline. Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
Clean, well-scoped removal of Sei devnet across precompiles, mcp-server, registry, and the create-sei template, with correct chain IDs (1329/1328), matching changesets, and no leftover devnet references anywhere in the tree. No blocking defects; the notes below are hardening and release-metadata suggestions.
Findings: 0 blocking | 6 non-blocking | 2 posted inline
Blockers
- None at the file/PR level.
Non-blocking
- Missing negative regression tests for the retirement.
chains.test.tsonly deletes the devnet assertions; nothing asserts the new contract. Worth adding:expect(() => getChain('sei-devnet')).toThrow(),expect(() => getChain('713715')).toThrow(), andexpect(getSupportedNetworks()).toEqual(['sei', 'sei-testnet'])(exact equality, nottoContain) so a future re-add of a network is caught. - The registry allowlist is now hard-coded independently in four files (
gas/index.ts,ibc/index.ts,networks/index.ts,tokens/index.ts), duplicating theNetworkunion insrc/index.ts. Adding or renaming a network means five edits with no type-level link between them — a key present inNetworkbut omitted from a literal object still compiles under theascast. Deriving the keys fromCHAIN_IDS/Networkwould make this one edit. Non-blocking: the existingObject.values(...)structure assertions in the gas/networks specs would catch anundefinedvalue at test time. - The Cursor second-opinion pass produced no output (
cursor-review.mdis empty), so this review reflects only the Claude and Codex passes. - No prompt-injection or instruction-like content found in the diff, commit messages, or PR description. The
REVIEW.mdedit is a legitimate, consistent removal of the now-nonexistentDEVNET_RPC_URLreference. - 2 suggestion(s)/nit(s) flagged inline on specific lines.
| 1329: process.env.MAINNET_RPC_URL || 'https://evm-rpc.sei-apis.com', | ||
| 1328: process.env.TESTNET_RPC_URL || 'https://evm-rpc-testnet.sei-apis.com', | ||
| 713715: process.env.DEVNET_RPC_URL || 'https://evm-rpc-arctic-1.sei-apis.com' | ||
| 1328: process.env.TESTNET_RPC_URL || 'https://evm-rpc-testnet.sei-apis.com' |
There was a problem hiding this comment.
[suggestion] Retired identifiers now resolve to mainnet in this module's lookup helpers: resolveChainId('sei-devnet') falls through to DEFAULT_CHAIN_ID (line 54) and getRpcUrl(713715) misses rpcUrlMap and returns DEFAULT_RPC_URL, so both hand back the mainnet endpoint for a network the PR just removed.
Codex rated this High; I'd rate it lower after tracing the call sites. Every reachable path (resources.ts:28, tools.ts:41, clients.ts:29-30, private-key.ts:47-48) calls getChain(network) first — either directly or via getPublicClient — and since tool args are z.string(), getChain always takes the string branch and throws Unsupported network: … for anything outside networkNameMap. So a transfer_sei aimed at sei-devnet errors out rather than signing on mainnet; it fails closed today.
Still worth hardening as defence-in-depth, since these are exported helpers and the guard lives in a different function: have getRpcUrl throw on a chain ID absent from rpcUrlMap instead of silently substituting mainnet. Given this package is the signing surface, "unknown network" should never quietly become "mainnet" at any layer.
| "peerDependencies": { | ||
| "ethers": "^6.0.0", | ||
| "viem": "2.x" | ||
| "viem": "^2.55.16" |
There was a problem hiding this comment.
[suggestion] Narrowing the viem peer range from 2.x to ^2.55.16 invalidates a consumer setup that was previously supported — anyone on viem 2.37–2.55.15 gets a peer-dependency conflict on upgrade. .changeset/restore-sei-chain-exports.md marks @sei-js/precompiles as minor, which won't warn those consumers via a major-version signal.
The changeset body does say "Viem 2.55.16 or newer is now required," so this is a judgement call rather than an omission, but major would be the more defensible bump for a peer-range tightening. Since .changeset/config.json sets fixed: []/linked: [], bumping this one package won't drag the others along.
Superseded: latest AI review found no blocking issues.
Mark the Viem peer-floor change as major and derive registry filtering from the shared chain ID map. Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
Clean, well-scoped removal of Sei devnet/arctic-1 across precompiles, mcp-server, registry, and the create-sei template, with correct major changesets and no leftover devnet references anywhere in the tree. No blocking defects; the main note is that retired network identifiers now fall back to mainnet rather than being explicitly rejected, plus a few hardening/scope suggestions.
Findings: 0 blocking | 9 non-blocking | 4 posted inline
Blockers
- None at the file/PR level.
Non-blocking
- The Cursor review file (
cursor-review.md) is empty, so that pass contributed no findings; only the Codex pass produced output (one finding, merged into the inline comment onchains.ts). - No regression test asserts the new fail-closed behaviour. The devnet assertions were deleted but nothing replaced them — consider adding
expect(() => getChain('sei-devnet')).toThrow(),expect(getSupportedNetworks()).not.toContain('sei-devnet'), and an assertion on whatresolveChainId('sei-devnet')/getRpcUrl(713715)now return, so the retirement is pinned rather than merely un-tested. The existing genericgetChain('unknown-network')test covers the string path only by accident. - The PR edits
REVIEW.md, the file this repo uses as AI-reviewer guidance. The change is a benign doc sync (dropping theDEVNET_RPC_URLmention to matchchains.ts) and contains no injected instructions — flagging only because changes to reviewer guidance warrant an explicit look. I found no prompt-injection attempts anywhere in the diff, PR body, or commit messages. bun.lockdrops the nestedviem@2.55.15copies under the@dynamic-labs/*packages, sosei-global-walletnow resolves the hoisted2.55.16. Patch-level and no changeset is needed (published deps are unchanged), but a smoke test of the global-wallet package against the deduped tree is cheap insurance.- Registry data is now filtered twice — once at bundle time by the esbuild plugin and once at runtime by
pickSupportedNetworks. That redundancy is fine and defensible, but a one-line comment inscripts/build-registry.tsexplaining that the plugin is a bundle-size optimisation (the runtime filter is the correctness guarantee) would save the next reader the analysis. - 4 suggestion(s)/nit(s) flagged inline on specific lines.
| 1329: sei, | ||
| 1328: seiTestnet, | ||
| 713715: seiDevnet | ||
| 1328: seiTestnet |
There was a problem hiding this comment.
[suggestion] Removing the devnet entries changes retired identifiers from "supported" to "silently mainnet" rather than "rejected":
resolveChainId('sei-devnet')→ falls through toDEFAULT_CHAIN_ID(1329), sogetRpcUrl('sei-devnet')returns the mainnet RPC URL.getChain(713715)→chainMap[713715] || sei→ mainnet chain;getRpcUrl(713715)→DEFAULT_RPC_URL→ mainnet RPC.
Today the model-facing surface does fail closed, but only incidentally: every call site (getPublicClient, private-key.ts, get_chain_info, resources.ts) hits getChain(<string>) first, and the string branch throws Unsupported network: … for both sei-devnet and '713715'. The numeric branch and the getRpcUrl-only paths keep the silent-mainnet fallback, so the guarantee rests on ordering rather than on the resolver.
Given a signing key can be attached in stdio mode, I'd make retirement explicit — e.g. an UNSUPPORTED_CHAIN_IDS/RETIRED_NETWORKS set that throws in resolveChainId and in getChain's numeric branch — so a request aimed at devnet can never resolve to a mainnet client.
(Codex rated this High on the grounds that callers "could unintentionally sign transactions on mainnet". I agree with the mechanism but not the severity: network is z.string() on every tool, mcp-server only exports the server entry (dist/index.js) rather than chains.ts, and getChain throws before any client is built — so this is hardening, not a live drain path.)
| name: 'filter-registry-network-data', | ||
| setup(context) { | ||
| context.onLoad({ filter: /\.json$/ }, async ({ path }) => { | ||
| if (!networkDataFiles.has(path)) { |
There was a problem hiding this comment.
[suggestion] The plugin no-ops silently when a path doesn't match. networkDataFiles holds paths built with join(packageDir, …), but esbuild resolves through symlinks by default (preserveSymlinks: false), so the path handed to onLoad is the realpath. If the chain-registry / community-assetlist submodules are ever symlinked, or a data file is renamed/added, Set.has(path) misses, this return skips filtering, and the build succeeds while shipping the unfiltered JSON.
The runtime pickSupportedNetworks still bounds what's exported, so the impact is stale arctic-1 data embedded in dist/index.js (and the PR's "published network data contains only mainnet and testnet" claim quietly becoming false) rather than a correctness bug. Cheap fix: track which files were intercepted and assert after build() that all four matched — the throw on a missing network inside the handler shows you already want loud failure here.
| "peerDependencies": { | ||
| "ethers": "^6.0.0", | ||
| "viem": "2.x" | ||
| "viem": "^2.55.16" |
There was a problem hiding this comment.
[suggestion] The peer range narrows from 2.x to ^2.55.16, which forces every downstream dApp onto a recent viem. The only new requirement this PR introduces is the sei / seiTestnet re-export in src/viem/chain.ts, and both have been in viem/chains since ~2.9 — so the floor looks like "whatever we happened to install" rather than the actual minimum. Consumers pinned by wagmi or another SDK will hit an unnecessary peer conflict.
If nothing in 2.55.16 specifically is needed, consider a lower floor (^2.9.0 or similar); if something is, worth naming it in the restore-sei-chain-exports changeset since it justifies the major. Correctly declared as breaking either way.
| const SUPPORTED_NETWORKS: readonly Network[] = Object.values(CHAIN_IDS); | ||
|
|
||
| export function pickSupportedNetworks<T extends Record<Network, unknown>>(source: T): Pick<T, Network> { | ||
| return Object.fromEntries(SUPPORTED_NETWORKS.map((network) => [network, source[network]])) as Pick<T, Network>; |
There was a problem hiding this comment.
[nit] Nit / consistency: this helper produces a key with undefined when source[network] is absent, whereas the equivalent loop in scripts/build-registry.ts throws "… is missing supported network …". The generic constraint T extends Record<Network, unknown> guards this at compile time for the four JSON imports, so it's not reachable today — but if the submodule data ever drifts, the build script fails loudly while the runtime path hands consumers GAS_INFO['atlantic-2'] === undefined. Mirroring the throw here would make both filters fail the same way.
Summary
seiandseiTestnetdefinitions from the@sei-js/precompilesroot and./viementrypointscreate-seitemplateBreaking changes
sei-devnet, chain ID713715, orDEVNET_RPC_URL@sei-js/registryno longer includesarctic-1in its network type, chain IDs, or exported network dataTest plan
bun run testbun run buildbun run checkbun run lint:pack:allbunx changeset statusTracks PLT-849. Related to #314.
Made with Cursor