@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.
@@ -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: Core L1 originates, venues receive", () => {
69
- // Deposits arrive from the Core L1 without naming a venue; a delivery
70
- // must name the account it credits. Declaring it here is what makes a
71
- // perp *deposit source* unrepresentable rather than merely unimplemented,
72
- // and all three ids share the `hypercore:` namespace — so a namespace
73
- // switch could not draw this line at any level of effort.
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`: deposits arrive from the Core L1 venue-agnostically,
17
- * but a delivery must name the account it credits, so `hypercore:spot` /
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`: deposits arrive from the Core L1 venue-agnostically,
50
- * but a delivery must name the account it credits, so `hypercore:spot` /
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
  */