@rhinestone/shared-configs 2.20.1 → 2.21.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/dist/configs/chain-indexer.json +68 -23
- package/dist/configs/chains.json +2 -0
- package/dist/scripts/__tests__/validation.test.js +152 -2
- package/dist/scripts/generate.js +6 -0
- package/dist/scripts/validation.d.ts +13 -8
- package/dist/scripts/validation.js +174 -21
- package/dist/src/chains.d.ts +6 -0
- package/dist/src/chains.js +2 -0
- package/dist/src/generated/packageVersion.d.ts +1 -1
- package/dist/src/generated/packageVersion.js +1 -1
- package/dist/src/types.d.ts +20 -0
- package/package.json +1 -1
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
"reorg_window_k": 32,
|
|
11
11
|
"reverify_delay_ms": 500,
|
|
12
12
|
"scan_batch_max_blocks": 200,
|
|
13
|
-
"streaming_poll_interval_ms": 36000
|
|
13
|
+
"streaming_poll_interval_ms": 36000,
|
|
14
|
+
"vm": "evm"
|
|
14
15
|
},
|
|
15
16
|
"10": {
|
|
16
17
|
"finality": "sequencer",
|
|
@@ -23,7 +24,8 @@
|
|
|
23
24
|
"reorg_window_k": 16,
|
|
24
25
|
"reverify_delay_ms": 500,
|
|
25
26
|
"scan_batch_max_blocks": 200,
|
|
26
|
-
"streaming_poll_interval_ms": 6000
|
|
27
|
+
"streaming_poll_interval_ms": 6000,
|
|
28
|
+
"vm": "evm"
|
|
27
29
|
},
|
|
28
30
|
"56": {
|
|
29
31
|
"finality": "probabilistic",
|
|
@@ -36,7 +38,8 @@
|
|
|
36
38
|
"reorg_window_k": 32,
|
|
37
39
|
"reverify_delay_ms": 500,
|
|
38
40
|
"scan_batch_max_blocks": 200,
|
|
39
|
-
"streaming_poll_interval_ms": 3000
|
|
41
|
+
"streaming_poll_interval_ms": 3000,
|
|
42
|
+
"vm": "evm"
|
|
40
43
|
},
|
|
41
44
|
"100": {
|
|
42
45
|
"finality": "probabilistic",
|
|
@@ -49,7 +52,8 @@
|
|
|
49
52
|
"reorg_window_k": 32,
|
|
50
53
|
"reverify_delay_ms": 500,
|
|
51
54
|
"scan_batch_max_blocks": 200,
|
|
52
|
-
"streaming_poll_interval_ms": 15000
|
|
55
|
+
"streaming_poll_interval_ms": 15000,
|
|
56
|
+
"vm": "evm"
|
|
53
57
|
},
|
|
54
58
|
"130": {
|
|
55
59
|
"finality": "sequencer",
|
|
@@ -62,7 +66,8 @@
|
|
|
62
66
|
"reorg_window_k": 16,
|
|
63
67
|
"reverify_delay_ms": 500,
|
|
64
68
|
"scan_batch_max_blocks": 200,
|
|
65
|
-
"streaming_poll_interval_ms": 3000
|
|
69
|
+
"streaming_poll_interval_ms": 3000,
|
|
70
|
+
"vm": "evm"
|
|
66
71
|
},
|
|
67
72
|
"137": {
|
|
68
73
|
"finality": "probabilistic",
|
|
@@ -75,7 +80,8 @@
|
|
|
75
80
|
"reorg_window_k": 64,
|
|
76
81
|
"reverify_delay_ms": 500,
|
|
77
82
|
"scan_batch_max_blocks": 200,
|
|
78
|
-
"streaming_poll_interval_ms": 4500
|
|
83
|
+
"streaming_poll_interval_ms": 4500,
|
|
84
|
+
"vm": "evm"
|
|
79
85
|
},
|
|
80
86
|
"143": {
|
|
81
87
|
"finality": "instant",
|
|
@@ -88,7 +94,8 @@
|
|
|
88
94
|
"reorg_window_k": 4,
|
|
89
95
|
"reverify_delay_ms": 0,
|
|
90
96
|
"scan_batch_max_blocks": 200,
|
|
91
|
-
"streaming_poll_interval_ms": 3000
|
|
97
|
+
"streaming_poll_interval_ms": 3000,
|
|
98
|
+
"vm": "evm"
|
|
92
99
|
},
|
|
93
100
|
"146": {
|
|
94
101
|
"finality": "instant",
|
|
@@ -101,7 +108,8 @@
|
|
|
101
108
|
"reorg_window_k": 4,
|
|
102
109
|
"reverify_delay_ms": 0,
|
|
103
110
|
"scan_batch_max_blocks": 200,
|
|
104
|
-
"streaming_poll_interval_ms": 4500
|
|
111
|
+
"streaming_poll_interval_ms": 4500,
|
|
112
|
+
"vm": "evm"
|
|
105
113
|
},
|
|
106
114
|
"196": {
|
|
107
115
|
"finality": "sequencer",
|
|
@@ -114,7 +122,8 @@
|
|
|
114
122
|
"reorg_window_k": 16,
|
|
115
123
|
"reverify_delay_ms": 500,
|
|
116
124
|
"scan_batch_max_blocks": 200,
|
|
117
|
-
"streaming_poll_interval_ms": 3000
|
|
125
|
+
"streaming_poll_interval_ms": 3000,
|
|
126
|
+
"vm": "evm"
|
|
118
127
|
},
|
|
119
128
|
"999": {
|
|
120
129
|
"finality": "instant",
|
|
@@ -127,7 +136,8 @@
|
|
|
127
136
|
"reorg_window_k": 4,
|
|
128
137
|
"reverify_delay_ms": 0,
|
|
129
138
|
"scan_batch_max_blocks": 200,
|
|
130
|
-
"streaming_poll_interval_ms": 3000
|
|
139
|
+
"streaming_poll_interval_ms": 3000,
|
|
140
|
+
"vm": "evm"
|
|
131
141
|
},
|
|
132
142
|
"1868": {
|
|
133
143
|
"finality": "sequencer",
|
|
@@ -140,7 +150,8 @@
|
|
|
140
150
|
"reorg_window_k": 16,
|
|
141
151
|
"reverify_delay_ms": 500,
|
|
142
152
|
"scan_batch_max_blocks": 200,
|
|
143
|
-
"streaming_poll_interval_ms": 6000
|
|
153
|
+
"streaming_poll_interval_ms": 6000,
|
|
154
|
+
"vm": "evm"
|
|
144
155
|
},
|
|
145
156
|
"4663": {
|
|
146
157
|
"finality": "sequencer",
|
|
@@ -153,7 +164,8 @@
|
|
|
153
164
|
"reorg_window_k": 16,
|
|
154
165
|
"reverify_delay_ms": 500,
|
|
155
166
|
"scan_batch_max_blocks": 200,
|
|
156
|
-
"streaming_poll_interval_ms": 3000
|
|
167
|
+
"streaming_poll_interval_ms": 3000,
|
|
168
|
+
"vm": "evm"
|
|
157
169
|
},
|
|
158
170
|
"8453": {
|
|
159
171
|
"finality": "sequencer",
|
|
@@ -166,7 +178,8 @@
|
|
|
166
178
|
"reorg_window_k": 16,
|
|
167
179
|
"reverify_delay_ms": 500,
|
|
168
180
|
"scan_batch_max_blocks": 200,
|
|
169
|
-
"streaming_poll_interval_ms": 6000
|
|
181
|
+
"streaming_poll_interval_ms": 6000,
|
|
182
|
+
"vm": "evm"
|
|
170
183
|
},
|
|
171
184
|
"9745": {
|
|
172
185
|
"finality": "instant",
|
|
@@ -179,7 +192,8 @@
|
|
|
179
192
|
"reorg_window_k": 4,
|
|
180
193
|
"reverify_delay_ms": 0,
|
|
181
194
|
"scan_batch_max_blocks": 200,
|
|
182
|
-
"streaming_poll_interval_ms": 3000
|
|
195
|
+
"streaming_poll_interval_ms": 3000,
|
|
196
|
+
"vm": "evm"
|
|
183
197
|
},
|
|
184
198
|
"9746": {
|
|
185
199
|
"finality": "instant",
|
|
@@ -192,7 +206,8 @@
|
|
|
192
206
|
"reorg_window_k": 4,
|
|
193
207
|
"reverify_delay_ms": 0,
|
|
194
208
|
"scan_batch_max_blocks": 200,
|
|
195
|
-
"streaming_poll_interval_ms": 3000
|
|
209
|
+
"streaming_poll_interval_ms": 3000,
|
|
210
|
+
"vm": "evm"
|
|
196
211
|
},
|
|
197
212
|
"42161": {
|
|
198
213
|
"finality": "sequencer",
|
|
@@ -205,7 +220,8 @@
|
|
|
205
220
|
"reorg_window_k": 16,
|
|
206
221
|
"reverify_delay_ms": 500,
|
|
207
222
|
"scan_batch_max_blocks": 200,
|
|
208
|
-
"streaming_poll_interval_ms": 3000
|
|
223
|
+
"streaming_poll_interval_ms": 3000,
|
|
224
|
+
"vm": "evm"
|
|
209
225
|
},
|
|
210
226
|
"43114": {
|
|
211
227
|
"finality": "instant",
|
|
@@ -218,7 +234,8 @@
|
|
|
218
234
|
"reorg_window_k": 4,
|
|
219
235
|
"reverify_delay_ms": 0,
|
|
220
236
|
"scan_batch_max_blocks": 200,
|
|
221
|
-
"streaming_poll_interval_ms": 3000
|
|
237
|
+
"streaming_poll_interval_ms": 3000,
|
|
238
|
+
"vm": "evm"
|
|
222
239
|
},
|
|
223
240
|
"57073": {
|
|
224
241
|
"finality": "sequencer",
|
|
@@ -231,7 +248,8 @@
|
|
|
231
248
|
"reorg_window_k": 16,
|
|
232
249
|
"reverify_delay_ms": 500,
|
|
233
250
|
"scan_batch_max_blocks": 200,
|
|
234
|
-
"streaming_poll_interval_ms": 3000
|
|
251
|
+
"streaming_poll_interval_ms": 3000,
|
|
252
|
+
"vm": "evm"
|
|
235
253
|
},
|
|
236
254
|
"84532": {
|
|
237
255
|
"finality": "sequencer",
|
|
@@ -244,7 +262,8 @@
|
|
|
244
262
|
"reorg_window_k": 8,
|
|
245
263
|
"reverify_delay_ms": 500,
|
|
246
264
|
"scan_batch_max_blocks": 200,
|
|
247
|
-
"streaming_poll_interval_ms": 6000
|
|
265
|
+
"streaming_poll_interval_ms": 6000,
|
|
266
|
+
"vm": "evm"
|
|
248
267
|
},
|
|
249
268
|
"421614": {
|
|
250
269
|
"finality": "sequencer",
|
|
@@ -257,7 +276,8 @@
|
|
|
257
276
|
"reorg_window_k": 8,
|
|
258
277
|
"reverify_delay_ms": 500,
|
|
259
278
|
"scan_batch_max_blocks": 200,
|
|
260
|
-
"streaming_poll_interval_ms": 3000
|
|
279
|
+
"streaming_poll_interval_ms": 3000,
|
|
280
|
+
"vm": "evm"
|
|
261
281
|
},
|
|
262
282
|
"747474": {
|
|
263
283
|
"finality": "sequencer",
|
|
@@ -270,7 +290,8 @@
|
|
|
270
290
|
"reorg_window_k": 16,
|
|
271
291
|
"reverify_delay_ms": 500,
|
|
272
292
|
"scan_batch_max_blocks": 200,
|
|
273
|
-
"streaming_poll_interval_ms": 3000
|
|
293
|
+
"streaming_poll_interval_ms": 3000,
|
|
294
|
+
"vm": "evm"
|
|
274
295
|
},
|
|
275
296
|
"11155111": {
|
|
276
297
|
"finality": "probabilistic",
|
|
@@ -283,7 +304,8 @@
|
|
|
283
304
|
"reorg_window_k": 32,
|
|
284
305
|
"reverify_delay_ms": 500,
|
|
285
306
|
"scan_batch_max_blocks": 200,
|
|
286
|
-
"streaming_poll_interval_ms": 36000
|
|
307
|
+
"streaming_poll_interval_ms": 36000,
|
|
308
|
+
"vm": "evm"
|
|
287
309
|
},
|
|
288
310
|
"11155420": {
|
|
289
311
|
"finality": "sequencer",
|
|
@@ -296,6 +318,29 @@
|
|
|
296
318
|
"reorg_window_k": 8,
|
|
297
319
|
"reverify_delay_ms": 500,
|
|
298
320
|
"scan_batch_max_blocks": 200,
|
|
299
|
-
"streaming_poll_interval_ms": 6000
|
|
321
|
+
"streaming_poll_interval_ms": 6000,
|
|
322
|
+
"vm": "evm"
|
|
323
|
+
},
|
|
324
|
+
"792703809": {
|
|
325
|
+
"commitment": "finalized",
|
|
326
|
+
"max_catchup_signatures": 5000,
|
|
327
|
+
"overlap_slots": 300,
|
|
328
|
+
"page_limit": 1000,
|
|
329
|
+
"poll_interval_ms": 2000,
|
|
330
|
+
"topics": [
|
|
331
|
+
"5q44zFF8gziimbCipqNQ7GYed8pkTRDmUKFUFTwd4DjB"
|
|
332
|
+
],
|
|
333
|
+
"vm": "svm"
|
|
334
|
+
},
|
|
335
|
+
"792703810": {
|
|
336
|
+
"commitment": "finalized",
|
|
337
|
+
"max_catchup_signatures": 5000,
|
|
338
|
+
"overlap_slots": 300,
|
|
339
|
+
"page_limit": 1000,
|
|
340
|
+
"poll_interval_ms": 2000,
|
|
341
|
+
"topics": [
|
|
342
|
+
"5q44zFF8gziimbCipqNQ7GYed8pkTRDmUKFUFTwd4DjB"
|
|
343
|
+
],
|
|
344
|
+
"vm": "svm"
|
|
300
345
|
}
|
|
301
346
|
}
|
package/dist/configs/chains.json
CHANGED
|
@@ -3142,6 +3142,7 @@
|
|
|
3142
3142
|
"alchemy": null,
|
|
3143
3143
|
"zerion": null
|
|
3144
3144
|
},
|
|
3145
|
+
"rpcId": "mainnet-beta",
|
|
3145
3146
|
"sessionCapable": false,
|
|
3146
3147
|
"settlementConfig": {
|
|
3147
3148
|
"cctp": {
|
|
@@ -3246,6 +3247,7 @@
|
|
|
3246
3247
|
"alchemy": null,
|
|
3247
3248
|
"zerion": null
|
|
3248
3249
|
},
|
|
3250
|
+
"rpcId": "devnet",
|
|
3249
3251
|
"sessionCapable": false,
|
|
3250
3252
|
"settlementConfig": {
|
|
3251
3253
|
"cctp": {
|
|
@@ -327,6 +327,56 @@ function errorsFor(chain) {
|
|
|
327
327
|
(0, bun_test_1.expect)(error.message).toContain("TELEPORT");
|
|
328
328
|
(0, bun_test_1.expect)(error.message).toContain("src/types.ts");
|
|
329
329
|
});
|
|
330
|
+
// Absent is fine: chainRegistry() defaults it to the chain id, and a
|
|
331
|
+
// consumer reading a document written before the field existed does the
|
|
332
|
+
// same. Only the two ways AUTHORING can get it wrong are checked.
|
|
333
|
+
(0, bun_test_1.it)("accepts a chain with no rpcId", () => {
|
|
334
|
+
(0, bun_test_1.expect)(errorsFor(validChain())).toEqual([]);
|
|
335
|
+
});
|
|
336
|
+
(0, bun_test_1.it)("accepts an rpcId that matches the chain id", () => {
|
|
337
|
+
(0, bun_test_1.expect)(errorsFor(validChain({ rpcId: "1" }))).toEqual([]);
|
|
338
|
+
});
|
|
339
|
+
(0, bun_test_1.it)("rejects an rpcId override on an EVM chain", () => {
|
|
340
|
+
const [error] = errorsFor(validChain({ rpcId: "mainnet" }));
|
|
341
|
+
(0, bun_test_1.expect)(error.message).toContain("does not match the chain id");
|
|
342
|
+
});
|
|
343
|
+
// The failure this prevents is an empty feed, which reads exactly like a
|
|
344
|
+
// chain with no traffic — so it has to fail the build, not runtime.
|
|
345
|
+
(0, bun_test_1.it)("rejects a Solana chain whose rpcId is still the numeric default", () => {
|
|
346
|
+
const errors = (0, validation_1.validateChainConfig)({
|
|
347
|
+
"792703810": validChain({
|
|
348
|
+
name: "Solana Devnet",
|
|
349
|
+
vmType: "svm",
|
|
350
|
+
network: "testnet",
|
|
351
|
+
caip2: "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1",
|
|
352
|
+
rpcId: "792703810",
|
|
353
|
+
}),
|
|
354
|
+
}).issues.filter((i) => i.severity === "error");
|
|
355
|
+
(0, bun_test_1.expect)(errors.map((e) => e.message).join("\n")).toContain("addressed by its cluster name");
|
|
356
|
+
});
|
|
357
|
+
(0, bun_test_1.it)("rejects a Solana chain with no rpcId at all", () => {
|
|
358
|
+
const errors = (0, validation_1.validateChainConfig)({
|
|
359
|
+
"792703810": validChain({
|
|
360
|
+
name: "Solana Devnet",
|
|
361
|
+
vmType: "svm",
|
|
362
|
+
network: "testnet",
|
|
363
|
+
caip2: "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1",
|
|
364
|
+
}),
|
|
365
|
+
}).issues.filter((i) => i.severity === "error");
|
|
366
|
+
(0, bun_test_1.expect)(errors.map((e) => e.message).join("\n")).toContain("addressed by its cluster name");
|
|
367
|
+
});
|
|
368
|
+
(0, bun_test_1.it)("accepts a Solana chain addressed by its cluster", () => {
|
|
369
|
+
const errors = (0, validation_1.validateChainConfig)({
|
|
370
|
+
"792703810": validChain({
|
|
371
|
+
name: "Solana Devnet",
|
|
372
|
+
vmType: "svm",
|
|
373
|
+
network: "testnet",
|
|
374
|
+
caip2: "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1",
|
|
375
|
+
rpcId: "devnet",
|
|
376
|
+
}),
|
|
377
|
+
}).issues.filter((i) => i.severity === "error");
|
|
378
|
+
(0, bun_test_1.expect)(errors).toEqual([]);
|
|
379
|
+
});
|
|
330
380
|
});
|
|
331
381
|
(0, bun_test_1.describe)("validateChainConfig — reachability", () => {
|
|
332
382
|
(0, bun_test_1.it)("returns no issues for chains with multiple settlement layers", () => {
|
|
@@ -516,6 +566,7 @@ function errorsFor(chain) {
|
|
|
516
566
|
/** A complete class-B entry, so semantic tests don't trip the shape checks. */
|
|
517
567
|
function validIndexerEntry(overrides = {}) {
|
|
518
568
|
return {
|
|
569
|
+
vm: "evm",
|
|
519
570
|
poll_interval_ms: 1000,
|
|
520
571
|
streaming_poll_interval_ms: 15000,
|
|
521
572
|
reorg_window_k: 16,
|
|
@@ -541,12 +592,12 @@ function validIndexerEntry(overrides = {}) {
|
|
|
541
592
|
(0, bun_test_1.expect)(issues).toHaveLength(1);
|
|
542
593
|
(0, bun_test_1.expect)(issues[0].message).toContain("no chain-indexer entry");
|
|
543
594
|
});
|
|
544
|
-
(0, bun_test_1.it)("reports an entry addressing
|
|
595
|
+
(0, bun_test_1.it)("reports an entry addressing a chain the indexer does not dial", () => {
|
|
545
596
|
const issues = (0, validation_1.validateChainIndexerCoverage)({}, {
|
|
546
597
|
"1": validIndexerEntry(),
|
|
547
598
|
});
|
|
548
599
|
(0, bun_test_1.expect)(issues).toHaveLength(1);
|
|
549
|
-
(0, bun_test_1.expect)(issues[0].message).toContain("no
|
|
600
|
+
(0, bun_test_1.expect)(issues[0].message).toContain("no chain the indexer dials");
|
|
550
601
|
});
|
|
551
602
|
// HyperCore is vmType "evm" with a hypercore: caip2 — the eip155 namespace,
|
|
552
603
|
// not vmType, decides whether the indexer dials a chain.
|
|
@@ -627,6 +678,105 @@ function validIndexerEntry(overrides = {}) {
|
|
|
627
678
|
});
|
|
628
679
|
(0, bun_test_1.expect)(issues.map((i) => i.message).join("\n")).toContain("the only legal value is false");
|
|
629
680
|
});
|
|
681
|
+
/** The Solana entry as the artifact publishes it. */
|
|
682
|
+
function validSvmEntry(overrides = {}) {
|
|
683
|
+
return {
|
|
684
|
+
vm: "svm",
|
|
685
|
+
poll_interval_ms: 2000,
|
|
686
|
+
commitment: "finalized",
|
|
687
|
+
topics: ["5q44zFF8gziimbCipqNQ7GYed8pkTRDmUKFUFTwd4DjB"],
|
|
688
|
+
page_limit: 1000,
|
|
689
|
+
overlap_slots: 300,
|
|
690
|
+
max_catchup_signatures: 5000,
|
|
691
|
+
...overrides,
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
function solanaChain(overrides = {}) {
|
|
695
|
+
return validChain({
|
|
696
|
+
name: "Solana Devnet",
|
|
697
|
+
vmType: "svm",
|
|
698
|
+
network: "testnet",
|
|
699
|
+
caip2: "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1",
|
|
700
|
+
rpcId: "devnet",
|
|
701
|
+
...overrides,
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
(0, bun_test_1.it)("accepts a complete SVM entry", () => {
|
|
705
|
+
(0, bun_test_1.expect)((0, validation_1.validateChainIndexerCoverage)({ "792703810": solanaChain() }, {
|
|
706
|
+
"792703810": validSvmEntry(),
|
|
707
|
+
})).toEqual([]);
|
|
708
|
+
});
|
|
709
|
+
(0, bun_test_1.it)("requires an entry for a Solana chain, like any other dialable chain", () => {
|
|
710
|
+
const issues = (0, validation_1.validateChainIndexerCoverage)({ "792703810": solanaChain() }, {});
|
|
711
|
+
(0, bun_test_1.expect)(issues).toHaveLength(1);
|
|
712
|
+
(0, bun_test_1.expect)(issues[0].message).toContain("SVM chain has no chain-indexer entry");
|
|
713
|
+
});
|
|
714
|
+
// The knob sets are disjoint, so judging an entry against the wrong one
|
|
715
|
+
// reports every field of both as wrong — which is the point: an SVM entry is
|
|
716
|
+
// not an EVM entry missing eleven fields.
|
|
717
|
+
(0, bun_test_1.it)("rejects EVM knobs on a Solana chain", () => {
|
|
718
|
+
const issues = (0, validation_1.validateChainIndexerCoverage)({ "792703810": solanaChain() }, { "792703810": validIndexerEntry() });
|
|
719
|
+
(0, bun_test_1.expect)(issues.map((i) => i.message).join("\n")).toContain('`vm` = "evm" but solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1 is a svm chain');
|
|
720
|
+
});
|
|
721
|
+
// The dangerous direction: the indexer reads `vm` first and passes over
|
|
722
|
+
// families it does not index, so an svm tag on an EVM chain would validate
|
|
723
|
+
// as a coherent SVM entry here and then leave that chain silently unscanned.
|
|
724
|
+
(0, bun_test_1.it)("rejects an svm tag on an EVM chain", () => {
|
|
725
|
+
const issues = (0, validation_1.validateChainIndexerCoverage)({ "1": validChain() }, {
|
|
726
|
+
"1": validSvmEntry(),
|
|
727
|
+
});
|
|
728
|
+
(0, bun_test_1.expect)(issues.map((i) => i.message).join("\n")).toContain("would never be scanned");
|
|
729
|
+
});
|
|
730
|
+
bun_test_1.it.each([
|
|
731
|
+
["a missing SVM field", { topics: undefined }, "missing `topics`"],
|
|
732
|
+
["an empty topic list", { topics: [] }, "`topics` = [] is not a legal value"],
|
|
733
|
+
[
|
|
734
|
+
"a malformed topic",
|
|
735
|
+
{ topics: ["not-base58-и"] },
|
|
736
|
+
"is not a legal value",
|
|
737
|
+
],
|
|
738
|
+
// A character-count regex accepts both of these — they are legal base58
|
|
739
|
+
// characters at a legal length — but neither decodes to a 32-byte account.
|
|
740
|
+
// A marker like this would pass the build and then name a feed no
|
|
741
|
+
// transaction can appear in, which reads as a chain with no traffic.
|
|
742
|
+
[
|
|
743
|
+
"a topic that decodes to 33 bytes",
|
|
744
|
+
{ topics: ["z".repeat(44)] },
|
|
745
|
+
"is not a legal value",
|
|
746
|
+
],
|
|
747
|
+
[
|
|
748
|
+
"a topic that decodes to 43 zero bytes",
|
|
749
|
+
{ topics: ["1".repeat(43)] },
|
|
750
|
+
"is not a legal value",
|
|
751
|
+
],
|
|
752
|
+
[
|
|
753
|
+
"a commitment nothing settles at",
|
|
754
|
+
{ commitment: "processed" },
|
|
755
|
+
'`commitment` = "processed" is not a legal value',
|
|
756
|
+
],
|
|
757
|
+
// Not a narrow allowlist: the SVM entry omits every reorg and retraction
|
|
758
|
+
// knob BECAUSE a finalized signature is settled. Reading at `confirmed`
|
|
759
|
+
// has to arrive with the retraction kind, not as a value a facts release
|
|
760
|
+
// can set on its own.
|
|
761
|
+
[
|
|
762
|
+
"a reversible commitment the schema cannot retract",
|
|
763
|
+
{ commitment: "confirmed" },
|
|
764
|
+
'`commitment` = "confirmed" is not a legal value',
|
|
765
|
+
],
|
|
766
|
+
[
|
|
767
|
+
"a page limit above the RPC ceiling",
|
|
768
|
+
{ page_limit: 1001 },
|
|
769
|
+
"`page_limit` = 1001 is not a legal value",
|
|
770
|
+
],
|
|
771
|
+
[
|
|
772
|
+
"an EVM knob smuggled in",
|
|
773
|
+
{ reorg_window_k: 16 },
|
|
774
|
+
"unrecognized field `reorg_window_k`",
|
|
775
|
+
],
|
|
776
|
+
])("rejects %s on an SVM entry", (_label, overrides, expected) => {
|
|
777
|
+
const issues = (0, validation_1.validateChainIndexerCoverage)({ "792703810": solanaChain() }, { "792703810": validSvmEntry(overrides) });
|
|
778
|
+
(0, bun_test_1.expect)(issues.map((i) => i.message).join("\n")).toContain(expected);
|
|
779
|
+
});
|
|
630
780
|
(0, bun_test_1.it)("accepts a coherent instant-class entry", () => {
|
|
631
781
|
const entry = validIndexerEntry({
|
|
632
782
|
finality: "instant",
|
package/dist/scripts/generate.js
CHANGED
|
@@ -162,6 +162,12 @@ function renderChains(chainRegistry, mainnets, testnets) {
|
|
|
162
162
|
" /** Public, unauthenticated https JSON-RPC endpoint — what a browser needs to",
|
|
163
163
|
" * build this chain with `defineChain`. Absent means display-only. */",
|
|
164
164
|
" publicRpcUrl?: string;",
|
|
165
|
+
" /** How this chain is addressed when dialling it, within its architecture:",
|
|
166
|
+
" * the last segment of `{proxy}/{architecture}/{rpcId}`. Present only where",
|
|
167
|
+
" * the chain id is NOT what you dial — the cluster name (`mainnet-beta`,",
|
|
168
|
+
" * `devnet`) on Solana. Absent means the chain id is the id, so read it as",
|
|
169
|
+
" * `chain.rpcId ?? String(chainId)` rather than branching on vmType. */",
|
|
170
|
+
" rpcId?: string;",
|
|
165
171
|
" /** True for virtual chains (e.g. HyperCore) that settle on another chain. */",
|
|
166
172
|
" virtual?: boolean;",
|
|
167
173
|
" /** Which directions of transfer the chain supports; absent means both.",
|
|
@@ -226,19 +226,24 @@ export declare function validateChainConfig(chains: ChainRegistry): ValidationRe
|
|
|
226
226
|
*/
|
|
227
227
|
export declare function validateOftCoverage(chains: ChainRegistry, oft: Record<string, unknown>): ValidationIssue[];
|
|
228
228
|
/**
|
|
229
|
-
* Insists that every
|
|
230
|
-
* entry, and that no entry addresses a chain that does not exist.
|
|
229
|
+
* Insists that every chain the indexer dials carries a complete, coherent
|
|
230
|
+
* chain-indexer entry, and that no entry addresses a chain that does not exist.
|
|
231
231
|
*
|
|
232
|
-
* The indexer scans every dialable
|
|
233
|
-
* unclassified chain is a hard failure — this check moves that
|
|
234
|
-
* the indexer's boot to this build, where the person adding the
|
|
235
|
-
* (RHI-6107). Selected by the
|
|
232
|
+
* The indexer scans every dialable chain it has a machine for, and its contract
|
|
233
|
+
* is that an unclassified chain is a hard failure — this check moves that
|
|
234
|
+
* failure from the indexer's boot to this build, where the person adding the
|
|
235
|
+
* chain sees it (RHI-6107). Selected by the caip2 NAMESPACE, NOT by vmType:
|
|
236
236
|
* HyperCore is vmType "evm" with a `hypercore:` caip2 and must not require an
|
|
237
237
|
* entry — the same discriminator the publish workflow's contracts guard uses,
|
|
238
238
|
* for the same reason. Runs after `classifyChainCaip2`, which stamps the field.
|
|
239
239
|
*
|
|
240
|
-
*
|
|
241
|
-
*
|
|
240
|
+
* Two families today (`eip155:` → evm, `solana:` → svm), with disjoint knob
|
|
241
|
+
* sets — an entry is judged against the one its chain implies, and its own `vm`
|
|
242
|
+
* is cross-checked against that rather than trusted (RHI-7338).
|
|
243
|
+
*
|
|
244
|
+
* The EVM cross-field rules mirror chain-indexer's
|
|
245
|
+
* `ChainScanSettings::validate`, so a value set this check accepts is one the
|
|
246
|
+
* indexer will accept.
|
|
242
247
|
*
|
|
243
248
|
* A chain deliberately not indexed carries `{ indexed: false }` as its whole
|
|
244
249
|
* entry — a decision the check accepts, unlike a missing entry.
|
|
@@ -104,6 +104,47 @@ const isObject = (v) => typeof v === 'object' && v !== null && !Array.isArray(v)
|
|
|
104
104
|
const isNonEmptyString = (v) => typeof v === 'string' && v.length > 0;
|
|
105
105
|
const isNonNegativeInt = (v) => typeof v === 'number' && Number.isInteger(v) && v >= 0;
|
|
106
106
|
const isAbsentOr = (v, check) => v === undefined || check(v);
|
|
107
|
+
const BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
|
|
108
|
+
/**
|
|
109
|
+
* Decode base58 (Bitcoin/Solana alphabet), or null if the string is not
|
|
110
|
+
* base58. Each leading `1` stands for one leading zero byte.
|
|
111
|
+
*
|
|
112
|
+
* Decoded rather than pattern-matched because the length is what actually
|
|
113
|
+
* matters and a character-count regex does not imply it: 44 `z`s are all legal
|
|
114
|
+
* base58 characters and decode to THIRTY-THREE bytes, while 43 `1`s decode to
|
|
115
|
+
* 43 zero bytes. Both would pass a `{43,44}` regex and neither is an account.
|
|
116
|
+
* The failure that buys is a marker that validates here and then names a feed
|
|
117
|
+
* no transaction can ever appear in — silence indistinguishable from a chain
|
|
118
|
+
* with no traffic.
|
|
119
|
+
*/
|
|
120
|
+
function decodeBase58(value) {
|
|
121
|
+
if (value.length === 0)
|
|
122
|
+
return null;
|
|
123
|
+
const bytes = [];
|
|
124
|
+
for (const char of value) {
|
|
125
|
+
let carry = BASE58_ALPHABET.indexOf(char);
|
|
126
|
+
if (carry < 0)
|
|
127
|
+
return null;
|
|
128
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
129
|
+
carry += bytes[i] * 58;
|
|
130
|
+
bytes[i] = carry & 0xff;
|
|
131
|
+
carry >>= 8;
|
|
132
|
+
}
|
|
133
|
+
while (carry > 0) {
|
|
134
|
+
bytes.push(carry & 0xff);
|
|
135
|
+
carry >>= 8;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
let zeros = 0;
|
|
139
|
+
while (zeros < value.length && value[zeros] === '1')
|
|
140
|
+
zeros += 1;
|
|
141
|
+
return Uint8Array.from([
|
|
142
|
+
...new Array(zeros).fill(0),
|
|
143
|
+
...bytes.reverse(),
|
|
144
|
+
]);
|
|
145
|
+
}
|
|
146
|
+
/** A base58 Solana account address: exactly 32 bytes once decoded. */
|
|
147
|
+
const isBase58Account = (v) => typeof v === 'string' && decodeBase58(v)?.length === 32;
|
|
107
148
|
// Every vendor layer's REQUIRED payload, keyed by the SettlementLayer value
|
|
108
149
|
// that switches it on. This is what replaces the chain-id allowlists the
|
|
109
150
|
// jsonnet used to carry with "must match <map> in the orchestrator" comments —
|
|
@@ -282,6 +323,30 @@ function structuralProblems(registryChainId, chain) {
|
|
|
282
323
|
if (!isAbsentOr(chain.publicRpcUrl, isPublicRpcUrl)) {
|
|
283
324
|
problems.push(`publicRpcUrl ${JSON.stringify(chain.publicRpcUrl)} is not an https:// URL`);
|
|
284
325
|
}
|
|
326
|
+
// `rpcId` is authored, and only where the chain id is not what you dial — so
|
|
327
|
+
// absence is the normal answer and carries meaning ("the chain id is the
|
|
328
|
+
// id"). There is deliberately no blanket presence check; what is checked is
|
|
329
|
+
// the two ways authoring it can go wrong.
|
|
330
|
+
//
|
|
331
|
+
// An `rpcId` on an EVM chain that is not its own id routes every call for
|
|
332
|
+
// that chain to the wrong network. A Solana chain with none leaves a consumer
|
|
333
|
+
// falling back to `{proxy}/svm/792703810`, which reaches nothing — and the
|
|
334
|
+
// symptom is an empty feed, indistinguishable from a chain with no traffic,
|
|
335
|
+
// which is why it has to fail here rather than at runtime.
|
|
336
|
+
if (!isAbsentOr(chain.rpcId, isNonEmptyString)) {
|
|
337
|
+
problems.push(`rpcId ${JSON.stringify(chain.rpcId)} is not a non-empty string`);
|
|
338
|
+
}
|
|
339
|
+
else if (typeof chain.caip2 === 'string' &&
|
|
340
|
+
chain.caip2.startsWith('eip155:') &&
|
|
341
|
+
chain.rpcId !== undefined &&
|
|
342
|
+
chain.rpcId !== registryChainId) {
|
|
343
|
+
problems.push(`rpcId ${JSON.stringify(chain.rpcId)} does not match the chain id for an eip155 chain — EVM chains are addressed by their id, so drop the override`);
|
|
344
|
+
}
|
|
345
|
+
else if (typeof chain.caip2 === 'string' &&
|
|
346
|
+
chain.caip2.startsWith('solana:') &&
|
|
347
|
+
(chain.rpcId === undefined || chain.rpcId === registryChainId)) {
|
|
348
|
+
problems.push('rpcId is missing or is the chain id, but a Solana chain is addressed by its cluster name (e.g. "devnet") — author it in config/non_evm_chains.jsonnet');
|
|
349
|
+
}
|
|
285
350
|
if (!isAbsentOr(chain.virtual, (v) => typeof v === 'boolean')) {
|
|
286
351
|
problems.push('virtual is not a boolean');
|
|
287
352
|
}
|
|
@@ -1236,12 +1301,15 @@ const CHAIN_INDEXER_FINALITY_SOURCE = new Set([
|
|
|
1236
1301
|
'safe',
|
|
1237
1302
|
'instant',
|
|
1238
1303
|
]);
|
|
1239
|
-
// Every field of one chain-indexer entry, all required. The wire is exact
|
|
1240
|
-
// purpose: the indexer's own contract is "every field mandatory — a knob
|
|
1241
|
-
// set is a decision nobody made", so a missing field here is the same
|
|
1242
|
-
// mistake as a missing entry, and an unrecognized field is a typo
|
|
1243
|
-
// otherwise ship silently.
|
|
1304
|
+
// Every field of one EVM chain-indexer entry, all required. The wire is exact
|
|
1305
|
+
// on purpose: the indexer's own contract is "every field mandatory — a knob
|
|
1306
|
+
// nobody set is a decision nobody made", so a missing field here is the same
|
|
1307
|
+
// authoring mistake as a missing entry, and an unrecognized field is a typo
|
|
1308
|
+
// that would otherwise ship silently.
|
|
1309
|
+
//
|
|
1310
|
+
// The SVM set below is disjoint from this one — see CHAIN_INDEXER_FIELDS_BY_VM.
|
|
1244
1311
|
const CHAIN_INDEXER_FIELDS = {
|
|
1312
|
+
vm: (v) => v === 'evm',
|
|
1245
1313
|
poll_interval_ms: isNonNegativeInt,
|
|
1246
1314
|
streaming_poll_interval_ms: isNonNegativeInt,
|
|
1247
1315
|
reorg_window_k: isNonNegativeInt,
|
|
@@ -1254,38 +1322,104 @@ const CHAIN_INDEXER_FIELDS = {
|
|
|
1254
1322
|
finality_poll_interval_ms: isNonNegativeInt,
|
|
1255
1323
|
grace_ms: isNonNegativeInt,
|
|
1256
1324
|
};
|
|
1325
|
+
// `finalized` is the ONLY legal commitment, and that is a coherence rule, not
|
|
1326
|
+
// a narrow allowlist waiting to be widened.
|
|
1327
|
+
//
|
|
1328
|
+
// The SVM entry carries no reorg window, no grace period and no retraction
|
|
1329
|
+
// vocabulary — the family has one message kind where EVM has four — and it can
|
|
1330
|
+
// omit all of that precisely BECAUSE a finalized signature is settled. Admit
|
|
1331
|
+
// `confirmed` here and that stops being true: the indexer would publish
|
|
1332
|
+
// reversible signatures as settled with nothing in the schema able to take
|
|
1333
|
+
// them back, and the build would pass.
|
|
1334
|
+
//
|
|
1335
|
+
// So the two travel together. Reading at `confirmed` is a real thing we may
|
|
1336
|
+
// want one day; it arrives with the retraction kind, as one change, not as a
|
|
1337
|
+
// value someone can set in a facts release. Same shape as the EVM rule that
|
|
1338
|
+
// `finality: instant` and `finality_source: instant` imply each other.
|
|
1339
|
+
//
|
|
1340
|
+
// `processed` is absent for a further reason: eRPC refuses it outright for
|
|
1341
|
+
// getSignaturesForAddress and getTransaction.
|
|
1342
|
+
const CHAIN_INDEXER_COMMITMENT = new Set(['finalized']);
|
|
1343
|
+
// The SVM knob set — a different machine, so none of the EVM fields above
|
|
1344
|
+
// apply. There is no block scan, no parent-hash chain and no log filter: the
|
|
1345
|
+
// unit is a transaction signature reached through the address → signature
|
|
1346
|
+
// index. And no reorg or finality knobs, because ingest is at `finalized`
|
|
1347
|
+
// commitment where a signature is settled (RHI-7338).
|
|
1348
|
+
//
|
|
1349
|
+
// `commitment` is its own field rather than an assumed constant because
|
|
1350
|
+
// omitting it on the wire silently yields eRPC's `confirmed` network default —
|
|
1351
|
+
// unrooted reads that look exactly like correct operation.
|
|
1352
|
+
const CHAIN_INDEXER_SVM_FIELDS = {
|
|
1353
|
+
vm: (v) => v === 'svm',
|
|
1354
|
+
poll_interval_ms: isNonNegativeInt,
|
|
1355
|
+
commitment: (v) => typeof v === 'string' && CHAIN_INDEXER_COMMITMENT.has(v),
|
|
1356
|
+
// A list, not a scalar: rotating the marker or adding our own no-op program
|
|
1357
|
+
// id as a second topic must be a facts release, not an indexer deploy. Each
|
|
1358
|
+
// entry is a base58 account address.
|
|
1359
|
+
topics: (v) => Array.isArray(v) && v.length > 0 && v.every((t) => isBase58Account(t)),
|
|
1360
|
+
page_limit: (v) => isNonNegativeInt(v) && v > 0 && v <= 1000,
|
|
1361
|
+
overlap_slots: isNonNegativeInt,
|
|
1362
|
+
max_catchup_signatures: isNonNegativeInt,
|
|
1363
|
+
};
|
|
1364
|
+
// Which machine an entry's knobs belong to. Mirrors the `vm` tag chain-indexer
|
|
1365
|
+
// reads before anything else in the entry: an unrecognised value there is
|
|
1366
|
+
// skipped rather than indexed, so a typo here would silently stop a chain
|
|
1367
|
+
// being scanned, which is why it fails the build instead.
|
|
1368
|
+
const CHAIN_INDEXER_FIELDS_BY_VM = {
|
|
1369
|
+
evm: CHAIN_INDEXER_FIELDS,
|
|
1370
|
+
svm: CHAIN_INDEXER_SVM_FIELDS,
|
|
1371
|
+
};
|
|
1372
|
+
// Which caip2 namespace implies which machine, for the cross-check that an
|
|
1373
|
+
// entry's `vm` matches the chain it addresses. Namespace rather than vmType,
|
|
1374
|
+
// for the same reason coverage is selected that way: HyperCore is vmType "evm"
|
|
1375
|
+
// under a `hypercore:` caip2 and is not a chain the indexer dials.
|
|
1376
|
+
const CHAIN_INDEXER_VM_BY_NAMESPACE = {
|
|
1377
|
+
eip155: 'evm',
|
|
1378
|
+
solana: 'svm',
|
|
1379
|
+
};
|
|
1257
1380
|
/**
|
|
1258
|
-
* Insists that every
|
|
1259
|
-
* entry, and that no entry addresses a chain that does not exist.
|
|
1381
|
+
* Insists that every chain the indexer dials carries a complete, coherent
|
|
1382
|
+
* chain-indexer entry, and that no entry addresses a chain that does not exist.
|
|
1260
1383
|
*
|
|
1261
|
-
* The indexer scans every dialable
|
|
1262
|
-
* unclassified chain is a hard failure — this check moves that
|
|
1263
|
-
* the indexer's boot to this build, where the person adding the
|
|
1264
|
-
* (RHI-6107). Selected by the
|
|
1384
|
+
* The indexer scans every dialable chain it has a machine for, and its contract
|
|
1385
|
+
* is that an unclassified chain is a hard failure — this check moves that
|
|
1386
|
+
* failure from the indexer's boot to this build, where the person adding the
|
|
1387
|
+
* chain sees it (RHI-6107). Selected by the caip2 NAMESPACE, NOT by vmType:
|
|
1265
1388
|
* HyperCore is vmType "evm" with a `hypercore:` caip2 and must not require an
|
|
1266
1389
|
* entry — the same discriminator the publish workflow's contracts guard uses,
|
|
1267
1390
|
* for the same reason. Runs after `classifyChainCaip2`, which stamps the field.
|
|
1268
1391
|
*
|
|
1269
|
-
*
|
|
1270
|
-
*
|
|
1392
|
+
* Two families today (`eip155:` → evm, `solana:` → svm), with disjoint knob
|
|
1393
|
+
* sets — an entry is judged against the one its chain implies, and its own `vm`
|
|
1394
|
+
* is cross-checked against that rather than trusted (RHI-7338).
|
|
1395
|
+
*
|
|
1396
|
+
* The EVM cross-field rules mirror chain-indexer's
|
|
1397
|
+
* `ChainScanSettings::validate`, so a value set this check accepts is one the
|
|
1398
|
+
* indexer will accept.
|
|
1271
1399
|
*
|
|
1272
1400
|
* A chain deliberately not indexed carries `{ indexed: false }` as its whole
|
|
1273
1401
|
* entry — a decision the check accepts, unlike a missing entry.
|
|
1274
1402
|
*/
|
|
1275
1403
|
function validateChainIndexerCoverage(chains, chainIndexer) {
|
|
1276
1404
|
const issues = [];
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1405
|
+
// Which machine's knobs a chain takes, or undefined for one the indexer does
|
|
1406
|
+
// not dial at all. Keyed on the caip2 NAMESPACE for the reason in the doc
|
|
1407
|
+
// comment: HyperCore is vmType "evm" under `hypercore:` and must stay
|
|
1408
|
+
// exempt.
|
|
1409
|
+
const machineOf = (chain) => typeof chain.caip2 === 'string'
|
|
1410
|
+
? CHAIN_INDEXER_VM_BY_NAMESPACE[chain.caip2.split(':')[0]]
|
|
1411
|
+
: undefined;
|
|
1412
|
+
const requiresEntry = (chain) => machineOf(chain) !== undefined && chain.virtual !== true;
|
|
1280
1413
|
for (const [chainId, chain] of Object.entries(chains)) {
|
|
1281
|
-
|
|
1414
|
+
const machine = machineOf(chain);
|
|
1415
|
+
if (machine === undefined || chain.virtual === true)
|
|
1282
1416
|
continue;
|
|
1283
1417
|
if (chainIndexer[chainId] === undefined) {
|
|
1284
1418
|
issues.push({
|
|
1285
1419
|
severity: 'error',
|
|
1286
1420
|
chainId,
|
|
1287
1421
|
chainName: isNonEmptyString(chain.name) ? chain.name : '<unnamed>',
|
|
1288
|
-
message:
|
|
1422
|
+
message: `${machine.toUpperCase()} chain has no chain-indexer entry — classify it in config/chain-indexer.jsonnet (see docs/add-chain.md, "Classify the chain for the indexer")`,
|
|
1289
1423
|
});
|
|
1290
1424
|
}
|
|
1291
1425
|
}
|
|
@@ -1294,7 +1428,7 @@ function validateChainIndexerCoverage(chains, chainIndexer) {
|
|
|
1294
1428
|
const chainName = chain && isNonEmptyString(chain.name) ? chain.name : '<not in registry>';
|
|
1295
1429
|
const issue = (message) => issues.push({ severity: 'error', chainId, chainName, message });
|
|
1296
1430
|
if (!chain || !requiresEntry(chain)) {
|
|
1297
|
-
issue('chain-indexer entry addresses no
|
|
1431
|
+
issue('chain-indexer entry addresses no chain the indexer dials — remove it from config/chain-indexer.jsonnet, or add the chain first');
|
|
1298
1432
|
continue;
|
|
1299
1433
|
}
|
|
1300
1434
|
if (!isObject(entry)) {
|
|
@@ -1316,7 +1450,19 @@ function validateChainIndexerCoverage(chains, chainIndexer) {
|
|
|
1316
1450
|
}
|
|
1317
1451
|
continue;
|
|
1318
1452
|
}
|
|
1319
|
-
|
|
1453
|
+
// Which machine's knobs this entry is judged against. Cross-checked
|
|
1454
|
+
// against the chain rather than taken from the entry: an `svm` tag on an
|
|
1455
|
+
// eip155 chain would otherwise be validated as a coherent SVM entry and
|
|
1456
|
+
// then silently skipped by the indexer, which reads `vm` first and passes
|
|
1457
|
+
// over families it does not index — a chain that stops being scanned with
|
|
1458
|
+
// nothing failing anywhere (RHI-7338).
|
|
1459
|
+
const machine = machineOf(chain);
|
|
1460
|
+
if (entry.vm !== undefined && entry.vm !== machine) {
|
|
1461
|
+
issue(`chain-indexer \`vm\` = ${JSON.stringify(entry.vm)} but ${chain.caip2} is a ${machine} chain — the indexer skips entries for machines it does not index, so this one would never be scanned`);
|
|
1462
|
+
continue;
|
|
1463
|
+
}
|
|
1464
|
+
const fields = CHAIN_INDEXER_FIELDS_BY_VM[machine];
|
|
1465
|
+
for (const [field, check] of Object.entries(fields)) {
|
|
1320
1466
|
if (entry[field] === undefined) {
|
|
1321
1467
|
issue(`chain-indexer entry is missing \`${field}\` — every field is mandatory, a knob nobody set is a decision nobody made`);
|
|
1322
1468
|
}
|
|
@@ -1325,10 +1471,17 @@ function validateChainIndexerCoverage(chains, chainIndexer) {
|
|
|
1325
1471
|
}
|
|
1326
1472
|
}
|
|
1327
1473
|
for (const field of Object.keys(entry)) {
|
|
1328
|
-
if (!(field in
|
|
1474
|
+
if (!(field in fields)) {
|
|
1329
1475
|
issue(`chain-indexer entry carries unrecognized field \`${field}\` — the artifact's shape is exact, fix the name or drop it`);
|
|
1330
1476
|
}
|
|
1331
1477
|
}
|
|
1478
|
+
// The cross-field rules below are EVM classification coherence — reorg
|
|
1479
|
+
// window against finality source against watermark cadence. The SVM entry
|
|
1480
|
+
// has no reorg or finality knobs to be incoherent about: at `finalized`
|
|
1481
|
+
// commitment a signature is settled, so there is no second classification
|
|
1482
|
+
// to agree with.
|
|
1483
|
+
if (machine !== 'evm')
|
|
1484
|
+
continue;
|
|
1332
1485
|
const instantClass = entry.finality === 'instant';
|
|
1333
1486
|
const instantSource = entry.finality_source === 'instant';
|
|
1334
1487
|
if (instantClass !== instantSource) {
|
package/dist/src/chains.d.ts
CHANGED
|
@@ -66,6 +66,12 @@ interface Chain {
|
|
|
66
66
|
/** Public, unauthenticated https JSON-RPC endpoint — what a browser needs to
|
|
67
67
|
* build this chain with `defineChain`. Absent means display-only. */
|
|
68
68
|
publicRpcUrl?: string;
|
|
69
|
+
/** How this chain is addressed when dialling it, within its architecture:
|
|
70
|
+
* the last segment of `{proxy}/{architecture}/{rpcId}`. Present only where
|
|
71
|
+
* the chain id is NOT what you dial — the cluster name (`mainnet-beta`,
|
|
72
|
+
* `devnet`) on Solana. Absent means the chain id is the id, so read it as
|
|
73
|
+
* `chain.rpcId ?? String(chainId)` rather than branching on vmType. */
|
|
74
|
+
rpcId?: string;
|
|
69
75
|
/** True for virtual chains (e.g. HyperCore) that settle on another chain. */
|
|
70
76
|
virtual?: boolean;
|
|
71
77
|
/** Which directions of transfer the chain supports; absent means both.
|
package/dist/src/chains.js
CHANGED
|
@@ -3245,6 +3245,7 @@ const chains = {
|
|
|
3245
3245
|
"alchemy": null,
|
|
3246
3246
|
"zerion": null
|
|
3247
3247
|
},
|
|
3248
|
+
"rpcId": "mainnet-beta",
|
|
3248
3249
|
"sessionCapable": false,
|
|
3249
3250
|
"settlementConfig": {
|
|
3250
3251
|
"cctp": {
|
|
@@ -3350,6 +3351,7 @@ const chains = {
|
|
|
3350
3351
|
"alchemy": null,
|
|
3351
3352
|
"zerion": null
|
|
3352
3353
|
},
|
|
3354
|
+
"rpcId": "devnet",
|
|
3353
3355
|
"sessionCapable": false,
|
|
3354
3356
|
"settlementConfig": {
|
|
3355
3357
|
"cctp": {
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
declare const PACKAGE_VERSION = "2.
|
|
1
|
+
declare const PACKAGE_VERSION = "2.21.0";
|
|
2
2
|
export { PACKAGE_VERSION };
|
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.PACKAGE_VERSION = void 0;
|
|
4
4
|
// Auto-generated by scripts/generate.ts. Do not edit manually.
|
|
5
|
-
const PACKAGE_VERSION = "2.
|
|
5
|
+
const PACKAGE_VERSION = "2.21.0";
|
|
6
6
|
exports.PACKAGE_VERSION = PACKAGE_VERSION;
|
package/dist/src/types.d.ts
CHANGED
|
@@ -349,6 +349,26 @@ interface ChainEntry {
|
|
|
349
349
|
* `validateChainIconSlugs`.
|
|
350
350
|
*/
|
|
351
351
|
iconSlug?: string;
|
|
352
|
+
/**
|
|
353
|
+
* The identifier this chain is addressed by when you dial it, within its
|
|
354
|
+
* architecture — the last path segment of `{proxy}/{architecture}/{rpcId}`.
|
|
355
|
+
*
|
|
356
|
+
* For an EVM chain it is the chain id as a string. For Solana it is the
|
|
357
|
+
* cluster name (`mainnet-beta`, `devnet`), which is what every Solana RPC
|
|
358
|
+
* endpoint and the CLI call it, and what eRPC keys its network by. Not a
|
|
359
|
+
* proxy-specific spelling: a vendor endpoint uses the same word.
|
|
360
|
+
*
|
|
361
|
+
* Read this instead of branching on `vmType` and rebuilding the segment per
|
|
362
|
+
* family: every consumer that reached Solana previously kept its own caip2 →
|
|
363
|
+
* cluster table, and those tables drifted from each other.
|
|
364
|
+
*
|
|
365
|
+
* Present only where the chain id is NOT what you dial — the two Solana
|
|
366
|
+
* entries today, and nowhere else. **Absent means the chain id is the id**,
|
|
367
|
+
* so a consumer reads `chain.rpcId ?? String(chainId)`. Storing the id again
|
|
368
|
+
* on every EVM entry would be a second copy of the key it sits under, which
|
|
369
|
+
* is the kind of copy that drifts.
|
|
370
|
+
*/
|
|
371
|
+
rpcId?: string;
|
|
352
372
|
/**
|
|
353
373
|
* A public, unauthenticated JSON-RPC endpoint over HTTPS, when one is known.
|
|
354
374
|
*
|