@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.
@@ -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
+ }
@@ -1,4 +1,4 @@
1
- import type { ChainNetwork, ChainStack, PegGroup, ProviderName, SettlementLayer, SupportedChain, SwapQuoter, SwapQuoterConfig, VmType } from "./types";
1
+ import type { ChainNetwork, ChainStack, PegGroup, ProviderName, SettlementConfig, SettlementLayer, SupportedChain, SwapQuoter, SwapQuoterConfig, VmType } from "./types";
2
2
  interface Token {
3
3
  /** EVM: 0x-prefixed hex. SVM: base58 SPL mint. TVM: base58 (T-prefixed). */
4
4
  address: string;
@@ -10,6 +10,11 @@ interface Token {
10
10
  approvalSlot: number | null;
11
11
  /** Bridge layers that can carry this token on this chain, when known. */
12
12
  settlementLayers?: SettlementLayer[];
13
+ /** Rhino's own symbol for this token, where it differs from ours. */
14
+ rhinoSymbol?: string;
15
+ /** NEAR 1Click asset id, for tokens whose id 1Click does not derive from
16
+ * the on-chain address (non-EVM chains). */
17
+ nearAssetId?: string;
13
18
  unpriced?: boolean;
14
19
  }
15
20
  interface NativeToken {
@@ -58,6 +63,10 @@ interface Chain {
58
63
  wrappedNativeToken: NativeToken;
59
64
  tokens: Token[];
60
65
  settlementLayers: SettlementLayer[];
66
+ /** Per-vendor addressing for the enabled layers that need it (Rhino chain
67
+ * name, CCTP domain, NEAR prefix, OFT adapter). Third-party addresses —
68
+ * never the contracts block, which is read as Rhinestone-deployed. */
69
+ settlementConfig?: SettlementConfig;
61
70
  providers: ProviderName[];
62
71
  swapQuoters: SwapQuoter[];
63
72
  swapQuoterConfig?: Partial<Record<SwapQuoter, SwapQuoterConfig>>;