Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 31 additions & 6 deletions src/app/wallets/get-transactions-for-wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`,
Expand Down Expand Up @@ -118,24 +118,49 @@ 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({ err: usdtAmount, amount }, "Failed to parse IBEX USDT amount")
return 0 as UsdtMicros
Comment thread
islandbitcoin marked this conversation as resolved.
}
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 zeroSettlementMinorUnit(currency)
if (currency === WalletCurrency.Usd) return amount as UsdCents
if (currency === WalletCurrency.Usdt) return toUsdtMicros(amount)
return amount as Satoshis
}
Comment thread
islandbitcoin marked this conversation as resolved.

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 {
Expand Down
1 change: 1 addition & 0 deletions src/domain/shared/index.types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ type WalletDescriptor<T extends WalletCurrency> = PartialWalletDescriptor<T> & {

type BtcPaymentAmount = PaymentAmount<"BTC">
type UsdPaymentAmount = PaymentAmount<"USD">
type UsdtMicros = number & { readonly brand: unique symbol }

type RequireField<T, K extends keyof T> = T & Required<Pick<T, K>>

Expand Down
4 changes: 2 additions & 2 deletions src/domain/wallets/index.types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
95 changes: 92 additions & 3 deletions test/flash/unit/app/wallets/get-transactions-for-wallet.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -28,20 +29,108 @@ 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: 0.5,
networkFee: 0.000001,
currencyId: 29,
transactionTypeId: 2,
createdAt: "2026-05-13T00:00:00.000Z",
},
] as GResponse200)

expect(transaction.settlementCurrency).toBe(WalletCurrency.Usdt)
expect(transaction.settlementAmount).toBe(-500_000)
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",
amount: 19446,
currencyId: 29,
transactionTypeId: 1,
createdAt: "2026-05-13T00:00:00.000Z",
},
] as GResponse200)

expect(transaction.settlementCurrency).toBe(WalletCurrency.Usdt)
expect(transaction.settlementAmount).toBe(19446)
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([
{
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", () => {
Expand Down