diff --git a/.changeset/ord-2039.md b/.changeset/ord-2039.md new file mode 100644 index 00000000..0cca7717 --- /dev/null +++ b/.changeset/ord-2039.md @@ -0,0 +1,5 @@ +--- +"@lifi/perps-sdk-provider-lighter": patch +--- + +Read Lighter order, client order and trade ids from the exact string fields, so ids above 2^53 no longer round in `Order.orderId`, `Order.clientOrderId`, `Fill.id`, `Fill.orderId` and the `getOrder` lookup. diff --git a/packages/perps-sdk-provider-lighter/src/LighterProvider.ts b/packages/perps-sdk-provider-lighter/src/LighterProvider.ts index 7c6511f6..dfe5343a 100644 --- a/packages/perps-sdk-provider-lighter/src/LighterProvider.ts +++ b/packages/perps-sdk-provider-lighter/src/LighterProvider.ts @@ -1617,7 +1617,7 @@ export const createLighterProvider = ( // `accountOrders` reaches active and inactive rows alike, so a reused // client order index can match more than one row. Prefer the live one. const matches = orders - .filter((o) => String(o.client_order_index) === clientOrderIndex) + .filter((o) => o.client_order_id === clientOrderIndex) .flatMap((o) => mapDetail(o) ?? []) const hit = matches.find((o) => isActiveOrderStatus(o.status)) ?? matches[0] @@ -1627,8 +1627,7 @@ export const createLighterProvider = ( return hit } - const byOrderIndex = (o: LtOrder): boolean => - String(o.order_index) === params.id + const byOrderIndex = (o: LtOrder): boolean => o.order_id === params.id const active = await retryOnRevoked(params.address, token, (t) => fetchActiveOrders( diff --git a/packages/perps-sdk-provider-lighter/src/LighterProvider.unit.spec.ts b/packages/perps-sdk-provider-lighter/src/LighterProvider.unit.spec.ts index c8818be1..b65e36cd 100644 --- a/packages/perps-sdk-provider-lighter/src/LighterProvider.unit.spec.ts +++ b/packages/perps-sdk-provider-lighter/src/LighterProvider.unit.spec.ts @@ -342,6 +342,7 @@ const TRADES_RESPONSE = { trades: [ { trade_id: 7, + trade_id_str: '7', tx_hash: '0xfeed', type: 'trade', market_id: 0, @@ -350,6 +351,10 @@ const TRADES_RESPONSE = { usd_amount: '25000', ask_id: 11, bid_id: 22, + ask_id_str: '11', + bid_id_str: '22', + ask_client_id_str: '0', + bid_client_id_str: '0', ask_account_id: 99, bid_account_id: 42, is_maker_ask: true, @@ -3171,7 +3176,7 @@ describe('LighterProvider — getOrders pagination contract', () => { order_index: orderIndex, client_order_index: 0, order_id: String(orderIndex), - client_order_id: String(orderIndex), + client_order_id: '0', market_index: 0, owner_account_index: 42, initial_base_amount: '0.1', @@ -5352,6 +5357,11 @@ describe('LighterProvider — getOrder', () => { }) describe('LighterProvider — one-call order reads', () => { + // A market 33 order index above 2^53: `JSON.parse` rounds the numeric twin + // to ...276, so only the string twin carries the exact id. + const UNSAFE_ORDER_ID = '9570149379440277' + const UNSAFE_ORDER_ID_PARSED = 9570149379440276 + const TWO_MARKETS_RESPONSE = { markets: [ MARKETS_RESPONSE.markets[0], @@ -5461,8 +5471,8 @@ describe('LighterProvider — one-call order reads', () => { code: 0, next_cursor: '', orders: [ - makeOrder({ order_index: 900, market_index: 0 }), - makeOrder({ order_index: 901, market_index: 1 }), + makeOrder({ order_index: 900, order_id: '900', market_index: 0 }), + makeOrder({ order_index: 901, order_id: '901', market_index: 1 }), ], }) } @@ -5496,7 +5506,9 @@ describe('LighterProvider — one-call order reads', () => { return respond({ code: 0, next_cursor: '', - orders: [makeOrder({ order_index: 901, market_index: 1 })], + orders: [ + makeOrder({ order_index: 901, order_id: '901', market_index: 1 }), + ], }) } return undefined @@ -5516,7 +5528,12 @@ describe('LighterProvider — one-call order reads', () => { it('resolves an active order by client order index through one accountOrders request', async () => { overrideFetch((u) => u.includes('/api/v1/accountOrders') - ? respond({ code: 200, orders: [makeOrder({ client_order_index: 7 })] }) + ? respond({ + code: 200, + orders: [ + makeOrder({ client_order_index: 7, client_order_id: '7' }), + ], + }) : undefined ) @@ -5542,6 +5559,7 @@ describe('LighterProvider — one-call order reads', () => { orders: [ makeOrder({ client_order_index: 8, + client_order_id: '8', status: 'filled', remaining_base_amount: '0', filled_base_amount: '0.1', @@ -5572,10 +5590,17 @@ describe('LighterProvider — one-call order reads', () => { orders: [ makeOrder({ client_order_index: 7, + client_order_id: '7', order_index: 800, + order_id: '800', status: 'canceled', }), - makeOrder({ client_order_index: 7, order_index: 901 }), + makeOrder({ + client_order_index: 7, + client_order_id: '7', + order_index: 901, + order_id: '901', + }), ], }) : undefined @@ -5594,7 +5619,12 @@ describe('LighterProvider — one-call order reads', () => { it('ignores an accountOrders row whose client order index differs', async () => { overrideFetch((u) => u.includes('/api/v1/accountOrders') - ? respond({ code: 200, orders: [makeOrder({ client_order_index: 9 })] }) + ? respond({ + code: 200, + orders: [ + makeOrder({ client_order_index: 9, client_order_id: '9' }), + ], + }) : undefined ) @@ -5649,6 +5679,54 @@ describe('LighterProvider — one-call order reads', () => { expect(requestsTo('/api/v1/accountOrders')).toEqual([]) }) + it('matches a bare order id above 2^53 on order_id, not the rounded order_index', async () => { + overrideFetch((u) => + u.includes('/api/v1/accountActiveOrders') + ? respond({ + code: 0, + next_cursor: '', + orders: [ + makeOrder({ + order_index: UNSAFE_ORDER_ID_PARSED, + order_id: UNSAFE_ORDER_ID, + }), + ], + }) + : undefined + ) + + const order = await (await boundProvider()).getOrder({ + address: ADDRESS, + id: UNSAFE_ORDER_ID, + }) + + expect(order.orderId).toBe(UNSAFE_ORDER_ID) + expect(requestsTo('/api/v1/accountInactiveOrders')).toEqual([]) + }) + + it('matches a client order index above 2^53 on client_order_id', async () => { + overrideFetch((u) => + u.includes('/api/v1/accountOrders') + ? respond({ + code: 200, + orders: [ + makeOrder({ + client_order_index: UNSAFE_ORDER_ID_PARSED, + client_order_id: UNSAFE_ORDER_ID, + }), + ], + }) + : undefined + ) + + const order = await (await boundProvider()).getOrder({ + address: ADDRESS, + id: `${LIGHTER_CLIENT_ORDER_INDEX_ID_PREFIX}${UNSAFE_ORDER_ID}`, + }) + + expect(order.clientOrderId).toBe(UNSAFE_ORDER_ID) + }) + it('falls back to the inactive orders for an order_index that no longer rests', async () => { overrideFetch((u) => { if (u.includes('/api/v1/accountActiveOrders')) { @@ -5693,6 +5771,7 @@ describe('LighterProvider — getFills logos and realized PnL', () => { // 50000 → realized PnL (50000 - 40000) × 1 = 10000. const REDUCING_TRADE = { trade_id: 9, + trade_id_str: '9', tx_hash: '0xfeed', type: 'trade', market_id: 0, @@ -5701,6 +5780,10 @@ describe('LighterProvider — getFills logos and realized PnL', () => { usd_amount: '50000', ask_id: 7, bid_id: 8, + ask_id_str: '7', + bid_id_str: '8', + ask_client_id_str: '0', + bid_client_id_str: '0', ask_account_id: 42, bid_account_id: 0, is_maker_ask: false, @@ -5771,6 +5854,7 @@ describe('LighterProvider — getFills unresolvable market rows', () => { const tradeRow = (tradeId: number, marketId: number) => ({ ...TRADES_RESPONSE.trades[0], trade_id: tradeId, + trade_id_str: String(tradeId), market_id: marketId, }) diff --git a/packages/perps-sdk-provider-lighter/src/contract.decimal.unit.spec.ts b/packages/perps-sdk-provider-lighter/src/contract.decimal.unit.spec.ts index 0e93702e..960fa105 100644 --- a/packages/perps-sdk-provider-lighter/src/contract.decimal.unit.spec.ts +++ b/packages/perps-sdk-provider-lighter/src/contract.decimal.unit.spec.ts @@ -249,7 +249,7 @@ const ACTIVE_ORDERS = { { order_index: 88, client_order_index: 0, - order_id: 'lt-88', + order_id: '88', client_order_id: '0', market_index: BTC_MARKET_ID, owner_account_index: ACCOUNT_INDEX, @@ -284,7 +284,7 @@ const ACTIVE_ORDERS = { { order_index: 89, client_order_index: 0, - order_id: 'lt-89', + order_id: '89', client_order_id: '0', market_index: BTC_MARKET_ID, owner_account_index: ACCOUNT_INDEX, @@ -323,6 +323,7 @@ const TRADES = { trades: [ { trade_id: 7, + trade_id_str: '7', tx_hash: '0xabc', type: 'trade', market_id: BTC_MARKET_ID, @@ -331,6 +332,10 @@ const TRADES = { usd_amount: '0.047', ask_id: 100, bid_id: 200, + ask_id_str: '100', + bid_id_str: '200', + ask_client_id_str: '0', + bid_client_id_str: '0', ask_account_id: 0, bid_account_id: ACCOUNT_INDEX, is_maker_ask: false, diff --git a/packages/perps-sdk-provider-lighter/src/getOrders.unit.spec.ts b/packages/perps-sdk-provider-lighter/src/getOrders.unit.spec.ts index 23c8328d..17519b82 100644 --- a/packages/perps-sdk-provider-lighter/src/getOrders.unit.spec.ts +++ b/packages/perps-sdk-provider-lighter/src/getOrders.unit.spec.ts @@ -45,7 +45,7 @@ const MARKET: Market = { const order = (overrides: Partial = {}): LtOrder => ({ order_index: 88, client_order_index: 0, - order_id: 'lt-88', + order_id: '88', client_order_id: '0', market_index: 1, owner_account_index: 42, @@ -141,10 +141,16 @@ describe('Lighter getOrders lifecycle reads', () => { order(), order({ order_index: 90, + order_id: '90', filled_base_amount: '0', filled_quote_amount: '0', }), - order({ type: 'twap-sub', order_index: 89, parent_order_id: '88' }), + order({ + type: 'twap-sub', + order_index: 89, + order_id: '89', + parent_order_id: '88', + }), ]) const result = await provider.getOrders({ address: ADDRESS, marketId: '1' }) expect(result.orders).toMatchObject([ @@ -178,8 +184,13 @@ describe('Lighter getOrders lifecycle reads', () => { [], [ order({ status: 'filled' }), - order({ type: 'limit', order_index: 91, status: 'canceled-expired' }), - order({ order_index: 92, status: 'canceled' }), + order({ + type: 'limit', + order_index: 91, + order_id: '91', + status: 'canceled-expired', + }), + order({ order_index: 92, order_id: '92', status: 'canceled' }), ], 'next-page' ) @@ -213,8 +224,11 @@ describe('Lighter getOrders lifecycle reads', () => { it('combines active and terminal filters without retaining unrequested statuses', async () => { const { provider, requests } = await setup( - [order(), order({ order_index: 89, filled_base_amount: '0' })], - [order({ order_index: 90, status: 'filled' })] + [ + order(), + order({ order_index: 89, order_id: '89', filled_base_amount: '0' }), + ], + [order({ order_index: 90, order_id: '90', status: 'filled' })] ) const result = await provider.getOrders({ address: ADDRESS, @@ -231,7 +245,7 @@ describe('Lighter getOrders lifecycle reads', () => { it('does not repeat the active snapshot on a mixed history continuation page', async () => { const { provider, requests } = await setup( [order()], - [order({ order_index: 90, status: 'filled' })] + [order({ order_index: 90, order_id: '90', status: 'filled' })] ) const result = await provider.getOrders({ address: ADDRESS, @@ -287,7 +301,12 @@ describe('Lighter getOrders lifecycle reads', () => { [], [ order({ status: 'filled' }), - order({ order_index: 91, market_index: 999, status: 'filled' }), + order({ + order_index: 91, + order_id: '91', + market_index: 999, + status: 'filled', + }), ] ) const result = await provider.getOrders({ @@ -300,7 +319,14 @@ describe('Lighter getOrders lifecycle reads', () => { it('keeps a history row whose market is delisted', async () => { const { provider } = await setup( [], - [order({ order_index: 91, market_index: 2, status: 'filled' })], + [ + order({ + order_index: 91, + order_id: '91', + market_index: 2, + status: 'filled', + }), + ], '', [MARKET, { ...MARKET, id: '2', isDelisted: true }] ) @@ -324,7 +350,7 @@ describe('Lighter getOrders lifecycle reads', () => { it('uses the same order shape for list reads and single-order reads', async () => { const { provider } = await setup( [], - [order({ status: 'filled', client_order_index: 7 })] + [order({ status: 'filled', client_order_index: 7, client_order_id: '7' })] ) const params: ProviderGetOrdersParams = { address: ADDRESS, diff --git a/packages/perps-sdk-provider-lighter/src/types/fill.ts b/packages/perps-sdk-provider-lighter/src/types/fill.ts index 7e62e891..a4eb2d5b 100644 --- a/packages/perps-sdk-provider-lighter/src/types/fill.ts +++ b/packages/perps-sdk-provider-lighter/src/types/fill.ts @@ -16,6 +16,7 @@ */ export type LtTrade = { trade_id: number + trade_id_str: string tx_hash: string type: string market_id: number @@ -24,6 +25,10 @@ export type LtTrade = { usd_amount: string ask_id: number bid_id: number + ask_id_str: string + bid_id_str: string + ask_client_id_str: string + bid_client_id_str: string ask_account_id: number bid_account_id: number is_maker_ask: boolean diff --git a/packages/perps-sdk-provider-lighter/src/utils/mapFill.ts b/packages/perps-sdk-provider-lighter/src/utils/mapFill.ts index f579d43a..5b5720a1 100644 --- a/packages/perps-sdk-provider-lighter/src/utils/mapFill.ts +++ b/packages/perps-sdk-provider-lighter/src/utils/mapFill.ts @@ -161,8 +161,8 @@ export const mapFill = ( : tickToFeeAmount(trade.usd_amount, feeTick, integratorFeeTick) return { - id: trade.trade_id.toString(), - orderId: String(isBuyer ? trade.bid_id : trade.ask_id), + id: trade.trade_id_str, + orderId: isBuyer ? trade.bid_id_str : trade.ask_id_str, market, side: isBuyer ? OrderSide.BUY : OrderSide.SELL, type: OrderType.LIMIT, diff --git a/packages/perps-sdk-provider-lighter/src/utils/mapFill.unit.spec.ts b/packages/perps-sdk-provider-lighter/src/utils/mapFill.unit.spec.ts index e14f2fe1..f09b89cb 100644 --- a/packages/perps-sdk-provider-lighter/src/utils/mapFill.unit.spec.ts +++ b/packages/perps-sdk-provider-lighter/src/utils/mapFill.unit.spec.ts @@ -52,8 +52,14 @@ const PLACEHOLDER_TX_HASH = const SETTLED_TX_HASH = '3981a9639035409777f73feb18bb96c6c07fa55127863e58f2319691924a59b0e448ab560c1b135d' +// A market 33 order index above 2^53: `JSON.parse` rounds the numeric twin to +// ...276, so only the `_str` twin carries the exact id. +const UNSAFE_ORDER_ID = '9570149379440277' +const UNSAFE_ORDER_ID_PARSED = 9570149379440276 + const baseTrade = (overrides: Partial = {}): LtTrade => ({ trade_id: 7, + trade_id_str: '7', tx_hash: '0xabc', type: 'trade', market_id: 1, @@ -62,6 +68,10 @@ const baseTrade = (overrides: Partial = {}): LtTrade => ({ usd_amount: NOTIONAL, ask_id: 100, bid_id: 200, + ask_id_str: '100', + bid_id_str: '200', + ask_client_id_str: '0', + bid_client_id_str: '0', ask_account_id: 0, bid_account_id: ACCOUNT_INDEX, is_maker_ask: false, @@ -81,10 +91,17 @@ const baseTrade = (overrides: Partial = {}): LtTrade => ({ }) describe('mapFill (Lighter)', () => { - it('stringifies trade_id into Fill.id', () => { + it('reads Fill.id from trade_id_str, not the rounded trade_id', () => { expect( - mapValidFill(baseTrade({ trade_id: 7 }), ACCOUNT_INDEX, MARKET).id - ).toBe('7') + mapValidFill( + baseTrade({ + trade_id: UNSAFE_ORDER_ID_PARSED, + trade_id_str: UNSAFE_ORDER_ID, + }), + ACCOUNT_INDEX, + MARKET + ).id + ).toBe(UNSAFE_ORDER_ID) }) it('carries the resolved market identity onto the fill verbatim', () => { @@ -444,33 +461,35 @@ describe('mapFill (Lighter)', () => { }) }) - describe('orderId selects bid_id / ask_id by viewer side', () => { - it('buyer path: viewer on bid → orderId from bid_id', () => { + describe('orderId selects bid_id_str / ask_id_str by viewer side', () => { + it('buyer path: viewer on bid → orderId from bid_id_str', () => { const fill = mapValidFill( baseTrade({ bid_account_id: ACCOUNT_INDEX, ask_account_id: 0, - bid_id: 42, - ask_id: 99, + bid_id: UNSAFE_ORDER_ID_PARSED, + bid_id_str: UNSAFE_ORDER_ID, + ask_id_str: '99', }), ACCOUNT_INDEX, MARKET ) - expect(fill.orderId).toBe('42') + expect(fill.orderId).toBe(UNSAFE_ORDER_ID) }) - it('seller path: viewer on ask → orderId from ask_id', () => { + it('seller path: viewer on ask → orderId from ask_id_str', () => { const fill = mapValidFill( baseTrade({ ask_account_id: ACCOUNT_INDEX, bid_account_id: 0, - bid_id: 42, - ask_id: 77, + ask_id: UNSAFE_ORDER_ID_PARSED, + ask_id_str: UNSAFE_ORDER_ID, + bid_id_str: '42', }), ACCOUNT_INDEX, MARKET ) - expect(fill.orderId).toBe('77') + expect(fill.orderId).toBe(UNSAFE_ORDER_ID) }) }) diff --git a/packages/perps-sdk-provider-lighter/src/utils/mapOrder.ts b/packages/perps-sdk-provider-lighter/src/utils/mapOrder.ts index 2bdc2b78..2efb543e 100644 --- a/packages/perps-sdk-provider-lighter/src/utils/mapOrder.ts +++ b/packages/perps-sdk-provider-lighter/src/utils/mapOrder.ts @@ -20,6 +20,9 @@ import { LIGHTER_PROVIDER_KEY } from '../constants.js' import type { LtOrder } from '../types/index.js' import { rowTimestampToIsoStringOrUndefined } from './rowTimestamp.js' +// Lighter sends client index 0 as the string "0" for an order placed without one. +const NO_CLIENT_ORDER_ID = '0' + const mapOrderType = (type: string): Order['type'] => { switch (type.replace(/-/g, '_')) { case 'market': @@ -142,10 +145,10 @@ export const mapOrder = ( : undefined const status = mapOrderStatus(order) const base: OrderBase = { - orderId: String(order.order_index), - ...(order.client_order_index === 0 + orderId: order.order_id, + ...(order.client_order_id === NO_CLIENT_ORDER_ID ? {} - : { clientOrderId: String(order.client_order_index) }), + : { clientOrderId: order.client_order_id }), market, side: order.is_ask ? OrderSide.SELL : OrderSide.BUY, status, @@ -218,12 +221,12 @@ export const mapOrderUpdates = ( wsLog.droppedRow( LIGHTER_PROVIDER_KEY, 'order', - `${raw.order_index}: ${error.message}` + `${raw.order_id}: ${error.message}` ) continue } if (!isActiveOrderStatus(status)) { - terminated.push(String(raw.order_index)) + terminated.push(raw.order_id) } if (order !== undefined) { orders.push(order) diff --git a/packages/perps-sdk-provider-lighter/src/utils/mapOrder.unit.spec.ts b/packages/perps-sdk-provider-lighter/src/utils/mapOrder.unit.spec.ts index 19c36475..745cf182 100644 --- a/packages/perps-sdk-provider-lighter/src/utils/mapOrder.unit.spec.ts +++ b/packages/perps-sdk-provider-lighter/src/utils/mapOrder.unit.spec.ts @@ -29,10 +29,15 @@ const MARKET: MarketDisplay = { }, } +// A market 33 order index above 2^53: `JSON.parse` rounds the numeric twin to +// ...276, so only the string twin carries the exact id. +const UNSAFE_ORDER_ID = '9570149379440277' +const UNSAFE_ORDER_ID_PARSED = 9570149379440276 + const baseOrder = (overrides: Partial = {}): LtOrder => ({ order_index: 1, client_order_index: 0, - order_id: 'lt-1', + order_id: '1', client_order_id: '0', market_index: 1, owner_account_index: 42, @@ -67,13 +72,30 @@ const baseOrder = (overrides: Partial = {}): LtOrder => ({ describe('mapOrder (Lighter)', () => { it('separates the venue id and client id and omits an absent client id', () => { - const order = mapOrder(baseOrder({ client_order_index: 77 }), MARKET) + const order = mapOrder( + baseOrder({ client_order_index: 77, client_order_id: '77' }), + MARKET + ) expect(order?.orderId).toBe('1') expect(order?.clientOrderId).toBe('77') expect(mapOrder(baseOrder(), MARKET)).not.toHaveProperty('clientOrderId') expect(order).not.toHaveProperty('explorerLink') }) + it('reads orderId and clientOrderId from the string ids, not the rounded indexes', () => { + const order = mapOrder( + baseOrder({ + order_index: UNSAFE_ORDER_ID_PARSED, + order_id: UNSAFE_ORDER_ID, + client_order_index: UNSAFE_ORDER_ID_PARSED, + client_order_id: UNSAFE_ORDER_ID, + }), + MARKET + ) + expect(order?.orderId).toBe(UNSAFE_ORDER_ID) + expect(order?.clientOrderId).toBe(UNSAFE_ORDER_ID) + }) + it('maps a parent-dependent trigger with its parent id', () => { expect( mapOrder( @@ -384,9 +406,10 @@ describe('mapOrderUpdates (Lighter)', () => { const result = mapOrderUpdates( [ baseOrder(), - baseOrder({ order_index: 2, status: 'filled' }), + baseOrder({ order_index: 2, order_id: '2', status: 'filled' }), baseOrder({ order_index: 3, + order_id: '3', type: 'take-profit', trigger_status: 'parent-order', parent_order_id: '1', @@ -408,8 +431,8 @@ describe('mapOrderUpdates (Lighter)', () => { const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}) const result = mapOrderUpdates( [ - baseOrder({ order_index: 7, type: 'unknown' }), - baseOrder({ order_index: 8 }), + baseOrder({ order_index: 7, order_id: '7', type: 'unknown' }), + baseOrder({ order_index: 8, order_id: '8' }), ], () => MARKET ) @@ -438,12 +461,39 @@ describe('mapOrderUpdates (Lighter)', () => { it('skips rows with no registered market but retains terminal eviction ids', () => { expect( mapOrderUpdates( - [baseOrder(), baseOrder({ order_index: 2, status: 'filled' })], + [ + baseOrder(), + baseOrder({ order_index: 2, order_id: '2', status: 'filled' }), + ], () => undefined ) ).toEqual({ orders: [], terminated: ['2'] }) }) + it('evicts and warns with the string order id, not the rounded index', () => { + const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}) + const result = mapOrderUpdates( + [ + baseOrder({ + order_index: UNSAFE_ORDER_ID_PARSED, + order_id: UNSAFE_ORDER_ID, + status: 'filled', + }), + baseOrder({ + order_index: UNSAFE_ORDER_ID_PARSED, + order_id: UNSAFE_ORDER_ID, + type: 'unknown', + }), + ], + () => MARKET + ) + expect(result.terminated).toEqual([UNSAFE_ORDER_ID]) + expect(warn).toHaveBeenCalledWith( + `[lighter:ws] skipping order row: ${UNSAFE_ORDER_ID}: Unknown Lighter order type: unknown` + ) + warn.mockRestore() + }) + it('skips a pre-book row with no registered market and evicts nothing', () => { expect( mapOrderUpdates([baseOrder({ status: 'in-progress' })], () => undefined) diff --git a/packages/perps-sdk-provider-lighter/src/websocket/LighterWsProvider.unit.spec.ts b/packages/perps-sdk-provider-lighter/src/websocket/LighterWsProvider.unit.spec.ts index 04e299c6..8068dc90 100644 --- a/packages/perps-sdk-provider-lighter/src/websocket/LighterWsProvider.unit.spec.ts +++ b/packages/perps-sdk-provider-lighter/src/websocket/LighterWsProvider.unit.spec.ts @@ -12,6 +12,7 @@ import { } from '../constants.js' import { lighterProvider } from '../LighterProvider.js' import type { LtWsUserStatsMessage } from '../types/index.js' +import { mapFill, mapOrder } from '../utils/index.js' import { LighterWsProvider, lighterWsProvider } from './LighterWsProvider.js' type LighterWsProviderInternals = { @@ -43,8 +44,8 @@ const marketsFailureResponse = () => const RAW_ORDER = { order_index: 1, client_order_index: 0, - order_id: 'ord1', - client_order_id: '', + order_id: '1', + client_order_id: '0', market_index: 0, owner_account_index: 42, initial_base_amount: '1.0', @@ -72,6 +73,7 @@ const RAW_ORDER = { const RAW_TRADE = { trade_id: 1, + trade_id_str: '1', tx_hash: '0xabc', type: 'trade', market_id: 0, @@ -80,6 +82,10 @@ const RAW_TRADE = { usd_amount: '25000', ask_id: 1, bid_id: 2, + ask_id_str: '1', + bid_id_str: '2', + ask_client_id_str: '0', + bid_client_id_str: '0', ask_account_id: 99, bid_account_id: 42, is_maker_ask: true, @@ -115,6 +121,10 @@ const RAW_POSITION = { const TEST_ADDR = '0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef' const ACCOUNT_IDX = 42 +// A market 33 order index above 2^53: `JSON.parse` rounds the numeric twin to +// ...276, so only the string twin carries the exact id. +const UNSAFE_ORDER_ID = '9570149379440277' +const UNSAFE_ORDER_ID_PARSED = 9570149379440276 const LAST_FUNDING_PAYMENT_TIME = 1704067200000 const NEXT_FUNDING_PAYMENT_TIME = 1704070800000 @@ -508,6 +518,7 @@ describe('LighterWsProvider', () => { trades: [ { trade_id: 16164557907, + trade_id_str: '16164557907', size: '0.1336', price: '2181.83', is_maker_ask: false, @@ -815,7 +826,12 @@ describe('LighterWsProvider', () => { remaining_base_amount: '0.4', filled_base_amount: '0.6', }, - { ...RAW_ORDER, order_index: 2, status: 'canceled-expired' }, + { + ...RAW_ORDER, + order_index: 2, + order_id: '2', + status: 'canceled-expired', + }, ], }, }) @@ -845,10 +861,11 @@ describe('LighterWsProvider', () => { channel: `account_all_orders:${ACCOUNT_IDX}`, orders: { '99': [ - { ...RAW_ORDER, order_index: 3, market_index: 99 }, + { ...RAW_ORDER, order_index: 3, order_id: '3', market_index: 99 }, { ...RAW_ORDER, order_index: 4, + order_id: '4', market_index: 99, status: 'canceled-expired', }, @@ -869,7 +886,12 @@ describe('LighterWsProvider', () => { const listener = vi.fn() inject(p, `orderUpdates:${TEST_ADDR}`, listener) - const ethOrder = { ...RAW_ORDER, order_id: 'ord2', market_index: 1 } + const ethOrder = { + ...RAW_ORDER, + order_index: 2, + order_id: '2', + market_index: 1, + } ;(p as any).handleMessage( JSON.stringify({ type: 'update/account_all_orders', @@ -906,6 +928,69 @@ describe('LighterWsProvider', () => { p.close() }) + it('emits the exact ids above 2^53 that the REST mappers produce', async () => { + const p = makeProvider() + await seedAccountAndMarkets(p) + const fillListener = vi.fn() + const orderListener = vi.fn() + inject(p, `fills:${TEST_ADDR}`, fillListener) + inject(p, `orderUpdates:${TEST_ADDR}`, orderListener) + + // The wire carries the exact numeric twin; `JSON.parse` rounds it. + const toWire = (frame: unknown): string => + JSON.stringify(frame).replaceAll( + String(UNSAFE_ORDER_ID_PARSED), + UNSAFE_ORDER_ID + ) + const tradeFrame = toWire({ + type: 'update/account_all_trades', + channel: `account_all_trades:${ACCOUNT_IDX}`, + trades: { + '0': [ + { + ...RAW_TRADE, + trade_id: UNSAFE_ORDER_ID_PARSED, + trade_id_str: UNSAFE_ORDER_ID, + bid_id: UNSAFE_ORDER_ID_PARSED, + bid_id_str: UNSAFE_ORDER_ID, + }, + ], + }, + }) + const orderFrame = toWire({ + type: 'update/account_all_orders', + channel: `account_all_orders:${ACCOUNT_IDX}`, + orders: { + '0': [ + { + ...RAW_ORDER, + order_index: UNSAFE_ORDER_ID_PARSED, + order_id: UNSAFE_ORDER_ID, + client_order_index: UNSAFE_ORDER_ID_PARSED, + client_order_id: UNSAFE_ORDER_ID, + }, + ], + }, + }) + const internals = p as unknown as LighterWsProviderInternals + internals.handleMessage(tradeFrame) + internals.handleMessage(orderFrame) + + const [fill] = fillListener.mock.calls[0][0].data + expect(fill.id).toBe(UNSAFE_ORDER_ID) + expect(fill.orderId).toBe(UNSAFE_ORDER_ID) + expect(fill).toEqual( + mapFill(JSON.parse(tradeFrame).trades['0'][0], ACCOUNT_IDX, fill.market) + ) + const [order] = orderListener.mock.calls[0][0].data.orders + expect(order.orderId).toBe(UNSAFE_ORDER_ID) + expect(order.clientOrderId).toBe(UNSAFE_ORDER_ID) + expect(order).toEqual( + mapOrder(JSON.parse(orderFrame).orders['0'][0], order.market) + ) + p.close() + }) + it('emits fills with empty array on initial subscribed snapshot (trades: [])', async () => { const p = makeProvider() await seedAccountAndMarkets(p)