From 6c635816e6e97edc873dd87bb0149dd2f06ab4de Mon Sep 17 00:00:00 2001 From: Arthur Breton Date: Mon, 31 Aug 2026 10:23:35 +0800 Subject: [PATCH 1/2] fix(perps-controller): harden Lighter data boundaries --- .../src/providers/LighterProvider.ts | 69 ++++++++++++++----- .../src/services/LighterClientService.ts | 16 +++-- .../src/utils/lighterAdapter.ts | 23 ++++++- .../src/providers/LighterProvider.test.ts | 55 +++++++++++++++ .../src/services/LighterClientService.test.ts | 29 ++++++++ .../tests/src/utils/lighterAdapter.test.ts | 13 ++++ 6 files changed, 181 insertions(+), 24 deletions(-) diff --git a/packages/perps-controller/src/providers/LighterProvider.ts b/packages/perps-controller/src/providers/LighterProvider.ts index c933a87a7ce..7116a3c50b6 100644 --- a/packages/perps-controller/src/providers/LighterProvider.ts +++ b/packages/perps-controller/src/providers/LighterProvider.ts @@ -181,6 +181,29 @@ const NOOP_UNSUBSCRIBE = (): void => undefined; const LIGHTER_INACTIVE_HISTORY_ROW_LIMIT = 10_000; +const deriveLighterMaxLeverage = ( + minInitialMarginFraction: number | undefined, + market: string | number, +): number => { + if ( + typeof minInitialMarginFraction !== 'number' || + !Number.isFinite(minInitialMarginFraction) || + minInitialMarginFraction <= 0 || + minInitialMarginFraction > 10_000 + ) { + throw new Error( + `${LIGHTER_DATA_INTEGRITY_PREFIX} invalid initial margin fraction for market ${String(market)}`, + ); + } + const maxLeverage = Math.floor(10_000 / minInitialMarginFraction); + if (maxLeverage < 1) { + throw new Error( + `${LIGHTER_DATA_INTEGRITY_PREFIX} invalid max leverage for market ${String(market)}`, + ); + } + return maxLeverage; +}; + const adaptLighterTransferDelta = ( entry: LighterTransferHistoryItem, ): RawLedgerUpdate['delta'] => { @@ -4681,19 +4704,14 @@ export class LighterProvider implements PerpsProvider { .filter((market) => market.marketType === 'perp') .map((market) => { const margins = this.#marginBySymbol.get(market.symbol); - if ( - !margins || - !Number.isFinite(margins.minInitial) || - !margins.minInitial || - margins.minInitial <= 0 - ) { + if (!margins) { throw new Error( `${LIGHTER_DATA_INTEGRITY_PREFIX} missing authoritative leverage for ${market.symbol}`, ); } const adapted = adaptMarketFromLighter( market, - Math.floor(10_000 / margins.minInitial), + deriveLighterMaxLeverage(margins.minInitial, market.symbol), ); // minBaseAmount and minQuoteAmount are maker-only. Market and // IOC orders can use one base-size tick, so the public universal @@ -7309,13 +7327,12 @@ export class LighterProvider implements PerpsProvider { [...this.#marginBySymbol.values()].find( (entry) => entry.marketId === marketId, ); - const minInitial = metadata?.minInitial; - if (!minInitial || !(minInitial > 0)) { + if (!metadata) { throw new Error( `${LIGHTER_DATA_INTEGRITY_PREFIX} margin metadata unavailable for market ${marketId}`, ); } - return Math.floor(10_000 / minInitial); + return deriveLighterMaxLeverage(metadata.minInitial, marketId); }; /** @@ -7334,11 +7351,15 @@ export class LighterProvider implements PerpsProvider { } catch { return null; } - const minInitial = this.#marginBySymbol.get(symbol)?.minInitial; - if (typeof minInitial !== 'number' || !(minInitial > 0)) { + const metadata = this.#marginBySymbol.get(symbol); + if (!metadata) { + return null; + } + try { + return deriveLighterMaxLeverage(metadata.minInitial, symbol); + } catch { return null; } - return Math.floor(10_000 / minInitial); }; /** When the margin-metadata cache was last refreshed (0 = never). */ @@ -7375,6 +7396,22 @@ export class LighterProvider implements PerpsProvider { // The timestamp only advances on success. const fresh = new Map(); for (const detail of details.orderBookDetails) { + if (detail.minInitialMarginFraction !== undefined) { + deriveLighterMaxLeverage( + detail.minInitialMarginFraction, + detail.symbol, + ); + } + if ( + detail.maintenanceMarginFraction !== undefined && + (!Number.isFinite(detail.maintenanceMarginFraction) || + detail.maintenanceMarginFraction <= 0 || + detail.maintenanceMarginFraction > 10_000) + ) { + throw new Error( + `${LIGHTER_DATA_INTEGRITY_PREFIX} invalid maintenance margin fraction for market ${detail.symbol}`, + ); + } fresh.set(detail.symbol, { marketId: detail.marketId, minInitial: detail.minInitialMarginFraction, @@ -7400,9 +7437,9 @@ export class LighterProvider implements PerpsProvider { // (hundredths of a percent): 400 → 25x. Missing metadata is unavailable, // never evidence that the global maximum applies to this market. await this.#ensureMarketMargins(); - const minInitial = this.#marginBySymbol.get(asset)?.minInitial; - if (minInitial && minInitial > 0) { - return Math.floor(10_000 / minInitial); + const metadata = this.#marginBySymbol.get(asset); + if (metadata) { + return deriveLighterMaxLeverage(metadata.minInitial, asset); } throw new Error( `${LIGHTER_DATA_INTEGRITY_PREFIX} margin metadata unavailable for ${asset}`, diff --git a/packages/perps-controller/src/services/LighterClientService.ts b/packages/perps-controller/src/services/LighterClientService.ts index 0a4985e5320..f170118e498 100644 --- a/packages/perps-controller/src/services/LighterClientService.ts +++ b/packages/perps-controller/src/services/LighterClientService.ts @@ -113,9 +113,13 @@ const NonNegativeFiniteNumberStruct = define( 'non-negative finite number', (value) => typeof value === 'number' && Number.isFinite(value) && value >= 0, ); -const PositiveFiniteNumberStruct = define( - 'positive finite number', - (value) => typeof value === 'number' && Number.isFinite(value) && value > 0, +const MarginFractionStruct = define( + 'margin fraction in (0, 10000]', + (value) => + typeof value === 'number' && + Number.isFinite(value) && + value > 0 && + value <= 10_000, ); const NonNegativeFinancialNumberStruct = union([ NonNegativeFiniteNumberStruct, @@ -168,9 +172,9 @@ const MarketStruct = type({ const MarketDetailStruct = type({ ...MarketStruct.schema, lastTradePrice: NonNegativeFiniteNumberStruct, - defaultInitialMarginFraction: optional(PositiveFiniteNumberStruct), - minInitialMarginFraction: optional(PositiveFiniteNumberStruct), - maintenanceMarginFraction: optional(PositiveFiniteNumberStruct), + defaultInitialMarginFraction: optional(MarginFractionStruct), + minInitialMarginFraction: optional(MarginFractionStruct), + maintenanceMarginFraction: optional(MarginFractionStruct), dailyTradesCount: NonNegativeFiniteNumberStruct, dailyBaseTokenVolume: NonNegativeFiniteNumberStruct, dailyQuoteTokenVolume: NonNegativeFiniteNumberStruct, diff --git a/packages/perps-controller/src/utils/lighterAdapter.ts b/packages/perps-controller/src/utils/lighterAdapter.ts index be6145574d9..6ba035d9781 100644 --- a/packages/perps-controller/src/utils/lighterAdapter.ts +++ b/packages/perps-controller/src/utils/lighterAdapter.ts @@ -317,6 +317,21 @@ export function adaptFillFromLighterTrade( symbol: string, accountIndex: number, ): OrderFill { + const identifiers = [ + ['trade id', trade.tradeId], + ['market id', trade.marketId], + ['ask id', trade.askId], + ['bid id', trade.bidId], + ['ask account id', trade.askAccountId], + ['bid account id', trade.bidAccountId], + ] as const; + for (const [field, value] of identifiers) { + if (!Number.isSafeInteger(value) || value < 0) { + throw new Error( + `${LIGHTER_DATA_INTEGRITY_PREFIX} trade ${String(trade.tradeId)} has invalid ${field}`, + ); + } + } const accountIsAsk = trade.askAccountId === accountIndex; const accountIsBid = trade.bidAccountId === accountIndex; if (accountIsAsk === accountIsBid) { @@ -341,7 +356,7 @@ export function adaptFillFromLighterTrade( `${LIGHTER_DATA_INTEGRITY_PREFIX} trade ${trade.tradeId} has invalid timestamp`, ); } - if (trade.isMakerAsk === undefined) { + if (typeof trade.isMakerAsk !== 'boolean') { throw new Error( `${LIGHTER_DATA_INTEGRITY_PREFIX} trade ${trade.tradeId} is missing maker-role context`, ); @@ -357,10 +372,14 @@ export function adaptFillFromLighterTrade( // against a Premium account must keep their valid fill. const ourFee = accountIsMaker ? trade.makerFee : trade.takerFee; let ourFeeNumeric: number | null = 0; - if (typeof ourFee === 'number') { + if (ourFee === undefined) { + ourFeeNumeric = 0; + } else if (typeof ourFee === 'number') { ourFeeNumeric = ourFee; } else if (typeof ourFee === 'string') { ourFeeNumeric = parseLighterStrictDecimal(ourFee); + } else { + ourFeeNumeric = null; } if ( ourFeeNumeric === null || diff --git a/packages/perps-controller/tests/src/providers/LighterProvider.test.ts b/packages/perps-controller/tests/src/providers/LighterProvider.test.ts index 530193a27f6..b40939eca6c 100644 --- a/packages/perps-controller/tests/src/providers/LighterProvider.test.ts +++ b/packages/perps-controller/tests/src/providers/LighterProvider.test.ts @@ -1089,6 +1089,27 @@ describe('LighterProvider', () => { ).rejects.toThrow('Invalid Lighter venue data'); }); + it('rejects margin fractions that would derive leverage below 1x', async () => { + const built = buildProvider(); + built.clientInstance.getOrderBookDetails.mockResolvedValue({ + code: 200, + orderBookDetails: [ + { + ...BTC_MARKET, + lastTradePrice: 100000, + minInitialMarginFraction: 10001, + maintenanceMarginFraction: 10001, + }, + ], + }); + await expect(built.provider.getMarkets()).rejects.toThrow( + 'Invalid Lighter venue data', + ); + await expect(built.provider.getPositions()).rejects.toThrow( + 'Invalid Lighter venue data', + ); + }); + it('surfaces malformed successful order-book details instead of reporting no market data', async () => { const { provider, clientInstance } = buildProvider(); clientInstance.getOrderBookDetails.mockRejectedValue( @@ -4162,6 +4183,34 @@ describe('LighterProvider', () => { expect(built.clientInstance.getInactiveOrders).toHaveBeenCalledTimes(2); }); + it('fails closed when inactive history advances without returning rows', async () => { + const built = buildProvider(); + const venue = setupTriggerVenue(built.clientInstance, built.bridge); + venue.setCreateTerminal('filled'); + venue.failResponseOnce(14); + expect( + ( + await built.provider.updatePositionTPSL({ + symbol: 'BTC', + stopLossPrice: '85000', + }) + ).success, + ).toBe(false); + venue.setCreateTerminal('none'); + built.clientInstance.getInactiveOrders.mockResolvedValue({ + code: 200, + orders: [], + nextCursor: 'advanced', + }); + + const retry = await built.provider.updatePositionTPSL({ + symbol: 'BTC', + stopLossPrice: '86000', + }); + expect(retry.success).toBe(false); + expect(retry.error).toContain('advanced without returning rows'); + }); + it("exact status matching: 'unfilled' and 'execution-failed' are failures, never executions", async () => { for (const trickStatus of ['unfilled', 'execution-failed']) { const { provider, clientInstance, bridge } = buildProvider(); @@ -9781,6 +9830,12 @@ describe('LighterProvider', () => { it.each([ ['non-finite size', { size: '1e999' }], ['nonparticipant account', { ask_account_id: 7, bid_account_id: 8 }], + ['wrong maker role type', { is_maker_ask: 1 }], + ['null fee', { taker_fee: null }], + ['unsafe trade id', { trade_id: Number.MAX_SAFE_INTEGER + 1 }], + ['negative market id', { market_id: -1 }], + ['negative ask id', { ask_id: -1 }], + ['unsafe bid id', { bid_id: Number.MAX_SAFE_INTEGER + 1 }], ])('drops a WebSocket fill with %s', async (_scenario, overrides) => { const { provider, clientInstance } = buildProvider({ webSocketCtor: fakeStreamCtor, diff --git a/packages/perps-controller/tests/src/services/LighterClientService.test.ts b/packages/perps-controller/tests/src/services/LighterClientService.test.ts index 9013a420ac9..c940f2bf9fc 100644 --- a/packages/perps-controller/tests/src/services/LighterClientService.test.ts +++ b/packages/perps-controller/tests/src/services/LighterClientService.test.ts @@ -368,6 +368,35 @@ describe('LighterClientService', () => { service.getTransferHistory(28, 'auth-token'), ).rejects.toThrow('Invalid Lighter venue data'); }); + + it('rejects margin fractions above 10000', async () => { + fetchMock.mockResolvedValueOnce( + mockJsonResponse({ + code: 200, + order_book_details: [ + { + ...ORDER_BOOK, + last_trade_price: 100000, + default_initial_margin_fraction: 10001, + min_initial_margin_fraction: 10001, + maintenance_margin_fraction: 10001, + daily_trades_count: 1, + daily_base_token_volume: 1, + daily_quote_token_volume: 1, + daily_price_low: 1, + daily_price_high: 1, + daily_price_change: 0, + open_interest: 1, + daily_chart: {}, + }, + ], + }), + ); + const service = buildService(); + await expect(service.getOrderBookDetails()).rejects.toThrow( + 'Invalid Lighter venue data', + ); + }); }); describe('error handling', () => { diff --git a/packages/perps-controller/tests/src/utils/lighterAdapter.test.ts b/packages/perps-controller/tests/src/utils/lighterAdapter.test.ts index 360b5fadc48..3b0dee95042 100644 --- a/packages/perps-controller/tests/src/utils/lighterAdapter.test.ts +++ b/packages/perps-controller/tests/src/utils/lighterAdapter.test.ts @@ -564,6 +564,19 @@ describe('lighterAdapter', () => { 'Invalid Lighter venue data', ); }); + + it.each([ + ['wrong maker role type', { isMakerAsk: 1 }], + ['null fee', { takerFee: null }], + ['unsafe trade id', { tradeId: Number.MAX_SAFE_INTEGER + 1 }], + ['negative market id', { marketId: -1 }], + ['negative ask id', { askId: -1 }], + ['unsafe bid id', { bidId: Number.MAX_SAFE_INTEGER + 1 }], + ])('rejects %s at the raw fill boundary', (_field, overrides) => { + expect(() => + adaptFillFromLighterTrade({ ...REAL_TRADE, ...overrides }, 'SOL', 28), + ).toThrow('Invalid Lighter venue data'); + }); }); describe('adaptOrderFromLighter', () => { From 6f8126de96d329fd74e1dddbab24e824dce014fe Mon Sep 17 00:00:00 2001 From: Arthur Breton Date: Mon, 31 Aug 2026 10:46:04 +0800 Subject: [PATCH 2/2] fix(perps-controller): validate Lighter margins and fees --- .../src/providers/LighterProvider.ts | 10 ++-- .../src/services/LighterClientService.ts | 4 +- .../src/utils/lighterAdapter.ts | 59 ++++++++++++------- .../src/providers/LighterProvider.test.ts | 25 ++++++++ .../src/services/LighterClientService.test.ts | 31 ++++++++++ .../tests/src/utils/lighterAdapter.test.ts | 13 ++++ 6 files changed, 113 insertions(+), 29 deletions(-) diff --git a/packages/perps-controller/src/providers/LighterProvider.ts b/packages/perps-controller/src/providers/LighterProvider.ts index 7116a3c50b6..47aa0e27d5a 100644 --- a/packages/perps-controller/src/providers/LighterProvider.ts +++ b/packages/perps-controller/src/providers/LighterProvider.ts @@ -187,8 +187,8 @@ const deriveLighterMaxLeverage = ( ): number => { if ( typeof minInitialMarginFraction !== 'number' || - !Number.isFinite(minInitialMarginFraction) || - minInitialMarginFraction <= 0 || + !Number.isSafeInteger(minInitialMarginFraction) || + minInitialMarginFraction < 1 || minInitialMarginFraction > 10_000 ) { throw new Error( @@ -196,7 +196,7 @@ const deriveLighterMaxLeverage = ( ); } const maxLeverage = Math.floor(10_000 / minInitialMarginFraction); - if (maxLeverage < 1) { + if (!Number.isSafeInteger(maxLeverage) || maxLeverage < 1) { throw new Error( `${LIGHTER_DATA_INTEGRITY_PREFIX} invalid max leverage for market ${String(market)}`, ); @@ -7404,8 +7404,8 @@ export class LighterProvider implements PerpsProvider { } if ( detail.maintenanceMarginFraction !== undefined && - (!Number.isFinite(detail.maintenanceMarginFraction) || - detail.maintenanceMarginFraction <= 0 || + (!Number.isSafeInteger(detail.maintenanceMarginFraction) || + detail.maintenanceMarginFraction < 1 || detail.maintenanceMarginFraction > 10_000) ) { throw new Error( diff --git a/packages/perps-controller/src/services/LighterClientService.ts b/packages/perps-controller/src/services/LighterClientService.ts index f170118e498..5525e67ac57 100644 --- a/packages/perps-controller/src/services/LighterClientService.ts +++ b/packages/perps-controller/src/services/LighterClientService.ts @@ -117,8 +117,8 @@ const MarginFractionStruct = define( 'margin fraction in (0, 10000]', (value) => typeof value === 'number' && - Number.isFinite(value) && - value > 0 && + Number.isSafeInteger(value) && + value >= 1 && value <= 10_000, ); const NonNegativeFinancialNumberStruct = union([ diff --git a/packages/perps-controller/src/utils/lighterAdapter.ts b/packages/perps-controller/src/utils/lighterAdapter.ts index 6ba035d9781..3752b3e94e3 100644 --- a/packages/perps-controller/src/utils/lighterAdapter.ts +++ b/packages/perps-controller/src/utils/lighterAdapter.ts @@ -312,6 +312,38 @@ export function deriveLighterFillDirection(context: { return isBuy ? 'Buy' : 'Sell'; } +/** + * Parse one optional venue fee without treating malformed supplied values as + * an omitted zero. + * + * @param value - Raw maker or taker fee. + * @param tradeId - Trade identity for error context. + * @param role - Fee role for error context. + * @returns The finite nonnegative fee, with undefined mapped to venue-zero. + */ +function parseLighterTradeFee( + value: unknown, + tradeId: number, + role: 'maker' | 'taker', +): number { + let parsed: number | null; + if (value === undefined) { + parsed = 0; + } else if (typeof value === 'number') { + parsed = value; + } else if (typeof value === 'string') { + parsed = parseLighterStrictDecimal(value); + } else { + parsed = null; + } + if (parsed === null || !Number.isFinite(parsed) || parsed < 0) { + throw new Error( + `${LIGHTER_DATA_INTEGRITY_PREFIX} trade ${tradeId} has invalid ${role} fee`, + ); + } + return parsed; +} + export function adaptFillFromLighterTrade( trade: LighterRestTrade | LighterWsTrade, symbol: string, @@ -368,28 +400,11 @@ export function adaptFillFromLighterTrade( // Premium at account resolution) pay zero fees, so zero is venue truth. // A PRESENT nonzero fee ON OUR SIDE contradicts that gate and its wire // unit is unverified: refusing loudly beats silently coercing a real fee - // to $0. The counterparty's fee is irrelevant — a Standard user trading - // against a Premium account must keep their valid fill. - const ourFee = accountIsMaker ? trade.makerFee : trade.takerFee; - let ourFeeNumeric: number | null = 0; - if (ourFee === undefined) { - ourFeeNumeric = 0; - } else if (typeof ourFee === 'number') { - ourFeeNumeric = ourFee; - } else if (typeof ourFee === 'string') { - ourFeeNumeric = parseLighterStrictDecimal(ourFee); - } else { - ourFeeNumeric = null; - } - if ( - ourFeeNumeric === null || - !Number.isFinite(ourFeeNumeric) || - ourFeeNumeric < 0 - ) { - throw new Error( - `${LIGHTER_DATA_INTEGRITY_PREFIX} trade ${trade.tradeId} has invalid account fee`, - ); - } + // to $0. A valid nonzero counterparty fee is allowed, but malformed + // supplied counterparty data still invalidates the venue row. + const makerFee = parseLighterTradeFee(trade.makerFee, trade.tradeId, 'maker'); + const takerFee = parseLighterTradeFee(trade.takerFee, trade.tradeId, 'taker'); + const ourFeeNumeric = accountIsMaker ? makerFee : takerFee; if (ourFeeNumeric !== 0) { throw new Error( `${LIGHTER_UNSUPPORTED_CAPABILITY_PREFIX} nonzero fee in trade ${trade.tradeId}: fee unit is unverified`, diff --git a/packages/perps-controller/tests/src/providers/LighterProvider.test.ts b/packages/perps-controller/tests/src/providers/LighterProvider.test.ts index b40939eca6c..d6daafa2a46 100644 --- a/packages/perps-controller/tests/src/providers/LighterProvider.test.ts +++ b/packages/perps-controller/tests/src/providers/LighterProvider.test.ts @@ -1110,6 +1110,30 @@ describe('LighterProvider', () => { ); }); + it.each([Number.MIN_VALUE, 1.5])( + 'rejects non-integer margin fraction %p through market and position reads', + async (marginFraction) => { + const built = buildProvider(); + built.clientInstance.getOrderBookDetails.mockResolvedValue({ + code: 200, + orderBookDetails: [ + { + ...BTC_MARKET, + lastTradePrice: 100000, + minInitialMarginFraction: marginFraction, + maintenanceMarginFraction: marginFraction, + }, + ], + }); + await expect(built.provider.getMarkets()).rejects.toThrow( + 'Invalid Lighter venue data', + ); + await expect(built.provider.getPositions()).rejects.toThrow( + 'Invalid Lighter venue data', + ); + }, + ); + it('surfaces malformed successful order-book details instead of reporting no market data', async () => { const { provider, clientInstance } = buildProvider(); clientInstance.getOrderBookDetails.mockRejectedValue( @@ -9832,6 +9856,7 @@ describe('LighterProvider', () => { ['nonparticipant account', { ask_account_id: 7, bid_account_id: 8 }], ['wrong maker role type', { is_maker_ask: 1 }], ['null fee', { taker_fee: null }], + ['null counterparty fee', { maker_fee: null }], ['unsafe trade id', { trade_id: Number.MAX_SAFE_INTEGER + 1 }], ['negative market id', { market_id: -1 }], ['negative ask id', { ask_id: -1 }], diff --git a/packages/perps-controller/tests/src/services/LighterClientService.test.ts b/packages/perps-controller/tests/src/services/LighterClientService.test.ts index c940f2bf9fc..f7f5006f6cf 100644 --- a/packages/perps-controller/tests/src/services/LighterClientService.test.ts +++ b/packages/perps-controller/tests/src/services/LighterClientService.test.ts @@ -397,6 +397,37 @@ describe('LighterClientService', () => { 'Invalid Lighter venue data', ); }); + + it.each([Number.MIN_VALUE, 1.5])( + 'rejects non-integer margin fraction %p', + async (marginFraction) => { + fetchMock.mockResolvedValueOnce( + mockJsonResponse({ + code: 200, + order_book_details: [ + { + ...ORDER_BOOK, + last_trade_price: 100000, + default_initial_margin_fraction: marginFraction, + min_initial_margin_fraction: marginFraction, + maintenance_margin_fraction: marginFraction, + daily_trades_count: 1, + daily_base_token_volume: 1, + daily_quote_token_volume: 1, + daily_price_low: 1, + daily_price_high: 1, + daily_price_change: 0, + open_interest: 1, + daily_chart: {}, + }, + ], + }), + ); + await expect(buildService().getOrderBookDetails()).rejects.toThrow( + 'Invalid Lighter venue data', + ); + }, + ); }); describe('error handling', () => { diff --git a/packages/perps-controller/tests/src/utils/lighterAdapter.test.ts b/packages/perps-controller/tests/src/utils/lighterAdapter.test.ts index 3b0dee95042..3b00f4fd30f 100644 --- a/packages/perps-controller/tests/src/utils/lighterAdapter.test.ts +++ b/packages/perps-controller/tests/src/utils/lighterAdapter.test.ts @@ -547,6 +547,19 @@ describe('lighterAdapter', () => { expect(fill.direction).toBe('Close Long'); }); + it.each([null, false, 'not-a-fee', '1e999'])( + 'rejects malformed counterparty fee %p while preserving valid nonzero counterparty fees', + (makerFee) => { + expect(() => + adaptFillFromLighterTrade( + { ...REAL_TRADE, takerFee: 0, makerFee }, + 'SOL', + 28, + ), + ).toThrow('Invalid Lighter venue data'); + }, + ); + it.each([ ['size', { size: '1e999' }], ['price', { price: '75oops' }],