@oracle-agent/oracle 0.1.0 → 0.2.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,165 @@
1
+ // Hyperliquid asset ID resolution — main perps, HIP-3 builder dexs, HIP-4 outcomes.
2
+ // Docs: https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/asset-ids
3
+ //
4
+ // Main perps: asset = index in meta.universe
5
+ // HIP-3 builder: asset = 100000 + perp_dex_index * 10000 + index_in_meta
6
+ // HIP-4 outcomes: encoding = 10 * outcome + side; asset = 100_000_000 + encoding
7
+ // coin form: #<encoding>
8
+
9
+ import { hlInfo, hlMetaAndAssetCtxs } from "./hl-info.mjs";
10
+
11
+ const OUTCOME_ASSET_BASE = 100_000_000;
12
+ const BUILDER_PERP_BASE = 100_000;
13
+
14
+ let _dexCache = { at: 0, dexs: null };
15
+ let _metaCache = new Map(); // dex -> { at, universe, ctxs }
16
+
17
+ async function perpDexs(opts = {}) {
18
+ const now = Date.now();
19
+ if (_dexCache.dexs && now - _dexCache.at < 60_000 && !opts.fetchImpl) return _dexCache.dexs;
20
+ const dexs = await hlInfo({ type: "perpDexs" }, opts);
21
+ _dexCache = { at: now, dexs: Array.isArray(dexs) ? dexs : [] };
22
+ return _dexCache.dexs;
23
+ }
24
+
25
+ async function metaForDex(dex = "", opts = {}) {
26
+ const key = dex || "";
27
+ const now = Date.now();
28
+ const hit = _metaCache.get(key);
29
+ // skip cache when caller injects fetchImpl (unit tests / custom transport)
30
+ if (hit && now - hit.at < 30_000 && !opts.fetchImpl) return hit;
31
+ const raw = dex
32
+ ? await hlInfo({ type: "metaAndAssetCtxs", dex }, opts)
33
+ : await hlMetaAndAssetCtxs(opts);
34
+ const universe = raw?.[0]?.universe ?? raw?.universe ?? [];
35
+ const ctxs = Array.isArray(raw?.[1]) ? raw[1] : [];
36
+ const entry = { at: now, universe, ctxs };
37
+ _metaCache.set(key, entry);
38
+ return entry;
39
+ }
40
+
41
+ /** Parse HIP-4 coin "#1100" or encoding number → { encoding, outcome, side }. */
42
+ export function parseOutcomeCoin(coin) {
43
+ const text = String(coin || "").trim();
44
+ let encoding;
45
+ if (text.startsWith("#")) encoding = Number(text.slice(1));
46
+ else if (/^\d+$/.test(text)) encoding = Number(text);
47
+ else return null;
48
+ if (!Number.isInteger(encoding) || encoding < 0) return null;
49
+ const side = encoding % 10;
50
+ if (side !== 0 && side !== 1) return null;
51
+ const outcome = Math.floor(encoding / 10);
52
+ return { encoding, outcome, side, coin: `#${encoding}`, assetId: OUTCOME_ASSET_BASE + encoding };
53
+ }
54
+
55
+ export function outcomeAssetId(outcome, side) {
56
+ const o = Number(outcome);
57
+ const s = Number(side);
58
+ if (!Number.isInteger(o) || o < 0) throw new Error("hl-assets: outcome id must be a non-negative integer");
59
+ if (s !== 0 && s !== 1) throw new Error("hl-assets: outcome side must be 0 or 1");
60
+ const encoding = 10 * o + s;
61
+ return { encoding, outcome: o, side: s, coin: `#${encoding}`, assetId: OUTCOME_ASSET_BASE + encoding };
62
+ }
63
+
64
+ /**
65
+ * Resolve any tradeable HL coin name to asset id + precision.
66
+ * Accepts: "BTC", "xyz:TSLA", "#1100", { outcome, side }, { dex, coin }.
67
+ */
68
+ export async function resolveHlAsset(args = {}, opts = {}) {
69
+ // HIP-4 explicit
70
+ if (args.outcome != null || (args.coin && String(args.coin).startsWith("#"))) {
71
+ const parsed =
72
+ args.outcome != null
73
+ ? outcomeAssetId(args.outcome, args.side ?? args.outcomeSide ?? 0)
74
+ : parseOutcomeCoin(args.coin);
75
+ if (!parsed) throw new Error(`hl-assets: invalid outcome coin ${args.coin}`);
76
+ // szDecimals for outcomes: typically 2-4; use 2 default (binary contracts)
77
+ const szDecimals = args.szDecimals != null ? Number(args.szDecimals) : 2;
78
+ return {
79
+ kind: "outcome",
80
+ coin: parsed.coin,
81
+ assetId: parsed.assetId,
82
+ encoding: parsed.encoding,
83
+ outcome: parsed.outcome,
84
+ side: parsed.side,
85
+ szDecimals,
86
+ maxLeverage: 1,
87
+ dex: null,
88
+ markPx: null,
89
+ };
90
+ }
91
+
92
+ const rawName = String(args.coin || args.symbol || args.name || "").trim();
93
+ if (!rawName) throw new Error("hl-assets: coin required");
94
+
95
+ // HIP-3 builder dex coin: dex:COIN
96
+ if (rawName.includes(":")) {
97
+ const [dex, ...rest] = rawName.split(":");
98
+ const leaf = rest.join(":");
99
+ if (!dex || !leaf) throw new Error(`hl-assets: bad builder coin ${rawName}`);
100
+ const dexs = await perpDexs(opts);
101
+ const dexIndex = dexs.findIndex((d) => d && d.name === dex);
102
+ if (dexIndex < 1) throw new Error(`hl-assets: unknown builder dex "${dex}"`);
103
+ const { universe, ctxs } = await metaForDex(dex, opts);
104
+ const index = universe.findIndex((a) => a.name === rawName || a.name === `${dex}:${leaf}` || a.name.endsWith(`:${leaf}`));
105
+ if (index < 0) throw new Error(`hl-assets: ${rawName} not in dex ${dex} meta`);
106
+ const asset = universe[index];
107
+ if (asset.isDelisted) throw new Error(`hl-assets: ${rawName} is delisted`);
108
+ const assetId = BUILDER_PERP_BASE + dexIndex * 10_000 + index;
109
+ const ctx = ctxs[index] || {};
110
+ return {
111
+ kind: "hip3",
112
+ coin: asset.name || rawName,
113
+ assetId,
114
+ szDecimals: asset.szDecimals ?? 0,
115
+ maxLeverage: asset.maxLeverage ?? null,
116
+ dex,
117
+ dexIndex,
118
+ indexInMeta: index,
119
+ markPx: ctx.markPx ?? null,
120
+ };
121
+ }
122
+
123
+ // Main dex perps
124
+ const { universe, ctxs } = await metaForDex("", opts);
125
+ const coin = rawName.toUpperCase();
126
+ const index = universe.findIndex((a) => a.name === coin);
127
+ if (index < 0) {
128
+ // optional: bare leaf match on builder dexs (TSLA -> xyz:TSLA)
129
+ if (opts.allowBareBuilderLeaf === true) { // opt-in only; explicit dex:COIN preferred
130
+ try {
131
+ const dexs = await perpDexs(opts);
132
+ for (let di = 1; di < dexs.length; di++) {
133
+ const d = dexs[di];
134
+ if (!d?.name) continue;
135
+ const tryName = `${d.name}:${coin}`;
136
+ try {
137
+ return await resolveHlAsset({ coin: tryName }, opts);
138
+ } catch {
139
+ /* next dex */
140
+ }
141
+ }
142
+ } catch {
143
+ /* offline / mock without perpDexs */
144
+ }
145
+ }
146
+ throw new Error(`hl-perps: ${coin} is not a listed perp`);
147
+ }
148
+ const asset = universe[index];
149
+ if (asset.isDelisted) throw new Error(`hl-assets: ${coin} is delisted`);
150
+ const ctx = ctxs[index] || {};
151
+ return {
152
+ kind: "main",
153
+ coin: asset.name,
154
+ assetId: index,
155
+ szDecimals: asset.szDecimals ?? 0,
156
+ maxLeverage: asset.maxLeverage ?? null,
157
+ dex: "",
158
+ markPx: ctx.markPx ?? null,
159
+ };
160
+ }
161
+
162
+ export function clearHlAssetCache() {
163
+ _dexCache = { at: 0, dexs: null };
164
+ _metaCache.clear();
165
+ }
@@ -65,7 +65,13 @@ export async function hlMeta(opts = {}) {
65
65
  }
66
66
 
67
67
  export async function hlMetaAndAssetCtxs(opts = {}) {
68
- return hlInfo({ type: "metaAndAssetCtxs" }, opts);
68
+ const body = { type: "metaAndAssetCtxs" };
69
+ if (opts.dex) body.dex = opts.dex;
70
+ return hlInfo(body, opts);
71
+ }
72
+
73
+ export async function hlPerpDexs(opts = {}) {
74
+ return hlInfo({ type: "perpDexs" }, opts);
69
75
  }
70
76
 
71
77
  export async function hlCandleSnapshot({ coin, interval = "1h", startTime, endTime }, opts = {}) {
@@ -0,0 +1,61 @@
1
+ // HIP-4 outcome market prepare helpers (local, no VPS).
2
+ import { stampPrepared } from "../../prepare-envelope.mjs";
3
+ import { hlOutcomeMeta } from "./hl-info.mjs";
4
+ import { outcomeAssetId, parseOutcomeCoin } from "./hl-assets.mjs";
5
+ import { hlPreparePerpOrder, hlPrepareCancelOrder } from "./hl-perps.mjs";
6
+
7
+ export async function hlOutcomeList(opts = {}) {
8
+ const meta = await hlOutcomeMeta(opts);
9
+ return {
10
+ provider: "hl-outcome",
11
+ kind: "outcome-meta",
12
+ outcomes: meta?.outcomes || [],
13
+ questions: meta?.questions || [],
14
+ };
15
+ }
16
+
17
+ /**
18
+ * Prepare a HIP-4 outcome order.
19
+ * args: { outcome, side: 0|1|'yes'|'no', sideName?, coin?, ...hlPreparePerpOrder fields }
20
+ */
21
+ export async function hlPrepareOutcomeOrder(args = {}, opts = {}) {
22
+ let coin = args.coin;
23
+ let outcome = args.outcome;
24
+ let side = args.side;
25
+ if (typeof side === "string") {
26
+ const s = side.toLowerCase();
27
+ if (s === "yes" || s === "0") side = 0;
28
+ else if (s === "no" || s === "1") side = 1;
29
+ }
30
+ if (args.sideName && outcome != null) {
31
+ const meta = await hlOutcomeMeta(opts);
32
+ const o = (meta?.outcomes || []).find((x) => Number(x.outcome) === Number(outcome));
33
+ if (o?.sideSpecs) {
34
+ const idx = o.sideSpecs.findIndex(
35
+ (sp) => String(sp.name).toLowerCase() === String(args.sideName).toLowerCase()
36
+ );
37
+ if (idx >= 0) side = idx;
38
+ }
39
+ }
40
+ if (!coin) {
41
+ if (outcome == null || side == null) {
42
+ throw new Error("hl-outcome: provide coin (#N) or outcome + side (0/1 or yes/no)");
43
+ }
44
+ coin = outcomeAssetId(outcome, side).coin;
45
+ }
46
+ const prepared = await hlPreparePerpOrder({ ...args, coin }, opts);
47
+ // ensure stamped kind
48
+ return stampPrepared(
49
+ { ...prepared, kind: "hip4-order", provider: "hl-perps" },
50
+ { provider: "hl-perps", kind: "hip4-order" }
51
+ );
52
+ }
53
+
54
+ export async function hlPrepareOutcomeCancel(args = {}, opts = {}) {
55
+ let coin = args.coin;
56
+ if (!coin && args.outcome != null) {
57
+ const side = args.side ?? 0;
58
+ coin = outcomeAssetId(args.outcome, side).coin;
59
+ }
60
+ return hlPrepareCancelOrder({ ...args, coin }, opts);
61
+ }
@@ -12,8 +12,10 @@
12
12
  // rounded to szDecimals. Integers are always allowed regardless of sig figs.
13
13
  // We do that rounding here, once, in decimal — never through a float.
14
14
 
15
- import { hlMetaAndAssetCtxs } from "./hl-info.mjs";
15
+ import { hlMetaAndAssetCtxs, hlAllMids } from "./hl-info.mjs";
16
+ import { resolveHlAsset } from "./hl-assets.mjs";
16
17
  import { toScaledInteger } from "../../exact-integer.mjs";
18
+ import { stampPrepared } from "../../prepare-envelope.mjs";
17
19
 
18
20
  export const HL_PERP_MAX_SIG_FIGS = 5;
19
21
  export const HL_PERP_PRICE_DECIMALS = 6; // 6 - szDecimals for perps
@@ -102,23 +104,18 @@ export function formatPerpSize(size, szDecimals) {
102
104
 
103
105
  /** Resolve a coin to its asset index and precision. Required before ordering. */
104
106
  export async function hlPerpAssetInfo(args = {}, opts = {}) {
105
- const coin = String(args.coin || args.symbol || "").trim().toUpperCase();
106
- if (!coin) throw new Error("hl-perps: coin required");
107
- const meta = await hlMetaAndAssetCtxs(opts);
108
- const universe = meta?.[0]?.universe ?? meta?.universe ?? [];
109
- const ctxs = Array.isArray(meta?.[1]) ? meta[1] : [];
110
- const index = universe.findIndex((a) => a.name === coin);
111
- if (index < 0) throw new Error(`hl-perps: ${coin} is not a listed perp`);
112
- const asset = universe[index];
113
- if (asset.isDelisted) throw new Error(`hl-perps: ${coin} is delisted`);
114
- const ctx = ctxs[index] || {};
107
+ // Resolves main perps, HIP-3 builder dexs (dex:COIN), and HIP-4 outcomes (#N / outcome+side).
108
+ const info = await resolveHlAsset(args, opts);
115
109
  return {
116
- coin,
117
- assetId: index,
118
- szDecimals: asset.szDecimals ?? 0,
119
- maxLeverage: asset.maxLeverage ?? null,
120
- markPx: ctx.markPx ?? null,
121
- oraclePx: ctx.oraclePx ?? null,
110
+ coin: info.coin,
111
+ assetId: info.assetId,
112
+ szDecimals: info.szDecimals,
113
+ maxLeverage: info.maxLeverage,
114
+ markPx: info.markPx,
115
+ kind: info.kind,
116
+ dex: info.dex ?? null,
117
+ outcome: info.outcome ?? null,
118
+ side: info.side ?? null,
122
119
  };
123
120
  }
124
121
 
@@ -157,16 +154,16 @@ function envelope(action, nonce) {
157
154
  action,
158
155
  nonce,
159
156
  signatureChainId: HL_SIGNATURE_CHAIN_ID,
160
- // The wallet signs this; Oracle never does.
161
- requiresUserSignature: true,
162
- signingReady: false,
163
- broadcastReady: false,
164
- executionReady: false,
165
157
  submitTo: "https://api.hyperliquid.xyz/exchange",
166
- note: "Sign this action with the user's wallet and POST it. Oracle does not submit.",
158
+ note: "Sign locally via @oracle-agent/operator or a wallet. Oracle does not submit.",
167
159
  };
168
160
  }
169
161
 
162
+ /** Stamp a full HL prepare result (provider/kind + action envelope). */
163
+ function stamped(result) {
164
+ return stampPrepared(result, { provider: result.provider || "hl-perps", kind: result.kind });
165
+ }
166
+
170
167
  /**
171
168
  * Prepare a perp order.
172
169
  *
@@ -185,7 +182,11 @@ export async function hlPreparePerpOrder(args = {}, opts = {}) {
185
182
 
186
183
  let price;
187
184
  if (kind === ORDER_TYPES.MARKET) {
188
- const mark = Number(info.markPx);
185
+ let mark = Number(info.markPx);
186
+ if (!Number.isFinite(mark)) {
187
+ const mids = await hlAllMids(opts);
188
+ mark = Number(mids?.[info.coin] ?? mids?.[String(info.assetId)] ?? NaN);
189
+ }
189
190
  if (!Number.isFinite(mark)) throw new Error("hl-perps: no mark price available for a market order");
190
191
  const bps = Number(args.maxSlippageBps ?? 50);
191
192
  if (!Number.isInteger(bps) || bps < 0) throw new Error("hl-perps: maxSlippageBps must be a non-negative integer");
@@ -234,12 +235,16 @@ export async function hlPreparePerpOrder(args = {}, opts = {}) {
234
235
  const nonce = Number(args.nonce ?? Date.now());
235
236
  const notional = Number(price) * Number(size);
236
237
 
237
- return {
238
+ return stamped({
238
239
  provider: "hl-perps",
239
240
  venue: "hyperliquid",
240
- kind: "perp-order",
241
+ kind: info.kind === "outcome" ? "hip4-order" : info.kind === "hip3" ? "hip3-order" : "perp-order",
241
242
  coin: info.coin,
242
243
  assetId: info.assetId,
244
+ assetKind: info.kind,
245
+ outcome: info.outcome ?? null,
246
+ outcomeSide: info.side ?? null,
247
+ dex: info.dex ?? null,
243
248
  side: isBuy ? "buy" : "sell",
244
249
  orderType: kind,
245
250
  price,
@@ -249,7 +254,7 @@ export async function hlPreparePerpOrder(args = {}, opts = {}) {
249
254
  maxLeverage: info.maxLeverage,
250
255
  markPx: info.markPx,
251
256
  ...envelope(action, nonce),
252
- };
257
+ });
253
258
  }
254
259
 
255
260
  /** Prepare a cancel by order id. */
@@ -258,14 +263,14 @@ export async function hlPrepareCancelOrder(args = {}, opts = {}) {
258
263
  const oid = args.orderId ?? args.oid;
259
264
  if (oid == null) throw new Error("hl-perps: orderId required");
260
265
  const action = { type: "cancel", cancels: [{ a: info.assetId, o: Number(oid) }] };
261
- return {
266
+ return stamped({
262
267
  provider: "hl-perps",
263
268
  venue: "hyperliquid",
264
269
  kind: "perp-cancel",
265
270
  coin: info.coin,
266
271
  orderId: Number(oid),
267
272
  ...envelope(action, Number(args.nonce ?? Date.now())),
268
- };
273
+ });
269
274
  }
270
275
 
271
276
  /**
@@ -292,7 +297,7 @@ export async function hlPrepareUpdateLeverage(args = {}, opts = {}) {
292
297
  isCross: mode === MARGIN_MODE.CROSS,
293
298
  leverage,
294
299
  };
295
- return {
300
+ return stamped({
296
301
  provider: "hl-perps",
297
302
  venue: "hyperliquid",
298
303
  kind: "perp-leverage",
@@ -305,7 +310,7 @@ export async function hlPrepareUpdateLeverage(args = {}, opts = {}) {
305
310
  ? `${leverage}x liquidates on roughly a ${(100 / leverage).toFixed(2)}% adverse move before fees`
306
311
  : null,
307
312
  ...envelope(action, Number(args.nonce ?? Date.now())),
308
- };
313
+ });
309
314
  }
310
315
 
311
316
  /** Prepare an isolated-margin adjustment for an existing position. */
@@ -325,14 +330,14 @@ export async function hlPrepareUpdateIsolatedMargin(args = {}, opts = {}) {
325
330
  isBuy: true,
326
331
  ntli: micro <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(micro) : micro.toString(),
327
332
  };
328
- return {
333
+ return stamped({
329
334
  provider: "hl-perps",
330
335
  venue: "hyperliquid",
331
336
  kind: "perp-isolated-margin",
332
337
  coin: info.coin,
333
338
  usd: text,
334
339
  ...envelope(action, Number(args.nonce ?? Date.now())),
335
- };
340
+ });
336
341
  }
337
342
 
338
343
  /**
@@ -368,7 +373,7 @@ export async function hlPrepareBracketOrder(args = {}, opts = {}) {
368
373
  if (orders.length === 1) throw new Error("hl-perps: a bracket needs takeProfitPx and/or stopLossPx");
369
374
 
370
375
  const action = { type: "order", orders, grouping: "normalTpsl" };
371
- return {
376
+ return stamped({
372
377
  provider: "hl-perps",
373
378
  venue: "hyperliquid",
374
379
  kind: "perp-bracket",
@@ -378,5 +383,5 @@ export async function hlPrepareBracketOrder(args = {}, opts = {}) {
378
383
  takeProfitPx: args.takeProfitPx == null ? null : formatPerpPrice(args.takeProfitPx, info.szDecimals),
379
384
  stopLossPx: args.stopLossPx == null ? null : formatPerpPrice(args.stopLossPx, info.szDecimals),
380
385
  ...envelope(action, Number(args.nonce ?? Date.now())),
381
- };
386
+ });
382
387
  }
@@ -8,6 +8,7 @@
8
8
  import { httpJson } from "../http.mjs";
9
9
  import { solanaPubkey } from "./solana-rpc.mjs";
10
10
  import { resolveProviderEndpoint, credentialedHeaders } from "../provider-endpoint.mjs";
11
+ import { stampPrepared } from "../../prepare-envelope.mjs";
11
12
 
12
13
  export const MAGICEDEN_SOL_API = "https://api-mainnet.magiceden.dev/v2";
13
14
  export const LAMPORTS_PER_SOL = 1_000_000_000;
@@ -223,7 +224,7 @@ export async function magicEdenSolPrepareBuy(args = {}, opts = {}) {
223
224
  throw new Error("magiceden: buy_now returned no unsigned transaction payload");
224
225
  }
225
226
  const transaction = Buffer.from(data).toString("base64");
226
- return {
227
+ return stampPrepared({
227
228
  provider: "magiceden-sol",
228
229
  chain: "solana-mainnet-beta",
229
230
  kind: "nft-buy",
@@ -241,7 +242,7 @@ export async function magicEdenSolPrepareBuy(args = {}, opts = {}) {
241
242
  transaction,
242
243
  transactionEncoding: "base64",
243
244
  raw,
244
- };
245
+ });
245
246
  }
246
247
 
247
248
  /**
@@ -269,7 +270,7 @@ export async function magicEdenSolPrepareList(args = {}, opts = {}) {
269
270
  const data = raw?.v0?.tx?.data ?? raw?.tx?.data ?? null;
270
271
  const transaction = Array.isArray(data) ? Buffer.from(data).toString("base64") : null;
271
272
  if (!transaction) throw new Error("magiceden: sell returned no transaction payload");
272
- return {
273
+ return stampPrepared({
273
274
  provider: "magiceden-sol",
274
275
  chain: "solana-mainnet-beta",
275
276
  kind: "nft-list",
@@ -284,7 +285,7 @@ export async function magicEdenSolPrepareList(args = {}, opts = {}) {
284
285
  transaction,
285
286
  transactionEncoding: "base64",
286
287
  raw,
287
- };
288
+ });
288
289
  }
289
290
 
290
291
  /**
@@ -333,7 +334,7 @@ export async function magicEdenSolPrepareMint(args = {}, opts = {}) {
333
334
  throw new Error("magiceden: mint returned no unsigned transaction payload");
334
335
  }
335
336
  const transaction = Buffer.from(data).toString("base64");
336
- return {
337
+ return stampPrepared({
337
338
  provider: "magiceden-sol",
338
339
  chain: "solana-mainnet-beta",
339
340
  kind: "nft-mint",
@@ -351,5 +352,5 @@ export async function magicEdenSolPrepareMint(args = {}, opts = {}) {
351
352
  transaction,
352
353
  transactionEncoding: "base64",
353
354
  raw,
354
- };
355
+ });
355
356
  }