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.
- package/README.md +65 -5
- package/dist/chains/Chain.d.ts +117 -0
- package/dist/chains/Chain.d.ts.map +1 -0
- package/dist/chains/Chain.js +175 -0
- package/dist/chains/Chain.js.map +1 -0
- package/dist/chains/EthereumChain.d.ts +18 -0
- package/dist/chains/EthereumChain.d.ts.map +1 -0
- package/dist/chains/EthereumChain.js +26 -0
- package/dist/chains/EthereumChain.js.map +1 -0
- package/dist/chains/JapanOpenChain.d.ts +21 -0
- package/dist/chains/JapanOpenChain.d.ts.map +1 -0
- package/dist/chains/JapanOpenChain.js +36 -0
- package/dist/chains/JapanOpenChain.js.map +1 -0
- package/dist/chains/PolygonChain.d.ts +31 -0
- package/dist/chains/PolygonChain.d.ts.map +1 -0
- package/dist/chains/PolygonChain.js +50 -0
- package/dist/chains/PolygonChain.js.map +1 -0
- package/dist/chains/index.d.ts +22 -0
- package/dist/chains/index.d.ts.map +1 -0
- package/dist/chains/index.js +39 -0
- package/dist/chains/index.js.map +1 -0
- package/dist/chains/registry.d.ts +21 -0
- package/dist/chains/registry.d.ts.map +1 -0
- package/dist/chains/registry.js +53 -0
- package/dist/chains/registry.js.map +1 -0
- package/dist/utils/Utils.d.ts.map +1 -1
- package/dist/utils/Utils.js +3 -2
- package/dist/utils/Utils.js.map +1 -1
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +1 -0
- package/dist/utils/index.js.map +1 -1
- package/index.mjs +2 -1
- package/package.json +1 -1
- package/src/chains/Chain.ts +221 -0
- package/src/chains/EthereumChain.ts +23 -0
- package/src/chains/JapanOpenChain.ts +37 -0
- package/src/chains/PolygonChain.ts +53 -0
- package/src/chains/README.md +109 -0
- package/src/chains/index.ts +27 -0
- package/src/chains/registry.ts +53 -0
- package/src/utils/Utils.ts +3 -2
- 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
|
+
}
|
package/src/utils/Utils.ts
CHANGED
|
@@ -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
|
|
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}`);
|
package/src/utils/index.ts
CHANGED