@oracle-agent/oracle 0.1.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.
Files changed (169) hide show
  1. package/CONTRIBUTING.md +98 -0
  2. package/LICENSE +202 -0
  3. package/README.md +384 -0
  4. package/SECURITY.md +89 -0
  5. package/SETUP.md +235 -0
  6. package/artifacts/inscription/oracle-was-here-preview.png +0 -0
  7. package/artifacts/inscription/oracle-was-here.svg +1 -0
  8. package/artifacts/specialist-packs/oracle-full-crypto.json +273 -0
  9. package/bin/desk-server.mjs +438 -0
  10. package/bin/oracle-data-mcp.mjs +603 -0
  11. package/bin/oracle-init.mjs +423 -0
  12. package/bin/oracle-public-server.mjs +36 -0
  13. package/bin/oracle-route.mjs +254 -0
  14. package/bin/oracle-scan.mjs +192 -0
  15. package/docs/adding-a-chain.md +229 -0
  16. package/docs/architecture.md +135 -0
  17. package/docs/profiles.md +132 -0
  18. package/examples/add-a-chain.mjs +65 -0
  19. package/examples/research-a-token.mjs +70 -0
  20. package/package.json +90 -0
  21. package/profiles/_template/SOUL.md +47 -0
  22. package/profiles/_template/profile.json +22 -0
  23. package/profiles/bitcoin-agent/SOUL.md +31 -0
  24. package/profiles/bitcoin-agent/profile.json +32 -0
  25. package/profiles/hyperliquid-agent/SOUL.md +34 -0
  26. package/profiles/hyperliquid-agent/profile.json +37 -0
  27. package/profiles/oracle/SOUL.md +65 -0
  28. package/profiles/oracle/profile.json +35 -0
  29. package/profiles/polymarket-agent/SOUL.md +35 -0
  30. package/profiles/polymarket-agent/profile.json +34 -0
  31. package/profiles/profile.schema.json +90 -0
  32. package/profiles/protocol-builder/SOUL.md +50 -0
  33. package/profiles/protocol-builder/profile.json +37 -0
  34. package/profiles/robinhood-agent/SOUL.md +45 -0
  35. package/profiles/robinhood-agent/profile.json +39 -0
  36. package/profiles/solana-agent/SOUL.md +37 -0
  37. package/profiles/solana-agent/profile.json +37 -0
  38. package/profiles/stable-agent/SOUL.md +43 -0
  39. package/profiles/stable-agent/profile.json +37 -0
  40. package/public/oracle-console/app.js +272 -0
  41. package/public/oracle-console/bitcoin-wallets.js +206 -0
  42. package/public/oracle-console/index.html +91 -0
  43. package/public/oracle-console/styles.css +239 -0
  44. package/public/oracle-splash/index.html +931 -0
  45. package/scripts/build-inscription.py +230 -0
  46. package/scripts/check-test-count.mjs +105 -0
  47. package/scripts/e2e-hl-markets.mjs +21 -0
  48. package/scripts/e2e-hl-perps.mjs +48 -0
  49. package/scripts/e2e-hypercore-staking.mjs +128 -0
  50. package/scripts/e2e-solana-bitcoin.mjs +183 -0
  51. package/scripts/public-api-scan.mjs +23 -0
  52. package/scripts/secret-scan.mjs +181 -0
  53. package/scripts/verify-v3-venues.mjs +192 -0
  54. package/skills/oracle-best-execution/SKILL.md +127 -0
  55. package/skills/oracle-bitcoin/SKILL.md +53 -0
  56. package/skills/oracle-chain-graphs-telegram-cards/SKILL.md +59 -0
  57. package/skills/oracle-circuit-breaker/SKILL.md +51 -0
  58. package/skills/oracle-contract-research/SKILL.md +55 -0
  59. package/skills/oracle-desk/SKILL.md +58 -0
  60. package/skills/oracle-dex-launch/SKILL.md +38 -0
  61. package/skills/oracle-grants/SKILL.md +69 -0
  62. package/skills/oracle-hypercore-staking/SKILL.md +57 -0
  63. package/skills/oracle-hyperliquid/SKILL.md +56 -0
  64. package/skills/oracle-meme-token-sniper/SKILL.md +73 -0
  65. package/skills/oracle-nft-gacha-launch/SKILL.md +48 -0
  66. package/skills/oracle-nft-mint-gas-war/SKILL.md +63 -0
  67. package/skills/oracle-polymarket/SKILL.md +60 -0
  68. package/skills/oracle-protocol-builder/SKILL.md +38 -0
  69. package/skills/oracle-protocol-security/SKILL.md +60 -0
  70. package/skills/oracle-public-product/SKILL.md +44 -0
  71. package/skills/oracle-receipts/SKILL.md +52 -0
  72. package/skills/oracle-rfq-tokenized-assets/SKILL.md +69 -0
  73. package/skills/oracle-smart-wallet-scanner/SKILL.md +49 -0
  74. package/skills/oracle-solana/SKILL.md +65 -0
  75. package/skills/oracle-solana-nft/SKILL.md +54 -0
  76. package/skills/oracle-token-research/SKILL.md +67 -0
  77. package/src/agent-auth.mjs +191 -0
  78. package/src/approval-guard.mjs +282 -0
  79. package/src/attestation-secret.mjs +88 -0
  80. package/src/audit-log.mjs +196 -0
  81. package/src/auto-slippage.mjs +378 -0
  82. package/src/capability-posture.mjs +125 -0
  83. package/src/chains.mjs +62 -0
  84. package/src/data/catalog.mjs +495 -0
  85. package/src/data/desk-data.mjs +623 -0
  86. package/src/data/http.mjs +200 -0
  87. package/src/data/provider-endpoint.mjs +94 -0
  88. package/src/data/providers/aerodrome.mjs +244 -0
  89. package/src/data/providers/balancer.mjs +208 -0
  90. package/src/data/providers/bitcoin-esplora.mjs +230 -0
  91. package/src/data/providers/bitcoin-meta.mjs +378 -0
  92. package/src/data/providers/blockscout.mjs +14 -0
  93. package/src/data/providers/bridges.mjs +241 -0
  94. package/src/data/providers/cowswap.mjs +501 -0
  95. package/src/data/providers/curve.mjs +200 -0
  96. package/src/data/providers/defillama.mjs +88 -0
  97. package/src/data/providers/dexscreener.mjs +43 -0
  98. package/src/data/providers/evm-rpc.mjs +203 -0
  99. package/src/data/providers/geckoterminal.mjs +34 -0
  100. package/src/data/providers/gmx.mjs +495 -0
  101. package/src/data/providers/hl-info.mjs +102 -0
  102. package/src/data/providers/hl-markets.mjs +210 -0
  103. package/src/data/providers/hl-perps.mjs +382 -0
  104. package/src/data/providers/hl-staking.mjs +352 -0
  105. package/src/data/providers/hl-ws.mjs +119 -0
  106. package/src/data/providers/hyperevm-dex.mjs +49 -0
  107. package/src/data/providers/jupiter.mjs +182 -0
  108. package/src/data/providers/lifi.mjs +150 -0
  109. package/src/data/providers/magiceden-sol.mjs +355 -0
  110. package/src/data/providers/morpho.mjs +173 -0
  111. package/src/data/providers/odos.mjs +155 -0
  112. package/src/data/providers/oneinch.mjs +173 -0
  113. package/src/data/providers/opensea-multichain.mjs +136 -0
  114. package/src/data/providers/opensea-nft.mjs +99 -0
  115. package/src/data/providers/paraswap.mjs +117 -0
  116. package/src/data/providers/pendle.mjs +187 -0
  117. package/src/data/providers/poly-public.mjs +96 -0
  118. package/src/data/providers/poly-ws.mjs +103 -0
  119. package/src/data/providers/rh-agent.mjs +59 -0
  120. package/src/data/providers/satflow.mjs +336 -0
  121. package/src/data/providers/solana-rpc.mjs +186 -0
  122. package/src/data/providers/uniswap-v3.mjs +303 -0
  123. package/src/data/providers/zerox.mjs +166 -0
  124. package/src/data/public-api-scan.mjs +61 -0
  125. package/src/data/quote-placeholder.mjs +31 -0
  126. package/src/exact-integer.mjs +72 -0
  127. package/src/exec-policy.mjs +444 -0
  128. package/src/flags.mjs +15 -0
  129. package/src/fresh-window.mjs +76 -0
  130. package/src/gmx-attestation.mjs +175 -0
  131. package/src/index.mjs +50 -0
  132. package/src/nft-gas-war-guard.mjs +139 -0
  133. package/src/onboarding/agent-keys.mjs +157 -0
  134. package/src/onboarding/index.mjs +18 -0
  135. package/src/onboarding/tiers.mjs +139 -0
  136. package/src/oracle-env.mjs +38 -0
  137. package/src/protocol-execution.mjs +84 -0
  138. package/src/public-api/buzz-integration.mjs +256 -0
  139. package/src/public-api/connect-agent.mjs +397 -0
  140. package/src/public-api/grants.mjs +142 -0
  141. package/src/public-api/http.mjs +374 -0
  142. package/src/public-control/aa-adapter.mjs +402 -0
  143. package/src/public-control/build-registry.mjs +227 -0
  144. package/src/public-control/bundler-client.mjs +372 -0
  145. package/src/public-control/grant-indexer.mjs +296 -0
  146. package/src/public-control/policy-render.mjs +69 -0
  147. package/src/public-control/policy-schema.mjs +318 -0
  148. package/src/public-control/runtime-config.mjs +265 -0
  149. package/src/public-control/session-key-model.mjs +374 -0
  150. package/src/public-control/session-orchestrator.mjs +412 -0
  151. package/src/route-attestation.mjs +132 -0
  152. package/src/router/best-execution.mjs +221 -0
  153. package/src/router/index.mjs +185 -0
  154. package/src/router/prepare-bridge.mjs +288 -0
  155. package/src/router/prepare-route.mjs +341 -0
  156. package/src/router/proposal.mjs +311 -0
  157. package/src/router/risk-classifier.mjs +119 -0
  158. package/src/router/route-sources.mjs +292 -0
  159. package/src/scanner/chains.config.mjs +381 -0
  160. package/src/scanner/contract.mjs +270 -0
  161. package/src/scanner/evm-scanner.mjs +394 -0
  162. package/src/scanner/index.mjs +9 -0
  163. package/src/scanner/v2-venue.mjs +335 -0
  164. package/src/scanner/v3-venue.mjs +290 -0
  165. package/src/scopes.mjs +44 -0
  166. package/src/sell-simulation.mjs +167 -0
  167. package/src/token-transfer-guard.mjs +188 -0
  168. package/src/vault-attestation.mjs +145 -0
  169. package/src/venues.mjs +206 -0
@@ -0,0 +1,230 @@
1
+ #!/usr/bin/env python3
2
+ """Build the Oracle inscription SVG.
3
+
4
+ Text is converted to OUTLINED VECTOR PATHS so the inscription is fully
5
+ self-contained: no font file, no external fetch, no <text> element that renders
6
+ differently in every ordinals explorer. Design language is the Oracle splash:
7
+ deep #0B1018 field, #7CC4FF blue, Cormorant Garamond display italic accent,
8
+ IBM Plex Mono eyebrow, a single luminous orb.
9
+ """
10
+
11
+ import math
12
+ import os
13
+
14
+ from fontTools.ttLib import TTFont
15
+ from fontTools.pens.svgPathPen import SVGPathPen
16
+ from fontTools.varLib.instancer import instantiateVariableFont
17
+
18
+ import os
19
+ from pathlib import Path
20
+
21
+ REPO_ROOT = Path(__file__).resolve().parent.parent
22
+
23
+ W, H = 1000, 1000
24
+
25
+ BG = "#0B1018"
26
+ BLUE = "#7CC4FF"
27
+ BLUE2 = "#A7D8FF"
28
+ COOL = "#9BB6D1"
29
+ MUTE = "#5B738F"
30
+
31
+ # vertical rhythm
32
+ CX = W / 2.0
33
+ CY = 336.0
34
+ R_OUT = 166.0
35
+ R_MID = 128.0
36
+ R_IN = 92.0
37
+ MARK_BASELINE = 690.0
38
+ SUB_BASELINE = 786.0
39
+
40
+
41
+ def load(path, wght):
42
+ f = TTFont(path)
43
+ if "fvar" in f:
44
+ f = instantiateVariableFont(f, {"wght": wght}, inplace=False, updateFontNames=False)
45
+ return f
46
+
47
+
48
+ def text_paths(font, text, size, letter_spacing=0.0):
49
+ """Return (path_d, advance_width) with the text laid out at `size` px."""
50
+ upem = font["head"].unitsPerEm
51
+ scale = size / upem
52
+ cmap = font.getBestCmap()
53
+ gs = font.getGlyphSet()
54
+ hmtx = font["hmtx"]
55
+
56
+ try:
57
+ kern = font["kern"].kernTables[0].kernTable
58
+ except Exception:
59
+ kern = None
60
+
61
+ parts = []
62
+ x = 0.0
63
+ prev = None
64
+
65
+ for ch in text:
66
+ gname = cmap.get(ord(ch))
67
+ if gname is None:
68
+ x += size * 0.3
69
+ prev = None
70
+ continue
71
+ if prev and kern:
72
+ x += kern.get((prev, gname), 0) * scale
73
+ pen = SVGPathPen(gs)
74
+ gs[gname].draw(pen)
75
+ d = pen.getCommands()
76
+ if d:
77
+ # flip Y (font space is y-up, SVG is y-down) and place at x
78
+ parts.append(
79
+ f'<g transform="translate({x:.3f},0) scale({scale:.6f},{-scale:.6f})">'
80
+ f'<path d="{d}"/></g>'
81
+ )
82
+ x += hmtx[gname][0] * scale + letter_spacing
83
+ prev = gname
84
+ # trailing letter-spacing is not part of the inked width
85
+ if letter_spacing and text:
86
+ x -= letter_spacing
87
+ return "".join(parts), x
88
+
89
+
90
+ def centered(font, text, size, y, fill, letter_spacing=0.0, opacity=None):
91
+ d, width = text_paths(font, text, size, letter_spacing)
92
+ if not d:
93
+ return ""
94
+ x = (W - width) / 2.0
95
+ op = f' opacity="{opacity}"' if opacity is not None else ""
96
+ return f'<g transform="translate({x:.3f},{y:.3f})" fill="{fill}"{op}>{d}</g>'
97
+
98
+
99
+ def arc(cx, cy, r, a0, a1):
100
+ """SVG arc path between two angles in degrees."""
101
+ x0 = cx + math.cos(math.radians(a0)) * r
102
+ y0 = cy + math.sin(math.radians(a0)) * r
103
+ x1 = cx + math.cos(math.radians(a1)) * r
104
+ y1 = cy + math.sin(math.radians(a1)) * r
105
+ large = 1 if abs(a1 - a0) > 180 else 0
106
+ sweep = 1 if a1 > a0 else 0
107
+ return f"M{x0:.2f} {y0:.2f} A{r} {r} 0 {large} {sweep} {x1:.2f} {y1:.2f}"
108
+
109
+
110
+ def main():
111
+ display_italic = load("/tmp/CormorantGaramond-Italic.ttf", 400)
112
+ display = load("/tmp/CormorantGaramond.ttf", 300)
113
+ mono = TTFont(os.environ.get("IBM_PLEX_MONO", str(Path.home() / ".local/share/fonts/protocols/IBMPlexMono-Regular.ttf")))
114
+
115
+ o = []
116
+ o.append(f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {W} {H}" width="{W}" height="{H}">')
117
+
118
+ o.append(
119
+ "<defs>"
120
+ '<radialGradient id="halo" cx="50%" cy="34%" r="54%">'
121
+ f'<stop offset="0%" stop-color="{BLUE}" stop-opacity=".20"/>'
122
+ f'<stop offset="52%" stop-color="{BLUE}" stop-opacity=".05"/>'
123
+ f'<stop offset="100%" stop-color="{BLUE}" stop-opacity="0"/>'
124
+ "</radialGradient>"
125
+ '<radialGradient id="orb" cx="50%" cy="50%" r="50%">'
126
+ '<stop offset="0%" stop-color="#F2FAFF"/>'
127
+ f'<stop offset="38%" stop-color="{BLUE2}"/>'
128
+ f'<stop offset="100%" stop-color="{BLUE}" stop-opacity=".14"/>'
129
+ "</radialGradient>"
130
+ '<linearGradient id="rule" x1="0" y1="0" x2="1" y2="0">'
131
+ f'<stop offset="0%" stop-color="{BLUE}" stop-opacity="0"/>'
132
+ f'<stop offset="50%" stop-color="{BLUE}" stop-opacity=".6"/>'
133
+ f'<stop offset="100%" stop-color="{BLUE}" stop-opacity="0"/>'
134
+ "</linearGradient>"
135
+ '<linearGradient id="sweep" x1="0" y1="0" x2="1" y2="1">'
136
+ f'<stop offset="0%" stop-color="{BLUE}" stop-opacity="0"/>'
137
+ '<stop offset="100%" stop-color="#EAF6FF" stop-opacity=".95"/>'
138
+ "</linearGradient>"
139
+ "</defs>"
140
+ )
141
+
142
+ o.append(f'<rect width="{W}" height="{H}" fill="{BG}"/>')
143
+ o.append(f'<rect width="{W}" height="{H}" fill="url(#halo)"/>')
144
+
145
+ # --- orbit rings: readable at thumbnail size --------------------------
146
+ o.append(
147
+ f'<circle cx="{CX}" cy="{CY}" r="{R_OUT}" fill="none" '
148
+ f'stroke="{BLUE}" stroke-opacity=".24" stroke-width="1.1"/>'
149
+ )
150
+ o.append(
151
+ f'<circle cx="{CX}" cy="{CY}" r="{R_MID}" fill="none" '
152
+ f'stroke="{BLUE}" stroke-opacity=".16" stroke-width="1"/>'
153
+ )
154
+ o.append(
155
+ f'<circle cx="{CX}" cy="{CY}" r="{R_IN}" fill="none" '
156
+ f'stroke="{BLUE}" stroke-opacity=".34" stroke-width="1" stroke-dasharray="1.5 7"/>'
157
+ )
158
+
159
+ # the agentic sweep: one bright arc, the thing that is moving
160
+ o.append(
161
+ f'<path d="{arc(CX, CY, R_OUT, -96, 4)}" fill="none" '
162
+ 'stroke="url(#sweep)" stroke-width="2.2" stroke-linecap="round"/>'
163
+ )
164
+
165
+ # 8 lane ticks on the outer ring
166
+ for i in range(8):
167
+ a = math.radians(-90 + i * 45)
168
+ x1 = CX + math.cos(a) * R_OUT
169
+ y1 = CY + math.sin(a) * R_OUT
170
+ x2 = CX + math.cos(a) * (R_OUT - 13)
171
+ y2 = CY + math.sin(a) * (R_OUT - 13)
172
+ o.append(
173
+ f'<line x1="{x1:.2f}" y1="{y1:.2f}" x2="{x2:.2f}" y2="{y2:.2f}" '
174
+ f'stroke="{BLUE}" stroke-opacity=".5" stroke-width="1.5"/>'
175
+ )
176
+
177
+ # node dots on the mid ring
178
+ for i in range(4):
179
+ a = math.radians(-45 + i * 90)
180
+ x = CX + math.cos(a) * R_MID
181
+ y = CY + math.sin(a) * R_MID
182
+ o.append(f'<circle cx="{x:.2f}" cy="{y:.2f}" r="2.6" fill="{BLUE}" opacity=".72"/>')
183
+
184
+ # --- the orb ----------------------------------------------------------
185
+ o.append(f'<circle cx="{CX}" cy="{CY}" r="52" fill="url(#orb)" opacity=".20"/>')
186
+ o.append(f'<circle cx="{CX}" cy="{CY}" r="16" fill="url(#orb)"/>')
187
+ o.append(
188
+ f'<circle cx="{CX}" cy="{CY}" r="16" fill="none" '
189
+ 'stroke="#F2FAFF" stroke-opacity=".55" stroke-width=".9"/>'
190
+ )
191
+
192
+ # --- eyebrow ----------------------------------------------------------
193
+ o.append(centered(mono, "ORACLE", 16, 122, BLUE, letter_spacing=7.4))
194
+ o.append(f'<rect x="{CX - 84}" y="143" width="168" height="1" fill="url(#rule)"/>')
195
+
196
+ # --- the mark ---------------------------------------------------------
197
+ o.append(centered(display_italic, "oracle", 178, MARK_BASELINE, BLUE2))
198
+ o.append(centered(display, "was here", 92, SUB_BASELINE, COOL, opacity=".9"))
199
+
200
+ # --- footer -----------------------------------------------------------
201
+ o.append(f'<rect x="{CX - 132}" y="836" width="264" height="1" fill="url(#rule)"/>')
202
+ o.append(centered(mono, "THE FUTURE IS AGENTIC", 14.5, 880, BLUE, letter_spacing=4.0))
203
+ o.append(centered(mono, "BITCOIN L1", 12, 916, MUTE, letter_spacing=3.4))
204
+
205
+ # --- corner registration marks ---------------------------------------
206
+ m, ln = 52, 24
207
+ for (x, y, dx, dy) in ((m, m, 1, 1), (W - m, m, -1, 1), (m, H - m, 1, -1), (W - m, H - m, -1, -1)):
208
+ o.append(
209
+ f'<path d="M{x} {y + dy * ln} L{x} {y} L{x + dx * ln} {y}" fill="none" '
210
+ f'stroke="{BLUE}" stroke-opacity=".38" stroke-width="1.3"/>'
211
+ )
212
+
213
+ o.append("</svg>")
214
+
215
+ svg = "".join(o)
216
+ path = str(REPO_ROOT / "artifacts/inscription/oracle-was-here.svg")
217
+ os.makedirs(os.path.dirname(path), exist_ok=True)
218
+ with open(path, "w") as f:
219
+ f.write(svg)
220
+
221
+ # sanity: the mark must not collide with the ring group
222
+ mark_cap_top = MARK_BASELINE - 178 * 0.74
223
+ ring_bottom = CY + R_OUT
224
+ print(f"wrote {path}")
225
+ print(f"bytes {len(svg.encode())}")
226
+ print(f"ring_bottom={ring_bottom:.1f} mark_cap_top={mark_cap_top:.1f} clearance={mark_cap_top - ring_bottom:.1f}px")
227
+
228
+
229
+ if __name__ == "__main__":
230
+ main()
@@ -0,0 +1,105 @@
1
+ #!/usr/bin/env node
2
+ // Test-count drift gate.
3
+ //
4
+ // Two agents push to this branch. Each one runs the suite in its own tree and
5
+ // reports a number. Those numbers drift: round 7 landed a commit message
6
+ // claiming "707 tests, 0 failing" when the tree at that commit actually ran
7
+ // 729. Nobody lied — the claim was just written against a stale checkout, and
8
+ // git history keeps it forever.
9
+ //
10
+ // So the count is committed alongside the code. Add or remove a test and the
11
+ // build fails until the baseline is updated in the SAME commit, which puts the
12
+ // delta in the diff where review can see it. A stale claim becomes a red build
13
+ // instead of a permanent wrong number in the log.
14
+ //
15
+ // node scripts/check-test-count.mjs verify against the baseline
16
+ // node scripts/check-test-count.mjs --update rewrite it (intentional change)
17
+
18
+ import { spawnSync } from "node:child_process";
19
+ import { readFileSync, writeFileSync, existsSync } from "node:fs";
20
+ import { fileURLToPath } from "node:url";
21
+ import { dirname, join, resolve } from "node:path";
22
+
23
+ const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
24
+ const BASELINE = join(ROOT, "test-baseline.json");
25
+ const UPDATE = process.argv.includes("--update");
26
+
27
+ // Run the suite exactly the way `npm test` does, so the gate can never pass
28
+ // against a different set of files or env than the suite CI actually runs.
29
+ const pkg = JSON.parse(readFileSync(join(ROOT, "package.json"), "utf8"));
30
+ const testCmd = pkg.scripts?.test;
31
+ if (!testCmd) {
32
+ console.error("check-test-count: package.json has no test script");
33
+ process.exit(1);
34
+ }
35
+
36
+ const run = spawnSync("sh", ["-c", testCmd], {
37
+ cwd: ROOT,
38
+ encoding: "utf8",
39
+ maxBuffer: 64 * 1024 * 1024,
40
+ });
41
+
42
+ const output = `${run.stdout || ""}${run.stderr || ""}`;
43
+
44
+ // node --test emits two different summary formats. Interactive (TTY) gives the
45
+ // spec reporter's "ℹ tests 729". Non-interactive — which is every CI run —
46
+ // gives TAP: "# tests 729". Parsing only the first shape passed locally and
47
+ // failed in CI, which is the same stale-measurement class this gate exists to
48
+ // catch, so match both anchored to line start. A test NAME containing
49
+ // "tests 999" lives on an "ok N - ..." line and cannot reach either pattern.
50
+ function summaryValue(label) {
51
+ const m = output.match(new RegExp(`^\\s*(?:ℹ|i|#)\\s+${label}\\s+(\\d+)\\s*$`, "m"));
52
+ return m ? Number(m[1]) : null;
53
+ }
54
+
55
+ const actual = {
56
+ tests: summaryValue("tests"),
57
+ pass: summaryValue("pass"),
58
+ fail: summaryValue("fail"),
59
+ skipped: summaryValue("skipped"),
60
+ };
61
+
62
+ if (actual.tests == null || actual.pass == null || actual.fail == null) {
63
+ console.error("check-test-count: could not parse the node --test summary.");
64
+ console.error(output.slice(-2000));
65
+ process.exit(1);
66
+ }
67
+
68
+ // A red suite is the suite step's job to report, but never let a failing run
69
+ // silently rewrite the baseline.
70
+ if (actual.fail > 0) {
71
+ console.error(`check-test-count: suite is RED (${actual.fail} failing) — refusing to gate or update.`);
72
+ console.error(output.slice(-4000));
73
+ process.exit(1);
74
+ }
75
+
76
+ if (UPDATE) {
77
+ writeFileSync(BASELINE, `${JSON.stringify(actual, null, 2)}\n`);
78
+ console.log(`check-test-count: baseline updated -> ${JSON.stringify(actual)}`);
79
+ console.log("Commit test-baseline.json in the same commit as the test change.");
80
+ process.exit(0);
81
+ }
82
+
83
+ if (!existsSync(BASELINE)) {
84
+ console.error(`check-test-count: ${BASELINE} is missing. Create it with --update.`);
85
+ process.exit(1);
86
+ }
87
+
88
+ const expected = JSON.parse(readFileSync(BASELINE, "utf8"));
89
+ const drifted = ["tests", "pass", "fail", "skipped"].filter(
90
+ (k) => Number(expected[k]) !== Number(actual[k]),
91
+ );
92
+
93
+ if (drifted.length) {
94
+ console.error("check-test-count: TEST COUNT DRIFT");
95
+ console.error(` expected ${JSON.stringify(expected)}`);
96
+ console.error(` actual ${JSON.stringify(actual)}`);
97
+ console.error(` differs ${drifted.join(", ")}`);
98
+ console.error("");
99
+ console.error("If this change is intentional, run:");
100
+ console.error(" npm run test:count:update");
101
+ console.error("and commit test-baseline.json alongside the test change.");
102
+ process.exit(1);
103
+ }
104
+
105
+ console.log(`check-test-count: PASS — ${JSON.stringify(actual)} matches the committed baseline`);
@@ -0,0 +1,21 @@
1
+ import { data as desk } from "../src/data/desk-data.mjs";
2
+
3
+ const board = await desk.call("hl-markets", "markets", {});
4
+ console.log(`markets: ${board.count} live perps`);
5
+ console.log(`total OI: $${(board.totals.openInterestUsd / 1e9).toFixed(2)}B`);
6
+ console.log(`24h vol: $${(board.totals.dayNtlVolumeUsd / 1e9).toFixed(2)}B`);
7
+
8
+ const btc = board.markets.find((m) => m.coin === "BTC");
9
+ console.log(`\nBTC mark=$${btc.markPx} 24h=${btc.change24hPct?.toFixed(2)}% OI=$${(btc.openInterestUsd / 1e6).toFixed(1)}M fundingAPR=${btc.fundingRateAprPct?.toFixed(2)}% maxLev=${btc.maxLeverage}x`);
10
+
11
+ const lb = await desk.call("hl-markets", "leaderboards", { limit: 3 });
12
+ console.log(`\ntop gainers: ${lb.gainers.map((m) => `${m.coin} ${m.change24hPct.toFixed(1)}%`).join(", ")}`);
13
+ console.log(`top losers: ${lb.losers.map((m) => `${m.coin} ${m.change24hPct.toFixed(1)}%`).join(", ")}`);
14
+ console.log(`by volume: ${lb.byVolume.map((m) => `${m.coin} $${(m.dayNtlVolumeUsd / 1e6).toFixed(0)}M`).join(", ")}`);
15
+ console.log(`funding high: ${lb.fundingHighest.map((m) => `${m.coin} ${m.fundingRateAprPct.toFixed(1)}%`).join(", ")}`);
16
+
17
+ const hype = await desk.call("hl-markets", "coin", { coin: "HYPE", interval: "1h" });
18
+ console.log(`\nHYPE mark=$${hype.market.markPx} spread=${hype.book.spreadBps?.toFixed(2)}bps candles=${hype.candles.count}`);
19
+
20
+ const spot = await desk.call("hl-markets", "spot", {});
21
+ console.log(`spot: ${spot.tokenCount} tokens / ${spot.pairCount} pairs`);
@@ -0,0 +1,48 @@
1
+ import { data as desk } from "../src/data/desk-data.mjs";
2
+
3
+ const info = await desk.call("hl-perps", "assetInfo", { coin: "BTC" });
4
+ console.log(`BTC assetId=${info.assetId} szDecimals=${info.szDecimals} maxLev=${info.maxLeverage}x mark=$${info.markPx}`);
5
+
6
+ // 1. LIMIT BUY
7
+ const lim = await desk.call("hl-perps", "prepareOrder", {
8
+ coin: "BTC", side: "buy", type: "limit", price: "60000.123456789", size: "0.0123456", tif: "Gtc",
9
+ });
10
+ console.log(`\nLIMIT BUY px=${lim.price} sz=${lim.size} notional=$${lim.notionalUsd?.toFixed(2)} tif=${JSON.stringify(lim.action.orders[0].t)}`);
11
+ console.log(` signingReady=${lim.signingReady} broadcastReady=${lim.broadcastReady} requiresUserSig=${lim.requiresUserSignature}`);
12
+
13
+ // 2. MARKET BUY with slippage cap
14
+ const mkt = await desk.call("hl-perps", "prepareOrder", {
15
+ coin: "ETH", side: "buy", type: "market", size: "0.5", maxSlippageBps: 50,
16
+ });
17
+ console.log(`\nMARKET BUY ETH aggressive px=${mkt.price} sz=${mkt.size} tif=${mkt.action.orders[0].t.limit.tif}`);
18
+
19
+ // 3. slippage cap enforced
20
+ try {
21
+ await desk.call("hl-perps", "prepareOrder", { coin: "ETH", side: "buy", type: "market", size: "1", maxSlippageBps: 500 });
22
+ console.log(" !! 500bps ACCEPTED — cap broken");
23
+ } catch (e) { console.log(` 500bps rejected: ${e.message.slice(0, 60)}`); }
24
+
25
+ // 4. LEVERAGE
26
+ const lev = await desk.call("hl-perps", "prepareLeverage", { coin: "BTC", leverage: 25, marginMode: "isolated" });
27
+ console.log(`\nLEVERAGE ${lev.leverage}x ${lev.marginMode} isCross=${lev.action.isCross}`);
28
+ console.log(` warning: ${lev.liquidationWarning}`);
29
+
30
+ try {
31
+ await desk.call("hl-perps", "prepareLeverage", { coin: "BTC", leverage: 999 });
32
+ console.log(" !! 999x ACCEPTED — cap broken");
33
+ } catch (e) { console.log(` 999x rejected: ${e.message.slice(0, 70)}`); }
34
+
35
+ // 5. BRACKET: entry + TP + SL
36
+ const br = await desk.call("hl-perps", "prepareBracket", {
37
+ coin: "BTC", side: "buy", type: "limit", price: "60000", size: "0.01",
38
+ takeProfitPx: "72000", stopLossPx: "57000",
39
+ });
40
+ console.log(`\nBRACKET legs=${br.legs} entry=${br.entry.price} tp=${br.takeProfitPx} sl=${br.stopLossPx} grouping=${br.action.grouping}`);
41
+ console.log(` tp reduceOnly=${br.action.orders[1].r} trigger=${JSON.stringify(br.action.orders[1].t.trigger)}`);
42
+ console.log(` sl reduceOnly=${br.action.orders[2].r} trigger=${JSON.stringify(br.action.orders[2].t.trigger)}`);
43
+
44
+ // 6. CANCEL
45
+ const c = await desk.call("hl-perps", "prepareCancel", { coin: "BTC", orderId: 12345 });
46
+ console.log(`\nCANCEL oid=${c.orderId} action=${JSON.stringify(c.action)}`);
47
+
48
+ console.log("\nNo submit function exists in hl-perps — every result is user-signed.");
@@ -0,0 +1,128 @@
1
+ import { data as desk } from "../src/data/desk-data.mjs";
2
+ import {
3
+ hlPrepareStakeDeposit,
4
+ hlPrepareStakeWithdraw,
5
+ hlPrepareDelegate,
6
+ hypeToWei,
7
+ weiToHype,
8
+ } from "../src/data/providers/hl-staking.mjs";
9
+
10
+ const USER = process.env.ORACLE_E2E_EVM_ADDRESS || "0x0000000000000000000000000000000000000001";
11
+ const results = [];
12
+ function log(lane, status, detail) {
13
+ results.push({ lane, status });
14
+ console.log(`${status === "ok" ? "OK " : "FAIL"} ${lane} :: ${detail}`);
15
+ }
16
+ async function run(lane, fn) {
17
+ try {
18
+ log(lane, "ok", await fn());
19
+ } catch (e) {
20
+ log(lane, "fail", String(e?.message || e).slice(0, 150));
21
+ }
22
+ }
23
+
24
+ console.log("== HYPERCORE STAKING ==");
25
+
26
+ let validator = null;
27
+ await run("hl.staking.validators", async () => {
28
+ const v = await desk.hl.staking.validators();
29
+ const active = v.validators.filter((x) => x.isActive && !x.isJailed);
30
+ validator = active[0]?.validator || v.validators[0]?.validator;
31
+ return `validators=${v.count} activeUnjailed=${active.length} first=${(validator || "").slice(0, 10)}..`;
32
+ });
33
+
34
+ await run("hl.staking.health", async () => {
35
+ const h = await desk.hl.staking.health();
36
+ if (!h.ok) throw new Error("staking lane unhealthy");
37
+ return `ok validators=${h.validators} chain=${h.chain}`;
38
+ });
39
+
40
+ await run("hl.staking.delegatorSummary", async () => {
41
+ const s = await desk.hl.staking.summary(USER);
42
+ return `delegated=${s.delegatedWei} undelegated=${s.undelegatedWei} pendingWithdrawals=${s.nPendingWithdrawals}`;
43
+ });
44
+
45
+ await run("hl.staking.delegations", async () => {
46
+ const d = await desk.hl.staking.delegations(USER);
47
+ return `count=${d.count}`;
48
+ });
49
+
50
+ await run("hl.staking.rewards", async () => {
51
+ const r = await desk.hl.staking.rewards(USER);
52
+ return `rewards=${r.rewards.length}`;
53
+ });
54
+
55
+ await run("hl.staking.preflight", async () => {
56
+ const p = await desk.hl.staking.preflight(USER);
57
+ return `undelegated=${p.stakingBalanceUndelegated} queueDays=${p.unstakingQueueDays} lockupDays=${p.validatorLockupDays}`;
58
+ });
59
+
60
+ await run("hl.staking.units", async () => {
61
+ const w = hypeToWei("1.5");
62
+ if (w !== "150000000") throw new Error(`1.5 HYPE -> ${w}, expected 150000000`);
63
+ if (weiToHype("150000000") !== "1.5") throw new Error("roundtrip failed");
64
+ return `1.5 HYPE = ${w} wei roundtrip ok`;
65
+ });
66
+
67
+ await run("hl.staking.prepareStake", async () => {
68
+ const p = hlPrepareStakeDeposit({ amountHype: "2.5", maxHype: "10" });
69
+ if (p.action.type !== "cDeposit") throw new Error("wrong action type");
70
+ if (p.broadcastReady !== false || p.signingReady !== false) throw new Error("leaked readiness");
71
+ if (!p.requiresUserSignature) throw new Error("must require user signature");
72
+ return `cDeposit wei=${p.action.wei} primaryType=${p.typedData.primaryType} broadcastReady=false`;
73
+ });
74
+
75
+ await run("hl.staking.prepareStake.capBreach", async () => {
76
+ try {
77
+ hlPrepareStakeDeposit({ amountHype: "50", maxHype: "10" });
78
+ } catch (e) {
79
+ return `rejected over-cap: ${String(e.message).slice(0, 70)}`;
80
+ }
81
+ throw new Error("cap was NOT enforced");
82
+ });
83
+
84
+ await run("hl.staking.prepareDelegate", async () => {
85
+ if (!validator) throw new Error("no validator resolved");
86
+ const p = hlPrepareDelegate({ validator, amountHype: "2.5", isUndelegate: false, maxHype: "10" });
87
+ if (p.action.type !== "tokenDelegate" || p.action.isUndelegate !== false) throw new Error("wrong action");
88
+ return `tokenDelegate validator=${p.action.validator.slice(0, 10)}.. wei=${p.action.wei} isUndelegate=false`;
89
+ });
90
+
91
+ await run("hl.staking.prepareUndelegate", async () => {
92
+ const p = hlPrepareDelegate({ validator, amountHype: "2.5", isUndelegate: true });
93
+ if (p.action.isUndelegate !== true) throw new Error("undelegate flag lost");
94
+ return `tokenDelegate isUndelegate=true kind=${p.kind}`;
95
+ });
96
+
97
+ await run("hl.staking.prepareUnstake", async () => {
98
+ const p = hlPrepareStakeWithdraw({ amountHype: "2.5" });
99
+ if (p.action.type !== "cWithdraw") throw new Error("wrong action type");
100
+ if (!p.notes.join(" ").includes("7 day")) throw new Error("missing unstaking queue warning");
101
+ return `cWithdraw wei=${p.action.wei} queue warning present`;
102
+ });
103
+
104
+ await run("hl.staking.badAmount", async () => {
105
+ try {
106
+ hlPrepareStakeDeposit({ amountHype: "0" });
107
+ } catch (e) {
108
+ return `rejected zero amount`;
109
+ }
110
+ throw new Error("zero amount accepted");
111
+ });
112
+
113
+ await run("hl.staking.badValidator", async () => {
114
+ try {
115
+ hlPrepareDelegate({ validator: "not-an-address", amountHype: "1" });
116
+ } catch (e) {
117
+ return `rejected bad validator`;
118
+ }
119
+ throw new Error("bad validator accepted");
120
+ });
121
+
122
+ const failed = results.filter((r) => r.status === "fail");
123
+ console.log(`\nlanes=${results.length} ok=${results.length - failed.length} failed=${failed.length}`);
124
+ if (failed.length) {
125
+ console.log(`FAILED: ${failed.map((f) => f.lane).join(", ")}`);
126
+ process.exit(1);
127
+ }
128
+ console.log("HYPERCORE STAKING OK: reads live, stake/unstake/delegate prepare-only, caps enforced.");