diff --git a/.changeset/perp-cancel-oid-safe-integer.md b/.changeset/perp-cancel-oid-safe-integer.md new file mode 100644 index 00000000..53a6756b --- /dev/null +++ b/.changeset/perp-cancel-oid-safe-integer.md @@ -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. diff --git a/src/__tests__/perp.test.js b/src/__tests__/perp.test.js index f5a7d0b4..c8212070 100644 --- a/src/__tests__/perp.test.js +++ b/src/__tests__/perp.test.js @@ -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)', () => { diff --git a/src/perp.js b/src/perp.js index 4a601156..ea072afa 100644 --- a/src/perp.js +++ b/src/perp.js @@ -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; }