epistery 1.3.8 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +65 -5
  2. package/dist/chains/Chain.d.ts +117 -0
  3. package/dist/chains/Chain.d.ts.map +1 -0
  4. package/dist/chains/Chain.js +175 -0
  5. package/dist/chains/Chain.js.map +1 -0
  6. package/dist/chains/EthereumChain.d.ts +18 -0
  7. package/dist/chains/EthereumChain.d.ts.map +1 -0
  8. package/dist/chains/EthereumChain.js +26 -0
  9. package/dist/chains/EthereumChain.js.map +1 -0
  10. package/dist/chains/JapanOpenChain.d.ts +21 -0
  11. package/dist/chains/JapanOpenChain.d.ts.map +1 -0
  12. package/dist/chains/JapanOpenChain.js +36 -0
  13. package/dist/chains/JapanOpenChain.js.map +1 -0
  14. package/dist/chains/PolygonChain.d.ts +31 -0
  15. package/dist/chains/PolygonChain.d.ts.map +1 -0
  16. package/dist/chains/PolygonChain.js +50 -0
  17. package/dist/chains/PolygonChain.js.map +1 -0
  18. package/dist/chains/index.d.ts +22 -0
  19. package/dist/chains/index.d.ts.map +1 -0
  20. package/dist/chains/index.js +39 -0
  21. package/dist/chains/index.js.map +1 -0
  22. package/dist/chains/registry.d.ts +21 -0
  23. package/dist/chains/registry.d.ts.map +1 -0
  24. package/dist/chains/registry.js +53 -0
  25. package/dist/chains/registry.js.map +1 -0
  26. package/dist/utils/Utils.d.ts.map +1 -1
  27. package/dist/utils/Utils.js +3 -2
  28. package/dist/utils/Utils.js.map +1 -1
  29. package/dist/utils/index.d.ts +1 -0
  30. package/dist/utils/index.d.ts.map +1 -1
  31. package/dist/utils/index.js +1 -0
  32. package/dist/utils/index.js.map +1 -1
  33. package/index.mjs +2 -1
  34. package/package.json +1 -1
  35. package/src/chains/Chain.ts +221 -0
  36. package/src/chains/EthereumChain.ts +23 -0
  37. package/src/chains/JapanOpenChain.ts +37 -0
  38. package/src/chains/PolygonChain.ts +53 -0
  39. package/src/chains/README.md +109 -0
  40. package/src/chains/index.ts +27 -0
  41. package/src/chains/registry.ts +53 -0
  42. package/src/utils/Utils.ts +3 -2
  43. package/src/utils/index.ts +2 -1
@@ -0,0 +1,221 @@
1
+ import { ethers } from 'ethers';
2
+ import { ProviderConfig } from '../utils/types';
3
+
4
+ /**
5
+ * Per-chain fee data, returned in the shape ethers v5 expects on a transaction.
6
+ * EIP-1559 chains return maxFeePerGas/maxPriorityFeePerGas.
7
+ * Legacy chains return gasPrice.
8
+ */
9
+ export interface ChainFeeData {
10
+ maxFeePerGas?: ethers.BigNumber;
11
+ maxPriorityFeePerGas?: ethers.BigNumber;
12
+ gasPrice?: ethers.BigNumber;
13
+ }
14
+
15
+ /**
16
+ * Optional per-chain knobs that root config can override without editing code.
17
+ * Each subclass reads only what it cares about.
18
+ */
19
+ export interface ChainPolicy {
20
+ // EIP-1559 floors (in gwei)
21
+ minPriorityFeeGwei?: number;
22
+ maxFeeMultiplier?: number; // applied to max(networkMax, minPriority * 2)
23
+ // Legacy gas
24
+ minGasPriceGwei?: number;
25
+ // Gas limit estimation safety
26
+ gasLimitMultiplier?: number; // applied to estimateGas result
27
+ }
28
+
29
+ /**
30
+ * Extended provider config that may carry per-chain policy + a private RPC URL.
31
+ * The plain `ProviderConfig` from utils/types stays the wire/storage shape;
32
+ * `ChainConfig` is what the Chain object actually holds in memory.
33
+ */
34
+ export interface ChainConfig extends ProviderConfig {
35
+ publicRpc?: string;
36
+ privateRpc?: string;
37
+ policy?: ChainPolicy;
38
+ }
39
+
40
+ /**
41
+ * Base class for an EVM chain. Subclasses override only the policy hooks
42
+ * that are actually different from the EIP-1559 default.
43
+ *
44
+ * The Chain object owns:
45
+ * - the JsonRpcProvider (with explicit network info, fixing "could not detect network")
46
+ * - per-chain fee policy (getFeeData)
47
+ * - the contract Proxy that injects fee data into write calls
48
+ * - gas-limit estimation with a per-chain safety multiplier
49
+ *
50
+ * The Chain object does NOT own:
51
+ * - wallets / private keys
52
+ * - contract ABIs
53
+ * - domain config storage
54
+ */
55
+ export class Chain {
56
+ readonly chainId: number;
57
+ readonly name: string;
58
+ readonly rpc: string; // private/server-side RPC (with API key if any)
59
+ readonly publicRpc: string | undefined; // public RPC (safe to expose to browsers)
60
+ readonly currency: { name: string; symbol: string; decimals: number };
61
+ readonly policy: ChainPolicy;
62
+
63
+ private _provider: ethers.providers.JsonRpcProvider | null = null;
64
+
65
+ constructor(config: ChainConfig) {
66
+ if (config.chainId == null) {
67
+ throw new Error(`Chain config missing chainId: ${JSON.stringify(config)}`);
68
+ }
69
+ this.chainId = Number(config.chainId);
70
+ this.name = config.name;
71
+ this.rpc = config.privateRpc || config.rpc;
72
+ this.publicRpc = config.publicRpc || config.rpc;
73
+ this.currency = {
74
+ name: config.nativeCurrencyName || '',
75
+ symbol: config.nativeCurrencySymbol || '',
76
+ decimals: config.nativeCurrencyDecimals || 18,
77
+ };
78
+ this.policy = config.policy || {};
79
+ }
80
+
81
+ /**
82
+ * Lazily-built provider with explicit network info.
83
+ * Passing `{ name, chainId }` to the JsonRpcProvider constructor avoids
84
+ * ethers' "could not detect network" error when the RPC is briefly
85
+ * unreachable at startup — ethers will skip its eth_chainId probe.
86
+ */
87
+ get provider(): ethers.providers.JsonRpcProvider {
88
+ if (!this._provider) {
89
+ this._provider = new ethers.providers.JsonRpcProvider(this.rpc, {
90
+ name: this.name,
91
+ chainId: this.chainId,
92
+ });
93
+ }
94
+ return this._provider;
95
+ }
96
+
97
+ /** EIP-1559 by default. Subclasses override for legacy gasPrice chains. */
98
+ supportsEIP1559(): boolean {
99
+ return true;
100
+ }
101
+
102
+ /**
103
+ * Default fee policy: pass through whatever the network reports via
104
+ * eth_feeHistory / eth_gasPrice. Subclasses override to apply per-chain
105
+ * floors (Polygon's 25 gwei priority floor, JOC's gasPrice floor, etc.).
106
+ */
107
+ async getFeeData(): Promise<ChainFeeData> {
108
+ const fd = await this.provider.getFeeData();
109
+ if (this.supportsEIP1559() && fd.maxFeePerGas && fd.maxPriorityFeePerGas) {
110
+ return {
111
+ maxFeePerGas: fd.maxFeePerGas,
112
+ maxPriorityFeePerGas: fd.maxPriorityFeePerGas,
113
+ };
114
+ }
115
+ if (fd.gasPrice) {
116
+ return { gasPrice: fd.gasPrice };
117
+ }
118
+ throw new Error(`Chain ${this.name} (${this.chainId}) returned no usable fee data`);
119
+ }
120
+
121
+ /**
122
+ * Estimate gas limit with this chain's safety multiplier.
123
+ * Used by callers that need to populate gasLimit explicitly (e.g. for
124
+ * pre-funding calculations). Most write calls will let ethers estimate
125
+ * automatically; this is for the cases where ethers' estimate is unsafe
126
+ * (Polygon Amoy, JOC) and a multiplier is required.
127
+ */
128
+ async estimateGas(
129
+ populated: ethers.providers.TransactionRequest
130
+ ): Promise<ethers.BigNumber> {
131
+ const estimate = await this.provider.estimateGas(populated);
132
+ const multiplier = this.policy.gasLimitMultiplier ?? 1.3;
133
+ // Multiply via integer math to stay in BigNumber-land.
134
+ const num = Math.round(multiplier * 100);
135
+ return estimate.mul(num).div(100);
136
+ }
137
+
138
+ /**
139
+ * Wrap an ethers.Contract so every state-mutating method automatically
140
+ * receives this chain's fee data as the transaction overrides argument.
141
+ *
142
+ * Uses Object.create (prototype chain) — the wrapper object gets its own
143
+ * writable properties for the write methods while reads of everything else
144
+ * (.address, .signer, view functions, .interface, etc.) fall through to
145
+ * the original contract via the prototype.
146
+ *
147
+ * Why not a Proxy: ethers v5 defines ABI methods with defineReadOnly
148
+ * (non-writable, non-configurable). V8's proxy invariant requires get
149
+ * traps to return the *original* value for such properties — returning a
150
+ * wrapped function throws TypeError. Object.create avoids this because
151
+ * the own properties on the child shadow the prototype's frozen ones.
152
+ *
153
+ * NOTE: epistery-host's DomainChain does NOT use this because all its
154
+ * write call sites already pass feeData explicitly. This method exists
155
+ * for other consumers (e.g. CLI tools, agents) that want automatic fee
156
+ * injection without threading overrides through every call.
157
+ *
158
+ * @param contract - the ethers.Contract instance
159
+ * @param abi - the same ABI used to construct the contract; needed to
160
+ * identify which methods are state-mutating.
161
+ */
162
+ wrapContract<T extends ethers.Contract>(contract: T, abi: ReadonlyArray<any>): T {
163
+ const writeFns = new Set<string>();
164
+ for (const item of abi) {
165
+ if (item.type !== 'function') continue;
166
+ if (item.stateMutability === 'view' || item.stateMutability === 'pure') continue;
167
+ if (typeof item.name === 'string') writeFns.add(item.name);
168
+ }
169
+ const chain = this;
170
+ const wrapped = Object.create(contract);
171
+
172
+ for (const name of writeFns) {
173
+ const original = contract[name as keyof T];
174
+ if (typeof original !== 'function') continue;
175
+ Object.defineProperty(wrapped, name, {
176
+ value: async function (...args: any[]) {
177
+ let overrides: any;
178
+ const last = args[args.length - 1];
179
+ if (Chain.isOverridesObject(last)) {
180
+ const fee = await chain.getFeeData();
181
+ overrides = { ...fee, ...last };
182
+ args[args.length - 1] = overrides;
183
+ } else {
184
+ overrides = await chain.getFeeData();
185
+ args.push(overrides);
186
+ }
187
+ return (original as Function).apply(contract, args);
188
+ },
189
+ writable: true,
190
+ configurable: true,
191
+ });
192
+ }
193
+
194
+ return wrapped as T;
195
+ }
196
+
197
+ /**
198
+ * Recognize a transaction-overrides object so we don't mistake it for a
199
+ * positional argument. Excludes BigNumbers and arrays explicitly.
200
+ */
201
+ static isOverridesObject(x: any): boolean {
202
+ if (!x || typeof x !== 'object') return false;
203
+ if (Array.isArray(x)) return false;
204
+ if (ethers.BigNumber.isBigNumber(x)) return false;
205
+ return (
206
+ 'gasPrice' in x ||
207
+ 'maxFeePerGas' in x ||
208
+ 'maxPriorityFeePerGas' in x ||
209
+ 'gasLimit' in x ||
210
+ 'nonce' in x ||
211
+ 'value' in x ||
212
+ 'from' in x ||
213
+ 'type' in x
214
+ );
215
+ }
216
+
217
+ /** Convenience: gwei → BigNumber wei */
218
+ protected gwei(n: number): ethers.BigNumber {
219
+ return ethers.utils.parseUnits(String(n), 'gwei');
220
+ }
221
+ }
@@ -0,0 +1,23 @@
1
+ import { Chain } from './Chain';
2
+ import { registerChain } from './registry';
3
+
4
+ /**
5
+ * Ethereum mainnet (chainId 1).
6
+ *
7
+ * Standard EIP-1559 behavior — ethers' getFeeData is accurate here.
8
+ * No floors, no special multipliers. The base class default works as-is;
9
+ * this subclass exists for the registry and for future overrides.
10
+ */
11
+ export class EthereumChain extends Chain {
12
+ static chainId = 1;
13
+ }
14
+
15
+ /**
16
+ * Sepolia testnet (chainId 11155111). Same behavior as Ethereum mainnet.
17
+ */
18
+ export class SepoliaChain extends Chain {
19
+ static chainId = 11155111;
20
+ }
21
+
22
+ registerChain(EthereumChain.chainId, EthereumChain);
23
+ registerChain(SepoliaChain.chainId, SepoliaChain);
@@ -0,0 +1,37 @@
1
+ import { ethers } from 'ethers';
2
+ import { Chain, ChainFeeData } from './Chain';
3
+ import { registerChain } from './registry';
4
+
5
+ /**
6
+ * Japan Open Chain (chainId 81).
7
+ *
8
+ * Why this class exists: JOC is a legacy gasPrice chain. Its base fee is
9
+ * effectively zero, but the RPC enforces a high *minimum* gas price (around
10
+ * 30 gwei). EIP-1559 fields are not honored — submitting a transaction with
11
+ * maxFeePerGas/maxPriorityFeePerGas instead of gasPrice gets rejected.
12
+ *
13
+ * The base class would happily try to use EIP-1559; we override
14
+ * supportsEIP1559() to false and return only `gasPrice`, clamped to the
15
+ * configured floor.
16
+ */
17
+ export class JapanOpenChain extends Chain {
18
+ static chainId = 81;
19
+
20
+ supportsEIP1559(): boolean {
21
+ return false;
22
+ }
23
+
24
+ protected minGasPrice(): ethers.BigNumber {
25
+ return this.gwei(this.policy.minGasPriceGwei ?? 30);
26
+ }
27
+
28
+ async getFeeData(): Promise<ChainFeeData> {
29
+ const fd = await this.provider.getFeeData();
30
+ const floor = this.minGasPrice();
31
+ const networkPrice = fd.gasPrice ?? floor;
32
+ const gasPrice = networkPrice.gt(floor) ? networkPrice : floor;
33
+ return { gasPrice };
34
+ }
35
+ }
36
+
37
+ registerChain(JapanOpenChain.chainId, JapanOpenChain);
@@ -0,0 +1,53 @@
1
+ import { ethers } from 'ethers';
2
+ import { Chain, ChainFeeData } from './Chain';
3
+ import { registerChain } from './registry';
4
+
5
+ /**
6
+ * Polygon mainnet (chainId 137).
7
+ *
8
+ * Why this class exists: Polygon enforces a hard 25 gwei minimum priority fee
9
+ * at the RPC level. Transactions submitted with a lower priority fee are
10
+ * rejected with "max fee per gas less than block base fee" or simply silently
11
+ * dropped. ethers v5's getFeeData() does NOT know about this floor and will
12
+ * happily return ~1.5 gwei from eth_feeHistory.
13
+ *
14
+ * The fix: clamp maxPriorityFeePerGas to at least 25 gwei (overridable via
15
+ * policy.minPriorityFeeGwei in root config) and ensure maxFeePerGas >=
16
+ * 2 * maxPriorityFeePerGas.
17
+ */
18
+ export class PolygonChain extends Chain {
19
+ static chainId = 137;
20
+
21
+ protected minPriorityFee(): ethers.BigNumber {
22
+ return this.gwei(this.policy.minPriorityFeeGwei ?? 25);
23
+ }
24
+
25
+ async getFeeData(): Promise<ChainFeeData> {
26
+ const fd = await this.provider.getFeeData();
27
+ const floor = this.minPriorityFee();
28
+
29
+ const networkPriority = fd.maxPriorityFeePerGas ?? floor;
30
+ const maxPriorityFeePerGas = networkPriority.gt(floor) ? networkPriority : floor;
31
+
32
+ const multiplier = this.policy.maxFeeMultiplier ?? 2;
33
+ const minMaxFee = maxPriorityFeePerGas.mul(multiplier);
34
+ const networkMax = fd.maxFeePerGas ?? minMaxFee;
35
+ const maxFeePerGas = networkMax.gt(minMaxFee) ? networkMax : minMaxFee;
36
+
37
+ return { maxPriorityFeePerGas, maxFeePerGas };
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Polygon Amoy testnet (chainId 80002).
43
+ *
44
+ * Same fee floor concerns as Polygon mainnet — Amoy is the canonical Polygon
45
+ * PoS testnet and the RPC enforces an identical 25 gwei priority floor.
46
+ * Subclassing PolygonChain keeps the policy in one place.
47
+ */
48
+ export class AmoyChain extends PolygonChain {
49
+ static chainId = 80002;
50
+ }
51
+
52
+ registerChain(PolygonChain.chainId, PolygonChain);
53
+ registerChain(AmoyChain.chainId, AmoyChain);
@@ -0,0 +1,109 @@
1
+ # `epistery/chains`
2
+
3
+ Each EVM chain epistery talks to is represented by a `Chain` object. The
4
+ `Chain` class owns everything chain-specific: the JSON-RPC provider, the fee
5
+ policy, the gas-limit estimation strategy, and the contract Proxy that
6
+ injects per-chain fee data into write transactions. Wallets, ABIs, and
7
+ domain config storage are *not* the chain's job.
8
+
9
+ ## Adding a new chain
10
+
11
+ A new chain is one file. No edits to `Chain.ts`, `registry.ts`, or any
12
+ existing chain class.
13
+
14
+ ```ts
15
+ // src/chains/MyNewChain.ts
16
+ import { ethers } from 'ethers';
17
+ import { Chain, ChainFeeData } from './Chain';
18
+ import { registerChain } from './registry';
19
+
20
+ export class MyNewChain extends Chain {
21
+ static chainId = 1234;
22
+
23
+ // Override only what's actually different from the EIP-1559 default.
24
+ // Skip this method entirely if the network behaves normally.
25
+ async getFeeData(): Promise<ChainFeeData> {
26
+ const fd = await this.provider.getFeeData();
27
+ // ... whatever this chain needs ...
28
+ return { maxFeePerGas: ..., maxPriorityFeePerGas: ... };
29
+ }
30
+ }
31
+
32
+ registerChain(MyNewChain.chainId, MyNewChain);
33
+ ```
34
+
35
+ Then add **one** line to `src/chains/index.ts`:
36
+
37
+ ```ts
38
+ export { MyNewChain } from './MyNewChain';
39
+ ```
40
+
41
+ That export both makes the class importable and triggers the
42
+ `registerChain()` call at module load.
43
+
44
+ ## Adding a chain *without* editing the package
45
+
46
+ A downstream consumer (e.g. an agent, a host, an app) can register its own
47
+ chain without forking epistery. Write the same kind of file in your own
48
+ codebase, then `import` it once during startup:
49
+
50
+ ```ts
51
+ import 'my-app/chains/MyNewChain';
52
+ ```
53
+
54
+ The registry is a module-scoped `Map`, so the registration is idempotent
55
+ and survives across the entire process.
56
+
57
+ ## What goes in a Chain subclass
58
+
59
+ Override only the hooks that are actually different from the generic
60
+ EIP-1559 default. The base class is intentionally usable as-is for any
61
+ well-behaved EVM chain — you don't need a subclass to support a new chain
62
+ unless that chain misbehaves in some way.
63
+
64
+ | Hook | Why you'd override it |
65
+ | --------------------- | -------------------------------------------------------------- |
66
+ | `getFeeData()` | Network ignores `eth_feeHistory`, has a fee floor, etc. |
67
+ | `supportsEIP1559()` | Legacy gasPrice-only chain (e.g. Japan Open Chain). |
68
+ | `estimateGas()` | RPC's gas estimate is unreliable; need a different multiplier. |
69
+ | `wrapContract()` | Almost never. Only if the chain needs different override merging. |
70
+
71
+ ## Per-chain config knobs
72
+
73
+ Each chain reads its policy knobs from the `policy` field of the provider
74
+ config in epistery's root config file. These are *optional* — defaults are
75
+ in code, so a fresh install Just Works:
76
+
77
+ ```ini
78
+ [[default.providers]]
79
+ name = "Polygon Mainnet"
80
+ chainId = 137
81
+ publicRpc = "https://polygon-rpc.com"
82
+ privateRpc = "https://polygon-mainnet.g.alchemy.com/v2/YOUR_KEY"
83
+ nativeCurrencyName = "POL"
84
+ nativeCurrencySymbol = "POL"
85
+
86
+ # These all live under [[default.providers.policy]] and are optional.
87
+ [default.providers.policy]
88
+ minPriorityFeeGwei = 25 # Polygon's RPC floor
89
+ maxFeeMultiplier = 2 # maxFeePerGas >= 2 * maxPriorityFeePerGas
90
+ gasLimitMultiplier = 1.3 # estimateGas safety margin
91
+ ```
92
+
93
+ If you find yourself adding a new policy field, add it to `ChainPolicy` in
94
+ `Chain.ts` and document it here. Don't smuggle ad-hoc fields in via casts.
95
+
96
+ ## What this replaces
97
+
98
+ Before this module, gas/fee logic lived in three different places:
99
+
100
+ 1. `epistery/src/utils/Utils.ts` — `getGasPriceWithBuffer`, `addGasBuffer`,
101
+ `FALLBACK_GAS_LIMIT`, hard-coded constants for Polygon mainnet/Amoy.
102
+ 2. `epistery-host/utils/DomainChain.mjs` — a parallel `getFeeData()` and a
103
+ contract-mutation wrapper that crashed under ESM strict mode.
104
+ 3. Anywhere a developer constructed a bare `new ethers.providers.JsonRpcProvider(rpc)`
105
+ without an explicit network — the source of every "could not detect network"
106
+ warning in the logs.
107
+
108
+ All three should call `chainFor(domainConfig.provider)` and use the
109
+ returned `Chain` instance.
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Chains module barrel.
3
+ *
4
+ * Exports the public API and pulls in the built-in chain subclasses for
5
+ * their side-effect (each subclass calls registerChain() at module load).
6
+ *
7
+ * Adding a new built-in chain: drop a `MyChain.ts` next to this file that
8
+ * extends Chain and calls `registerChain(MyChain.chainId, MyChain)` at the
9
+ * bottom, then add one `import './MyChain';` line below. No edits to
10
+ * Chain.ts or registry.ts.
11
+ *
12
+ * Adding a chain from outside this package: write the same kind of file in
13
+ * your own codebase and `import 'your-package/dist/chains/MyChain';` at app
14
+ * startup. The Map in registry.ts is module-scoped, so the registration
15
+ * happens exactly once.
16
+ */
17
+
18
+ // Public API
19
+ export { Chain, ChainConfig, ChainFeeData, ChainPolicy } from './Chain';
20
+ export { chainFor, registerChain, hasRegisteredChain, registeredChainIds } from './registry';
21
+
22
+ // Built-in chains — imported for their registerChain() side effect.
23
+ // Re-exported so callers that want a concrete subclass (e.g. for instanceof
24
+ // checks or to subclass it themselves) can import from the barrel.
25
+ export { PolygonChain, AmoyChain } from './PolygonChain';
26
+ export { EthereumChain, SepoliaChain } from './EthereumChain';
27
+ export { JapanOpenChain } from './JapanOpenChain';
@@ -0,0 +1,53 @@
1
+ import { Chain, ChainConfig } from './Chain';
2
+
3
+ type ChainCtor = new (config: ChainConfig) => Chain;
4
+
5
+ /**
6
+ * Registry of chainId → Chain subclass.
7
+ *
8
+ * Adding a chain is a single-file operation: write `MyChain.ts` extending
9
+ * `Chain`, and at the bottom of that file call:
10
+ *
11
+ * registerChain(MyChain.chainId, MyChain);
12
+ *
13
+ * Then ensure the file is imported once during startup (the built-in
14
+ * `chains/index.ts` does this for the chains shipped with the package; for
15
+ * downstream additions, just `import 'mypackage/chains/MyChain'` from your
16
+ * app entry point).
17
+ *
18
+ * No edits to this file or the barrel are required.
19
+ */
20
+ const REGISTRY = new Map<number, ChainCtor>();
21
+
22
+ /**
23
+ * Register a Chain subclass for a given chainId. Overwrites any existing
24
+ * entry — last write wins, so a downstream app can override a built-in if it
25
+ * wants different fee policy.
26
+ */
27
+ export function registerChain(chainId: number, ctor: ChainCtor): void {
28
+ REGISTRY.set(Number(chainId), ctor);
29
+ }
30
+
31
+ /**
32
+ * Get a Chain instance for the given provider config. If no subclass is
33
+ * registered for the chainId, returns a generic Chain — which uses pure
34
+ * EIP-1559 with no floors and the standard estimateGas. That works for any
35
+ * well-behaved EVM chain; misbehaving chains need their own subclass.
36
+ */
37
+ export function chainFor(config: ChainConfig): Chain {
38
+ if (config.chainId == null) {
39
+ throw new Error(`chainFor: provider config missing chainId: ${JSON.stringify(config)}`);
40
+ }
41
+ const Ctor = REGISTRY.get(Number(config.chainId)) || Chain;
42
+ return new Ctor(config);
43
+ }
44
+
45
+ /** Visible for tests / debug. Returns true if a chainId has a registered subclass. */
46
+ export function hasRegisteredChain(chainId: number): boolean {
47
+ return REGISTRY.has(Number(chainId));
48
+ }
49
+
50
+ /** Visible for tests / debug. Returns the list of registered chainIds. */
51
+ export function registeredChainIds(): number[] {
52
+ return Array.from(REGISTRY.keys());
53
+ }
@@ -1,6 +1,7 @@
1
1
  import { BigNumberish, ethers, Wallet } from 'ethers';
2
2
  import { Config } from './Config';
3
3
  import { DomainConfig, RivetItem, Visibility } from './types';
4
+ import { chainFor } from '../chains';
4
5
  import * as AgentArtifact from '../../artifacts/contracts/agent.sol/Agent.json';
5
6
 
6
7
  export class Utils {
@@ -87,8 +88,8 @@ export class Utils {
87
88
  }
88
89
 
89
90
  if (domainConfig.wallet) {
90
- const provider = new ethers.providers.JsonRpcProvider(domainConfig.provider?.rpc);
91
- this.serverWallet = ethers.Wallet.fromMnemonic(domainConfig.wallet.mnemonic).connect(provider);
91
+ const chain = chainFor(domainConfig.provider || { chainId: 137, name: 'Polygon Mainnet', rpc: 'https://polygon-rpc.com' });
92
+ this.serverWallet = ethers.Wallet.fromMnemonic(domainConfig.wallet.mnemonic).connect(chain.provider);
92
93
 
93
94
  console.log(`Server wallet initialized for domain: ${domain}`);
94
95
  console.log(`Wallet address: ${domainConfig.wallet.address}`);
@@ -1,4 +1,5 @@
1
1
  export { Utils } from './Utils';
2
2
  export { Config } from './Config';
3
3
  export { CliWallet } from './CliWallet';
4
- export * from './types';
4
+ export * from './types';
5
+ export * from '../chains';