Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/perp-cancel-oid-safe-integer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"nansen-cli": patch
---

Reject `--oid` values above 2^53-1 on `perp cancel`: large Hyperliquid uint64 order IDs would be silently rounded by JS Number, potentially cancelling the wrong order.
15 changes: 15 additions & 0 deletions src/__tests__/perp.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,21 @@ describe('perp cancel validation', () => {
cmds.cancel([], null, {}, { coin: 'ETH', oid: '0', wallet: 'x' }),
).rejects.toThrow(/Invalid --oid "0"/);
});

it('rejects --oid above 2^53-1 to prevent silent rounding of large uint64 ids', async () => {
const unsafeOid = String(Number.MAX_SAFE_INTEGER + 1); // 9007199254740992
await expect(
cmds.cancel([], null, {}, { coin: 'ETH', oid: unsafeOid, wallet: 'x' }),
).rejects.toThrow(/exceeds safe integer precision/);
});

it('accepts --oid exactly at 2^53-1 (MAX_SAFE_INTEGER)', async () => {
const safeOid = String(Number.MAX_SAFE_INTEGER); // 9007199254740991
// Should not throw on validation; will fail later on missing wallet — that's fine.
await expect(
cmds.cancel([], null, {}, { coin: 'ETH', oid: safeOid, wallet: 'x' }),
).rejects.not.toThrow(/exceeds safe integer precision/);
});
});

describe('perp meta listing (L1)', () => {
Expand Down
6 changes: 6 additions & 0 deletions src/perp.js
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,12 @@ function parsePositiveInt(raw, name) {
if (!Number.isInteger(n) || n <= 0) {
throw invalid(`Invalid --${name} "${raw}". Must be a positive integer.`);
}
// Hyperliquid order IDs are uint64. JS Number loses precision above 2^53-1,
// so parseInt would silently round a large oid and cancel the wrong order.
// Refuse here for the same reason the response path withholds unsafe oids.
if (!Number.isSafeInteger(n)) {
throw invalid(`Invalid --${name} "${raw}". Value exceeds safe integer precision (2^53-1); copy the exact order ID from "nansen perp positions".`);
}
return n;
}

Expand Down
Loading