@rhinestone/shared-configs 1.17.0 → 1.18.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 +15 -0
- package/dist/configs/chains.json +73 -29
- package/dist/scripts/__tests__/vendor-drift.test.d.ts +1 -0
- package/dist/scripts/__tests__/vendor-drift.test.js +411 -0
- package/dist/scripts/vendor-drift/diff.d.ts +80 -0
- package/dist/scripts/vendor-drift/diff.js +569 -0
- package/dist/scripts/vendor-drift/identity.d.ts +58 -0
- package/dist/scripts/vendor-drift/identity.js +139 -0
- package/dist/scripts/vendor-drift/main.d.ts +10 -0
- package/dist/scripts/vendor-drift/main.js +77 -0
- package/dist/scripts/vendor-drift/report.d.ts +5 -0
- package/dist/scripts/vendor-drift/report.js +94 -0
- package/dist/scripts/vendor-drift/vendors.d.ts +34 -0
- package/dist/scripts/vendor-drift/vendors.js +193 -0
- package/dist/src/chains.js +73 -29
- package/dist/src/generated/packageVersion.d.ts +1 -1
- package/dist/src/generated/packageVersion.js +1 -1
- package/package.json +2 -1
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const bun_test_1 = require("bun:test");
|
|
4
|
+
const diff_1 = require("../vendor-drift/diff");
|
|
5
|
+
const identity_1 = require("../vendor-drift/identity");
|
|
6
|
+
const vendors_1 = require("../vendor-drift/vendors");
|
|
7
|
+
const report_1 = require("../vendor-drift/report");
|
|
8
|
+
const codes = (findings) => findings.map((f) => f.code);
|
|
9
|
+
const chain = (over = {}) => ({
|
|
10
|
+
name: "Test",
|
|
11
|
+
vmType: "evm",
|
|
12
|
+
settlementLayers: [],
|
|
13
|
+
tokens: [],
|
|
14
|
+
...over,
|
|
15
|
+
});
|
|
16
|
+
(0, bun_test_1.describe)("diffRhino", () => {
|
|
17
|
+
const vendor = (over = {}) => ({
|
|
18
|
+
name: "BASE",
|
|
19
|
+
networkId: 8453,
|
|
20
|
+
status: "enabled",
|
|
21
|
+
contractAddress: "0x2f59e9086ec8130e21bd052065a9e6b2497bb102",
|
|
22
|
+
...over,
|
|
23
|
+
});
|
|
24
|
+
(0, bun_test_1.test)("flags a chain we claim that the vendor no longer lists", () => {
|
|
25
|
+
const registry = {
|
|
26
|
+
"130": chain({ name: "Unichain", settlementLayers: ["RHINO"] }),
|
|
27
|
+
};
|
|
28
|
+
(0, bun_test_1.expect)(codes((0, diff_1.diffRhino)(registry, [vendor()]))).toContain("rhino_chain_dropped");
|
|
29
|
+
});
|
|
30
|
+
(0, bun_test_1.test)("a disabled vendor chain does not count as supported", () => {
|
|
31
|
+
const registry = {
|
|
32
|
+
"8453": chain({ name: "Base", settlementLayers: ["RHINO"] }),
|
|
33
|
+
};
|
|
34
|
+
(0, bun_test_1.expect)(codes((0, diff_1.diffRhino)(registry, [vendor({ status: "disabled" })]))).toContain("rhino_chain_disabled");
|
|
35
|
+
});
|
|
36
|
+
(0, bun_test_1.test)("a vendor entry with no contract address leaves ours unverified", () => {
|
|
37
|
+
const registry = {
|
|
38
|
+
"8453": chain({ name: "Base", settlementLayers: ["RHINO"] }),
|
|
39
|
+
};
|
|
40
|
+
const findings = (0, diff_1.diffRhino)(registry, [
|
|
41
|
+
vendor({ contractAddress: undefined }),
|
|
42
|
+
]);
|
|
43
|
+
(0, bun_test_1.expect)(codes(findings)).toContain("rhino_bridge_contract_unserved");
|
|
44
|
+
});
|
|
45
|
+
(0, bun_test_1.test)("flags a silently changed bridge contract", () => {
|
|
46
|
+
const registry = {
|
|
47
|
+
"8453": chain({ name: "Base", settlementLayers: ["RHINO"] }),
|
|
48
|
+
};
|
|
49
|
+
const findings = (0, diff_1.diffRhino)(registry, [
|
|
50
|
+
vendor({ contractAddress: "0xdeadbeef00000000000000000000000000000000" }),
|
|
51
|
+
]);
|
|
52
|
+
(0, bun_test_1.expect)(codes(findings)).toContain("rhino_bridge_contract_changed");
|
|
53
|
+
});
|
|
54
|
+
(0, bun_test_1.test)("case alone is not a bridge contract change", () => {
|
|
55
|
+
const registry = {
|
|
56
|
+
"8453": chain({ name: "Base", settlementLayers: ["RHINO"] }),
|
|
57
|
+
};
|
|
58
|
+
const findings = (0, diff_1.diffRhino)(registry, [
|
|
59
|
+
vendor({ contractAddress: "0x2F59E9086EC8130E21BD052065A9E6B2497BB102" }),
|
|
60
|
+
]);
|
|
61
|
+
(0, bun_test_1.expect)(codes(findings)).not.toContain("rhino_bridge_contract_changed");
|
|
62
|
+
});
|
|
63
|
+
(0, bun_test_1.test)("flags a chain the vendor serves that we do not claim", () => {
|
|
64
|
+
const registry = {
|
|
65
|
+
"8453": chain({ name: "Base", settlementLayers: [] }),
|
|
66
|
+
};
|
|
67
|
+
const findings = (0, diff_1.diffRhino)(registry, [vendor()]);
|
|
68
|
+
(0, bun_test_1.expect)(codes(findings)).toContain("rhino_chain_available");
|
|
69
|
+
(0, bun_test_1.expect)(findings.find((f) => f.code === "rhino_chain_available")?.kind).toBe("underclaim");
|
|
70
|
+
});
|
|
71
|
+
// Rhino serves every networkId as a string, EVM included, so this is the
|
|
72
|
+
// shape the scheduled run actually sees.
|
|
73
|
+
(0, bun_test_1.test)("a string networkId is still compared against the chain id", () => {
|
|
74
|
+
const registry = {
|
|
75
|
+
"8453": chain({ name: "Base", settlementLayers: ["RHINO"] }),
|
|
76
|
+
};
|
|
77
|
+
const findings = (0, diff_1.diffRhino)(registry, [
|
|
78
|
+
vendor({ networkId: "999" }),
|
|
79
|
+
]);
|
|
80
|
+
(0, bun_test_1.expect)(codes(findings)).toContain("rhino_network_id_mismatch");
|
|
81
|
+
});
|
|
82
|
+
(0, bun_test_1.test)("a matching string networkId is not a mismatch", () => {
|
|
83
|
+
const registry = {
|
|
84
|
+
"8453": chain({ name: "Base", settlementLayers: ["RHINO"] }),
|
|
85
|
+
};
|
|
86
|
+
const findings = (0, diff_1.diffRhino)(registry, [
|
|
87
|
+
vendor({ networkId: "8453" }),
|
|
88
|
+
]);
|
|
89
|
+
(0, bun_test_1.expect)(codes(findings)).not.toContain("rhino_network_id_mismatch");
|
|
90
|
+
});
|
|
91
|
+
(0, bun_test_1.test)("a non-numeric networkId is never compared against a chain id", () => {
|
|
92
|
+
const registry = {
|
|
93
|
+
"792703809": chain({
|
|
94
|
+
name: "Solana",
|
|
95
|
+
vmType: "svm",
|
|
96
|
+
settlementLayers: ["RHINO"],
|
|
97
|
+
}),
|
|
98
|
+
};
|
|
99
|
+
const findings = (0, diff_1.diffRhino)(registry, [
|
|
100
|
+
vendor({ name: "SOLANA", networkId: 900, contractAddress: undefined }),
|
|
101
|
+
]);
|
|
102
|
+
(0, bun_test_1.expect)(codes(findings)).not.toContain("rhino_network_id_mismatch");
|
|
103
|
+
(0, bun_test_1.expect)(codes(findings)).not.toContain("rhino_bridge_contract_unknown");
|
|
104
|
+
});
|
|
105
|
+
(0, bun_test_1.test)("registry config overrides the baseline bridge contract", () => {
|
|
106
|
+
const registry = {
|
|
107
|
+
"8453": chain({
|
|
108
|
+
name: "Base",
|
|
109
|
+
settlementLayers: ["RHINO"],
|
|
110
|
+
settlementLayerConfig: {
|
|
111
|
+
RHINO: { chainName: "BASE", bridgeContract: "0xabc" },
|
|
112
|
+
},
|
|
113
|
+
}),
|
|
114
|
+
};
|
|
115
|
+
const findings = (0, diff_1.diffRhino)(registry, [vendor()]);
|
|
116
|
+
(0, bun_test_1.expect)(codes(findings)).toContain("rhino_bridge_contract_changed");
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
(0, bun_test_1.describe)("nearAssetId", () => {
|
|
120
|
+
(0, bun_test_1.test)("derives the omft id from a lowercased EVM address", () => {
|
|
121
|
+
(0, bun_test_1.expect)((0, diff_1.nearAssetId)("base", 8453, "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913")).toBe("nep141:base-0x833589fcd6edb6e08f4c7c32d4f71b54bda02913.omft.near");
|
|
122
|
+
});
|
|
123
|
+
(0, bun_test_1.test)("native sentinels resolve to the bare chain asset", () => {
|
|
124
|
+
(0, bun_test_1.expect)((0, diff_1.nearAssetId)("base", 8453, "0x0000000000000000000000000000000000000000")).toBe("nep141:base.omft.near");
|
|
125
|
+
});
|
|
126
|
+
(0, bun_test_1.test)("non-EVM tokens never fall through to the EVM derivation", () => {
|
|
127
|
+
(0, bun_test_1.expect)((0, diff_1.nearAssetId)("sol", 792703809, "SomeUnknownMint")).toBeUndefined();
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
(0, bun_test_1.describe)("diffNear", () => {
|
|
131
|
+
const token = (over = {}) => ({
|
|
132
|
+
assetId: "nep141:base-0xaaa.omft.near",
|
|
133
|
+
blockchain: "base",
|
|
134
|
+
contractAddress: "0xaaa",
|
|
135
|
+
...over,
|
|
136
|
+
});
|
|
137
|
+
(0, bun_test_1.test)("flags a blockchain slug the vendor dropped", () => {
|
|
138
|
+
const registry = {
|
|
139
|
+
"8453": chain({ name: "Base", settlementLayers: ["NEAR"] }),
|
|
140
|
+
};
|
|
141
|
+
(0, bun_test_1.expect)(codes((0, diff_1.diffNear)(registry, [token({ blockchain: "eth" })]))).toContain("near_blockchain_dropped");
|
|
142
|
+
});
|
|
143
|
+
(0, bun_test_1.test)("flags a token whose asset id the vendor re-issued", () => {
|
|
144
|
+
const registry = {
|
|
145
|
+
"8453": chain({
|
|
146
|
+
name: "Base",
|
|
147
|
+
settlementLayers: ["NEAR"],
|
|
148
|
+
tokens: [{ address: "0xAAA", symbol: "AAA" }],
|
|
149
|
+
}),
|
|
150
|
+
};
|
|
151
|
+
const findings = (0, diff_1.diffNear)(registry, [
|
|
152
|
+
token({ assetId: "nep245:v2_1.omni.hot.tg:8453_zzz" }),
|
|
153
|
+
]);
|
|
154
|
+
(0, bun_test_1.expect)(codes(findings)).toContain("near_asset_id_changed");
|
|
155
|
+
});
|
|
156
|
+
// The authored id is what the consumer sends, so deriving instead would both
|
|
157
|
+
// report drift on a token already corrected and miss an authored id going
|
|
158
|
+
// stale — the case this check exists for.
|
|
159
|
+
(0, bun_test_1.test)("an authored nearAssetId is checked, not the derivation", () => {
|
|
160
|
+
const registry = {
|
|
161
|
+
"8453": chain({
|
|
162
|
+
name: "Base",
|
|
163
|
+
settlementLayers: ["NEAR"],
|
|
164
|
+
tokens: [
|
|
165
|
+
{
|
|
166
|
+
address: "0xAAA",
|
|
167
|
+
symbol: "AAA",
|
|
168
|
+
nearAssetId: "nep245:v2_1.omni.hot.tg:8453_zzz",
|
|
169
|
+
},
|
|
170
|
+
],
|
|
171
|
+
}),
|
|
172
|
+
};
|
|
173
|
+
const served = [
|
|
174
|
+
token({ assetId: "nep245:v2_1.omni.hot.tg:8453_zzz" }),
|
|
175
|
+
];
|
|
176
|
+
(0, bun_test_1.expect)(codes((0, diff_1.diffNear)(registry, served))).not.toContain("near_asset_id_changed");
|
|
177
|
+
// ...and a stale authored id is still caught.
|
|
178
|
+
const stale = {
|
|
179
|
+
"8453": chain({
|
|
180
|
+
name: "Base",
|
|
181
|
+
settlementLayers: ["NEAR"],
|
|
182
|
+
tokens: [
|
|
183
|
+
{ address: "0xAAA", symbol: "AAA", nearAssetId: "nep141:base-old" },
|
|
184
|
+
],
|
|
185
|
+
}),
|
|
186
|
+
};
|
|
187
|
+
(0, bun_test_1.expect)(codes((0, diff_1.diffNear)(stale, served))).toContain("near_asset_id_changed");
|
|
188
|
+
});
|
|
189
|
+
(0, bun_test_1.test)("a matching asset id produces nothing", () => {
|
|
190
|
+
const registry = {
|
|
191
|
+
"8453": chain({
|
|
192
|
+
name: "Base",
|
|
193
|
+
settlementLayers: ["NEAR"],
|
|
194
|
+
tokens: [{ address: "0xAAA", symbol: "AAA" }],
|
|
195
|
+
}),
|
|
196
|
+
};
|
|
197
|
+
(0, bun_test_1.expect)(codes((0, diff_1.diffNear)(registry, [token()]))).not.toContain("near_asset_id_changed");
|
|
198
|
+
});
|
|
199
|
+
(0, bun_test_1.describe)("per-token coverage", () => {
|
|
200
|
+
const registryWith = (tokens) => ({
|
|
201
|
+
"8453": chain({ name: "Base", settlementLayers: ["NEAR"], tokens }),
|
|
202
|
+
});
|
|
203
|
+
(0, bun_test_1.test)("a declared token the vendor does not serve is an overclaim", () => {
|
|
204
|
+
const findings = (0, diff_1.diffNear)(registryWith([
|
|
205
|
+
{ address: "0xAAA", symbol: "AAA", settlementLayers: ["NEAR"] },
|
|
206
|
+
{ address: "0xBBB", symbol: "BBB", settlementLayers: ["NEAR"] },
|
|
207
|
+
]), [token()]);
|
|
208
|
+
const dropped = findings.filter((f) => f.code === "near_token_dropped");
|
|
209
|
+
(0, bun_test_1.expect)(dropped).toHaveLength(1);
|
|
210
|
+
(0, bun_test_1.expect)(dropped[0]?.kind).toBe("overclaim");
|
|
211
|
+
(0, bun_test_1.expect)(dropped[0]?.message).toContain("BBB");
|
|
212
|
+
});
|
|
213
|
+
(0, bun_test_1.test)("an undeclared token the vendor does not serve is not reported", () => {
|
|
214
|
+
// The four this coverage change excludes. Reporting them as overclaims
|
|
215
|
+
// was the false positive.
|
|
216
|
+
const findings = (0, diff_1.diffNear)(registryWith([
|
|
217
|
+
{ address: "0xAAA", symbol: "AAA", settlementLayers: ["NEAR"] },
|
|
218
|
+
{ address: "0xBBB", symbol: "BBB", settlementLayers: ["ACROSS"] },
|
|
219
|
+
]), [token()]);
|
|
220
|
+
(0, bun_test_1.expect)(codes(findings)).not.toContain("near_token_dropped");
|
|
221
|
+
(0, bun_test_1.expect)(codes(findings)).not.toContain("near_token_available");
|
|
222
|
+
});
|
|
223
|
+
(0, bun_test_1.test)("an undeclared token the vendor DOES serve is an underclaim", () => {
|
|
224
|
+
const findings = (0, diff_1.diffNear)(registryWith([
|
|
225
|
+
{ address: "0xAAA", symbol: "AAA", settlementLayers: ["NEAR"] },
|
|
226
|
+
{ address: "0xBBB", symbol: "BBB", settlementLayers: [] },
|
|
227
|
+
]), [
|
|
228
|
+
token(),
|
|
229
|
+
token({
|
|
230
|
+
assetId: "nep141:base-0xbbb.omft.near",
|
|
231
|
+
contractAddress: "0xbbb",
|
|
232
|
+
symbol: "BBB",
|
|
233
|
+
}),
|
|
234
|
+
]);
|
|
235
|
+
const available = findings.filter((f) => f.code === "near_token_available");
|
|
236
|
+
(0, bun_test_1.expect)(available).toHaveLength(1);
|
|
237
|
+
(0, bun_test_1.expect)(available[0]?.kind).toBe("underclaim");
|
|
238
|
+
(0, bun_test_1.expect)(available[0]?.severity).toBe("info");
|
|
239
|
+
(0, bun_test_1.expect)(available[0]?.message).toContain("BBB");
|
|
240
|
+
});
|
|
241
|
+
(0, bun_test_1.test)("a native whose asset id went stale is a mismatch, not a drop", () => {
|
|
242
|
+
// 1Click returns no contractAddress for a gas token, so matching on the
|
|
243
|
+
// registry's zero-address placeholder has to normalise or every native
|
|
244
|
+
// reports as unlisted.
|
|
245
|
+
const findings = (0, diff_1.diffNear)({
|
|
246
|
+
"8453": chain({
|
|
247
|
+
name: "Base",
|
|
248
|
+
settlementLayers: ["NEAR"],
|
|
249
|
+
tokens: [
|
|
250
|
+
{
|
|
251
|
+
address: "0x0000000000000000000000000000000000000000",
|
|
252
|
+
symbol: "ETH",
|
|
253
|
+
settlementLayers: ["NEAR"],
|
|
254
|
+
nearAssetId: "nep141:base.omft.near",
|
|
255
|
+
},
|
|
256
|
+
],
|
|
257
|
+
}),
|
|
258
|
+
}, [
|
|
259
|
+
token({
|
|
260
|
+
assetId: "nep245:v2_1.omni.hot.tg:8453_native",
|
|
261
|
+
contractAddress: undefined,
|
|
262
|
+
symbol: "ETH",
|
|
263
|
+
}),
|
|
264
|
+
]);
|
|
265
|
+
(0, bun_test_1.expect)(codes(findings)).toContain("near_asset_id_changed");
|
|
266
|
+
(0, bun_test_1.expect)(codes(findings)).not.toContain("near_token_dropped");
|
|
267
|
+
(0, bun_test_1.expect)(findings[0]?.message).toContain("nep245:v2_1.omni.hot.tg:8453_native");
|
|
268
|
+
});
|
|
269
|
+
(0, bun_test_1.test)("an undeclared native the vendor serves is still an underclaim", () => {
|
|
270
|
+
const findings = (0, diff_1.diffNear)({
|
|
271
|
+
"8453": chain({
|
|
272
|
+
name: "Base",
|
|
273
|
+
settlementLayers: ["NEAR"],
|
|
274
|
+
tokens: [
|
|
275
|
+
{ address: "0xAAA", symbol: "AAA", settlementLayers: ["NEAR"] },
|
|
276
|
+
{
|
|
277
|
+
address: "0x0000000000000000000000000000000000000000",
|
|
278
|
+
symbol: "ETH",
|
|
279
|
+
settlementLayers: [],
|
|
280
|
+
},
|
|
281
|
+
],
|
|
282
|
+
}),
|
|
283
|
+
}, [
|
|
284
|
+
token(),
|
|
285
|
+
token({
|
|
286
|
+
assetId: "nep141:base.omft.near",
|
|
287
|
+
contractAddress: undefined,
|
|
288
|
+
symbol: "ETH",
|
|
289
|
+
}),
|
|
290
|
+
]);
|
|
291
|
+
(0, bun_test_1.expect)(codes(findings)).toContain("near_token_available");
|
|
292
|
+
});
|
|
293
|
+
(0, bun_test_1.test)("a registry declaring NEAR on no token checks every token", () => {
|
|
294
|
+
// Pre-field artifact: the consumer advertises the whole chain, so every
|
|
295
|
+
// token is still a claim.
|
|
296
|
+
const findings = (0, diff_1.diffNear)(registryWith([
|
|
297
|
+
{ address: "0xAAA", symbol: "AAA" },
|
|
298
|
+
{ address: "0xBBB", symbol: "BBB" },
|
|
299
|
+
]), [token()]);
|
|
300
|
+
(0, bun_test_1.expect)(codes(findings)).toContain("near_token_dropped");
|
|
301
|
+
(0, bun_test_1.expect)(codes(findings)).not.toContain("near_token_available");
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
(0, bun_test_1.describe)("diffCctp", () => {
|
|
306
|
+
const registry = {
|
|
307
|
+
"8453": chain({
|
|
308
|
+
name: "Base",
|
|
309
|
+
network: "mainnet",
|
|
310
|
+
settlementLayers: ["CCTP"],
|
|
311
|
+
}),
|
|
312
|
+
"57073": chain({ name: "Ink", network: "mainnet", settlementLayers: [] }),
|
|
313
|
+
};
|
|
314
|
+
const probe = (over) => ({
|
|
315
|
+
chainId: 8453,
|
|
316
|
+
reachable: true,
|
|
317
|
+
messenger: identity_1.CCTP_CONTRACTS.mainnet.tokenMessenger,
|
|
318
|
+
deployed: true,
|
|
319
|
+
transmitter: identity_1.CCTP_CONTRACTS.mainnet.messageTransmitter,
|
|
320
|
+
domain: 6,
|
|
321
|
+
...over,
|
|
322
|
+
});
|
|
323
|
+
(0, bun_test_1.test)("a deployment on a chain we do not claim is an addition", () => {
|
|
324
|
+
const findings = (0, diff_1.diffCctp)(registry, [probe({ chainId: 57073, domain: 21 })]);
|
|
325
|
+
(0, bun_test_1.expect)(codes(findings)).toContain("cctp_chain_available");
|
|
326
|
+
});
|
|
327
|
+
(0, bun_test_1.test)("a domain that disagrees with ours is drift", () => {
|
|
328
|
+
const findings = (0, diff_1.diffCctp)(registry, [probe({ domain: 99 })]);
|
|
329
|
+
(0, bun_test_1.expect)(codes(findings)).toContain("cctp_domain_mismatch");
|
|
330
|
+
});
|
|
331
|
+
(0, bun_test_1.test)("an unrelated contract at the messenger address is not a new chain", () => {
|
|
332
|
+
const findings = (0, diff_1.diffCctp)(registry, [
|
|
333
|
+
probe({
|
|
334
|
+
chainId: 57073,
|
|
335
|
+
transmitter: "0x1111111111111111111111111111111111111111",
|
|
336
|
+
}),
|
|
337
|
+
]);
|
|
338
|
+
(0, bun_test_1.expect)(codes(findings)).toContain("cctp_transmitter_changed");
|
|
339
|
+
(0, bun_test_1.expect)(codes(findings)).not.toContain("cctp_chain_available");
|
|
340
|
+
});
|
|
341
|
+
(0, bun_test_1.test)("an unreadable transmitter is unconfirmed, not a new chain", () => {
|
|
342
|
+
const findings = (0, diff_1.diffCctp)(registry, [
|
|
343
|
+
probe({ chainId: 57073, transmitter: undefined }),
|
|
344
|
+
]);
|
|
345
|
+
(0, bun_test_1.expect)(codes(findings)).toContain("cctp_transmitter_unreadable");
|
|
346
|
+
(0, bun_test_1.expect)(codes(findings)).not.toContain("cctp_chain_available");
|
|
347
|
+
(0, bun_test_1.expect)(findings.every((f) => f.severity !== "drift")).toBe(true);
|
|
348
|
+
});
|
|
349
|
+
(0, bun_test_1.test)("an unreachable chain is a warning, never drift", () => {
|
|
350
|
+
const findings = (0, diff_1.diffCctp)(registry, [
|
|
351
|
+
{ chainId: 8453, reachable: false, error: "timeout" },
|
|
352
|
+
]);
|
|
353
|
+
(0, bun_test_1.expect)(findings.every((f) => f.severity !== "drift")).toBe(true);
|
|
354
|
+
});
|
|
355
|
+
(0, bun_test_1.test)("a claimed chain that was never probed is reported, not assumed fine", () => {
|
|
356
|
+
(0, bun_test_1.expect)(codes((0, diff_1.diffCctp)(registry, []))).toContain("cctp_not_probed");
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
(0, bun_test_1.describe)("diffRegistryConsistency", () => {
|
|
360
|
+
(0, bun_test_1.test)("flags a chain claiming a layer with no per-layer config", () => {
|
|
361
|
+
const registry = {
|
|
362
|
+
"1868": chain({ name: "Soneium", settlementLayers: ["CCTP"] }),
|
|
363
|
+
};
|
|
364
|
+
(0, bun_test_1.expect)(codes((0, diff_1.diffRegistryConsistency)(registry))).toContain("layer_claimed_without_config");
|
|
365
|
+
});
|
|
366
|
+
(0, bun_test_1.test)("flags per-layer config on a chain that does not claim the layer", () => {
|
|
367
|
+
const registry = {
|
|
368
|
+
"999": chain({ name: "HyperEVM", settlementLayers: [] }),
|
|
369
|
+
};
|
|
370
|
+
(0, bun_test_1.expect)(codes((0, diff_1.diffRegistryConsistency)(registry))).toContain("layer_config_without_claim");
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
(0, bun_test_1.describe)("rpcCandidates", () => {
|
|
374
|
+
const providers = {
|
|
375
|
+
Alchemy: {
|
|
376
|
+
chain_mapping: { "8453": "base-mainnet" },
|
|
377
|
+
url_template: "https://{{chain_param}}.g.alchemy.com/v2/${ALCHEMY_API_KEY}",
|
|
378
|
+
},
|
|
379
|
+
};
|
|
380
|
+
const registry = {
|
|
381
|
+
"8453": chain({ name: "Base", publicRpcUrl: "https://mainnet.base.org" }),
|
|
382
|
+
};
|
|
383
|
+
(0, bun_test_1.test)("expands a provider template from the environment", () => {
|
|
384
|
+
(0, bun_test_1.expect)((0, vendors_1.rpcCandidates)(8453, registry, providers, { ALCHEMY_API_KEY: "k" })[0]).toBe("https://base-mainnet.g.alchemy.com/v2/k");
|
|
385
|
+
});
|
|
386
|
+
(0, bun_test_1.test)("drops a template whose key is unset rather than calling it keyless", () => {
|
|
387
|
+
(0, bun_test_1.expect)((0, vendors_1.rpcCandidates)(8453, registry, providers, {})).toEqual([
|
|
388
|
+
"https://mainnet.base.org",
|
|
389
|
+
]);
|
|
390
|
+
});
|
|
391
|
+
(0, bun_test_1.test)("an explicit override wins outright", () => {
|
|
392
|
+
(0, bun_test_1.expect)((0, vendors_1.rpcCandidates)(8453, registry, providers, {
|
|
393
|
+
ALCHEMY_API_KEY: "k",
|
|
394
|
+
VENDOR_DRIFT_RPC_8453: "http://localhost:8545",
|
|
395
|
+
})).toEqual(["http://localhost:8545"]);
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
(0, bun_test_1.describe)("formatMarkdown", () => {
|
|
399
|
+
// The workflow always exits green, so the summary headline is the only
|
|
400
|
+
// signal. A clean tick after an unreachable vendor reports a check we did
|
|
401
|
+
// not perform.
|
|
402
|
+
(0, bun_test_1.test)("a run with skipped checks never claims agreement", () => {
|
|
403
|
+
const md = (0, report_1.formatMarkdown)([], ["Rhino /bridge/configs unreachable"]);
|
|
404
|
+
(0, bun_test_1.expect)(md).not.toContain(":white_check_mark:");
|
|
405
|
+
(0, bun_test_1.expect)(md).toContain("not run");
|
|
406
|
+
(0, bun_test_1.expect)(md).toContain("Rhino /bridge/configs unreachable");
|
|
407
|
+
});
|
|
408
|
+
(0, bun_test_1.test)("a fully covered clean run still reports agreement", () => {
|
|
409
|
+
(0, bun_test_1.expect)((0, report_1.formatMarkdown)([], [])).toContain(":white_check_mark:");
|
|
410
|
+
});
|
|
411
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure diff between what `configs/chains.json` claims and what each vendor
|
|
3
|
+
* serves. No I/O — `vendors.ts` collects the vendor side, `main.ts` wires them.
|
|
4
|
+
*/
|
|
5
|
+
import { type SettlementLayerName } from "./identity";
|
|
6
|
+
export type Severity = "drift" | "warning" | "info";
|
|
7
|
+
/**
|
|
8
|
+
* Which direction the disagreement runs. `overclaim` is the dangerous one — we
|
|
9
|
+
* advertise a route the vendor will refuse, and an offered-but-unserved route
|
|
10
|
+
* fails at fill, not at quote.
|
|
11
|
+
*/
|
|
12
|
+
export type Kind = "overclaim" | "underclaim" | "mismatch" | "note";
|
|
13
|
+
export type Finding = {
|
|
14
|
+
layer: SettlementLayerName | "REGISTRY";
|
|
15
|
+
severity: Severity;
|
|
16
|
+
kind: Kind;
|
|
17
|
+
code: string;
|
|
18
|
+
chainId?: number;
|
|
19
|
+
message: string;
|
|
20
|
+
};
|
|
21
|
+
export type RegistryToken = {
|
|
22
|
+
address: string;
|
|
23
|
+
symbol: string;
|
|
24
|
+
/** Authored override; when present it is what the consumer actually sends. */
|
|
25
|
+
nearAssetId?: string;
|
|
26
|
+
settlementLayers?: string[];
|
|
27
|
+
};
|
|
28
|
+
export type RegistryEntry = {
|
|
29
|
+
name?: string;
|
|
30
|
+
vmType?: string;
|
|
31
|
+
network?: string;
|
|
32
|
+
publicRpcUrl?: string;
|
|
33
|
+
virtual?: boolean;
|
|
34
|
+
settlementLayers?: string[];
|
|
35
|
+
tokens?: RegistryToken[];
|
|
36
|
+
settlementConfig?: Record<string, Record<string, unknown>>;
|
|
37
|
+
settlementLayerConfig?: Record<string, Record<string, unknown>>;
|
|
38
|
+
vendorConfig?: Record<string, Record<string, unknown>>;
|
|
39
|
+
};
|
|
40
|
+
export type Registry = Record<string, RegistryEntry>;
|
|
41
|
+
export type RhinoVendorEntry = {
|
|
42
|
+
name: string;
|
|
43
|
+
networkId: unknown;
|
|
44
|
+
status?: string;
|
|
45
|
+
contractAddress?: string;
|
|
46
|
+
};
|
|
47
|
+
export type OneClickToken = {
|
|
48
|
+
assetId: string;
|
|
49
|
+
blockchain: string;
|
|
50
|
+
symbol?: string;
|
|
51
|
+
contractAddress?: string;
|
|
52
|
+
};
|
|
53
|
+
export type CctpProbe = {
|
|
54
|
+
chainId: number;
|
|
55
|
+
reachable: true;
|
|
56
|
+
messenger: string;
|
|
57
|
+
deployed: boolean;
|
|
58
|
+
transmitter?: string;
|
|
59
|
+
domain?: number;
|
|
60
|
+
} | {
|
|
61
|
+
chainId: number;
|
|
62
|
+
reachable: false;
|
|
63
|
+
error: string;
|
|
64
|
+
};
|
|
65
|
+
export declare function diffRhino(registry: Registry, vendor: RhinoVendorEntry[]): Finding[];
|
|
66
|
+
export declare function nearAssetId(slug: string, chainId: number, address: string, registryConfig?: Record<string, unknown>): string | undefined;
|
|
67
|
+
/**
|
|
68
|
+
* Both directions, because a token is either claimed or not and each side has
|
|
69
|
+
* its own failure. A token we claim that 1Click will not serve is an
|
|
70
|
+
* **overclaim** — the route is offered and the quote fails `tokenOut is not
|
|
71
|
+
* valid`. A token 1Click does serve that we do not claim is an
|
|
72
|
+
* **underclaim** — capability left on the table, informational rather than
|
|
73
|
+
* drift because nothing breaks. Checking only the first would let coverage
|
|
74
|
+
* silently ossify; checking every token as a claim, which is what this did
|
|
75
|
+
* before per-token coverage existed, reports a deliberate exclusion as an
|
|
76
|
+
* overclaim.
|
|
77
|
+
*/
|
|
78
|
+
export declare function diffNear(registry: Registry, vendor: OneClickToken[]): Finding[];
|
|
79
|
+
export declare function diffCctp(registry: Registry, probes: CctpProbe[]): Finding[];
|
|
80
|
+
export declare function diffRegistryConsistency(registry: Registry): Finding[];
|