diff --git a/core/src/exchanges/limitless/client.ts b/core/src/exchanges/limitless/client.ts index 93488fb1..0ba431af 100644 --- a/core/src/exchanges/limitless/client.ts +++ b/core/src/exchanges/limitless/client.ts @@ -1,6 +1,7 @@ import { HttpClient, OrderClient, OrderBuilder, OrderSigner, MarketFetcher, Side, OrderType } from '@limitless-exchange/sdk'; -import { Wallet, providers, Contract, utils } from 'ethers'; +import { Wallet, providers, Contract } from 'ethers'; import { LIMITLESS_RPC_URL } from './config'; +import { scaledIntegerToNumber } from './utils'; const DEFAULT_LIMITLESS_API_URL = process.env.LIMITLESS_BASE_URL || 'https://api.limitless.exchange'; @@ -397,6 +398,6 @@ export class LimitlessClient { const balance = await contract.balanceOf(this.signer.address); const decimals = await contract.decimals(); // Should be 6 - return parseFloat(utils.formatUnits(balance, decimals)); + return scaledIntegerToNumber(balance, Number(decimals)); } } diff --git a/core/src/exchanges/limitless/index.ts b/core/src/exchanges/limitless/index.ts index f081d575..36ed5e72 100644 --- a/core/src/exchanges/limitless/index.ts +++ b/core/src/exchanges/limitless/index.ts @@ -36,7 +36,7 @@ import { LIMITLESS_RPC_URL } from './config'; import { limitlessErrorMapper } from './errors'; import { LimitlessFetcher } from './fetcher'; import { LimitlessNormalizer } from './normalizer'; -import { DEFAULT_LIMITLESS_API_URL } from './utils'; +import { DEFAULT_LIMITLESS_API_URL, scaledIntegerToNumber } from './utils'; import { LimitlessWebSocket, LimitlessWebSocketConfig } from './websocket'; import { logger } from '../../utils/logger'; @@ -582,7 +582,7 @@ export class LimitlessExchange extends PredictionMarketExchange { ); const rawBalance = await usdcContract.balanceOf(targetAddress); const USDC_DECIMALS = 6; - const total = parseFloat(rawBalance.toString()) / Math.pow(10, USDC_DECIMALS); + const total = scaledIntegerToNumber(rawBalance, USDC_DECIMALS); return [{ currency: 'USDC', diff --git a/core/src/exchanges/limitless/utils.ts b/core/src/exchanges/limitless/utils.ts index 78e2b4ae..bfe8c683 100644 --- a/core/src/exchanges/limitless/utils.ts +++ b/core/src/exchanges/limitless/utils.ts @@ -3,6 +3,26 @@ import { addBinaryOutcomes } from '../../utils/market-utils'; export const DEFAULT_LIMITLESS_API_URL = 'https://api.limitless.exchange'; +export function scaledIntegerToNumber(value: bigint | { toBigInt?: () => bigint; toString(): string }, decimals: number): number { + if (!Number.isInteger(decimals) || decimals < 0) { + throw new Error(`[limitless] Invalid token decimals: ${decimals}`); + } + + const raw = typeof value === 'bigint' + ? value + : typeof value.toBigInt === 'function' + ? value.toBigInt() + : BigInt(value.toString()); + const sign = raw < 0n ? -1 : 1; + const abs = raw < 0n ? -raw : raw; + const scale = 10n ** BigInt(decimals); + const whole = abs / scale; + const fraction = abs % scale; + const amount = Number(whole) + (Number(fraction) / Number(scale)); + + return sign * amount; +} + export interface LimitlessMarketContext { eventId?: string; eventTitle?: string; diff --git a/core/test/unit/limitless-balance.core.test.ts b/core/test/unit/limitless-balance.core.test.ts new file mode 100644 index 00000000..c62925b3 --- /dev/null +++ b/core/test/unit/limitless-balance.core.test.ts @@ -0,0 +1,18 @@ +import { BigNumber } from 'ethers'; +import { scaledIntegerToNumber } from '../../src/exchanges/limitless/utils'; + +describe('Limitless balance conversion', () => { + test('scales raw integer balances before converting to number', () => { + const raw = 9007199254740993n; + const legacy = parseFloat(raw.toString()) / 1_000_000; + + expect(legacy).toBe(9007199254.740992); + expect(scaledIntegerToNumber(raw, 6)).toBe(9007199254.740993); + }); + + test('accepts ethers BigNumber balances without formatUnits parsing', () => { + const raw = BigNumber.from('1234567890123'); + + expect(scaledIntegerToNumber(raw, 6)).toBe(1234567.890123); + }); +});