nansen-cli 1.43.0 → 1.44.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.
- package/CHANGELOG.md +80 -0
- package/README.md +34 -0
- package/package.json +3 -3
- package/src/api.js +184 -23
- package/src/bridge.js +103 -5
- package/src/cli.js +47 -53
- package/src/commands/completion.js +652 -0
- package/src/commands/mcp.js +19 -3
- package/src/commands/research.js +199 -25
- package/src/hl-client.js +20 -8
- package/src/limit-order.js +31 -20
- package/src/mcp-verify.js +66 -1
- package/src/perp.js +72 -24
- package/src/query-options.js +32 -0
- package/src/schema.json +631 -131
- package/src/semver.js +26 -0
- package/src/telemetry.js +118 -19
- package/src/trading.js +196 -22
- package/src/transfer.js +4 -1
- package/src/update-check.js +13 -6
- package/src/wallet.js +10 -6
- package/src/x402.js +8 -1
package/src/perp.js
CHANGED
|
@@ -245,17 +245,28 @@ async function signHlAction(eip712, { privateKeyHex, privyClient, privyWalletId,
|
|
|
245
245
|
// Parse the per-order statuses HL returns for an `order` action so the oid and
|
|
246
246
|
// fill are surfaced — the perp analogue of spot printing its quote id. HL replies:
|
|
247
247
|
// response.data.statuses[] = { resting:{oid} } | { filled:{oid,totalSz,avgPx} } | { error }
|
|
248
|
-
//
|
|
249
|
-
//
|
|
250
|
-
//
|
|
248
|
+
// Successful responses and structured rejection responses both pass through
|
|
249
|
+
// here. A TP/SL bracket returns multiple legs; label them the same way
|
|
250
|
+
// extractActionErrors does (parent / take-profit / stop-loss).
|
|
251
251
|
// Gated on the SUBMITTED action being an order: leverage/transfer/builder-fee
|
|
252
252
|
// actions (type "default") and cancels return no oids, so [] falls back to the
|
|
253
253
|
// concise raw response line in buildScreenSignSubmit.
|
|
254
254
|
export function summarizeOrderResult(result, action) {
|
|
255
255
|
if (action?.type !== 'order') return [];
|
|
256
|
-
const
|
|
257
|
-
if (!Array.isArray(
|
|
258
|
-
const
|
|
256
|
+
const responseStatuses = result?.response?.data?.statuses;
|
|
257
|
+
if (!Array.isArray(responseStatuses)) return [];
|
|
258
|
+
const orderCount = action.orders?.length ?? 0;
|
|
259
|
+
const multiLeg = orderCount > 1;
|
|
260
|
+
// Hyperliquid may return one pre-validation error for an entire batch rather
|
|
261
|
+
// than one status per order. Treat that single error as the outcome of every
|
|
262
|
+
// submitted leg so bracket attribution remains one-to-one with action.orders.
|
|
263
|
+
const statuses = multiLeg
|
|
264
|
+
&& responseStatuses.length === 1
|
|
265
|
+
&& responseStatuses[0]
|
|
266
|
+
&& typeof responseStatuses[0] === 'object'
|
|
267
|
+
&& 'error' in responseStatuses[0]
|
|
268
|
+
? Array.from({ length: orderCount }, () => responseStatuses[0])
|
|
269
|
+
: responseStatuses;
|
|
259
270
|
const out = [];
|
|
260
271
|
for (const [index, entry] of statuses.entries()) {
|
|
261
272
|
if (!entry || typeof entry !== 'object') continue;
|
|
@@ -273,31 +284,51 @@ export function summarizeOrderResult(result, action) {
|
|
|
273
284
|
// above 2^53 arrived rounded. Flag precision (oidSafe) so the caller can
|
|
274
285
|
// withhold a copy-paste cancel — and BI can drop the id — rather than act on
|
|
275
286
|
// a wrong oid presented as authoritative.
|
|
287
|
+
const orderSide = action.orders?.[index]?.b;
|
|
288
|
+
const side = orderSide === true ? 'buy' : orderSide === false ? 'sell' : undefined;
|
|
289
|
+
const reduceOnly = action.orders?.[index]?.r === true;
|
|
290
|
+
const positionSide = side === undefined
|
|
291
|
+
? undefined
|
|
292
|
+
: reduceOnly
|
|
293
|
+
? (side === 'buy' ? 'short' : 'long')
|
|
294
|
+
: (side === 'buy' ? 'long' : 'short');
|
|
276
295
|
if (entry.filled && entry.filled.oid !== undefined) {
|
|
277
296
|
const { oid, totalSz, avgPx } = entry.filled;
|
|
278
|
-
out.push({ leg, kind: 'filled', oid, oidSafe: Number.isSafeInteger(oid), totalSz, avgPx });
|
|
297
|
+
out.push({ index, leg, side, positionSide, kind: 'filled', oid, oidSafe: Number.isSafeInteger(oid), totalSz, avgPx });
|
|
279
298
|
} else if (entry.resting && entry.resting.oid !== undefined) {
|
|
280
299
|
const { oid } = entry.resting;
|
|
281
|
-
out.push({ leg, kind: 'resting', oid, oidSafe: Number.isSafeInteger(oid) });
|
|
300
|
+
out.push({ index, leg, side, positionSide, kind: 'resting', oid, oidSafe: Number.isSafeInteger(oid) });
|
|
301
|
+
} else if ('error' in entry) {
|
|
302
|
+
out.push({ index, leg, side, positionSide, kind: 'rejected' });
|
|
282
303
|
}
|
|
283
304
|
}
|
|
284
305
|
return out;
|
|
285
306
|
}
|
|
286
307
|
|
|
287
|
-
// Fire
|
|
288
|
-
//
|
|
289
|
-
//
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
// rounded past 2^53 (oidSafe false) so BI never records a wrong id. `summary` is
|
|
293
|
-
// summarizeOrderResult's output; its parent leg carries the order id.
|
|
294
|
-
function emitPerpOrderCompleted(telemetry, summary) {
|
|
295
|
-
const parent = summary.find((o) => o.leg === 'parent') ?? summary[0];
|
|
296
|
-
return telemetry.track({
|
|
308
|
+
// Fire one privacy-minimal event per response leg. Each oid is joined to fills
|
|
309
|
+
// in BI; the shared nonce reconstructs the submitted batch. Raw wallet, prices,
|
|
310
|
+
// sizes and error text never leave the CLI. Unsafe uint64 ids are omitted.
|
|
311
|
+
function emitPerpOrderCompleted(telemetry, summary, walletAddress, submissionId, errorCode) {
|
|
312
|
+
return Promise.all(summary.map((order) => telemetry.track({
|
|
297
313
|
command: telemetry.command,
|
|
298
|
-
side
|
|
299
|
-
|
|
300
|
-
|
|
314
|
+
// Pass the leg's own side (not the command-level side) so telemetry can
|
|
315
|
+
// distinguish a leg whose side is unknown and omit position_side rather
|
|
316
|
+
// than fabricate one. order.side is always set for real HL legs.
|
|
317
|
+
side: order.side,
|
|
318
|
+
position_side: order.positionSide,
|
|
319
|
+
outcome: order.kind,
|
|
320
|
+
// Guard submission_id so a future refactor that drops the nonce can never
|
|
321
|
+
// stringify undefined into the BI dedup key (perpOutcomeEventId derives
|
|
322
|
+
// the event id from it).
|
|
323
|
+
...(submissionId !== undefined && { submission_id: String(submissionId) }),
|
|
324
|
+
leg_index: order.index,
|
|
325
|
+
leg: order.leg,
|
|
326
|
+
wallet_address: walletAddress,
|
|
327
|
+
oid: order.oidSafe ? order.oid : undefined,
|
|
328
|
+
// A mixed response is a command-level PARTIAL_FILL, but successful legs
|
|
329
|
+
// remain successful. Attach the stable code only to rejected legs.
|
|
330
|
+
...(order.kind === 'rejected' && errorCode ? { error_code: errorCode } : {}),
|
|
331
|
+
})));
|
|
301
332
|
}
|
|
302
333
|
|
|
303
334
|
async function buildScreenSignSubmit(apiInstance, prepared, ctx) {
|
|
@@ -317,7 +348,24 @@ async function buildScreenSignSubmit(apiInstance, prepared, ctx) {
|
|
|
317
348
|
const signature = await signHlAction(eip712, ctx);
|
|
318
349
|
|
|
319
350
|
log(' Submitting to Hyperliquid...');
|
|
320
|
-
|
|
351
|
+
let result;
|
|
352
|
+
try {
|
|
353
|
+
result = await submitExchange({ action, nonce, signature, vaultAddress: null });
|
|
354
|
+
} catch (error) {
|
|
355
|
+
if (telemetry && error.exchangeResult) {
|
|
356
|
+
// Emit only authoritative per-leg outcomes. An opaque or malformed HTTP
|
|
357
|
+
// body has an indeterminate result and remains covered by cli_command_failed.
|
|
358
|
+
const outcomes = summarizeOrderResult(error.exchangeResult, action);
|
|
359
|
+
if (outcomes.length) {
|
|
360
|
+
try {
|
|
361
|
+
await emitPerpOrderCompleted(telemetry, outcomes, walletAddress, nonce, error.code);
|
|
362
|
+
} catch {
|
|
363
|
+
// Best-effort; preserve the original exchange error.
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
throw error;
|
|
368
|
+
}
|
|
321
369
|
|
|
322
370
|
const status = result.status ?? 'ok';
|
|
323
371
|
log(` Status: ${status}`);
|
|
@@ -345,7 +393,7 @@ async function buildScreenSignSubmit(apiInstance, prepared, ctx) {
|
|
|
345
393
|
log(` Response: ${resp}`);
|
|
346
394
|
}
|
|
347
395
|
|
|
348
|
-
// Emit the order
|
|
396
|
+
// Emit the order outcome to BI (one privacy-minimal event per response leg) — the
|
|
349
397
|
// perp analogue of the command-level telemetry, which fires too early (before
|
|
350
398
|
// this HL response) to observe any of it. Order/close only: cancel / leverage
|
|
351
399
|
// / transfer / builder-fee actions carry no `telemetry` and also summarize to
|
|
@@ -353,7 +401,7 @@ async function buildScreenSignSubmit(apiInstance, prepared, ctx) {
|
|
|
353
401
|
// completed order into a cli_command_failed.
|
|
354
402
|
if (telemetry && orders.length) {
|
|
355
403
|
try {
|
|
356
|
-
await emitPerpOrderCompleted(telemetry, orders);
|
|
404
|
+
await emitPerpOrderCompleted(telemetry, orders, walletAddress, nonce);
|
|
357
405
|
} catch {
|
|
358
406
|
// Best-effort; never surface a tracking error after a real fill.
|
|
359
407
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nansen CLI - Shared list-query helpers
|
|
3
|
+
*
|
|
4
|
+
* Builds pagination and order_by request fragments from CLI options.
|
|
5
|
+
* Lives in a leaf module so src/cli.js and src/commands/*.js share one
|
|
6
|
+
* implementation without circular imports.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { NansenError, ErrorCode } from './api.js';
|
|
10
|
+
|
|
11
|
+
export function buildPagination(options) {
|
|
12
|
+
if (options.limit === undefined && options.page === undefined) return undefined;
|
|
13
|
+
const perPage = options.limit === undefined ? undefined : Number(options.limit);
|
|
14
|
+
if (perPage !== undefined && (!Number.isInteger(perPage) || perPage < 1)) {
|
|
15
|
+
throw new NansenError('--limit must be a positive integer', ErrorCode.INVALID_PARAMS);
|
|
16
|
+
}
|
|
17
|
+
return {
|
|
18
|
+
page: Math.max(1, parseInt(options.page, 10) || 1),
|
|
19
|
+
per_page: perPage,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Parse simple sort syntax: "field:direction" or "field" (defaults to DESC)
|
|
24
|
+
export function parseSort(sortOption, orderByOption) {
|
|
25
|
+
// If --order-by is provided, use it (full JSON control)
|
|
26
|
+
if (orderByOption) return orderByOption;
|
|
27
|
+
if (!sortOption) return undefined;
|
|
28
|
+
const parts = String(sortOption).split(':');
|
|
29
|
+
const field = parts[0];
|
|
30
|
+
const direction = (parts[1] || 'desc').toUpperCase();
|
|
31
|
+
return [{ field, direction }];
|
|
32
|
+
}
|