@rhinestone/shared-configs 1.16.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 +303 -27
- package/dist/scripts/__tests__/validation.test.js +117 -0
- package/dist/scripts/__tests__/vendor-drift.test.d.ts +1 -0
- package/dist/scripts/__tests__/vendor-drift.test.js +411 -0
- package/dist/scripts/generate.js +15 -1
- package/dist/scripts/validation.d.ts +10 -0
- package/dist/scripts/validation.js +137 -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.d.ts +10 -1
- package/dist/src/chains.js +303 -27
- package/dist/src/generated/packageVersion.d.ts +1 -1
- package/dist/src/generated/packageVersion.js +1 -1
- package/dist/src/index.d.ts +2 -2
- package/dist/src/types.d.ts +72 -1
- package/package.json +2 -1
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
const bun_test_1 = require("bun:test");
|
|
4
7
|
const validation_1 = require("../validation");
|
|
5
8
|
const index_1 = require("../../src/index");
|
|
9
|
+
const oft_json_1 = __importDefault(require("../../configs/oft.json"));
|
|
6
10
|
const nativeToken = { address: "0x0", symbol: "ETH", decimals: 18 };
|
|
7
11
|
/** A structurally complete entry, so semantic tests don't trip structural checks. */
|
|
8
12
|
function validChain(overrides = {}) {
|
|
@@ -147,6 +151,119 @@ function errorsFor(chain) {
|
|
|
147
151
|
(0, bun_test_1.expect)(result.hasErrors).toBe(false);
|
|
148
152
|
});
|
|
149
153
|
});
|
|
154
|
+
// These replace the "must match <map> in the orchestrator" comments the jsonnet
|
|
155
|
+
// used to carry over four flat chain-id allowlists. A layer advertised without
|
|
156
|
+
// the identifiers that address it is a route offered and then not taken, so the
|
|
157
|
+
// build is the only place left to catch it.
|
|
158
|
+
(0, bun_test_1.describe)("validateChainConfig — settlementConfig", () => {
|
|
159
|
+
const withLayer = (layer, config) => validChain({
|
|
160
|
+
settlementLayers: ["RELAY", layer],
|
|
161
|
+
settlementConfig: config,
|
|
162
|
+
});
|
|
163
|
+
bun_test_1.it.each([
|
|
164
|
+
["RHINO", "rhino", { chainName: "ETHEREUM" }],
|
|
165
|
+
["CCTP", "cctp", { domain: 0 }],
|
|
166
|
+
["NEAR", "near", { prefix: "eth" }],
|
|
167
|
+
["OFT", "oft", { adapter: "0xadapter", layerZeroEid: 30101 }],
|
|
168
|
+
])("accepts %s with its payload", (layer, key, payload) => {
|
|
169
|
+
(0, bun_test_1.expect)(errorsFor(withLayer(layer, { [key]: payload }))).toEqual([]);
|
|
170
|
+
});
|
|
171
|
+
bun_test_1.it.each([
|
|
172
|
+
["RHINO", "rhino"],
|
|
173
|
+
["CCTP", "cctp"],
|
|
174
|
+
["NEAR", "near"],
|
|
175
|
+
["OFT", "oft"],
|
|
176
|
+
])("rejects %s declared with no payload", (layer, key) => {
|
|
177
|
+
const errors = errorsFor(withLayer(layer, {}));
|
|
178
|
+
(0, bun_test_1.expect)(errors).toHaveLength(1);
|
|
179
|
+
(0, bun_test_1.expect)(errors[0].message).toContain(`settlementConfig.${key} is missing`);
|
|
180
|
+
});
|
|
181
|
+
bun_test_1.it.each([
|
|
182
|
+
["RHINO", { rhino: {} }, "rhino.chainName"],
|
|
183
|
+
["CCTP", { cctp: { domain: "0" } }, "cctp.domain"],
|
|
184
|
+
["NEAR", { near: { prefix: "" } }, "near.prefix"],
|
|
185
|
+
["OFT", { oft: { adapter: "0xa" } }, "oft.layerZeroEid"],
|
|
186
|
+
])("rejects %s whose payload is incomplete", (layer, config, field) => {
|
|
187
|
+
const errors = errorsFor(withLayer(layer, config));
|
|
188
|
+
(0, bun_test_1.expect)(errors.some((e) => e.message.includes(field))).toBe(true);
|
|
189
|
+
});
|
|
190
|
+
// Domain 0 is Ethereum, so a truthiness check would drop a real route.
|
|
191
|
+
(0, bun_test_1.it)("accepts CCTP domain 0", () => {
|
|
192
|
+
(0, bun_test_1.expect)(errorsFor(withLayer("CCTP", { cctp: { domain: 0 } }))).toEqual([]);
|
|
193
|
+
});
|
|
194
|
+
// Absent for a destination-only chain, which nothing originates from.
|
|
195
|
+
(0, bun_test_1.it)("accepts a rhino entry with no bridgeContract", () => {
|
|
196
|
+
(0, bun_test_1.expect)(errorsFor(withLayer("RHINO", { rhino: { chainName: "SOLANA" } }))).toEqual([]);
|
|
197
|
+
});
|
|
198
|
+
(0, bun_test_1.it)("rejects a payload for a layer that is not enabled", () => {
|
|
199
|
+
const errors = errorsFor(validChain({ settlementConfig: { cctp: { domain: 6 } } }));
|
|
200
|
+
(0, bun_test_1.expect)(errors).toHaveLength(1);
|
|
201
|
+
(0, bun_test_1.expect)(errors[0].message).toContain("CCTP is not in settlementLayers");
|
|
202
|
+
});
|
|
203
|
+
(0, bun_test_1.it)("rejects an unknown layer key", () => {
|
|
204
|
+
const errors = errorsFor(validChain({ settlementConfig: { hop: {} } }));
|
|
205
|
+
(0, bun_test_1.expect)(errors).toHaveLength(1);
|
|
206
|
+
(0, bun_test_1.expect)(errors[0].message).toContain("unknown layer");
|
|
207
|
+
});
|
|
208
|
+
// 1Click's non-EVM ids are NEAR-internal; deriving one produces a string it
|
|
209
|
+
// rejects, so a missing id throws mid-plan rather than degrading.
|
|
210
|
+
(0, bun_test_1.it)("rejects a non-EVM NEAR chain whose token has no nearAssetId", () => {
|
|
211
|
+
const errors = errorsFor(validChain({
|
|
212
|
+
vmType: "svm",
|
|
213
|
+
settlementLayers: ["RELAY", "NEAR"],
|
|
214
|
+
settlementConfig: { near: { prefix: "sol" } },
|
|
215
|
+
tokens: [{ ...nativeToken, balanceSlot: null, approvalSlot: null }],
|
|
216
|
+
}));
|
|
217
|
+
(0, bun_test_1.expect)(errors).toHaveLength(1);
|
|
218
|
+
(0, bun_test_1.expect)(errors[0].message).toContain("nearAssetId");
|
|
219
|
+
});
|
|
220
|
+
(0, bun_test_1.it)("does not require nearAssetId on an EVM NEAR chain", () => {
|
|
221
|
+
(0, bun_test_1.expect)(errorsFor(validChain({
|
|
222
|
+
settlementLayers: ["RELAY", "NEAR"],
|
|
223
|
+
settlementConfig: { near: { prefix: "eth" } },
|
|
224
|
+
}))).toEqual([]);
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
// The chain block states OFT membership and reads its addresses back out of
|
|
228
|
+
// config/oft.jsonnet, so the one way the two can disagree is an adapter added
|
|
229
|
+
// there whose chain block was never given an `oft` entry — invisible in a diff
|
|
230
|
+
// of either file alone.
|
|
231
|
+
(0, bun_test_1.describe)("validateOftCoverage", () => {
|
|
232
|
+
(0, bun_test_1.it)("accepts the real generated registry", () => {
|
|
233
|
+
(0, bun_test_1.expect)((0, validation_1.validateOftCoverage)(index_1.chainRegistry, oft_json_1.default)).toEqual([]);
|
|
234
|
+
});
|
|
235
|
+
(0, bun_test_1.it)("reports a chain with an adapter but no OFT layer", () => {
|
|
236
|
+
const issues = (0, validation_1.validateOftCoverage)({ "1": validChain() }, {
|
|
237
|
+
USDT0: { adapters: { "1": "0xadapter" } },
|
|
238
|
+
});
|
|
239
|
+
(0, bun_test_1.expect)(issues).toHaveLength(1);
|
|
240
|
+
(0, bun_test_1.expect)(issues[0].message).toContain("declares no OFT layer");
|
|
241
|
+
});
|
|
242
|
+
(0, bun_test_1.it)("reports an adapter the chain entry disagrees with", () => {
|
|
243
|
+
const chain = validChain({
|
|
244
|
+
settlementLayers: ["RELAY", "OFT"],
|
|
245
|
+
settlementConfig: { oft: { adapter: "0xstale", layerZeroEid: 30101 } },
|
|
246
|
+
});
|
|
247
|
+
const issues = (0, validation_1.validateOftCoverage)({ "1": chain }, {
|
|
248
|
+
USDT0: { adapters: { "1": "0xadapter" } },
|
|
249
|
+
});
|
|
250
|
+
(0, bun_test_1.expect)(issues).toHaveLength(1);
|
|
251
|
+
(0, bun_test_1.expect)(issues[0].message).toContain("does not match");
|
|
252
|
+
});
|
|
253
|
+
// The registry is authored in checksummed hex; the chain block is not.
|
|
254
|
+
(0, bun_test_1.it)("compares adapters case-insensitively", () => {
|
|
255
|
+
const chain = validChain({
|
|
256
|
+
settlementLayers: ["RELAY", "OFT"],
|
|
257
|
+
settlementConfig: { oft: { adapter: "0xABC", layerZeroEid: 30101 } },
|
|
258
|
+
});
|
|
259
|
+
(0, bun_test_1.expect)((0, validation_1.validateOftCoverage)({ "1": chain }, {
|
|
260
|
+
USDT0: { adapters: { "1": "0xabc" } },
|
|
261
|
+
})).toEqual([]);
|
|
262
|
+
});
|
|
263
|
+
(0, bun_test_1.it)("ignores an OFT chain that is not in the registry", () => {
|
|
264
|
+
(0, bun_test_1.expect)((0, validation_1.validateOftCoverage)({}, { USDT0: { adapters: { "1": "0xa" } } })).toEqual([]);
|
|
265
|
+
});
|
|
266
|
+
});
|
|
150
267
|
(0, bun_test_1.describe)("formatValidationIssues", () => {
|
|
151
268
|
(0, bun_test_1.it)("returns empty string for no issues", () => {
|
|
152
269
|
(0, bun_test_1.expect)((0, validation_1.formatValidationIssues)({ issues: [], hasErrors: false })).toBe("");
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -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
|
+
});
|
package/dist/scripts/generate.js
CHANGED
|
@@ -89,7 +89,7 @@ function renderChains(chainRegistry, mainnets, testnets) {
|
|
|
89
89
|
}
|
|
90
90
|
return [
|
|
91
91
|
"// Auto-generated by scripts/generate.ts. Do not edit manually.",
|
|
92
|
-
'import type { ChainNetwork, ChainStack, PegGroup, ProviderName, SettlementLayer, SupportedChain, SwapQuoter, SwapQuoterConfig, VmType } from "./types";',
|
|
92
|
+
'import type { ChainNetwork, ChainStack, PegGroup, ProviderName, SettlementConfig, SettlementLayer, SupportedChain, SwapQuoter, SwapQuoterConfig, VmType } from "./types";',
|
|
93
93
|
"",
|
|
94
94
|
"interface Token {",
|
|
95
95
|
" /** EVM: 0x-prefixed hex. SVM: base58 SPL mint. TVM: base58 (T-prefixed). */",
|
|
@@ -102,6 +102,11 @@ function renderChains(chainRegistry, mainnets, testnets) {
|
|
|
102
102
|
" approvalSlot: number | null;",
|
|
103
103
|
" /** Bridge layers that can carry this token on this chain, when known. */",
|
|
104
104
|
" settlementLayers?: SettlementLayer[];",
|
|
105
|
+
" /** Rhino's own symbol for this token, where it differs from ours. */",
|
|
106
|
+
" rhinoSymbol?: string;",
|
|
107
|
+
" /** NEAR 1Click asset id, for tokens whose id 1Click does not derive from",
|
|
108
|
+
" * the on-chain address (non-EVM chains). */",
|
|
109
|
+
" nearAssetId?: string;",
|
|
105
110
|
" unpriced?: boolean;",
|
|
106
111
|
"}",
|
|
107
112
|
"",
|
|
@@ -150,6 +155,10 @@ function renderChains(chainRegistry, mainnets, testnets) {
|
|
|
150
155
|
" wrappedNativeToken: NativeToken;",
|
|
151
156
|
" tokens: Token[];",
|
|
152
157
|
" settlementLayers: SettlementLayer[];",
|
|
158
|
+
" /** Per-vendor addressing for the enabled layers that need it (Rhino chain",
|
|
159
|
+
" * name, CCTP domain, NEAR prefix, OFT adapter). Third-party addresses —",
|
|
160
|
+
" * never the contracts block, which is read as Rhinestone-deployed. */",
|
|
161
|
+
" settlementConfig?: SettlementConfig;",
|
|
153
162
|
" providers: ProviderName[];",
|
|
154
163
|
" swapQuoters: SwapQuoter[];",
|
|
155
164
|
" swapQuoterConfig?: Partial<Record<SwapQuoter, SwapQuoterConfig>>;",
|
|
@@ -412,6 +421,10 @@ async function generate() {
|
|
|
412
421
|
// to insist on. A chain added without one would otherwise reach production as
|
|
413
422
|
// a blank tile, which nothing anywhere reports.
|
|
414
423
|
const iconSlugIssues = (0, validation_1.validateChainIconSlugs)(classifiedChains);
|
|
424
|
+
// The one check that needs both files: a chain block declares OFT membership
|
|
425
|
+
// and reads its addresses back out of the OFT registry, so only an adapter
|
|
426
|
+
// added there without the matching chain entry can drift.
|
|
427
|
+
const oftCoverageIssues = (0, validation_1.validateOftCoverage)(classifiedChains, output.oft);
|
|
415
428
|
const classificationIssues = [
|
|
416
429
|
...networkIssues,
|
|
417
430
|
...stackIssues,
|
|
@@ -419,6 +432,7 @@ async function generate() {
|
|
|
419
432
|
...explorerIssues,
|
|
420
433
|
...publicRpcUrlIssues,
|
|
421
434
|
...iconSlugIssues,
|
|
435
|
+
...oftCoverageIssues,
|
|
422
436
|
];
|
|
423
437
|
await (0, promises_1.writeFile)(node_path_1.default.join(configsDir, "chains.json"), `${JSON.stringify(classifiedChains, null, 2)}\n`);
|
|
424
438
|
await (0, promises_1.writeFile)(node_path_1.default.join(configsDir, "mainnets.json"), `${JSON.stringify(output.mainnets, null, 2)}\n`);
|
|
@@ -215,6 +215,16 @@ export declare function classifyChainStacks(chains: ChainRegistry, networks: {
|
|
|
215
215
|
* - Single-layer fragility: chains with exactly one settlement layer (warning)
|
|
216
216
|
*/
|
|
217
217
|
export declare function validateChainConfig(chains: ChainRegistry): ValidationResult;
|
|
218
|
+
/**
|
|
219
|
+
* Insists that every chain the OFT registry deploys on declares OFT.
|
|
220
|
+
*
|
|
221
|
+
* The chain block states the membership and reads the adapter and endpoint id
|
|
222
|
+
* back out of config/oft.jsonnet, so the two can only disagree one way: a
|
|
223
|
+
* deployment added to the OFT registry whose chain block was never given an
|
|
224
|
+
* `oft` entry. That chain silently stops advertising OFT, which is invisible in
|
|
225
|
+
* a diff of either file on its own — this is the check that sees both.
|
|
226
|
+
*/
|
|
227
|
+
export declare function validateOftCoverage(chains: ChainRegistry, oft: Record<string, unknown>): ValidationIssue[];
|
|
218
228
|
/**
|
|
219
229
|
* Formats validation issues for console output.
|
|
220
230
|
*
|