@rhinestone/shared-configs 1.17.0 → 1.19.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 +74 -30
- 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/__tests__/chainVirtualization.test.js +20 -10
- package/dist/src/chainVirtualization.d.ts +2 -2
- package/dist/src/chainVirtualization.js +2 -2
- package/dist/src/chains.js +74 -30
- package/dist/src/generated/packageVersion.d.ts +1 -1
- package/dist/src/generated/packageVersion.js +1 -1
- package/dist/src/hypercore.d.ts +4 -3
- package/dist/src/hypercore.js +4 -3
- package/package.json +2 -1
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Chain identity across three settlement-layer vendors, plus the pre-migration
|
|
4
|
+
* fallback for vendor detail `configs/chains.json` does not carry yet.
|
|
5
|
+
*
|
|
6
|
+
* Identity is not derivable and not published by the vendor: Rhino names chains
|
|
7
|
+
* (`MATIC_POS`) and its `networkId` is not a chain id off the EVM set (SOLANA is
|
|
8
|
+
* `900`, and `BITCOIN`/`SN_MAIN`/`TON` are strings); 1Click uses its own slug
|
|
9
|
+
* (`pol`, `avax`). Both tables are deliberately wider than what we claim — a
|
|
10
|
+
* vendor chain with no entry here can only be reported as unmapped, never as a
|
|
11
|
+
* chain we could newly serve.
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.ONECLICK_TOKENS_URL = exports.RHINO_CONFIGS_URL = exports.CCTP_CONTRACTS = exports.BASELINE_CCTP_DOMAINS = exports.BASELINE_NEAR_CHAIN_IDS = exports.BASELINE_RHINO_CHAIN_IDS = exports.BASELINE_RHINO_BRIDGE_CONTRACTS = exports.NEAR_NON_EVM_ASSET_IDS = exports.NEAR_CHAIN_SLUGS = exports.RHINO_CHAIN_NAMES = void 0;
|
|
15
|
+
exports.RHINO_CHAIN_NAMES = {
|
|
16
|
+
1: "ETHEREUM",
|
|
17
|
+
10: "OPTIMISM",
|
|
18
|
+
56: "BINANCE",
|
|
19
|
+
100: "GNOSIS",
|
|
20
|
+
130: "UNICHAIN",
|
|
21
|
+
137: "MATIC_POS",
|
|
22
|
+
146: "SONIC",
|
|
23
|
+
999: "HYPEREVM",
|
|
24
|
+
8453: "BASE",
|
|
25
|
+
9745: "PLASMA",
|
|
26
|
+
42161: "ARBITRUM",
|
|
27
|
+
43114: "AVALANCHE",
|
|
28
|
+
57073: "INK",
|
|
29
|
+
747474: "KATANA",
|
|
30
|
+
728126428: "TRON",
|
|
31
|
+
792703809: "SOLANA",
|
|
32
|
+
};
|
|
33
|
+
exports.NEAR_CHAIN_SLUGS = {
|
|
34
|
+
1: "eth",
|
|
35
|
+
10: "op",
|
|
36
|
+
56: "bsc",
|
|
37
|
+
100: "gnosis",
|
|
38
|
+
137: "pol",
|
|
39
|
+
143: "monad",
|
|
40
|
+
8453: "base",
|
|
41
|
+
9745: "plasma",
|
|
42
|
+
42161: "arb",
|
|
43
|
+
43114: "avax",
|
|
44
|
+
728126428: "tron",
|
|
45
|
+
792703809: "sol",
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Per-token 1Click asset ids for non-EVM chains. 1Click wraps each non-EVM
|
|
49
|
+
* token under a NEAR-internal hex id that the EVM
|
|
50
|
+
* `nep141:{slug}-{address}.omft.near` derivation cannot produce, so these
|
|
51
|
+
* cannot be computed from the registry address.
|
|
52
|
+
*
|
|
53
|
+
* Keyed by the chain-native address as `chains.json` stores it — base58 is
|
|
54
|
+
* case-sensitive, so these keys are not lowercased.
|
|
55
|
+
*/
|
|
56
|
+
exports.NEAR_NON_EVM_ASSET_IDS = {
|
|
57
|
+
792703809: {
|
|
58
|
+
"11111111111111111111111111111111": "nep141:sol.omft.near",
|
|
59
|
+
So11111111111111111111111111111111111111112: "nep141:sol.omft.near",
|
|
60
|
+
EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v: "nep141:sol-5ce3bf3a31af18be40ba30f721101b4341690186.omft.near",
|
|
61
|
+
Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB: "nep141:sol-c800a4bd850783ccb82c2b2c7e84175443606352.omft.near",
|
|
62
|
+
},
|
|
63
|
+
728126428: {
|
|
64
|
+
"0x0000000000000000000000000000000000000000": "nep141:tron.omft.near",
|
|
65
|
+
TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t: "nep141:tron-d28a265909efecdcee7c5028585214ea0b96f015.omft.near",
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* Vendor detail that `configs/chains.json` does not carry yet — the per-layer
|
|
70
|
+
* maps the orchestrator holds in `src/settlement/layer/{rhino,cctp}/config.ts`
|
|
71
|
+
* and `near/httpClient.ts`.
|
|
72
|
+
*
|
|
73
|
+
* Read only where the registry entry has no `settlementLayerConfig` for the
|
|
74
|
+
* layer, so it becomes dead the moment that config lands in the registry.
|
|
75
|
+
* Deleting an entry here silently narrows what the check compares — do not
|
|
76
|
+
* prune it to "fix" a finding.
|
|
77
|
+
*/
|
|
78
|
+
exports.BASELINE_RHINO_BRIDGE_CONTRACTS = {
|
|
79
|
+
1: "0xbca3039a18c0d2f2f84ba8a028c67290bc045afa",
|
|
80
|
+
10: "0x0bca65bf4b4c8803d2f0b49353ed57caaf3d66dc",
|
|
81
|
+
56: "0xb80a582fa430645a043bb4f6135321ee01005fef",
|
|
82
|
+
100: "0x5e023c31e1d3dcd08a1b3e8c96f6ef8aa8fcacd1",
|
|
83
|
+
130: "0xb0d7040b93fe5b099d3ce02ea86c4a1b695732d0",
|
|
84
|
+
137: "0xBA4EEE20F434bC3908A0B18DA496348657133A7E",
|
|
85
|
+
146: "0x5e023c31e1d3dcd08a1b3e8c96f6ef8aa8fcacd1",
|
|
86
|
+
999: "0x5e023c31e1d3dcd08a1b3e8c96f6ef8aa8fcacd1",
|
|
87
|
+
8453: "0x2f59e9086ec8130e21bd052065a9e6b2497bb102",
|
|
88
|
+
9745: "0x5e023c31e1d3dcd08a1b3e8c96f6ef8aa8fcacd1",
|
|
89
|
+
42161: "0x10417734001162Ea139e8b044DFe28DbB8B28ad0",
|
|
90
|
+
43114: "0x5e023c31e1d3dcd08a1b3e8c96f6ef8aa8fcacd1",
|
|
91
|
+
747474: "0x04317f0e4795b1e1bab333234153fa10aaac79e9",
|
|
92
|
+
4217: "0x5e023c31e1d3dcd08a1b3e8c96f6ef8aa8fcacd1",
|
|
93
|
+
};
|
|
94
|
+
/** Chains the orchestrator's own Rhino map covers, contract or not. */
|
|
95
|
+
exports.BASELINE_RHINO_CHAIN_IDS = [
|
|
96
|
+
1, 10, 56, 100, 130, 137, 146, 999, 4217, 8453, 9745, 42161, 43114, 747474,
|
|
97
|
+
728126428, 792703809,
|
|
98
|
+
];
|
|
99
|
+
/** Chains the orchestrator's own NEAR prefix map covers. */
|
|
100
|
+
exports.BASELINE_NEAR_CHAIN_IDS = [
|
|
101
|
+
1, 10, 56, 100, 137, 196, 8453, 42161, 43114, 80094, 534352, 728126428,
|
|
102
|
+
792703809,
|
|
103
|
+
];
|
|
104
|
+
exports.BASELINE_CCTP_DOMAINS = {
|
|
105
|
+
1: 0,
|
|
106
|
+
10: 2,
|
|
107
|
+
56: 17,
|
|
108
|
+
130: 10,
|
|
109
|
+
137: 7,
|
|
110
|
+
143: 15,
|
|
111
|
+
146: 13,
|
|
112
|
+
999: 19,
|
|
113
|
+
8453: 6,
|
|
114
|
+
42161: 3,
|
|
115
|
+
43114: 1,
|
|
116
|
+
792703809: 5,
|
|
117
|
+
11155111: 0,
|
|
118
|
+
11155420: 2,
|
|
119
|
+
84532: 6,
|
|
120
|
+
421614: 3,
|
|
121
|
+
};
|
|
122
|
+
/**
|
|
123
|
+
* TokenMessengerV2 and MessageTransmitterV2 are each deployed at one address
|
|
124
|
+
* across every EVM chain, but mainnet and testnet use different ones —
|
|
125
|
+
* probing a testnet with the mainnet address reads "not deployed" on four
|
|
126
|
+
* chains that are fine.
|
|
127
|
+
*/
|
|
128
|
+
exports.CCTP_CONTRACTS = {
|
|
129
|
+
mainnet: {
|
|
130
|
+
tokenMessenger: "0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d",
|
|
131
|
+
messageTransmitter: "0x81D40F21F12A8F0E3252Bccb954D722d4c464B64",
|
|
132
|
+
},
|
|
133
|
+
testnet: {
|
|
134
|
+
tokenMessenger: "0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA",
|
|
135
|
+
messageTransmitter: "0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275",
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
exports.RHINO_CONFIGS_URL = "https://api.rhino.fi/bridge/configs";
|
|
139
|
+
exports.ONECLICK_TOKENS_URL = "https://1click.chaindefuser.com/v0/tokens";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compares `configs/chains.json` against what the Rhino, NEAR/1Click and CCTP
|
|
3
|
+
* vendors serve right now, in both directions.
|
|
4
|
+
*
|
|
5
|
+
* Reports, never gates. It exits 0 even with drift so it can never sit between
|
|
6
|
+
* us and a release; `--strict` is for a human running it locally.
|
|
7
|
+
*
|
|
8
|
+
* bun run scripts/vendor-drift/main.ts [--json] [--strict]
|
|
9
|
+
*/
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Compares `configs/chains.json` against what the Rhino, NEAR/1Click and CCTP
|
|
4
|
+
* vendors serve right now, in both directions.
|
|
5
|
+
*
|
|
6
|
+
* Reports, never gates. It exits 0 even with drift so it can never sit between
|
|
7
|
+
* us and a release; `--strict` is for a human running it locally.
|
|
8
|
+
*
|
|
9
|
+
* bun run scripts/vendor-drift/main.ts [--json] [--strict]
|
|
10
|
+
*/
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
const node_fs_1 = require("node:fs");
|
|
16
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
17
|
+
const diff_1 = require("./diff");
|
|
18
|
+
const report_1 = require("./report");
|
|
19
|
+
const vendors_1 = require("./vendors");
|
|
20
|
+
function loadJson(relative) {
|
|
21
|
+
const file = node_path_1.default.join(__dirname, "..", "..", relative);
|
|
22
|
+
return JSON.parse((0, node_fs_1.readFileSync)(file, "utf8"));
|
|
23
|
+
}
|
|
24
|
+
async function main() {
|
|
25
|
+
const argv = process.argv.slice(2);
|
|
26
|
+
const asJson = argv.includes("--json");
|
|
27
|
+
const strict = argv.includes("--strict");
|
|
28
|
+
const registry = loadJson("configs/chains.json");
|
|
29
|
+
const providers = loadJson("configs/providers.json");
|
|
30
|
+
const findings = (0, diff_1.diffRegistryConsistency)(registry);
|
|
31
|
+
const skipped = [];
|
|
32
|
+
const [rhino, oneClick] = await Promise.all([
|
|
33
|
+
(0, vendors_1.collectRhino)(),
|
|
34
|
+
(0, vendors_1.collectOneClick)(),
|
|
35
|
+
]);
|
|
36
|
+
if (rhino.ok) {
|
|
37
|
+
findings.push(...(0, diff_1.diffRhino)(registry, rhino.value));
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
skipped.push(`RHINO — ${rhino.error}`);
|
|
41
|
+
}
|
|
42
|
+
if (oneClick.ok) {
|
|
43
|
+
findings.push(...(0, diff_1.diffNear)(registry, oneClick.value));
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
skipped.push(`NEAR — ${oneClick.error}`);
|
|
47
|
+
}
|
|
48
|
+
const probes = await (0, vendors_1.collectCctp)(registry, providers, process.env);
|
|
49
|
+
const reachable = probes.filter((probe) => probe.reachable).length;
|
|
50
|
+
if (reachable === 0) {
|
|
51
|
+
skipped.push("CCTP — no EVM chain was reachable; set ALCHEMY_API_KEY or DRPC_API_KEY, or a VENDOR_DRIFT_RPC_<chainId> override.");
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
findings.push(...(0, diff_1.diffCctp)(registry, probes));
|
|
55
|
+
const unreachable = probes.length - reachable;
|
|
56
|
+
if (unreachable > 0) {
|
|
57
|
+
skipped.push(`CCTP — ${unreachable} of ${probes.length} EVM chains were unreachable; a chain Circle added there would not be seen.`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (asJson) {
|
|
61
|
+
console.log(JSON.stringify({ findings, skipped }, null, 2));
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
console.log((0, report_1.formatText)(findings, skipped));
|
|
65
|
+
}
|
|
66
|
+
const summaryFile = process.env.GITHUB_STEP_SUMMARY;
|
|
67
|
+
if (summaryFile) {
|
|
68
|
+
(0, node_fs_1.appendFileSync)(summaryFile, `${(0, report_1.formatMarkdown)(findings, skipped)}\n`);
|
|
69
|
+
}
|
|
70
|
+
const drift = (0, report_1.countBy)(findings, "drift");
|
|
71
|
+
if (strict && drift > 0)
|
|
72
|
+
process.exitCode = 1;
|
|
73
|
+
}
|
|
74
|
+
main().catch((error) => {
|
|
75
|
+
console.error(`vendor-drift failed: ${error.stack ?? error}`);
|
|
76
|
+
process.exitCode = 1;
|
|
77
|
+
});
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Finding, Kind, Severity } from "./diff";
|
|
2
|
+
export declare function countBy(findings: Finding[], severity: Severity): number;
|
|
3
|
+
export declare function countKind(findings: Finding[], kind: Kind): number;
|
|
4
|
+
export declare function formatText(findings: Finding[], skipped: string[]): string;
|
|
5
|
+
export declare function formatMarkdown(findings: Finding[], skipped: string[]): string;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.countBy = countBy;
|
|
4
|
+
exports.countKind = countKind;
|
|
5
|
+
exports.formatText = formatText;
|
|
6
|
+
exports.formatMarkdown = formatMarkdown;
|
|
7
|
+
const SEVERITY_ORDER = ["drift", "warning", "info"];
|
|
8
|
+
const KIND_ORDER = ["overclaim", "underclaim", "mismatch", "note"];
|
|
9
|
+
const KIND_HEADING = {
|
|
10
|
+
overclaim: "We claim it, the vendor does not serve it",
|
|
11
|
+
underclaim: "The vendor serves it, we do not claim it",
|
|
12
|
+
mismatch: "Both sides claim it and disagree on the detail",
|
|
13
|
+
note: "Notes",
|
|
14
|
+
};
|
|
15
|
+
function countBy(findings, severity) {
|
|
16
|
+
return findings.filter((finding) => finding.severity === severity).length;
|
|
17
|
+
}
|
|
18
|
+
function countKind(findings, kind) {
|
|
19
|
+
return findings.filter((finding) => finding.kind === kind && finding.severity !== "info").length;
|
|
20
|
+
}
|
|
21
|
+
function group(findings, kind) {
|
|
22
|
+
return findings
|
|
23
|
+
.filter((finding) => finding.kind === kind)
|
|
24
|
+
.sort((a, b) => SEVERITY_ORDER.indexOf(a.severity) -
|
|
25
|
+
SEVERITY_ORDER.indexOf(b.severity) ||
|
|
26
|
+
a.layer.localeCompare(b.layer) ||
|
|
27
|
+
(a.chainId ?? 0) - (b.chainId ?? 0));
|
|
28
|
+
}
|
|
29
|
+
function headline(findings) {
|
|
30
|
+
return `${countKind(findings, "overclaim")} overclaimed, ${countKind(findings, "underclaim")} unclaimed but available, ${countKind(findings, "mismatch")} detail mismatches (${countBy(findings, "drift")} drift, ${countBy(findings, "warning")} warnings, ${countBy(findings, "info")} informational)`;
|
|
31
|
+
}
|
|
32
|
+
function formatText(findings, skipped) {
|
|
33
|
+
const lines = [];
|
|
34
|
+
lines.push("Settlement-layer vendor drift");
|
|
35
|
+
lines.push(headline(findings));
|
|
36
|
+
for (const kind of KIND_ORDER) {
|
|
37
|
+
const rows = group(findings, kind);
|
|
38
|
+
if (rows.length === 0)
|
|
39
|
+
continue;
|
|
40
|
+
lines.push("");
|
|
41
|
+
lines.push(`${KIND_HEADING[kind]} (${rows.length})`);
|
|
42
|
+
for (const finding of rows) {
|
|
43
|
+
lines.push(` [${finding.layer}/${finding.code}] ${finding.message}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (skipped.length > 0) {
|
|
47
|
+
lines.push("");
|
|
48
|
+
lines.push("NOT CHECKED");
|
|
49
|
+
for (const reason of skipped)
|
|
50
|
+
lines.push(` ${reason}`);
|
|
51
|
+
}
|
|
52
|
+
if (findings.length === 0 && skipped.length === 0) {
|
|
53
|
+
lines.push("");
|
|
54
|
+
lines.push("No disagreement with any vendor.");
|
|
55
|
+
}
|
|
56
|
+
return lines.join("\n");
|
|
57
|
+
}
|
|
58
|
+
function formatMarkdown(findings, skipped) {
|
|
59
|
+
const drift = countBy(findings, "drift");
|
|
60
|
+
const lines = [];
|
|
61
|
+
lines.push("## Settlement-layer vendor drift");
|
|
62
|
+
lines.push("");
|
|
63
|
+
// The workflow always exits green, so this line is the only signal anyone
|
|
64
|
+
// reads. Claiming agreement after a run that could not reach a vendor would
|
|
65
|
+
// report a clean check we did not perform.
|
|
66
|
+
lines.push(drift > 0
|
|
67
|
+
? `:warning: **${drift} disagreement${drift === 1 ? "" : "s"}** between \`configs/chains.json\` and what the vendors serve today.`
|
|
68
|
+
: skipped.length > 0
|
|
69
|
+
? `:grey_question: No disagreement found, but ${skipped.length} check${skipped.length === 1 ? " was" : "s were"} not run — coverage below is incomplete.`
|
|
70
|
+
: ":white_check_mark: `configs/chains.json` agrees with Rhino, 1Click and CCTP on-chain.");
|
|
71
|
+
lines.push("");
|
|
72
|
+
lines.push(headline(findings));
|
|
73
|
+
if (skipped.length > 0) {
|
|
74
|
+
lines.push("");
|
|
75
|
+
lines.push("> [!IMPORTANT]");
|
|
76
|
+
lines.push("> **Not checked this run:**");
|
|
77
|
+
for (const reason of skipped)
|
|
78
|
+
lines.push(`> - ${reason}`);
|
|
79
|
+
}
|
|
80
|
+
for (const kind of KIND_ORDER) {
|
|
81
|
+
const rows = group(findings, kind);
|
|
82
|
+
if (rows.length === 0)
|
|
83
|
+
continue;
|
|
84
|
+
lines.push("");
|
|
85
|
+
lines.push(`### ${KIND_HEADING[kind]} (${rows.length})`);
|
|
86
|
+
lines.push("");
|
|
87
|
+
lines.push("| Layer | Chain | Finding |");
|
|
88
|
+
lines.push("| --- | --- | --- |");
|
|
89
|
+
for (const finding of rows) {
|
|
90
|
+
lines.push(`| ${finding.layer} | ${finding.chainId ?? "—"} | ${finding.message.replace(/\|/g, "\\|")} |`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return lines.join("\n");
|
|
94
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vendor-side collection. Every fetch resolves to a result, never throws — a
|
|
3
|
+
* vendor being down degrades that layer to a warning and leaves the rest of the
|
|
4
|
+
* report intact.
|
|
5
|
+
*/
|
|
6
|
+
import type { CctpProbe, OneClickToken, Registry, RhinoVendorEntry } from "./diff";
|
|
7
|
+
export type Collected<T> = {
|
|
8
|
+
ok: true;
|
|
9
|
+
value: T;
|
|
10
|
+
} | {
|
|
11
|
+
ok: false;
|
|
12
|
+
error: string;
|
|
13
|
+
};
|
|
14
|
+
export declare function collectRhino(): Promise<Collected<RhinoVendorEntry[]>>;
|
|
15
|
+
export declare function collectOneClick(): Promise<Collected<OneClickToken[]>>;
|
|
16
|
+
type ProviderRegistry = Record<string, {
|
|
17
|
+
chain_mapping?: Record<string, string>;
|
|
18
|
+
url_template?: string;
|
|
19
|
+
}>;
|
|
20
|
+
/**
|
|
21
|
+
* RPC candidates for one chain, most reliable first. Mirrors how the rest of
|
|
22
|
+
* the repo resolves RPCs: `configs/providers.json` templates expanded from the
|
|
23
|
+
* same `ALCHEMY_API_KEY` / `DRPC_API_KEY` the health and balance workflows
|
|
24
|
+
* already carry, then the registry's own public RPC as a keyless fallback so
|
|
25
|
+
* the sweep still runs for a contributor with no keys.
|
|
26
|
+
*/
|
|
27
|
+
export declare function rpcCandidates(chainId: number, registry: Registry, providers: ProviderRegistry, env: Record<string, string | undefined>): string[];
|
|
28
|
+
/**
|
|
29
|
+
* Sweeps every real EVM chain in the registry, not just the ones claiming
|
|
30
|
+
* CCTP — a chain Circle newly supports is invisible otherwise. `virtual`
|
|
31
|
+
* chains (the HyperCore venue ids) have no RPC of their own and are skipped.
|
|
32
|
+
*/
|
|
33
|
+
export declare function collectCctp(registry: Registry, providers: ProviderRegistry, env: Record<string, string | undefined>): Promise<CctpProbe[]>;
|
|
34
|
+
export {};
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Vendor-side collection. Every fetch resolves to a result, never throws — a
|
|
4
|
+
* vendor being down degrades that layer to a warning and leaves the rest of the
|
|
5
|
+
* report intact.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.collectRhino = collectRhino;
|
|
9
|
+
exports.collectOneClick = collectOneClick;
|
|
10
|
+
exports.rpcCandidates = rpcCandidates;
|
|
11
|
+
exports.collectCctp = collectCctp;
|
|
12
|
+
const identity_1 = require("./identity");
|
|
13
|
+
const HTTP_TIMEOUT_MS = 30_000;
|
|
14
|
+
const RPC_TIMEOUT_MS = 15_000;
|
|
15
|
+
const RPC_CONCURRENCY = 6;
|
|
16
|
+
// keccak256("localMessageTransmitter()") and keccak256("localDomain()").
|
|
17
|
+
const SELECTOR_LOCAL_MESSAGE_TRANSMITTER = "0x2c121921";
|
|
18
|
+
const SELECTOR_LOCAL_DOMAIN = "0x8d3638f4";
|
|
19
|
+
async function getJson(url) {
|
|
20
|
+
try {
|
|
21
|
+
const response = await fetch(url, {
|
|
22
|
+
headers: { accept: "application/json" },
|
|
23
|
+
signal: AbortSignal.timeout(HTTP_TIMEOUT_MS),
|
|
24
|
+
});
|
|
25
|
+
if (!response.ok) {
|
|
26
|
+
return { ok: false, error: `HTTP ${response.status} from ${url}` };
|
|
27
|
+
}
|
|
28
|
+
return { ok: true, value: await response.json() };
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
return { ok: false, error: `${url}: ${error.message}` };
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
async function collectRhino() {
|
|
35
|
+
const result = await getJson(identity_1.RHINO_CONFIGS_URL);
|
|
36
|
+
if (!result.ok)
|
|
37
|
+
return result;
|
|
38
|
+
const payload = result.value;
|
|
39
|
+
if (typeof payload !== "object" || payload === null) {
|
|
40
|
+
return { ok: false, error: `${identity_1.RHINO_CONFIGS_URL} returned a non-object` };
|
|
41
|
+
}
|
|
42
|
+
const entries = Object.entries(payload).map(([name, entry]) => ({
|
|
43
|
+
name,
|
|
44
|
+
networkId: entry?.networkId,
|
|
45
|
+
status: typeof entry?.status === "string" ? entry.status : undefined,
|
|
46
|
+
contractAddress: typeof entry?.contractAddress === "string" && entry.contractAddress
|
|
47
|
+
? entry.contractAddress
|
|
48
|
+
: undefined,
|
|
49
|
+
}));
|
|
50
|
+
if (entries.length === 0) {
|
|
51
|
+
return { ok: false, error: `${identity_1.RHINO_CONFIGS_URL} returned no chains` };
|
|
52
|
+
}
|
|
53
|
+
return { ok: true, value: entries };
|
|
54
|
+
}
|
|
55
|
+
async function collectOneClick() {
|
|
56
|
+
const result = await getJson(identity_1.ONECLICK_TOKENS_URL);
|
|
57
|
+
if (!result.ok)
|
|
58
|
+
return result;
|
|
59
|
+
const payload = result.value;
|
|
60
|
+
if (!Array.isArray(payload)) {
|
|
61
|
+
return { ok: false, error: `${identity_1.ONECLICK_TOKENS_URL} returned a non-array` };
|
|
62
|
+
}
|
|
63
|
+
const tokens = payload
|
|
64
|
+
.filter((token) => typeof token?.assetId === "string" &&
|
|
65
|
+
typeof token?.blockchain === "string")
|
|
66
|
+
.map((token) => ({
|
|
67
|
+
assetId: token.assetId,
|
|
68
|
+
blockchain: token.blockchain,
|
|
69
|
+
symbol: typeof token.symbol === "string" ? token.symbol : undefined,
|
|
70
|
+
contractAddress: typeof token.contractAddress === "string"
|
|
71
|
+
? token.contractAddress
|
|
72
|
+
: undefined,
|
|
73
|
+
}));
|
|
74
|
+
if (tokens.length === 0) {
|
|
75
|
+
return { ok: false, error: `${identity_1.ONECLICK_TOKENS_URL} returned no tokens` };
|
|
76
|
+
}
|
|
77
|
+
return { ok: true, value: tokens };
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* RPC candidates for one chain, most reliable first. Mirrors how the rest of
|
|
81
|
+
* the repo resolves RPCs: `configs/providers.json` templates expanded from the
|
|
82
|
+
* same `ALCHEMY_API_KEY` / `DRPC_API_KEY` the health and balance workflows
|
|
83
|
+
* already carry, then the registry's own public RPC as a keyless fallback so
|
|
84
|
+
* the sweep still runs for a contributor with no keys.
|
|
85
|
+
*/
|
|
86
|
+
function rpcCandidates(chainId, registry, providers, env) {
|
|
87
|
+
const override = env[`VENDOR_DRIFT_RPC_${chainId}`];
|
|
88
|
+
if (override)
|
|
89
|
+
return [override];
|
|
90
|
+
const urls = [];
|
|
91
|
+
for (const providerName of ["Alchemy", "DRPC"]) {
|
|
92
|
+
const provider = providers[providerName];
|
|
93
|
+
const param = provider?.chain_mapping?.[String(chainId)];
|
|
94
|
+
const template = provider?.url_template;
|
|
95
|
+
if (!param || !template)
|
|
96
|
+
continue;
|
|
97
|
+
const expanded = template.replace(/\$\{([A-Z0-9_]+)\}/g, (_match, key) => env[key] ?? "");
|
|
98
|
+
if (/\$\{|\/v2\/$|dkey=$/.test(expanded))
|
|
99
|
+
continue;
|
|
100
|
+
urls.push(expanded.replace("{{chain_param}}", param));
|
|
101
|
+
}
|
|
102
|
+
const publicRpc = registry[String(chainId)]?.publicRpcUrl;
|
|
103
|
+
if (publicRpc)
|
|
104
|
+
urls.push(publicRpc);
|
|
105
|
+
return urls;
|
|
106
|
+
}
|
|
107
|
+
async function rpcCall(url, method, params) {
|
|
108
|
+
const response = await fetch(url, {
|
|
109
|
+
method: "POST",
|
|
110
|
+
headers: { "content-type": "application/json" },
|
|
111
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }),
|
|
112
|
+
signal: AbortSignal.timeout(RPC_TIMEOUT_MS),
|
|
113
|
+
});
|
|
114
|
+
if (!response.ok)
|
|
115
|
+
throw new Error(`HTTP ${response.status}`);
|
|
116
|
+
const body = (await response.json());
|
|
117
|
+
if (body.error)
|
|
118
|
+
throw new Error(body.error.message ?? "rpc error");
|
|
119
|
+
if (typeof body.result !== "string")
|
|
120
|
+
throw new Error("no result");
|
|
121
|
+
return body.result;
|
|
122
|
+
}
|
|
123
|
+
async function probeChain(chainId, messenger, urls) {
|
|
124
|
+
if (urls.length === 0) {
|
|
125
|
+
return { chainId, reachable: false, error: "no RPC URL available" };
|
|
126
|
+
}
|
|
127
|
+
let lastError = "unknown";
|
|
128
|
+
for (const url of urls) {
|
|
129
|
+
try {
|
|
130
|
+
const code = await rpcCall(url, "eth_getCode", [messenger, "latest"]);
|
|
131
|
+
if (code === "0x" || code === "0x0") {
|
|
132
|
+
return { chainId, reachable: true, messenger, deployed: false };
|
|
133
|
+
}
|
|
134
|
+
let transmitter;
|
|
135
|
+
let domain;
|
|
136
|
+
try {
|
|
137
|
+
const raw = await rpcCall(url, "eth_call", [
|
|
138
|
+
{ to: messenger, data: SELECTOR_LOCAL_MESSAGE_TRANSMITTER },
|
|
139
|
+
"latest",
|
|
140
|
+
]);
|
|
141
|
+
transmitter = `0x${raw.slice(-40)}`;
|
|
142
|
+
const rawDomain = await rpcCall(url, "eth_call", [
|
|
143
|
+
{ to: transmitter, data: SELECTOR_LOCAL_DOMAIN },
|
|
144
|
+
"latest",
|
|
145
|
+
]);
|
|
146
|
+
domain = Number.parseInt(rawDomain, 16);
|
|
147
|
+
if (!Number.isFinite(domain))
|
|
148
|
+
domain = undefined;
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
// Deployment is established by the code check; a failed follow-up read
|
|
152
|
+
// is reported as an unreadable domain rather than a missing chain.
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
chainId,
|
|
156
|
+
reachable: true,
|
|
157
|
+
messenger,
|
|
158
|
+
deployed: true,
|
|
159
|
+
transmitter,
|
|
160
|
+
domain,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
lastError = error.message;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return { chainId, reachable: false, error: lastError };
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Sweeps every real EVM chain in the registry, not just the ones claiming
|
|
171
|
+
* CCTP — a chain Circle newly supports is invisible otherwise. `virtual`
|
|
172
|
+
* chains (the HyperCore venue ids) have no RPC of their own and are skipped.
|
|
173
|
+
*/
|
|
174
|
+
async function collectCctp(registry, providers, env) {
|
|
175
|
+
const targets = Object.keys(registry)
|
|
176
|
+
.map(Number)
|
|
177
|
+
.filter((id) => {
|
|
178
|
+
const entry = registry[String(id)];
|
|
179
|
+
return Number.isFinite(id) && entry?.vmType === "evm" && !entry.virtual;
|
|
180
|
+
})
|
|
181
|
+
.sort((a, b) => a - b);
|
|
182
|
+
const probes = [];
|
|
183
|
+
for (let i = 0; i < targets.length; i += RPC_CONCURRENCY) {
|
|
184
|
+
const batch = targets.slice(i, i + RPC_CONCURRENCY);
|
|
185
|
+
probes.push(...(await Promise.all(batch.map((chainId) => {
|
|
186
|
+
const contracts = registry[String(chainId)]?.network === "testnet"
|
|
187
|
+
? identity_1.CCTP_CONTRACTS.testnet
|
|
188
|
+
: identity_1.CCTP_CONTRACTS.mainnet;
|
|
189
|
+
return probeChain(chainId, contracts.tokenMessenger, rpcCandidates(chainId, registry, providers, env));
|
|
190
|
+
}))));
|
|
191
|
+
}
|
|
192
|
+
return probes;
|
|
193
|
+
}
|
|
@@ -65,18 +65,28 @@ const TRON_CAIP2 = "tron:mainnet";
|
|
|
65
65
|
}
|
|
66
66
|
});
|
|
67
67
|
(0, bun_test_1.describe)("chain roles", () => {
|
|
68
|
-
(0, bun_test_1.it)("splits HyperCore by direction:
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
//
|
|
68
|
+
(0, bun_test_1.it)("splits HyperCore by direction: spot both ways, perp delivery-only", () => {
|
|
69
|
+
// Declaring it here is what makes a perp *deposit source* unrepresentable
|
|
70
|
+
// rather than merely unimplemented, and all three ids share the
|
|
71
|
+
// `hypercore:` namespace — so a namespace switch could not draw this line
|
|
72
|
+
// at any level of effort.
|
|
73
|
+
//
|
|
74
|
+
// Spot originates because a HyperCore deposit already IS a spot credit:
|
|
75
|
+
// ingestion admits only an absent `destinationDex` or an explicit
|
|
76
|
+
// `"spot"`. Perp stays delivery-only — a managed account cannot sign
|
|
77
|
+
// perp→spot, so a perp deposit would strand.
|
|
78
|
+
(0, bun_test_1.expect)((0, index_1.canBeOrigin)(index_1.HYPERCORE_SPOT_CHAIN_ID)).toBe(true);
|
|
79
|
+
(0, bun_test_1.expect)((0, index_1.canBeDestination)(index_1.HYPERCORE_SPOT_CHAIN_ID)).toBe(true);
|
|
80
|
+
(0, bun_test_1.expect)((0, index_1.canBeOrigin)(index_1.HYPERCORE_PERP_CHAIN_ID)).toBe(false);
|
|
81
|
+
(0, bun_test_1.expect)((0, index_1.canBeDestination)(index_1.HYPERCORE_PERP_CHAIN_ID)).toBe(true);
|
|
82
|
+
});
|
|
83
|
+
(0, bun_test_1.it)("keeps the retiring hypercore:mainnet readable as an origin", () => {
|
|
84
|
+
// Stored `deposits.chain` rows and account rules still carry the spelling
|
|
85
|
+
// (RHI-5606). Until they are all migrated and the entry dropped, a
|
|
86
|
+
// consumer resolving one must still get an origin, or those deposits are
|
|
87
|
+
// rejected as unmapped rather than degrading quietly.
|
|
74
88
|
(0, bun_test_1.expect)((0, index_1.canBeOrigin)(1337)).toBe(true);
|
|
75
89
|
(0, bun_test_1.expect)((0, index_1.canBeDestination)(1337)).toBe(false);
|
|
76
|
-
for (const venue of [index_1.HYPERCORE_SPOT_CHAIN_ID, index_1.HYPERCORE_PERP_CHAIN_ID]) {
|
|
77
|
-
(0, bun_test_1.expect)((0, index_1.canBeOrigin)(venue)).toBe(false);
|
|
78
|
-
(0, bun_test_1.expect)((0, index_1.canBeDestination)(venue)).toBe(true);
|
|
79
|
-
}
|
|
80
90
|
});
|
|
81
91
|
(0, bun_test_1.it)("defaults both directions for chains that declare no roles", () => {
|
|
82
92
|
// Absent `roles` must mean both, so adding the field is a no-op for every
|
|
@@ -13,8 +13,8 @@ export declare function canBeOrigin(chainId: number): boolean;
|
|
|
13
13
|
* Whether funds can be DELIVERED to this chain — i.e. it is a valid intent
|
|
14
14
|
* destination.
|
|
15
15
|
*
|
|
16
|
-
* False for `hypercore:mainnet
|
|
17
|
-
*
|
|
16
|
+
* False for the retiring `hypercore:mainnet`, which names the Core L1 without
|
|
17
|
+
* naming the account a delivery would credit, so `hypercore:spot` /
|
|
18
18
|
* `hypercore:perp` are the only HyperCore destinations. Answering that from data
|
|
19
19
|
* is what lets a consumer refuse the ambiguous id without knowing its number.
|
|
20
20
|
*/
|
|
@@ -46,8 +46,8 @@ function canBeOrigin(chainId) {
|
|
|
46
46
|
* Whether funds can be DELIVERED to this chain — i.e. it is a valid intent
|
|
47
47
|
* destination.
|
|
48
48
|
*
|
|
49
|
-
* False for `hypercore:mainnet
|
|
50
|
-
*
|
|
49
|
+
* False for the retiring `hypercore:mainnet`, which names the Core L1 without
|
|
50
|
+
* naming the account a delivery would credit, so `hypercore:spot` /
|
|
51
51
|
* `hypercore:perp` are the only HyperCore destinations. Answering that from data
|
|
52
52
|
* is what lets a consumer refuse the ambiguous id without knowing its number.
|
|
53
53
|
*/
|