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
128 changes: 78 additions & 50 deletions src/app/wallets/get-transactions-for-wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -14,37 +14,43 @@ export const getTransactionsForWallets = async ({
paginationArgs?: PaginationArgs
}): Promise<PartialResult<PaginatedArray<IbexTransaction>>> => {
const walletIds = wallets.map((wallet) => wallet.id)

const ibexCalls = await Promise.all(walletIds
.map(id => Ibex.getAccountTransactions({
account_id: id,
...toIbexPaginationArgs(paginationArgs)
}))

const ibexCalls = await Promise.all(
walletIds.map((id) =>
Ibex.getAccountTransactions({
account_id: id,
...toIbexPaginationArgs(paginationArgs),
}),
),
)

const transactions = ibexCalls.flatMap(resp => {
if (resp instanceof IbexError) return []
const transactions = ibexCalls.flatMap((resp) => {
if (resp instanceof IbexError) return []
else return toWalletTransactions(resp)
})

return PartialResult.ok({
slice: transactions,
total: transactions.length
total: transactions.length,
})
}

const currencyFromIbexCurrencyId = (currencyId: number | undefined): WalletCurrency | undefined => {
const currencyFromIbexCurrencyId = (
currencyId: number | undefined,
): WalletCurrency | undefined => {
if (currencyId === USDAmount.currencyId) return WalletCurrency.Usd
if (currencyId === USDTAmount.currencyId) return WalletCurrency.Usdt
return undefined
}

export const toWalletTransactions = (ibexResp: GResponse200): IbexTransaction[] => {
return ibexResp.map(trx => {
return ibexResp.map((trx) => {
const currency = currencyFromIbexCurrencyId(trx.currencyId)

if (!currency) {
baseLogger.error(`Failed to parse Ibex transaction currency. { WalletId: ${trx.accountId}, TransactionId: ${trx.id}, currencyId: ${trx.currencyId} }`)
baseLogger.error(
`Failed to parse Ibex transaction currency. { WalletId: ${trx.accountId}, TransactionId: ${trx.id}, currencyId: ${trx.currencyId} }`,
)
return {
walletId: (trx.accountId || "") as WalletId,
settlementAmount: 0 as Satoshis,
Expand All @@ -67,23 +73,31 @@ export const toWalletTransactions = (ibexResp: GResponse200): IbexTransaction[]
} as UnknownTypeTransaction
}

const settlementDisplayPrice: WalletMinorUnitDisplayPrice<WalletCurrency, DisplayCurrency> = {
base: trx.exchangeRateCurrencySats ? BigInt(Math.floor(trx.exchangeRateCurrencySats)) : 0n,
const settlementDisplayPrice: WalletMinorUnitDisplayPrice<
WalletCurrency,
DisplayCurrency
> = {
base: trx.exchangeRateCurrencySats
? BigInt(Math.floor(trx.exchangeRateCurrencySats))
: 0n,
offset: 0n, // what is this?
displayCurrency: "USD" as DisplayCurrency,
walletCurrency: currency
walletCurrency: currency,
}

const baseTrx: BaseWalletTransaction = {
walletId: (trx.accountId || "") as WalletId,
walletId: (trx.accountId || "") as WalletId,
settlementAmount: toSettlementAmount(trx.amount, trx.transactionTypeId, currency),
settlementFee: toSettlementMinorUnit(trx.networkFee, currency),
settlementCurrency: currency,
settlementDisplayAmount: `${trx.amount}`,
settlementDisplayFee: `${trx.networkFee}`,
settlementCurrency: currency,
settlementDisplayAmount: toSettlementDisplayAmount(
trx.amount,
trx.transactionTypeId,
),
settlementDisplayFee: `${trx.networkFee}`,
settlementDisplayPrice: settlementDisplayPrice,
createdAt: trx.createdAt ? new Date(trx.createdAt) : new Date(), // should always return
id: trx.id || "null", // "LedgerTransactionId" - this is likely unused
id: trx.id || "null", // "LedgerTransactionId" - this is likely unused
status: "success" as TxStatus, // assuming Ibex returns on completed
memo: null, // query transaction details
}
Expand All @@ -94,25 +108,28 @@ export const toWalletTransactions = (ibexResp: GResponse200): IbexTransaction[]
return {
...baseTrx,
// Ibex does not provide paymentHash, pubkey and preimage in transactions endpoint. To get these fields,
// we need to query the transaction details for each trx individually.
initiationVia: { type: 'lightning', paymentHash: "", pubkey: "" },
settlementVia: { type: 'lightning', revealedPreImage: undefined }
// we need to query the transaction details for each trx individually.
initiationVia: { type: "lightning", paymentHash: "", pubkey: "" },
settlementVia: { type: "lightning", revealedPreImage: undefined },
} as WalletLnSettledTransaction
case 3:
case 4:
case 10:
Comment thread
islandbitcoin marked this conversation as resolved.
return {
...baseTrx,
// Ibex does not provide paymentHash, pubkey and preimage in transactions endpoint. To get these fields,
// we need to query the transaction details for each trx individually.
initiationVia: { type: 'onchain', address: "" },
settlementVia: { type: 'onchain', transactionHash: '', vout: undefined }
// we need to query the transaction details for each trx individually.
initiationVia: { type: "onchain", address: "" },
settlementVia: { type: "onchain", transactionHash: "", vout: undefined },
} as WalletOnChainSettledTransaction // assuming Ibex only gives us settled
default:
baseLogger.error(`Failed to parse Ibex transaction type. { WalletId: ${baseTrx.walletId}, TransactionId: ${trx.id}, transactionTypeId: ${trx.transactionTypeId}`)
return {
baseLogger.error(
`Failed to parse Ibex transaction type. { WalletId: ${baseTrx.walletId}, TransactionId: ${trx.id}, transactionTypeId: ${trx.transactionTypeId}`,
)
return {
...baseTrx,
initiationVia: { type: 'unknown' },
settlementVia: { type: 'unknown' }
initiationVia: { type: "unknown" },
settlementVia: { type: "unknown" },
} as UnknownTypeTransaction
}
})
Expand All @@ -129,9 +146,7 @@ const toUsdtMicros = (amount: number): UsdtMicros => {
return Number(usdtAmount.asSmallestUnits()) as UsdtMicros
}

const zeroSettlementMinorUnit = (
currency: WalletCurrency,
): SettlementMinorUnitAmount => {
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
Expand All @@ -148,53 +163,66 @@ const toSettlementMinorUnit = (
}

const toSettlementAmount = (
ibexAmount: number | undefined,
transactionTypeId: number | undefined,
currency: WalletCurrency
ibexAmount: number | undefined,
transactionTypeId: number | undefined,
currency: WalletCurrency,
): SettlementMinorUnitAmount => {
if (ibexAmount === undefined) {
baseLogger.warn("Ibex did not return transaction amount")
return toSettlementMinorUnit(ibexAmount, currency)
}
// When sending, make negative
const amt = (transactionTypeId === 2 || transactionTypeId === 4)
? -1 * ibexAmount
: ibexAmount
const amt =
transactionTypeId === 2 || transactionTypeId === 4 || transactionTypeId === 10
Comment thread
islandbitcoin marked this conversation as resolved.
? -1 * ibexAmount
: ibexAmount
return toSettlementMinorUnit(amt, currency)
}

const toSettlementDisplayAmount = (
ibexAmount: number | undefined,
transactionTypeId: number | undefined,
): string => {
if (ibexAmount === undefined) return `${ibexAmount}`
const amount =
transactionTypeId === 2 || transactionTypeId === 4 || transactionTypeId === 10
? -1 * ibexAmount
: ibexAmount
return `${amount}`
}

enum SortOrder {
RECENT = "settledAt",
OLDEST = "-settledAt"
OLDEST = "-settledAt",
}

type IbexPaginationArgs = {
page?: number | undefined; // ibex default (0) start at page 0
limit?: number | undefined; // ibex default (0) returns all
sort?: SortOrder | undefined; // defaults to SortOrder.RECENT
page?: number | undefined // ibex default (0) start at page 0
limit?: number | undefined // ibex default (0) returns all
sort?: SortOrder | undefined // defaults to SortOrder.RECENT
}

export function toIbexPaginationArgs(
args: ConnectionArguments | undefined
args: ConnectionArguments | undefined,
): IbexPaginationArgs {
const DEFAULTS = {
page: 0,
limit: 0,
sort: SortOrder.RECENT,
page: 0,
limit: 0,
sort: SortOrder.RECENT,
}

// Prefer 'first' over 'last')
if (args && args.first != null) {
return {
...DEFAULTS,
limit: args.first,
sort: SortOrder.RECENT,
sort: SortOrder.RECENT,
}
} else if (args && args.last != null) {
return {
...DEFAULTS,
limit: args.last,
sort: SortOrder.OLDEST,
sort: SortOrder.OLDEST,
}
} else return DEFAULTS
}
23 changes: 23 additions & 0 deletions test/flash/unit/app/wallets/get-transactions-for-wallet.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,32 @@ describe("toWalletTransactions", () => {

expect(transaction.settlementCurrency).toBe(WalletCurrency.Usdt)
expect(transaction.settlementAmount).toBe(-500_000)
expect(transaction.settlementDisplayAmount).toBe("-0.5")
expect(transaction.settlementFee).toBe(1)
})

it("maps IBEX crypto send transaction type to outgoing on-chain USDT", () => {
const [transaction] = toWalletTransactions([
{
id: "crypto-send-trx-id",
accountId: "wallet-id",
amount: 2.5,
networkFee: 0.179554,
currencyId: 29,
transactionTypeId: 10,
createdAt: "2026-06-17T05:42:53.512218Z",
},
] as GResponse200)

expect(transaction.settlementCurrency).toBe(WalletCurrency.Usdt)
expect(transaction.settlementAmount).toBe(-2_500_000)
expect(transaction.settlementDisplayAmount).toBe("-2.5")
expect(transaction.settlementFee).toBe(179_554)
expect(transaction.settlementDisplayFee).toBe("0.179554")
expect(transaction.initiationVia.type).toBe("onchain")
expect(transaction.settlementVia.type).toBe("onchain")
})

it("defaults omitted IBEX USDT amount and network fee to zero micros", () => {
const [transaction] = toWalletTransactions([
{
Expand Down
Loading