From d8f494723cbb63fea3ce235157da23f6a0d101f7 Mon Sep 17 00:00:00 2001 From: Vandana Date: Tue, 2 Jun 2026 16:49:42 -0700 Subject: [PATCH 1/3] fix(wallets): preserve USDT transaction history precision --- .../wallets/get-transactions-for-wallet.ts | 31 +++++++--- src/domain/shared/index.types.d.ts | 1 + src/domain/wallets/index.types.d.ts | 4 +- .../get-transactions-for-wallet.spec.ts | 57 +++++++++++++++++-- 4 files changed, 80 insertions(+), 13 deletions(-) diff --git a/src/app/wallets/get-transactions-for-wallet.ts b/src/app/wallets/get-transactions-for-wallet.ts index 4d3bec459..e8583f34e 100644 --- a/src/app/wallets/get-transactions-for-wallet.ts +++ b/src/app/wallets/get-transactions-for-wallet.ts @@ -4,7 +4,7 @@ import Ibex from "@services/ibex/client" import { IbexError } from "@services/ibex/errors" import { baseLogger } from "@services/logger" import { GResponse200 } from "ibex-client" -import { ConnectionArguments, ConnectionCursor } from "graphql-relay" +import { ConnectionArguments } from "graphql-relay" export const getTransactionsForWallets = async ({ wallets, @@ -77,7 +77,7 @@ export const toWalletTransactions = (ibexResp: GResponse200): IbexTransaction[] const baseTrx: BaseWalletTransaction = { walletId: (trx.accountId || "") as WalletId, settlementAmount: toSettlementAmount(trx.amount, trx.transactionTypeId, currency), - settlementFee: asCurrency(trx.networkFee, currency), + settlementFee: toSettlementMinorUnit(trx.networkFee, currency), settlementCurrency: currency, settlementDisplayAmount: `${trx.amount}`, settlementDisplayFee: `${trx.networkFee}`, @@ -118,24 +118,41 @@ export const toWalletTransactions = (ibexResp: GResponse200): IbexTransaction[] }) } -const asCurrency = (amount: number | undefined, currency: WalletCurrency): Satoshis | UsdCents => { - return currency === "USD" ? amount as UsdCents : amount as Satoshis +type SettlementMinorUnitAmount = Satoshis | UsdCents | UsdtMicros + +const toUsdtMicros = (amount: number): UsdtMicros => { + const usdtAmount = USDTAmount.fromNumber(amount.toString()) + if (usdtAmount instanceof Error) { + baseLogger.error(`Failed to parse IBEX USDT amount. { amount: ${amount} }`) + return 0 as UsdtMicros + } + return Number(usdtAmount.asSmallestUnits()) as UsdtMicros +} + +const toSettlementMinorUnit = ( + amount: number | undefined, + currency: WalletCurrency, +): SettlementMinorUnitAmount => { + if (amount === undefined) return amount as unknown as SettlementMinorUnitAmount + if (currency === WalletCurrency.Usd) return amount as UsdCents + if (currency === WalletCurrency.Usdt) return toUsdtMicros(amount) + return amount as Satoshis } const toSettlementAmount = ( ibexAmount: number | undefined, transactionTypeId: number | undefined, currency: WalletCurrency -): Satoshis | UsdCents => { +): SettlementMinorUnitAmount => { if (ibexAmount === undefined) { baseLogger.warn("Ibex did not return transaction amount") - return asCurrency(ibexAmount, currency) + return toSettlementMinorUnit(ibexAmount, currency) } // When sending, make negative const amt = (transactionTypeId === 2 || transactionTypeId === 4) ? -1 * ibexAmount : ibexAmount - return asCurrency(amt, currency) + return toSettlementMinorUnit(amt, currency) } enum SortOrder { diff --git a/src/domain/shared/index.types.d.ts b/src/domain/shared/index.types.d.ts index 4e2015b95..a8e6d7ec1 100644 --- a/src/domain/shared/index.types.d.ts +++ b/src/domain/shared/index.types.d.ts @@ -49,6 +49,7 @@ type WalletDescriptor = PartialWalletDescriptor & { type BtcPaymentAmount = PaymentAmount<"BTC"> type UsdPaymentAmount = PaymentAmount<"USD"> +type UsdtMicros = number & { readonly brand: unique symbol } type RequireField = T & Required> diff --git a/src/domain/wallets/index.types.d.ts b/src/domain/wallets/index.types.d.ts index d86dcbc21..ea93eac15 100644 --- a/src/domain/wallets/index.types.d.ts +++ b/src/domain/wallets/index.types.d.ts @@ -71,8 +71,8 @@ type PartialBaseWalletTransaction = { type BaseWalletTransaction = { readonly walletId: WalletId | undefined - readonly settlementAmount: Satoshis | UsdCents - readonly settlementFee: Satoshis | UsdCents + readonly settlementAmount: Satoshis | UsdCents | UsdtMicros + readonly settlementFee: Satoshis | UsdCents | UsdtMicros readonly settlementCurrency: WalletCurrency readonly settlementDisplayAmount: DisplayCurrencyMajorAmount diff --git a/test/flash/unit/app/wallets/get-transactions-for-wallet.spec.ts b/test/flash/unit/app/wallets/get-transactions-for-wallet.spec.ts index 28790116e..6a5178f0b 100644 --- a/test/flash/unit/app/wallets/get-transactions-for-wallet.spec.ts +++ b/test/flash/unit/app/wallets/get-transactions-for-wallet.spec.ts @@ -28,20 +28,69 @@ const wallet = ({ id, currency }: { id: string; currency: WalletCurrency }): Wal }) as Wallet describe("toWalletTransactions", () => { - it("maps IBEX USDT currency id to USDT wallet currency", () => { + it("maps IBEX USDT currency id to USDT wallet currency with integer micros", () => { + const transactions = toWalletTransactions([ + { + id: "trx-id-1", + accountId: "wallet-id", + amount: 0.17531, + currencyId: 29, + transactionTypeId: 1, + createdAt: "2026-05-13T00:00:00.000Z", + }, + { + id: "trx-id-2", + accountId: "wallet-id", + amount: 9.824690376349, + currencyId: 29, + transactionTypeId: 1, + createdAt: "2026-05-13T00:00:00.000Z", + }, + ] as GResponse200) + + expect(transactions).toHaveLength(2) + expect(transactions[0].settlementCurrency).toBe(WalletCurrency.Usdt) + expect(transactions[0].settlementAmount).toBe(175_310) + expect(transactions[1].settlementAmount).toBe(9_824_690) + expect( + transactions.reduce((sum, transaction) => sum + transaction.settlementAmount, 0), + ).toBe(10_000_000) + }) + + it("maps IBEX USDT send amounts to negative integer micros", () => { const [transaction] = toWalletTransactions([ { id: "trx-id", accountId: "wallet-id", - amount: 19446, + amount: 0.5, + networkFee: 0.000001, currencyId: 29, - transactionTypeId: 1, + transactionTypeId: 2, createdAt: "2026-05-13T00:00:00.000Z", }, ] as GResponse200) expect(transaction.settlementCurrency).toBe(WalletCurrency.Usdt) - expect(transaction.settlementAmount).toBe(19446) + expect(transaction.settlementAmount).toBe(-500_000) + expect(transaction.settlementFee).toBe(1) + }) + + it("keeps IBEX USD amounts in integer cents", () => { + const [transaction] = toWalletTransactions([ + { + id: "trx-id", + accountId: "wallet-id", + amount: 500, + networkFee: 12, + currencyId: 3, + transactionTypeId: 1, + createdAt: "2026-05-13T00:00:00.000Z", + }, + ] as GResponse200) + + expect(transaction.settlementCurrency).toBe(WalletCurrency.Usd) + expect(transaction.settlementAmount).toBe(500) + expect(transaction.settlementFee).toBe(12) }) it("does not silently classify unknown IBEX currency ids as BTC", () => { From 13a9ef1bab76a0150bf8943197bfadc64c3f7a79 Mon Sep 17 00:00:00 2001 From: Vandana Date: Tue, 2 Jun 2026 17:27:58 -0700 Subject: [PATCH 2/3] chore(wallets): restore base relay import --- src/app/wallets/get-transactions-for-wallet.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/wallets/get-transactions-for-wallet.ts b/src/app/wallets/get-transactions-for-wallet.ts index e8583f34e..e96aa5e79 100644 --- a/src/app/wallets/get-transactions-for-wallet.ts +++ b/src/app/wallets/get-transactions-for-wallet.ts @@ -4,7 +4,7 @@ import Ibex from "@services/ibex/client" import { IbexError } from "@services/ibex/errors" import { baseLogger } from "@services/logger" import { GResponse200 } from "ibex-client" -import { ConnectionArguments } from "graphql-relay" +import { ConnectionArguments, ConnectionCursor } from "graphql-relay" export const getTransactionsForWallets = async ({ wallets, From 9f488e556b88b88de724b9a7aebb98c90b109fd5 Mon Sep 17 00:00:00 2001 From: Vandana Date: Wed, 3 Jun 2026 08:09:49 -0700 Subject: [PATCH 3/3] fix(wallets): handle missing IBEX USDT history amounts --- .../wallets/get-transactions-for-wallet.ts | 12 +++++- .../get-transactions-for-wallet.spec.ts | 40 +++++++++++++++++++ 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/src/app/wallets/get-transactions-for-wallet.ts b/src/app/wallets/get-transactions-for-wallet.ts index e96aa5e79..9b1d6fd47 100644 --- a/src/app/wallets/get-transactions-for-wallet.ts +++ b/src/app/wallets/get-transactions-for-wallet.ts @@ -123,17 +123,25 @@ type SettlementMinorUnitAmount = Satoshis | UsdCents | UsdtMicros const toUsdtMicros = (amount: number): UsdtMicros => { const usdtAmount = USDTAmount.fromNumber(amount.toString()) if (usdtAmount instanceof Error) { - baseLogger.error(`Failed to parse IBEX USDT amount. { amount: ${amount} }`) + baseLogger.error({ err: usdtAmount, amount }, "Failed to parse IBEX USDT amount") return 0 as UsdtMicros } return Number(usdtAmount.asSmallestUnits()) as UsdtMicros } +const zeroSettlementMinorUnit = ( + currency: WalletCurrency, +): SettlementMinorUnitAmount => { + if (currency === WalletCurrency.Usd) return 0 as UsdCents + if (currency === WalletCurrency.Usdt) return 0 as UsdtMicros + return 0 as Satoshis +} + const toSettlementMinorUnit = ( amount: number | undefined, currency: WalletCurrency, ): SettlementMinorUnitAmount => { - if (amount === undefined) return amount as unknown as SettlementMinorUnitAmount + if (amount === undefined) return zeroSettlementMinorUnit(currency) if (currency === WalletCurrency.Usd) return amount as UsdCents if (currency === WalletCurrency.Usdt) return toUsdtMicros(amount) return amount as Satoshis diff --git a/test/flash/unit/app/wallets/get-transactions-for-wallet.spec.ts b/test/flash/unit/app/wallets/get-transactions-for-wallet.spec.ts index 6a5178f0b..1ac0611fe 100644 --- a/test/flash/unit/app/wallets/get-transactions-for-wallet.spec.ts +++ b/test/flash/unit/app/wallets/get-transactions-for-wallet.spec.ts @@ -12,6 +12,7 @@ import { import { WalletCurrency } from "@domain/shared" import { WalletType } from "@domain/wallets" import Ibex from "@services/ibex/client" +import { baseLogger } from "@services/logger" import { GResponse200 } from "ibex-client" const accountId = "account-id" as AccountId @@ -75,6 +76,45 @@ describe("toWalletTransactions", () => { expect(transaction.settlementFee).toBe(1) }) + it("defaults omitted IBEX USDT amount and network fee to zero micros", () => { + const [transaction] = toWalletTransactions([ + { + id: "trx-id", + accountId: "wallet-id", + currencyId: 29, + transactionTypeId: 1, + createdAt: "2026-05-13T00:00:00.000Z", + }, + ] as GResponse200) + + expect(transaction.settlementCurrency).toBe(WalletCurrency.Usdt) + expect(transaction.settlementAmount).toBe(0) + expect(transaction.settlementFee).toBe(0) + }) + + it("logs USDT conversion errors with error details", () => { + const errorSpy = jest.spyOn(baseLogger, "error").mockImplementation() + + const [transaction] = toWalletTransactions([ + { + id: "trx-id", + accountId: "wallet-id", + amount: Number.NaN, + currencyId: 29, + transactionTypeId: 1, + createdAt: "2026-05-13T00:00:00.000Z", + }, + ] as GResponse200) + + expect(transaction.settlementAmount).toBe(0) + expect(errorSpy).toHaveBeenCalledWith( + expect.objectContaining({ err: expect.any(Error), amount: expect.any(Number) }), + "Failed to parse IBEX USDT amount", + ) + + errorSpy.mockRestore() + }) + it("keeps IBEX USD amounts in integer cents", () => { const [transaction] = toWalletTransactions([ {