chainlesschain 0.160.1 → 0.161.2

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.
Files changed (129) hide show
  1. package/package.json +1 -1
  2. package/src/assets/web-panel/.build-hash +1 -1
  3. package/src/assets/web-panel/assets/{AIOps-BHiKMxFI.js → AIOps-CoZ9bIqF.js} +1 -1
  4. package/src/assets/web-panel/assets/{ActionButton-5QD5uzWP.js → ActionButton-BvMi4awy.js} +1 -1
  5. package/src/assets/web-panel/assets/{Analytics-D1JxsGBN.js → Analytics-hRk2ziup.js} +1 -1
  6. package/src/assets/web-panel/assets/{AppLayout-DykU9tOE.js → AppLayout-_JR3Gko8.js} +1 -1
  7. package/src/assets/web-panel/assets/{Audit-TGBqld9c.js → Audit-D8WmaHdX.js} +1 -1
  8. package/src/assets/web-panel/assets/{Backup-DjpzIwA6.js → Backup-CogYVeiE.js} +1 -1
  9. package/src/assets/web-panel/assets/{BaseInput-joQlVauq.js → BaseInput-NAp5_OPY.js} +1 -1
  10. package/src/assets/web-panel/assets/{Chat-9TYfosy-.js → Chat-DkQnhjfk.js} +1 -1
  11. package/src/assets/web-panel/assets/{Checkbox-OEOFA9GM.js → Checkbox-C9dkWb-7.js} +1 -1
  12. package/src/assets/web-panel/assets/{Codegen-5H5UgHJu.js → Codegen-BHyJ3j-p.js} +1 -1
  13. package/src/assets/web-panel/assets/{Col-BnLUipDp.js → Col-JyQOivHb.js} +1 -1
  14. package/src/assets/web-panel/assets/{Community-BF3R5GAl.js → Community-UMq5QuBA.js} +1 -1
  15. package/src/assets/web-panel/assets/{Compact-C1EkTFek.js → Compact-DGlwooBJ.js} +1 -1
  16. package/src/assets/web-panel/assets/{Compliance-CpP-ODRU.js → Compliance-2rWGO55k.js} +1 -1
  17. package/src/assets/web-panel/assets/{Cowork-CCgGSKVR.js → Cowork-V-tDxtrt.js} +1 -1
  18. package/src/assets/web-panel/assets/{Cron-CZ5pjRxn.js → Cron-YgEeQvdV.js} +1 -1
  19. package/src/assets/web-panel/assets/{Crosschain-C0P-5sm3.js → Crosschain-Cgd5cRKn.js} +1 -1
  20. package/src/assets/web-panel/assets/{DID-DPZKMApP.js → DID-swdBCdMZ.js} +1 -1
  21. package/src/assets/web-panel/assets/{Dashboard-G-BDDAov.js → Dashboard-ClnWtxsT.js} +2 -2
  22. package/src/assets/web-panel/assets/{Dropdown-CeywCcVQ.js → Dropdown-mlITwb7d.js} +1 -1
  23. package/src/assets/web-panel/assets/{Federation-DuFRY867.js → Federation-DtUN3wQa.js} +1 -1
  24. package/src/assets/web-panel/assets/{FormItemContext-CFSiPqbu.js → FormItemContext-BYmWDwAT.js} +1 -1
  25. package/src/assets/web-panel/assets/{Git-CjVRJDLg.js → Git-Du1k1iHz.js} +1 -1
  26. package/src/assets/web-panel/assets/{Governance-C0lyocJc.js → Governance-B2TFaWsf.js} +1 -1
  27. package/src/assets/web-panel/assets/{Inference-BVSAexgk.js → Inference-Cm_hmXla.js} +1 -1
  28. package/src/assets/web-panel/assets/{KnowledgeGraph-SE4jCwIn.js → KnowledgeGraph-DLaLMo4r.js} +1 -1
  29. package/src/assets/web-panel/assets/{Logs-BD5C-wTx.js → Logs-CElvIBBJ.js} +1 -1
  30. package/src/assets/web-panel/assets/{Marketplace-CL93dFBs.js → Marketplace-BlR3RCDV.js} +1 -1
  31. package/src/assets/web-panel/assets/{McpTools-x-Tibae-.js → McpTools-BpHkrlka.js} +1 -1
  32. package/src/assets/web-panel/assets/{Memory-CR8LXq37.js → Memory-DeU9ys_m.js} +1 -1
  33. package/src/assets/web-panel/assets/Mtc-CCJZpnJo.js +6 -0
  34. package/src/assets/web-panel/assets/Mtc-Cc8OJxe_.css +1 -0
  35. package/src/assets/web-panel/assets/{NLProgramming-B09F6gt2.js → NLProgramming-B_Tie6j1.js} +1 -1
  36. package/src/assets/web-panel/assets/{Notes-BYIn2GOe.js → Notes-BcpuirPj.js} +1 -1
  37. package/src/assets/web-panel/assets/{Organization-CybJTFN9.js → Organization-B_SHESSc.js} +1 -1
  38. package/src/assets/web-panel/assets/{Overflow-W4YLQ7yY.js → Overflow-R2SOGT0l.js} +1 -1
  39. package/src/assets/web-panel/assets/{P2P-kVj43R4j.js → P2P-IYYy3cEd.js} +1 -1
  40. package/src/assets/web-panel/assets/{Permissions-CfYE4XFJ.js → Permissions-CR1N42yW.js} +1 -1
  41. package/src/assets/web-panel/assets/{Pipeline-BVLo32Ak.js → Pipeline-nwFpKsU_.js} +1 -1
  42. package/src/assets/web-panel/assets/{Privacy-Efyb3xpJ.js → Privacy-BGpz72PX.js} +1 -1
  43. package/src/assets/web-panel/assets/{ProjectSettings-cBqrIhNN.js → ProjectSettings-CEDhpgbs.js} +1 -1
  44. package/src/assets/web-panel/assets/{Projects-BYY38oZd.js → Projects-DABi6ylb.js} +1 -1
  45. package/src/assets/web-panel/assets/{Providers-BsS27cWs.js → Providers-HzrcE8ma.js} +1 -1
  46. package/src/assets/web-panel/assets/{Recommend-BvMXwWFN.js → Recommend-BPhQwye7.js} +1 -1
  47. package/src/assets/web-panel/assets/{Reputation-DmwTtBfl.js → Reputation-BL6hTN1s.js} +1 -1
  48. package/src/assets/web-panel/assets/{Row-N-X7EJ3w.js → Row-2akLU3YS.js} +1 -1
  49. package/src/assets/web-panel/assets/{RssFeed-D9TjnwgF.js → RssFeed-D6qNq6Ht.js} +1 -1
  50. package/src/assets/web-panel/assets/{Search-Hapv-QkV.js → Search-R_b-u9oL.js} +1 -1
  51. package/src/assets/web-panel/assets/{Security-DWbFJK10.js → Security-DdW4hu_4.js} +1 -1
  52. package/src/assets/web-panel/assets/{Services-BPUmhVoH.js → Services-CnzEzGFN.js} +1 -1
  53. package/src/assets/web-panel/assets/{Skeleton-Bo5qPHbE.js → Skeleton-D6RevdW2.js} +1 -1
  54. package/src/assets/web-panel/assets/{Skills-JJ8uInMW.js → Skills-DD5ReHH7.js} +1 -1
  55. package/src/assets/web-panel/assets/{Sla-CEDF9zdV.js → Sla-CaQOOsjD.js} +1 -1
  56. package/src/assets/web-panel/assets/{SpeechSettings-oIoX_vCx.js → SpeechSettings-D-pGIn9Z.js} +1 -1
  57. package/src/assets/web-panel/assets/{Tasks-Cx5wgv5Z.js → Tasks-BbdO_i4Q.js} +1 -1
  58. package/src/assets/web-panel/assets/{Templates-BomcBlkN.js → Templates-CupAugDn.js} +1 -1
  59. package/src/assets/web-panel/assets/{Tenant-BxSQZUNh.js → Tenant-rseAzHcY.js} +1 -1
  60. package/src/assets/web-panel/assets/{Tokens-BlPPoB3C.js → Tokens-DXMokNbR.js} +1 -1
  61. package/src/assets/web-panel/assets/{Trigger-Bhjmjsc5.js → Trigger-DkSZjOlY.js} +1 -1
  62. package/src/assets/web-panel/assets/{Trust-Dsjv7rkb.js → Trust-CKb7QDH1.js} +1 -1
  63. package/src/assets/web-panel/assets/{VideoEditing-BsVR1PN8.js → VideoEditing-CSOjdBZg.js} +1 -1
  64. package/src/assets/web-panel/assets/{Wallet-dcRAYsdL.js → Wallet-DHbi5dHt.js} +1 -1
  65. package/src/assets/web-panel/assets/{WebAuthn-oqIS5PCi.js → WebAuthn-BkGDI33-.js} +1 -1
  66. package/src/assets/web-panel/assets/{WorkflowEditor-C_fYMBvB.js → WorkflowEditor-BFZ3RYva.js} +1 -1
  67. package/src/assets/web-panel/assets/{colors-D2P6CqS5.js → colors-D2tTvuDI.js} +1 -1
  68. package/src/assets/web-panel/assets/{compact-item-CG7qutT_.js → compact-item-CqCEUZiy.js} +1 -1
  69. package/src/assets/web-panel/assets/{createContext-y4UPKgbA.js → createContext-C6HFlAQP.js} +1 -1
  70. package/src/assets/web-panel/assets/{hasIn-Butbu9jZ.js → hasIn-yp2CbhYc.js} +1 -1
  71. package/src/assets/web-panel/assets/index-4cn1LmJ9.js +1 -0
  72. package/src/assets/web-panel/assets/{index-BtuwtDUE.js → index-B0jkl2Zb.js} +1 -1
  73. package/src/assets/web-panel/assets/{index-YmGOWX7h.js → index-B2qFUwGb.js} +1 -1
  74. package/src/assets/web-panel/assets/{index-BEfvpbz-.js → index-B74gWYqD.js} +1 -1
  75. package/src/assets/web-panel/assets/{index-BJN_3RTO.js → index-B8Qxu0q2.js} +1 -1
  76. package/src/assets/web-panel/assets/{index-C2K61jP8.js → index-B9b_mz4I.js} +1 -1
  77. package/src/assets/web-panel/assets/index-BAA1SFp1.js +1 -0
  78. package/src/assets/web-panel/assets/{index-rIbVsjde.js → index-BJeE7n_I.js} +1 -1
  79. package/src/assets/web-panel/assets/{index-Ceaxjpqh.js → index-BJoK7MkB.js} +1 -1
  80. package/src/assets/web-panel/assets/{index-CYlDKn3O.js → index-BTL2yIvT.js} +1 -1
  81. package/src/assets/web-panel/assets/{index-DrVnyYpX.js → index-BlVnFOFL.js} +1 -1
  82. package/src/assets/web-panel/assets/{index-89HJLKZ-.js → index-Blq49aTW.js} +1 -1
  83. package/src/assets/web-panel/assets/{index-vC5cTycG.js → index-BxSsO6Sm.js} +1 -1
  84. package/src/assets/web-panel/assets/{index-38mVlGHc.js → index-CA3g3EpL.js} +1 -1
  85. package/src/assets/web-panel/assets/{index-DLMJy9pE.js → index-CFoFkVUt.js} +1 -1
  86. package/src/assets/web-panel/assets/{index-DtNHlrxp.js → index-CWhXxdyo.js} +1 -1
  87. package/src/assets/web-panel/assets/{index-Bs9aHxDD.js → index-CXf0zL5i.js} +1 -1
  88. package/src/assets/web-panel/assets/{index-BQr8Y0o5.js → index-CcwZodUl.js} +1 -1
  89. package/src/assets/web-panel/assets/{index-DdgjeX4z.js → index-Cd6m6ynF.js} +1 -1
  90. package/src/assets/web-panel/assets/{index-BQfow_sh.js → index-Cfy9l115.js} +1 -1
  91. package/src/assets/web-panel/assets/{index-C1mK1Ga3.js → index-CijiVpfO.js} +1 -1
  92. package/src/assets/web-panel/assets/{index-hSilB_Q-.js → index-Cpfx7-LN.js} +1 -1
  93. package/src/assets/web-panel/assets/{index-BYZPJS7A.js → index-CrEEL63u.js} +1 -1
  94. package/src/assets/web-panel/assets/{index-C_8hWf5_.js → index-CucxAdwN.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-qXvwlbkq.js → index-CvuBD5TK.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-CyqU4Tck.js → index-Cw4v7ezB.js} +3 -3
  97. package/src/assets/web-panel/assets/{index-B6U6cYUa.js → index-D34iabcS.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-BvJgRWBq.js → index-DEKuiAPQ.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-Cc77JZKd.js → index-Dj4P0iWm.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-CAeKBs9n.js → index-DjeNNVwu.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-B4Jfv4EB.js → index-DlgMVieO.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-CWh3IxEh.js → index-DnehXcB-.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-Ci6jXp3l.js → index-JszcDpsT.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-BCQ0WlB2.js → index-KdFFI-p3.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-DnI4Aq0q.js → index-NNymVAza.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-gWmZm8_Q.js → index-aw2DwKj-.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-Dx_ZTZo_.js → index-nzDx0JAR.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-C1ucrJLg.js → index-w1xShUDf.js} +1 -1
  109. package/src/assets/web-panel/assets/{initDefaultProps-CZRZ-1bk.js → initDefaultProps-DfdVxwz6.js} +1 -1
  110. package/src/assets/web-panel/assets/{motion-CvU8SiWF.js → motion-CL0bdvJg.js} +1 -1
  111. package/src/assets/web-panel/assets/{move-ipAfWhya.js → move-Bo9Fgzv7.js} +1 -1
  112. package/src/assets/web-panel/assets/{omit-D6bJEjz9.js → omit-C-cc6wHr.js} +1 -1
  113. package/src/assets/web-panel/assets/{pickAttrs-Dpvzf7sL.js → pickAttrs-CjLp5RN-.js} +1 -1
  114. package/src/assets/web-panel/assets/{placementArrow-D_tEolP1.js → placementArrow-CJa8gsqa.js} +1 -1
  115. package/src/assets/web-panel/assets/{responsiveObserve-BEFI7neO.js → responsiveObserve-CcDj3P-p.js} +1 -1
  116. package/src/assets/web-panel/assets/{slide-Bte_KOqM.js → slide-CpvbHO26.js} +1 -1
  117. package/src/assets/web-panel/assets/{statusUtils-K4xaDRuO.js → statusUtils-BK69kP1U.js} +1 -1
  118. package/src/assets/web-panel/assets/{styleChecker-Cl9YgOVY.js → styleChecker-CDvBRzsG.js} +1 -1
  119. package/src/assets/web-panel/assets/{useFlexGapSupport-DNstl1wK.js → useFlexGapSupport-CRN_hzJt.js} +1 -1
  120. package/src/assets/web-panel/assets/{vnode-ChB-8cXr.js → vnode-Bzp-FsbB.js} +1 -1
  121. package/src/assets/web-panel/assets/{zoom-meTNBulL.js → zoom-BHqpWXJV.js} +1 -1
  122. package/src/assets/web-panel/index.html +1 -1
  123. package/src/commands/crosschain.js +564 -8
  124. package/src/commands/mtc.js +1334 -0
  125. package/src/lib/cross-chain-mtc.js +904 -0
  126. package/src/assets/web-panel/assets/Mtc-C-PfF5B3.css +0 -1
  127. package/src/assets/web-panel/assets/Mtc-CEtRtMcc.js +0 -1
  128. package/src/assets/web-panel/assets/index-B5FRjJMb.js +0 -1
  129. package/src/assets/web-panel/assets/index-B7FV5EnN.js +0 -1
@@ -0,0 +1,904 @@
1
+ /**
2
+ * Cross-Chain Bridge MTC integration (v0.1 — design doc 跨链桥设计 v1).
3
+ *
4
+ * Wraps core-mtc assembleBatch / verify with bridge-specific:
5
+ * - namespace formatting: `mtc/v1/bridge/<chain-pair>/<batch-seq>` (lex-ordered)
6
+ * - trust-anchor store per source chain (Independent mode, design §6.1)
7
+ * - bridge envelope verification (namespace prefix + bridge_op enum check)
8
+ *
9
+ * Layout under <configDir>/cross-chain-mtc/:
10
+ * config.json enabled, batch_interval_seconds, alg, mode (independent|federated|light-client)
11
+ * trust-anchors.json { chain_id → [{ pubkey_id, alg, issuer, pubkey_jwk, added_at }] }
12
+ * batches/<batch-id>/ one closed batch (landmark + envelopes)
13
+ *
14
+ * Per design doc §3.1: opt-in. Default config.enabled = false. Existing
15
+ * `cc crosschain bridge|swap|send` paths keep working unchanged when
16
+ * MTC integration is disabled.
17
+ */
18
+
19
+ import fs from "node:fs";
20
+ import path from "node:path";
21
+ import mtcLib from "@chainlesschain/core-mtc";
22
+
23
+ const {
24
+ assembleBatch,
25
+ verify,
26
+ NAMESPACE_RE,
27
+ SCHEMA_ENVELOPE,
28
+ ed25519,
29
+ slhDsa,
30
+ } = mtcLib;
31
+
32
+ const SUPPORTED_BRIDGE_CHAINS = Object.freeze([
33
+ "ethereum",
34
+ "polygon",
35
+ "bsc",
36
+ "arbitrum",
37
+ "solana",
38
+ ]);
39
+
40
+ const VALID_BRIDGE_OPS = Object.freeze([
41
+ "lock",
42
+ "mint",
43
+ "refund",
44
+ "swap-init",
45
+ "swap-claim",
46
+ "swap-refund",
47
+ "msg-send",
48
+ ]);
49
+
50
+ const CONFIG_DEFAULTS = Object.freeze({
51
+ enabled: false,
52
+ // 60s default per design doc §3.5 — short-window batch suitable for
53
+ // cross-chain latency expectations. Operators may bump for low-traffic pairs.
54
+ batch_interval_seconds: 60,
55
+ alg: "ed25519", // or "slh-dsa-128f" (PQC opt-in)
56
+ mode: "independent", // or "federated" | "light-client"
57
+ issuer: "mtca:cc:bridge-local",
58
+ });
59
+
60
+ const BRIDGE_NAMESPACE_PREFIX = "mtc/v1/bridge/";
61
+ const BRIDGE_NAMESPACE_RE = /^mtc\/v1\/bridge\/[a-z0-9]+-[a-z0-9]+\/[0-9]{6,}$/;
62
+
63
+ // ─────────────────────────────────────────────────────────────────────
64
+ // Path helpers
65
+ // ─────────────────────────────────────────────────────────────────────
66
+
67
+ export function getCrossChainMtcDir(configDir) {
68
+ if (!configDir) throw new Error("getCrossChainMtcDir: configDir required");
69
+ return path.join(configDir, "cross-chain-mtc");
70
+ }
71
+
72
+ function configPath(dir) {
73
+ return path.join(dir, "config.json");
74
+ }
75
+
76
+ function trustAnchorsPath(dir) {
77
+ return path.join(dir, "trust-anchors.json");
78
+ }
79
+
80
+ function batchesDir(dir) {
81
+ return path.join(dir, "batches");
82
+ }
83
+
84
+ function stagingDir(dir) {
85
+ return path.join(dir, "staging");
86
+ }
87
+
88
+ function batchSeqPath(dir) {
89
+ return path.join(dir, "batch-seq.json");
90
+ }
91
+
92
+ function ensureDirs(dir) {
93
+ for (const p of [dir, batchesDir(dir), stagingDir(dir)]) {
94
+ if (!fs.existsSync(p)) fs.mkdirSync(p, { recursive: true });
95
+ }
96
+ }
97
+
98
+ // ─────────────────────────────────────────────────────────────────────
99
+ // Namespace
100
+ // ─────────────────────────────────────────────────────────────────────
101
+
102
+ /**
103
+ * Build a cross-chain bridge namespace. Per design doc §5.2 chain-pair
104
+ * MUST be lexicographic (so A↔B always maps to one tree, never two).
105
+ *
106
+ * @param {string} srcChain
107
+ * @param {string} dstChain
108
+ * @param {number|string} batchSeq - integer ≥ 1 (zero-padded to 6 digits)
109
+ * @returns {string} namespace like "mtc/v1/bridge/ethereum-polygon/000142"
110
+ */
111
+ export function bridgeNamespace(srcChain, dstChain, batchSeq) {
112
+ if (!SUPPORTED_BRIDGE_CHAINS.includes(srcChain)) {
113
+ throw new RangeError(
114
+ `bridgeNamespace: unsupported src chain "${srcChain}"`,
115
+ );
116
+ }
117
+ if (!SUPPORTED_BRIDGE_CHAINS.includes(dstChain)) {
118
+ throw new RangeError(
119
+ `bridgeNamespace: unsupported dst chain "${dstChain}"`,
120
+ );
121
+ }
122
+ if (srcChain === dstChain) {
123
+ throw new RangeError("bridgeNamespace: src and dst chain must differ");
124
+ }
125
+ const seqInt = Number(batchSeq);
126
+ if (!Number.isInteger(seqInt) || seqInt < 1) {
127
+ throw new RangeError("bridgeNamespace: batchSeq must be positive integer");
128
+ }
129
+ const pair = [srcChain, dstChain].sort().join("-");
130
+ const seq = String(seqInt).padStart(6, "0");
131
+ return `${BRIDGE_NAMESPACE_PREFIX}${pair}/${seq}`;
132
+ }
133
+
134
+ export function isBridgeNamespace(ns) {
135
+ return typeof ns === "string" && BRIDGE_NAMESPACE_RE.test(ns);
136
+ }
137
+
138
+ // ─────────────────────────────────────────────────────────────────────
139
+ // Config
140
+ // ─────────────────────────────────────────────────────────────────────
141
+
142
+ export function loadCrossChainMtcConfig(dir) {
143
+ ensureDirs(dir);
144
+ const p = configPath(dir);
145
+ if (!fs.existsSync(p)) return { ...CONFIG_DEFAULTS };
146
+ try {
147
+ const raw = JSON.parse(fs.readFileSync(p, "utf-8"));
148
+ return { ...CONFIG_DEFAULTS, ...raw };
149
+ } catch (err) {
150
+ throw new Error(`cross-chain-mtc config malformed at ${p}: ${err.message}`);
151
+ }
152
+ }
153
+
154
+ export function saveCrossChainMtcConfig(dir, patch) {
155
+ ensureDirs(dir);
156
+ const merged = { ...loadCrossChainMtcConfig(dir), ...patch };
157
+ if (typeof merged.enabled !== "boolean") {
158
+ throw new TypeError("config.enabled must be boolean");
159
+ }
160
+ if (
161
+ !Number.isInteger(merged.batch_interval_seconds) ||
162
+ merged.batch_interval_seconds < 1
163
+ ) {
164
+ throw new RangeError(
165
+ "config.batch_interval_seconds must be positive integer",
166
+ );
167
+ }
168
+ if (!["ed25519", "slh-dsa-128f"].includes(merged.alg)) {
169
+ throw new RangeError("config.alg must be ed25519 or slh-dsa-128f");
170
+ }
171
+ if (!["independent", "federated", "light-client"].includes(merged.mode)) {
172
+ throw new RangeError(
173
+ "config.mode must be independent, federated, or light-client",
174
+ );
175
+ }
176
+ fs.writeFileSync(configPath(dir), JSON.stringify(merged, null, 2), "utf-8");
177
+ return merged;
178
+ }
179
+
180
+ // ─────────────────────────────────────────────────────────────────────
181
+ // Trust anchor store (Independent mode — design doc §6.1)
182
+ // ─────────────────────────────────────────────────────────────────────
183
+
184
+ function emptyTrustAnchorStore() {
185
+ return { schema: "mtc-bridge-trust-anchors/v1", anchors: {} };
186
+ }
187
+
188
+ export function loadTrustAnchors(dir) {
189
+ ensureDirs(dir);
190
+ const p = trustAnchorsPath(dir);
191
+ if (!fs.existsSync(p)) return emptyTrustAnchorStore();
192
+ try {
193
+ const raw = JSON.parse(fs.readFileSync(p, "utf-8"));
194
+ if (raw.schema !== "mtc-bridge-trust-anchors/v1" || !raw.anchors) {
195
+ throw new Error("schema mismatch");
196
+ }
197
+ return raw;
198
+ } catch (err) {
199
+ throw new Error(`trust-anchors store malformed at ${p}: ${err.message}`);
200
+ }
201
+ }
202
+
203
+ function saveTrustAnchorStore(dir, store) {
204
+ fs.writeFileSync(
205
+ trustAnchorsPath(dir),
206
+ JSON.stringify(store, null, 2),
207
+ "utf-8",
208
+ );
209
+ }
210
+
211
+ /**
212
+ * Register a trust anchor for a source chain.
213
+ *
214
+ * @param {string} dir
215
+ * @param {string} chain - one of SUPPORTED_BRIDGE_CHAINS
216
+ * @param {{ pubkey_id: string, alg: string, issuer: string, pubkey_jwk?: object }} anchor
217
+ * @returns {{ added: boolean, total_for_chain: number }}
218
+ */
219
+ export function addTrustAnchor(dir, chain, anchor) {
220
+ if (!SUPPORTED_BRIDGE_CHAINS.includes(chain)) {
221
+ throw new RangeError(`addTrustAnchor: unsupported chain "${chain}"`);
222
+ }
223
+ if (!anchor || typeof anchor !== "object") {
224
+ throw new TypeError("addTrustAnchor: anchor object required");
225
+ }
226
+ if (typeof anchor.pubkey_id !== "string" || !anchor.pubkey_id) {
227
+ throw new TypeError("addTrustAnchor: anchor.pubkey_id required");
228
+ }
229
+ if (!["ed25519", "slh-dsa-128f"].includes(anchor.alg)) {
230
+ throw new RangeError(
231
+ "addTrustAnchor: anchor.alg must be ed25519 or slh-dsa-128f",
232
+ );
233
+ }
234
+ if (typeof anchor.issuer !== "string" || !anchor.issuer) {
235
+ throw new TypeError("addTrustAnchor: anchor.issuer required");
236
+ }
237
+
238
+ const store = loadTrustAnchors(dir);
239
+ store.anchors[chain] ||= [];
240
+ const existing = store.anchors[chain].find(
241
+ (a) => a.pubkey_id === anchor.pubkey_id,
242
+ );
243
+ if (existing) {
244
+ return { added: false, total_for_chain: store.anchors[chain].length };
245
+ }
246
+ store.anchors[chain].push({
247
+ pubkey_id: anchor.pubkey_id,
248
+ alg: anchor.alg,
249
+ issuer: anchor.issuer,
250
+ pubkey_jwk: anchor.pubkey_jwk || null,
251
+ added_at: new Date().toISOString(),
252
+ });
253
+ saveTrustAnchorStore(dir, store);
254
+ return { added: true, total_for_chain: store.anchors[chain].length };
255
+ }
256
+
257
+ export function listTrustAnchors(dir, chain) {
258
+ const store = loadTrustAnchors(dir);
259
+ if (chain) {
260
+ if (!SUPPORTED_BRIDGE_CHAINS.includes(chain)) {
261
+ throw new RangeError(`listTrustAnchors: unsupported chain "${chain}"`);
262
+ }
263
+ return store.anchors[chain] || [];
264
+ }
265
+ return store.anchors;
266
+ }
267
+
268
+ export function removeTrustAnchor(dir, chain, pubkeyId) {
269
+ if (!SUPPORTED_BRIDGE_CHAINS.includes(chain)) {
270
+ throw new RangeError(`removeTrustAnchor: unsupported chain "${chain}"`);
271
+ }
272
+ const store = loadTrustAnchors(dir);
273
+ const list = store.anchors[chain] || [];
274
+ const before = list.length;
275
+ store.anchors[chain] = list.filter((a) => a.pubkey_id !== pubkeyId);
276
+ const removed = before > store.anchors[chain].length;
277
+ if (removed) saveTrustAnchorStore(dir, store);
278
+ return { removed, total_for_chain: store.anchors[chain].length };
279
+ }
280
+
281
+ // ─────────────────────────────────────────────────────────────────────
282
+ // Bridge op leaf shape — matches design doc §5.1 payload
283
+ // ─────────────────────────────────────────────────────────────────────
284
+
285
+ /**
286
+ * Validate one bridge op (raw leaf) before batching. Throws on bad shape.
287
+ * @param {object} op
288
+ */
289
+ export function validateBridgeOp(op) {
290
+ if (!op || typeof op !== "object") {
291
+ throw new TypeError("validateBridgeOp: op object required");
292
+ }
293
+ if (!VALID_BRIDGE_OPS.includes(op.bridge_op)) {
294
+ throw new RangeError(
295
+ `validateBridgeOp: bridge_op must be one of ${VALID_BRIDGE_OPS.join(", ")}`,
296
+ );
297
+ }
298
+ if (!SUPPORTED_BRIDGE_CHAINS.includes(op.src_chain)) {
299
+ throw new RangeError(
300
+ `validateBridgeOp: unsupported src_chain "${op.src_chain}"`,
301
+ );
302
+ }
303
+ if (!SUPPORTED_BRIDGE_CHAINS.includes(op.dst_chain)) {
304
+ throw new RangeError(
305
+ `validateBridgeOp: unsupported dst_chain "${op.dst_chain}"`,
306
+ );
307
+ }
308
+ if (op.src_chain === op.dst_chain) {
309
+ throw new RangeError("validateBridgeOp: src and dst chain must differ");
310
+ }
311
+ if (typeof op.issued_at !== "string" || !op.issued_at) {
312
+ throw new TypeError("validateBridgeOp: issued_at (RFC 3339) required");
313
+ }
314
+ return true;
315
+ }
316
+
317
+ // ─────────────────────────────────────────────────────────────────────
318
+ // Batch assembly — wraps core-mtc assembleBatch with bridge namespace
319
+ // ─────────────────────────────────────────────────────────────────────
320
+
321
+ /**
322
+ * Assemble a bridge batch. Pure (no fs).
323
+ *
324
+ * @param {Array<object>} bridgeOps - validated bridge ops
325
+ * @param {{ secretKey: Buffer, publicKey: Buffer }} keys
326
+ * @param {{ src_chain: string, dst_chain: string, batch_seq: number,
327
+ * issuer: string, alg?: string, signer?: object }} opts
328
+ * @returns {{ landmark, envelopes, treeHeadId, root, namespace }}
329
+ */
330
+ export function assembleBridgeBatch(bridgeOps, keys, opts) {
331
+ if (!Array.isArray(bridgeOps) || bridgeOps.length === 0) {
332
+ throw new RangeError("assembleBridgeBatch: bridgeOps must be non-empty");
333
+ }
334
+ if (!opts || typeof opts !== "object") {
335
+ throw new TypeError("assembleBridgeBatch: opts required");
336
+ }
337
+ for (const op of bridgeOps) validateBridgeOp(op);
338
+
339
+ const namespace = bridgeNamespace(
340
+ opts.src_chain,
341
+ opts.dst_chain,
342
+ opts.batch_seq,
343
+ );
344
+ const result = assembleBatch(
345
+ bridgeOps,
346
+ keys,
347
+ {
348
+ namespace,
349
+ issuer: opts.issuer,
350
+ issuedAt: opts.issuedAt,
351
+ expiresAt: opts.expiresAt,
352
+ },
353
+ opts.signer,
354
+ );
355
+ return { ...result, namespace };
356
+ }
357
+
358
+ // ─────────────────────────────────────────────────────────────────────
359
+ // Envelope verification — wraps core-mtc verify with bridge-specific checks
360
+ // ─────────────────────────────────────────────────────────────────────
361
+
362
+ /**
363
+ * Verify a bridge envelope. Layered checks:
364
+ * 1. namespace conforms to bridge pattern (BRIDGE_NAMESPACE_RE)
365
+ * 2. envelope.leaf has valid bridge_op + src/dst_chain
366
+ * 3. core-mtc verify passes (inclusion proof + landmark lookup)
367
+ *
368
+ * @param {object} envelope - MTC envelope JSON
369
+ * @param {{ lookup: Function }} cache - LandmarkCache
370
+ * @param {{ now?: number }} [options]
371
+ * @returns {{ ok: boolean, code?: string, recoverable?: boolean,
372
+ * leaf?: object, treeHead?: object, bridge_op?: string }}
373
+ */
374
+ export function verifyBridgeEnvelope(envelope, cache, options) {
375
+ if (!envelope || typeof envelope !== "object") {
376
+ return { ok: false, code: "BAD_ENVELOPE", recoverable: false };
377
+ }
378
+ if (envelope.schema !== SCHEMA_ENVELOPE) {
379
+ return { ok: false, code: "UNKNOWN_SCHEMA", recoverable: false };
380
+ }
381
+ if (!isBridgeNamespace(envelope.namespace)) {
382
+ return { ok: false, code: "BAD_BRIDGE_NAMESPACE", recoverable: false };
383
+ }
384
+ // Check leaf shape before paying merkle-proof cost
385
+ if (!envelope.leaf || typeof envelope.leaf !== "object") {
386
+ return { ok: false, code: "BAD_BRIDGE_LEAF", recoverable: false };
387
+ }
388
+ try {
389
+ validateBridgeOp(envelope.leaf);
390
+ } catch (_err) {
391
+ return { ok: false, code: "BAD_BRIDGE_LEAF", recoverable: false };
392
+ }
393
+
394
+ const result = verify(envelope, cache, options);
395
+ if (!result.ok) return result;
396
+
397
+ return { ...result, bridge_op: envelope.leaf.bridge_op };
398
+ }
399
+
400
+ // ─────────────────────────────────────────────────────────────────────
401
+ // Staging — `--mtc` opt-in writes one op per file; mtc-batch closes them
402
+ // ─────────────────────────────────────────────────────────────────────
403
+
404
+ function stagedFileName(op) {
405
+ // Time-prefixed so listdir = chronological. Pair-prefix groups visually.
406
+ const ts = new Date(op.issued_at)
407
+ .toISOString()
408
+ .replace(/[-:T.]/g, "")
409
+ .slice(0, 17);
410
+ const pair = [op.src_chain, op.dst_chain].sort().join("-");
411
+ // Use a content-derived 8-char tail to dedupe accidental double-stage of same op.
412
+ const sigSource = `${op.bridge_op}|${op.src_tx_hash || ""}|${op.swap_id || ""}|${op.amount || ""}|${ts}`;
413
+ let h = 0;
414
+ for (let i = 0; i < sigSource.length; i++)
415
+ h = ((h << 5) - h + sigSource.charCodeAt(i)) | 0;
416
+ const tail = (h >>> 0).toString(16).padStart(8, "0");
417
+ return `${ts}-${pair}-${tail}.json`;
418
+ }
419
+
420
+ /**
421
+ * Stage one bridge op for inclusion in the next batch close.
422
+ * Idempotent: same op written twice -> single staging file.
423
+ *
424
+ * @param {string} dir
425
+ * @param {object} op
426
+ * @param {{ requireEnabled?: boolean }} [opts]
427
+ * @returns {{ staged: boolean, path: string, reason?: string }}
428
+ */
429
+ export function stageBridgeOp(dir, op, opts = {}) {
430
+ ensureDirs(dir);
431
+ const cfg = loadCrossChainMtcConfig(dir);
432
+ const requireEnabled = opts.requireEnabled !== false;
433
+ if (requireEnabled && !cfg.enabled) {
434
+ return { staged: false, path: null, reason: "DISABLED" };
435
+ }
436
+ validateBridgeOp(op);
437
+ const file = path.join(stagingDir(dir), stagedFileName(op));
438
+ if (fs.existsSync(file)) {
439
+ return { staged: false, path: file, reason: "ALREADY_STAGED" };
440
+ }
441
+ fs.writeFileSync(file, JSON.stringify(op, null, 2), "utf-8");
442
+ return { staged: true, path: file };
443
+ }
444
+
445
+ /**
446
+ * Read all staged ops grouped by chain-pair.
447
+ * @returns {{ [pair: string]: Array<{ file: string, op: object }> }}
448
+ */
449
+ export function listStagedOps(dir) {
450
+ ensureDirs(dir);
451
+ const sd = stagingDir(dir);
452
+ if (!fs.existsSync(sd)) return {};
453
+ const out = {};
454
+ for (const name of fs.readdirSync(sd).sort()) {
455
+ if (!name.endsWith(".json")) continue;
456
+ const file = path.join(sd, name);
457
+ let op;
458
+ try {
459
+ op = JSON.parse(fs.readFileSync(file, "utf-8"));
460
+ validateBridgeOp(op);
461
+ } catch (_err) {
462
+ continue;
463
+ }
464
+ const pair = [op.src_chain, op.dst_chain].sort().join("-");
465
+ out[pair] ||= [];
466
+ out[pair].push({ file, op });
467
+ }
468
+ return out;
469
+ }
470
+
471
+ function nextBatchSeq(dir, pair) {
472
+ const p = batchSeqPath(dir);
473
+ let map = {};
474
+ if (fs.existsSync(p)) {
475
+ try {
476
+ map = JSON.parse(fs.readFileSync(p, "utf-8"));
477
+ } catch (_err) {
478
+ map = {};
479
+ }
480
+ }
481
+ const next = (map[pair] || 0) + 1;
482
+ map[pair] = next;
483
+ fs.writeFileSync(p, JSON.stringify(map, null, 2), "utf-8");
484
+ return next;
485
+ }
486
+
487
+ /**
488
+ * Close all currently-staged ops into one batch per chain-pair.
489
+ * Writes batches/<pair>-<seq>/{landmark.json, envelope-NN.json,...} and
490
+ * removes the staged files atomically per pair (best-effort).
491
+ *
492
+ * @param {string} dir
493
+ * @param {{ alg?: string, signer?: object, issuer?: string,
494
+ * keys?: { secretKey: Buffer, publicKey: Buffer } }} [opts]
495
+ * @returns {{ batches: Array<{ pair, seq, namespace, treeHeadId, count, dir }>,
496
+ * skipped: { reason: string } | null }}
497
+ */
498
+ export function closeBatch(dir, opts = {}) {
499
+ ensureDirs(dir);
500
+ const cfg = loadCrossChainMtcConfig(dir);
501
+ const grouped = listStagedOps(dir);
502
+ const pairs = Object.keys(grouped);
503
+ if (pairs.length === 0) {
504
+ return { batches: [], skipped: { reason: "NO_STAGED_OPS" } };
505
+ }
506
+
507
+ const alg = opts.alg || cfg.alg;
508
+ const signer = opts.signer || (alg === "slh-dsa-128f" ? slhDsa : ed25519);
509
+ const keys = opts.keys || signer.generateKeyPair();
510
+ const issuer = opts.issuer || cfg.issuer;
511
+
512
+ const batches = [];
513
+ for (const pair of pairs) {
514
+ const entries = grouped[pair];
515
+ const ops = entries.map((e) => e.op);
516
+ const [src, dst] = pair.split("-");
517
+ const seq = nextBatchSeq(dir, pair);
518
+ const result = assembleBridgeBatch(ops, keys, {
519
+ src_chain: src,
520
+ dst_chain: dst,
521
+ batch_seq: seq,
522
+ issuer,
523
+ signer,
524
+ });
525
+
526
+ const outDir = path.join(
527
+ batchesDir(dir),
528
+ `${pair}-${String(seq).padStart(6, "0")}`,
529
+ );
530
+ fs.mkdirSync(outDir, { recursive: true });
531
+ fs.writeFileSync(
532
+ path.join(outDir, "landmark.json"),
533
+ JSON.stringify(result.landmark, null, 2),
534
+ "utf-8",
535
+ );
536
+ result.envelopes.forEach((env, i) => {
537
+ fs.writeFileSync(
538
+ path.join(outDir, `envelope-${String(i).padStart(4, "0")}.json`),
539
+ JSON.stringify(env, null, 2),
540
+ "utf-8",
541
+ );
542
+ });
543
+
544
+ // Remove staging files only after on-disk artifacts persisted.
545
+ for (const e of entries) {
546
+ try {
547
+ fs.unlinkSync(e.file);
548
+ } catch (_err) {
549
+ /* best-effort cleanup */
550
+ }
551
+ }
552
+
553
+ batches.push({
554
+ pair,
555
+ seq,
556
+ namespace: result.namespace,
557
+ treeHeadId: result.treeHeadId,
558
+ count: ops.length,
559
+ dir: outDir,
560
+ });
561
+ }
562
+ return { batches, skipped: null };
563
+ }
564
+
565
+ // ─────────────────────────────────────────────────────────────────────
566
+ // v0.2 — Multi-hop envelope (envelope-of-envelope)
567
+ // ─────────────────────────────────────────────────────────────────────
568
+
569
+ const SCHEMA_MULTI_HOP_BRIDGE = "mtc-bridge-multihop/v1";
570
+
571
+ /**
572
+ * Build a multi-hop bridge envelope by chaining single-hop envelopes.
573
+ * For a route A → B → C, the caller supplies the leg envelopes
574
+ * [A→B, B→C] and gets back a wrapper envelope referencing both legs.
575
+ *
576
+ * Verification (verifyMultiHopBridgeEnvelope below) checks each leg's
577
+ * inclusion proof against its respective landmark + asserts that the
578
+ * intermediate chain is consistent (leg[i].dst_chain == leg[i+1].src_chain).
579
+ *
580
+ * @param {Array<object>} legEnvelopes - ≥ 2 single-hop bridge envelopes
581
+ * @param {{ route_id?: string, total_amount?: string, asset?: string }} [meta]
582
+ * @returns {object} multi-hop wrapper envelope
583
+ */
584
+ export function buildMultiHopBridgeEnvelope(legEnvelopes, meta = {}) {
585
+ if (!Array.isArray(legEnvelopes) || legEnvelopes.length < 2) {
586
+ throw new RangeError(
587
+ "buildMultiHopBridgeEnvelope: need at least 2 leg envelopes",
588
+ );
589
+ }
590
+ for (let i = 0; i < legEnvelopes.length; i++) {
591
+ const env = legEnvelopes[i];
592
+ if (!env || env.schema !== SCHEMA_ENVELOPE) {
593
+ throw new TypeError(`leg ${i}: not a valid mtc-envelope/v1 envelope`);
594
+ }
595
+ if (!isBridgeNamespace(env.namespace)) {
596
+ throw new TypeError(`leg ${i}: not a bridge namespace`);
597
+ }
598
+ validateBridgeOp(env.leaf);
599
+ if (i > 0) {
600
+ const prev = legEnvelopes[i - 1];
601
+ if (prev.leaf.dst_chain !== env.leaf.src_chain) {
602
+ throw new RangeError(
603
+ `route discontinuity: leg ${i - 1} dst_chain=${prev.leaf.dst_chain} but leg ${i} src_chain=${env.leaf.src_chain}`,
604
+ );
605
+ }
606
+ }
607
+ }
608
+ const route = legEnvelopes.map((env) => ({
609
+ src_chain: env.leaf.src_chain,
610
+ dst_chain: env.leaf.dst_chain,
611
+ leaf_hash: env.leaf_hash || null,
612
+ namespace: env.namespace,
613
+ tree_head_id: env.tree_head_id,
614
+ }));
615
+ const chainPath = [
616
+ legEnvelopes[0].leaf.src_chain,
617
+ ...legEnvelopes.map((e) => e.leaf.dst_chain),
618
+ ];
619
+ return {
620
+ schema: SCHEMA_MULTI_HOP_BRIDGE,
621
+ route_id:
622
+ meta.route_id ||
623
+ `mh-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`,
624
+ chain_path: chainPath,
625
+ leg_count: legEnvelopes.length,
626
+ legs: legEnvelopes,
627
+ route_summary: route,
628
+ total_amount: meta.total_amount || null,
629
+ asset: meta.asset || null,
630
+ issued_at: new Date().toISOString(),
631
+ };
632
+ }
633
+
634
+ /**
635
+ * Verify a multi-hop wrapper envelope. Each leg must verify against its
636
+ * own landmark + the chain path must be continuous + at least one leg
637
+ * must be the "lock" op (the trigger).
638
+ *
639
+ * @param {object} wrapper - multi-hop envelope from buildMultiHopBridgeEnvelope
640
+ * @param {Array<{landmark: object}>} legCacheEntries - one per leg, in order
641
+ * @returns {{ ok: boolean, code?: string, leg_results?: Array<{ok, code?}> }}
642
+ */
643
+ export function verifyMultiHopBridgeEnvelope(wrapper, legCacheEntries) {
644
+ if (!wrapper || wrapper.schema !== SCHEMA_MULTI_HOP_BRIDGE) {
645
+ return { ok: false, code: "BAD_MULTIHOP_SCHEMA" };
646
+ }
647
+ if (!Array.isArray(wrapper.legs) || wrapper.legs.length < 2) {
648
+ return { ok: false, code: "BAD_LEG_COUNT" };
649
+ }
650
+ if (
651
+ !Array.isArray(legCacheEntries) ||
652
+ legCacheEntries.length !== wrapper.legs.length
653
+ ) {
654
+ return { ok: false, code: "LEG_CACHE_COUNT_MISMATCH" };
655
+ }
656
+ const lib = mtcLib;
657
+ const results = [];
658
+ for (let i = 0; i < wrapper.legs.length; i++) {
659
+ const leg = wrapper.legs[i];
660
+ const cache = new lib.LandmarkCache({
661
+ signatureVerifier: lib.alwaysAcceptSignatureVerifier,
662
+ });
663
+ try {
664
+ cache.ingest(legCacheEntries[i].landmark);
665
+ } catch (err) {
666
+ results.push({ ok: false, code: err.code || "LANDMARK_REJECT" });
667
+ continue;
668
+ }
669
+ const r = verifyBridgeEnvelope(leg, cache);
670
+ results.push(r);
671
+ if (i > 0) {
672
+ const prev = wrapper.legs[i - 1];
673
+ if (prev.leaf.dst_chain !== leg.leaf.src_chain) {
674
+ return {
675
+ ok: false,
676
+ code: "ROUTE_DISCONTINUITY",
677
+ leg_results: results,
678
+ };
679
+ }
680
+ }
681
+ }
682
+ const allOk = results.every((r) => r.ok);
683
+ return { ok: allOk, leg_results: results };
684
+ }
685
+
686
+ // ─────────────────────────────────────────────────────────────────────
687
+ // v0.2 — Gas-aware batch trigger
688
+ // ─────────────────────────────────────────────────────────────────────
689
+
690
+ const DEFAULT_GAS_PROFILE = Object.freeze({
691
+ // gas-cost units per chain, normalized to USD per tx (heuristic)
692
+ // Aligns with the existing fee estimator in cross-chain.js
693
+ ethereum: 5.0,
694
+ polygon: 0.01,
695
+ bsc: 0.1,
696
+ arbitrum: 0.3,
697
+ solana: 0.005,
698
+ });
699
+
700
+ /**
701
+ * Decide whether to close the batch now or defer to the next interval
702
+ * based on a target chain's current gas price (USD-equivalent).
703
+ *
704
+ * Heuristic: defer when current gas > 1.5x the chain's baseline AND
705
+ * the staged-ops count is below the "hard close" floor. Otherwise close.
706
+ *
707
+ * @param {{
708
+ * target_chain: string,
709
+ * current_gas_usd?: number, // observed; if absent uses baseline
710
+ * staged_count: number,
711
+ * hard_close_floor?: number, // always close at or above this count
712
+ * defer_multiplier?: number, // gas-multiplier triggering defer
713
+ * }} args
714
+ * @returns {{ close: boolean, reason: string, baseline_usd: number, current_usd: number, staged_count: number }}
715
+ */
716
+ export function shouldCloseBatchGasAware(args = {}) {
717
+ if (!args.target_chain || typeof args.target_chain !== "string") {
718
+ throw new TypeError("shouldCloseBatchGasAware: target_chain required");
719
+ }
720
+ const baseline = DEFAULT_GAS_PROFILE[args.target_chain];
721
+ if (baseline === undefined) {
722
+ throw new RangeError(
723
+ `shouldCloseBatchGasAware: unknown target_chain "${args.target_chain}"`,
724
+ );
725
+ }
726
+ const current =
727
+ typeof args.current_gas_usd === "number" ? args.current_gas_usd : baseline;
728
+ const staged = Number.isInteger(args.staged_count) ? args.staged_count : 0;
729
+ const hardFloor = Number.isInteger(args.hard_close_floor)
730
+ ? args.hard_close_floor
731
+ : 50;
732
+ const deferMult =
733
+ typeof args.defer_multiplier === "number" ? args.defer_multiplier : 1.5;
734
+
735
+ if (staged >= hardFloor) {
736
+ return {
737
+ close: true,
738
+ reason: "STAGED_COUNT_AT_HARD_FLOOR",
739
+ baseline_usd: baseline,
740
+ current_usd: current,
741
+ staged_count: staged,
742
+ };
743
+ }
744
+ if (current > baseline * deferMult) {
745
+ return {
746
+ close: false,
747
+ reason: "GAS_HIGH_DEFERRED",
748
+ baseline_usd: baseline,
749
+ current_usd: current,
750
+ staged_count: staged,
751
+ };
752
+ }
753
+ return {
754
+ close: true,
755
+ reason: "GAS_NORMAL_OR_LOW",
756
+ baseline_usd: baseline,
757
+ current_usd: current,
758
+ staged_count: staged,
759
+ };
760
+ }
761
+
762
+ // ─────────────────────────────────────────────────────────────────────
763
+ // v0.2 — SLA integration (cc sla compatible export)
764
+ // ─────────────────────────────────────────────────────────────────────
765
+
766
+ /**
767
+ * Export bridge MTC operational metrics in cc sla compatible shape.
768
+ * Reads on-disk staging + batches + sync-stats and synthesizes:
769
+ * - latest_publish_age_seconds (newer than threshold = healthy)
770
+ * - staged_pending_count (low = healthy)
771
+ * - batches_per_hour (heuristic from batch dir mtimes)
772
+ * - sla_status: ok | degraded | down
773
+ *
774
+ * @param {string} dir - cross-chain-mtc dir
775
+ * @param {{ now?: number }} [opts]
776
+ */
777
+ export function getBridgeMtcSlaMetrics(dir, opts = {}) {
778
+ ensureDirs(dir);
779
+ const now = opts.now || Date.now();
780
+ const status = getBridgeMtcStatus(dir);
781
+
782
+ // Compute batches_per_hour from batches/ mtimes (last hour only)
783
+ let batchesLastHour = 0;
784
+ let latestBatchMtime = null;
785
+ const bDir = batchesDir(dir);
786
+ if (fs.existsSync(bDir)) {
787
+ for (const name of fs.readdirSync(bDir)) {
788
+ try {
789
+ const stat = fs.statSync(path.join(bDir, name));
790
+ if (now - stat.mtimeMs <= 3600 * 1000) batchesLastHour++;
791
+ if (!latestBatchMtime || stat.mtimeMs > latestBatchMtime) {
792
+ latestBatchMtime = stat.mtimeMs;
793
+ }
794
+ } catch (_err) {
795
+ /* skip stat failures */
796
+ }
797
+ }
798
+ }
799
+
800
+ const stagedAge = latestBatchMtime
801
+ ? Math.round((now - latestBatchMtime) / 1000)
802
+ : null;
803
+
804
+ // Status:
805
+ // - down: enabled but no batches in 24h AND staging > 0
806
+ // - degraded: staging > 100 OR no batch in 30 min while enabled
807
+ // - ok: otherwise
808
+ let slaStatus = "ok";
809
+ if (status.enabled) {
810
+ if (status.staging.pending > 100) slaStatus = "degraded";
811
+ else if (
812
+ stagedAge !== null &&
813
+ stagedAge > 30 * 60 &&
814
+ status.staging.pending > 0
815
+ ) {
816
+ slaStatus = "degraded";
817
+ }
818
+ if (
819
+ stagedAge !== null &&
820
+ stagedAge > 24 * 3600 &&
821
+ status.staging.pending > 0
822
+ ) {
823
+ slaStatus = "down";
824
+ }
825
+ }
826
+
827
+ return {
828
+ sla_status: slaStatus,
829
+ enabled: status.enabled,
830
+ mode: status.mode,
831
+ staged_pending_count: status.staging.pending,
832
+ batches_total: status.batches.total,
833
+ batches_last_hour: batchesLastHour,
834
+ seconds_since_last_batch: stagedAge,
835
+ measured_at: new Date(now).toISOString(),
836
+ };
837
+ }
838
+
839
+ // ─────────────────────────────────────────────────────────────────────
840
+ // Status snapshot (for `cc crosschain mtc-status`)
841
+ // ─────────────────────────────────────────────────────────────────────
842
+
843
+ /**
844
+ * Read filesystem state and return a status summary suitable for CLI / IPC.
845
+ * No DB access — pure fs/json reads.
846
+ */
847
+ export function getBridgeMtcStatus(dir) {
848
+ ensureDirs(dir);
849
+ const cfg = loadCrossChainMtcConfig(dir);
850
+ const taStore = loadTrustAnchors(dir);
851
+ const taChains = Object.keys(taStore.anchors);
852
+ const taTotal = taChains.reduce(
853
+ (sum, c) => sum + (taStore.anchors[c]?.length || 0),
854
+ 0,
855
+ );
856
+
857
+ let batchCount = 0;
858
+ let latestBatch = null;
859
+ const bDir = batchesDir(dir);
860
+ if (fs.existsSync(bDir)) {
861
+ const entries = fs.readdirSync(bDir).sort();
862
+ batchCount = entries.length;
863
+ if (batchCount > 0) latestBatch = entries[batchCount - 1];
864
+ }
865
+
866
+ let stagedCount = 0;
867
+ const sDir = stagingDir(dir);
868
+ if (fs.existsSync(sDir)) {
869
+ stagedCount = fs
870
+ .readdirSync(sDir)
871
+ .filter((n) => n.endsWith(".json")).length;
872
+ }
873
+
874
+ return {
875
+ enabled: cfg.enabled,
876
+ mode: cfg.mode,
877
+ alg: cfg.alg,
878
+ batch_interval_seconds: cfg.batch_interval_seconds,
879
+ issuer: cfg.issuer,
880
+ trust_anchors: {
881
+ chain_count: taChains.length,
882
+ total: taTotal,
883
+ by_chain: Object.fromEntries(
884
+ taChains.map((c) => [c, taStore.anchors[c].length]),
885
+ ),
886
+ },
887
+ staging: {
888
+ pending: stagedCount,
889
+ },
890
+ batches: {
891
+ total: batchCount,
892
+ latest: latestBatch,
893
+ },
894
+ };
895
+ }
896
+
897
+ // Internal exports for tests
898
+ export const _internal = {
899
+ CONFIG_DEFAULTS,
900
+ SUPPORTED_BRIDGE_CHAINS,
901
+ VALID_BRIDGE_OPS,
902
+ BRIDGE_NAMESPACE_PREFIX,
903
+ BRIDGE_NAMESPACE_RE,
904
+ };