From b87bfcd1539d35045551b1457ba478f8841041e2 Mon Sep 17 00:00:00 2001 From: Arvin <17693119+vindard@users.noreply.github.com> Date: Thu, 16 Nov 2023 07:34:19 -0400 Subject: [PATCH] fix(api): withdrawal volumes handling for failed payments (#3555) * refactor: nest existing facade tests * test: add passing volume tests * test: add failed-payment volume tests * fix: incorporate failed payment in withdrawal volume calc * test: add reimbursed-fee-payment volume tests * fix: incorporate fee reimbursements in withdrawal volume * test: add delayed-failed-payment volume tests * fix: add 'original_journal' timestamp check for failed payments --- core/api/src/domain/accounts/limits-volume.ts | 27 +- core/api/src/services/ledger/facade/volume.ts | 30 + core/api/test/helpers/ledger.ts | 11 +- .../services/ledger-facade.spec.ts | 890 +++++++++++------- 4 files changed, 614 insertions(+), 344 deletions(-) diff --git a/core/api/src/domain/accounts/limits-volume.ts b/core/api/src/domain/accounts/limits-volume.ts index 2beee3ce4a..1eb27e9a12 100644 --- a/core/api/src/domain/accounts/limits-volume.ts +++ b/core/api/src/domain/accounts/limits-volume.ts @@ -32,7 +32,21 @@ const WalletVolumesAggregator = ({ return volumeInUsdAmount } - return { outgoingUsdAmount } + const incomingUsdAmount = (): UsdPaymentAmount => { + let volumeInUsdAmount = ZERO_CENTS + for (const walletVolume of walletVolumes) { + const incomingUsdAmount = + walletVolume.incomingBaseAmount.currency === WalletCurrency.Btc + ? priceRatio.convertFromBtc(walletVolume.incomingBaseAmount as BtcPaymentAmount) + : (walletVolume.incomingBaseAmount as UsdPaymentAmount) + + volumeInUsdAmount = calc.add(volumeInUsdAmount, incomingUsdAmount) + } + + return volumeInUsdAmount + } + + return { outgoingUsdAmount, incomingUsdAmount } } export const AccountTxVolumeRemaining = ( @@ -73,10 +87,13 @@ export const AccountTxVolumeRemaining = ( priceRatio: WalletPriceRatio walletVolumes: TxBaseVolumeAmount[] }): Promise => { - const outgoingUsdVolumeAmount = WalletVolumesAggregator({ + const aggregator = WalletVolumesAggregator({ walletVolumes, priceRatio, - }).outgoingUsdAmount() + }) + + const outgoingUsdVolumeAmount = aggregator.outgoingUsdAmount() + const incomingUsdVolumeAmount = aggregator.incomingUsdAmount() const { withdrawalLimit: limit } = accountLimits const limitAmount = paymentAmountFromNumber({ @@ -87,11 +104,13 @@ export const AccountTxVolumeRemaining = ( addAttributesToCurrentSpan({ "txVolume.outgoingInBase": `${outgoingUsdVolumeAmount.amount}`, + "txVolume.incomingInBase": `${incomingUsdVolumeAmount.amount}`, "txVolume.threshold": `${limitAmount.amount}`, "txVolume.limitCheck": AccountLimitsType.Withdrawal, }) - return calc.sub(limitAmount, outgoingUsdVolumeAmount) + const netVolumeAmount = calc.sub(outgoingUsdVolumeAmount, incomingUsdVolumeAmount) + return calc.sub(limitAmount, netVolumeAmount) } const tradeIntraAccount = async ({ diff --git a/core/api/src/services/ledger/facade/volume.ts b/core/api/src/services/ledger/facade/volume.ts index 97b13fa29c..53d83bb02b 100644 --- a/core/api/src/services/ledger/facade/volume.ts +++ b/core/api/src/services/ledger/facade/volume.ts @@ -10,6 +10,7 @@ import { timestampDaysAgo } from "@/utils" import { paymentAmountFromNumber } from "@/domain/shared" import { addAttributesToCurrentSpan } from "@/services/tracing" +import { MS_PER_DAY } from "@/config" export const TxnGroups = { allPaymentVolumeSince: [ @@ -21,6 +22,7 @@ export const TxnGroups = { ], externalPaymentVolumeSince: [ LedgerTransactionType.Payment, + LedgerTransactionType.LnFeeReimbursement, LedgerTransactionType.OnchainPayment, ], intraledgerTxBaseVolumeSince: [ @@ -71,6 +73,34 @@ const TxVolumeAmountSinceFactory = () => { $and: [{ timestamp: { $gte: timestamp } }], }, }, + { + $lookup: { + from: "medici_transactions", + localField: "_original_journal", + foreignField: "_journal", + as: "original_transactions", + }, + }, + { + $addFields: { + is_transaction_valid: { + $or: [ + { $eq: [{ $size: "$original_transactions" }, 0] }, + { + $gte: [ + { $arrayElemAt: ["$original_transactions.datetime", 0] }, + new Date(Date.now() - MS_PER_DAY), + ], + }, + ], + }, + }, + }, + { + $match: { + is_transaction_valid: true, + }, + }, { $group: { _id: null, diff --git a/core/api/test/helpers/ledger.ts b/core/api/test/helpers/ledger.ts index ff644ec99a..505ffce7fe 100644 --- a/core/api/test/helpers/ledger.ts +++ b/core/api/test/helpers/ledger.ts @@ -171,9 +171,10 @@ export const recordSendLnPayment = async ({ bankFee, displayAmounts, }: RecordExternalTxTestArgs) => { + const paymentHash = crypto.randomUUID() as PaymentHash const { metadata, debitAccountAdditionalMetadata, internalAccountsAdditionalMetadata } = LedgerFacade.LnSendLedgerMetadata({ - paymentHash: crypto.randomUUID() as PaymentHash, + paymentHash, pubkey: crypto.randomUUID() as Pubkey, feeKnownInAdvance: true, paymentAmounts: { @@ -186,7 +187,7 @@ export const recordSendLnPayment = async ({ ...displayAmounts, }) - return LedgerFacade.recordSendOffChain({ + const recorded = await LedgerFacade.recordSendOffChain({ description: "sends bitcoin via ln", amountToDebitSender: paymentAmount, senderWalletDescriptor: walletDescriptor, @@ -195,6 +196,12 @@ export const recordSendLnPayment = async ({ additionalDebitMetadata: debitAccountAdditionalMetadata, additionalInternalMetadata: internalAccountsAdditionalMetadata, }) + if (recorded instanceof Error) throw recorded + + return { + ...recorded, + paymentHash, + } } export const recordSendOnChainPayment = async ({ diff --git a/core/api/test/integration/services/ledger-facade.spec.ts b/core/api/test/integration/services/ledger-facade.spec.ts index 012616fb67..47eece0035 100644 --- a/core/api/test/integration/services/ledger-facade.spec.ts +++ b/core/api/test/integration/services/ledger-facade.spec.ts @@ -1,11 +1,24 @@ import crypto from "crypto" -import { BtcWalletDescriptor, UsdWalletDescriptor, WalletCurrency } from "@/domain/shared" -import { LedgerTransactionType } from "@/domain/ledger" +import { MS_PER_DAY, ONE_DAY, getAccountLimits } from "@/config" + +import { + AmountCalculator, + BtcWalletDescriptor, + UsdWalletDescriptor, + WalletCurrency, + paymentAmountFromNumber, +} from "@/domain/shared" +import { AccountLevel, AccountTxVolumeRemaining } from "@/domain/accounts" import { UsdDisplayCurrency } from "@/domain/fiat" +import { LedgerTransactionType } from "@/domain/ledger" +import { WalletPriceRatio } from "@/domain/payments" import { CouldNotFindError } from "@/domain/errors" import { LedgerService } from "@/services/ledger" +import * as LedgerFacade from "@/services/ledger/facade" +import { Transaction, TransactionMetadata } from "@/services/ledger/schema" +import { toObjectId } from "@/services/mongoose/utils" import { createMandatoryUsers } from "test/helpers" import { @@ -24,22 +37,70 @@ import { recordWalletIdTradeIntraAccountTxn, } from "test/helpers/ledger" +let accountWalletDescriptors: AccountWalletDescriptors +let accountLimitsLevelOne: IAccountLimits +let accountLimitAmountsLevelOne: { + intraLedgerLimit: UsdPaymentAmount + withdrawalLimit: UsdPaymentAmount + tradeIntraAccountLimit: UsdPaymentAmount +} + +const calc = AmountCalculator() + beforeAll(async () => { await createMandatoryUsers() + + accountWalletDescriptors = { + BTC: BtcWalletDescriptor(crypto.randomUUID() as WalletId), + USD: UsdWalletDescriptor(crypto.randomUUID() as WalletId), + } + + accountLimitsLevelOne = getAccountLimits({ level: AccountLevel.One }) + + const intraLedgerLimitAmount = paymentAmountFromNumber({ + amount: accountLimitsLevelOne.intraLedgerLimit, + currency: WalletCurrency.Usd, + }) + if (intraLedgerLimitAmount instanceof Error) throw intraLedgerLimitAmount + + const withdrawalLimitAmount = paymentAmountFromNumber({ + amount: accountLimitsLevelOne.withdrawalLimit, + currency: WalletCurrency.Usd, + }) + if (withdrawalLimitAmount instanceof Error) throw withdrawalLimitAmount + + const tradeIntraAccountLimitAmount = paymentAmountFromNumber({ + amount: accountLimitsLevelOne.tradeIntraAccountLimit, + currency: WalletCurrency.Usd, + }) + if (tradeIntraAccountLimitAmount instanceof Error) throw tradeIntraAccountLimitAmount + + accountLimitAmountsLevelOne = { + intraLedgerLimit: intraLedgerLimitAmount, + withdrawalLimit: withdrawalLimitAmount, + tradeIntraAccountLimit: tradeIntraAccountLimitAmount, + } +}) + +afterEach(async () => { + await Transaction.deleteMany({}) + await TransactionMetadata.deleteMany({}) }) describe("Facade", () => { const receiveAmount = { usd: { amount: 100n, currency: WalletCurrency.Usd }, - btc: { amount: 200n, currency: WalletCurrency.Btc }, + btc: { amount: 300n, currency: WalletCurrency.Btc }, } + const sendAmount = { usd: { amount: 20n, currency: WalletCurrency.Usd }, - btc: { amount: 40n, currency: WalletCurrency.Btc }, + btc: { amount: 60n, currency: WalletCurrency.Btc }, } + const bankFee = { usd: { amount: 10n, currency: WalletCurrency.Usd }, - btc: { amount: 20n, currency: WalletCurrency.Btc }, + btc: { amount: 30n, currency: WalletCurrency.Btc }, } const displayReceiveUsdAmounts = { @@ -60,380 +121,533 @@ describe("Facade", () => { displayCurrency: "EUR" as DisplayCurrency, } - describe("recordReceive", () => { - it("recordReceiveLnPayment", async () => { - const btcWalletDescriptor = BtcWalletDescriptor(crypto.randomUUID() as WalletId) - - const res = await recordReceiveLnPayment({ - walletDescriptor: btcWalletDescriptor, - paymentAmount: receiveAmount, - bankFee, - displayAmounts: displayReceiveEurAmounts, + describe("record", () => { + describe("recordReceive", () => { + it("recordReceiveLnPayment", async () => { + const btcWalletDescriptor = BtcWalletDescriptor(crypto.randomUUID() as WalletId) + + const res = await recordReceiveLnPayment({ + walletDescriptor: btcWalletDescriptor, + paymentAmount: receiveAmount, + bankFee, + displayAmounts: displayReceiveEurAmounts, + }) + if (res instanceof Error) throw res + + const txns = await LedgerService().getTransactionsByWalletId( + btcWalletDescriptor.id, + ) + if (txns instanceof Error) throw txns + if (!(txns && txns.length)) throw new Error() + const txn = txns[0] + + expect(txn.type).toBe(LedgerTransactionType.Invoice) }) - if (res instanceof Error) throw res - const txns = await LedgerService().getTransactionsByWalletId(btcWalletDescriptor.id) - if (txns instanceof Error) throw txns - if (!(txns && txns.length)) throw new Error() - const txn = txns[0] - - expect(txn.type).toBe(LedgerTransactionType.Invoice) - }) - - it("recordReceiveOnChainPayment", async () => { - const btcWalletDescriptor = BtcWalletDescriptor(crypto.randomUUID() as WalletId) - - const res = await recordReceiveOnChainPayment({ - walletDescriptor: btcWalletDescriptor, - paymentAmount: receiveAmount, - bankFee, - displayAmounts: displayReceiveEurAmounts, + it("recordReceiveOnChainPayment", async () => { + const btcWalletDescriptor = BtcWalletDescriptor(crypto.randomUUID() as WalletId) + + const res = await recordReceiveOnChainPayment({ + walletDescriptor: btcWalletDescriptor, + paymentAmount: receiveAmount, + bankFee, + displayAmounts: displayReceiveEurAmounts, + }) + if (res instanceof Error) throw res + + const txns = await LedgerService().getTransactionsByWalletId( + btcWalletDescriptor.id, + ) + if (txns instanceof Error) throw txns + if (!(txns && txns.length)) throw new Error() + const txn = txns[0] + + expect(txn.type).toBe(LedgerTransactionType.OnchainReceipt) }) - if (res instanceof Error) throw res - - const txns = await LedgerService().getTransactionsByWalletId(btcWalletDescriptor.id) - if (txns instanceof Error) throw txns - if (!(txns && txns.length)) throw new Error() - const txn = txns[0] - expect(txn.type).toBe(LedgerTransactionType.OnchainReceipt) - }) - - it("recordLnFailedPayment", async () => { - const btcWalletDescriptor = BtcWalletDescriptor(crypto.randomUUID() as WalletId) - - const res = await recordLnFailedPayment({ - walletDescriptor: btcWalletDescriptor, - paymentAmount: receiveAmount, - bankFee, - displayAmounts: displayReceiveEurAmounts, + it("recordLnFailedPayment", async () => { + const btcWalletDescriptor = BtcWalletDescriptor(crypto.randomUUID() as WalletId) + + const res = await recordLnFailedPayment({ + walletDescriptor: btcWalletDescriptor, + paymentAmount: receiveAmount, + bankFee, + displayAmounts: displayReceiveEurAmounts, + }) + if (res instanceof Error) throw res + + const txns = await LedgerService().getTransactionsByWalletId( + btcWalletDescriptor.id, + ) + if (txns instanceof Error) throw txns + if (!(txns && txns.length)) throw new Error() + const txn = txns[0] + + expect(txn.type).toBe(LedgerTransactionType.Payment) }) - if (res instanceof Error) throw res - - const txns = await LedgerService().getTransactionsByWalletId(btcWalletDescriptor.id) - if (txns instanceof Error) throw txns - if (!(txns && txns.length)) throw new Error() - const txn = txns[0] - - expect(txn.type).toBe(LedgerTransactionType.Payment) - }) - it("recordLnFeeReimbursement", async () => { - const btcWalletDescriptor = BtcWalletDescriptor(crypto.randomUUID() as WalletId) - - const res = await recordLnFeeReimbursement({ - walletDescriptor: btcWalletDescriptor, - paymentAmount: receiveAmount, - bankFee, - displayAmounts: displayReceiveEurAmounts, + it("recordLnFeeReimbursement", async () => { + const btcWalletDescriptor = BtcWalletDescriptor(crypto.randomUUID() as WalletId) + + const res = await recordLnFeeReimbursement({ + walletDescriptor: btcWalletDescriptor, + paymentAmount: receiveAmount, + bankFee, + displayAmounts: displayReceiveEurAmounts, + }) + if (res instanceof Error) throw res + + const txns = await LedgerService().getTransactionsByWalletId( + btcWalletDescriptor.id, + ) + if (txns instanceof Error) throw txns + if (!(txns && txns.length)) throw new Error() + const txn = txns[0] + + expect(txn.type).toBe(LedgerTransactionType.LnFeeReimbursement) }) - if (res instanceof Error) throw res - - const txns = await LedgerService().getTransactionsByWalletId(btcWalletDescriptor.id) - if (txns instanceof Error) throw txns - if (!(txns && txns.length)) throw new Error() - const txn = txns[0] - - expect(txn.type).toBe(LedgerTransactionType.LnFeeReimbursement) }) - }) - - describe("recordSend", () => { - it("recordSendLnPayment", async () => { - const btcWalletDescriptor = BtcWalletDescriptor(crypto.randomUUID() as WalletId) - const res = await recordSendLnPayment({ - walletDescriptor: btcWalletDescriptor, - paymentAmount: sendAmount, - bankFee, - displayAmounts: displaySendEurAmounts, + describe("recordSend", () => { + it("recordSendLnPayment", async () => { + const btcWalletDescriptor = BtcWalletDescriptor(crypto.randomUUID() as WalletId) + + const res = await recordSendLnPayment({ + walletDescriptor: btcWalletDescriptor, + paymentAmount: sendAmount, + bankFee, + displayAmounts: displaySendEurAmounts, + }) + if (res instanceof Error) throw res + + const txns = await LedgerService().getTransactionsByWalletId( + btcWalletDescriptor.id, + ) + if (txns instanceof Error) throw txns + if (!(txns && txns.length)) throw new Error() + const txn = txns[0] + + expect(txn.type).toBe(LedgerTransactionType.Payment) }) - if (res instanceof Error) throw res - - const txns = await LedgerService().getTransactionsByWalletId(btcWalletDescriptor.id) - if (txns instanceof Error) throw txns - if (!(txns && txns.length)) throw new Error() - const txn = txns[0] - expect(txn.type).toBe(LedgerTransactionType.Payment) - }) - - it("recordSendOnChainPayment", async () => { - const btcWalletDescriptor = BtcWalletDescriptor(crypto.randomUUID() as WalletId) - - const res = await recordSendOnChainPayment({ - walletDescriptor: btcWalletDescriptor, - paymentAmount: sendAmount, - bankFee, - displayAmounts: displaySendEurAmounts, + it("recordSendOnChainPayment", async () => { + const btcWalletDescriptor = BtcWalletDescriptor(crypto.randomUUID() as WalletId) + + const res = await recordSendOnChainPayment({ + walletDescriptor: btcWalletDescriptor, + paymentAmount: sendAmount, + bankFee, + displayAmounts: displaySendEurAmounts, + }) + if (res instanceof Error) throw res + + const txns = await LedgerService().getTransactionsByWalletId( + btcWalletDescriptor.id, + ) + if (txns instanceof Error) throw txns + if (!(txns && txns.length)) throw new Error() + const txn = txns[0] + + expect(txn.type).toBe(LedgerTransactionType.OnchainPayment) }) - if (res instanceof Error) throw res - - const txns = await LedgerService().getTransactionsByWalletId(btcWalletDescriptor.id) - if (txns instanceof Error) throw txns - if (!(txns && txns.length)) throw new Error() - const txn = txns[0] - - expect(txn.type).toBe(LedgerTransactionType.OnchainPayment) }) - }) - describe("recordIntraledger", () => { - it("recordLnIntraLedgerPayment", async () => { - const btcWalletDescriptor = BtcWalletDescriptor(crypto.randomUUID() as WalletId) - const usdWalletDescriptor = UsdWalletDescriptor(crypto.randomUUID() as WalletId) - - const res = await recordLnIntraLedgerPayment({ - senderWalletDescriptor: btcWalletDescriptor, - recipientWalletDescriptor: usdWalletDescriptor, - paymentAmount: sendAmount, - senderDisplayAmounts: { - senderAmountDisplayCurrency: displaySendEurAmounts.amountDisplayCurrency, - senderFeeDisplayCurrency: displaySendEurAmounts.feeDisplayCurrency, - senderDisplayCurrency: displaySendEurAmounts.displayCurrency, - }, - recipientDisplayAmounts: { - recipientAmountDisplayCurrency: displayReceiveUsdAmounts.amountDisplayCurrency, - recipientFeeDisplayCurrency: displayReceiveUsdAmounts.feeDisplayCurrency, - recipientDisplayCurrency: displayReceiveUsdAmounts.displayCurrency, - }, + describe("recordIntraledger", () => { + it("recordLnIntraLedgerPayment", async () => { + const btcWalletDescriptor = BtcWalletDescriptor(crypto.randomUUID() as WalletId) + const usdWalletDescriptor = UsdWalletDescriptor(crypto.randomUUID() as WalletId) + + const res = await recordLnIntraLedgerPayment({ + senderWalletDescriptor: btcWalletDescriptor, + recipientWalletDescriptor: usdWalletDescriptor, + paymentAmount: sendAmount, + senderDisplayAmounts: { + senderAmountDisplayCurrency: displaySendEurAmounts.amountDisplayCurrency, + senderFeeDisplayCurrency: displaySendEurAmounts.feeDisplayCurrency, + senderDisplayCurrency: displaySendEurAmounts.displayCurrency, + }, + recipientDisplayAmounts: { + recipientAmountDisplayCurrency: + displayReceiveUsdAmounts.amountDisplayCurrency, + recipientFeeDisplayCurrency: displayReceiveUsdAmounts.feeDisplayCurrency, + recipientDisplayCurrency: displayReceiveUsdAmounts.displayCurrency, + }, + }) + if (res instanceof Error) throw res + + const senderTxns = await LedgerService().getTransactionsByWalletId( + btcWalletDescriptor.id, + ) + if (senderTxns instanceof Error) throw senderTxns + if (!(senderTxns && senderTxns.length)) throw new Error() + const senderTxn = senderTxns[0] + expect(senderTxn.type).toBe(LedgerTransactionType.LnIntraLedger) + + const recipientTxns = await LedgerService().getTransactionsByWalletId( + usdWalletDescriptor.id, + ) + if (recipientTxns instanceof Error) throw recipientTxns + if (!(recipientTxns && recipientTxns.length)) throw new Error() + const recipientTxn = recipientTxns[0] + expect(recipientTxn.type).toBe(LedgerTransactionType.LnIntraLedger) }) - if (res instanceof Error) throw res - const senderTxns = await LedgerService().getTransactionsByWalletId( - btcWalletDescriptor.id, - ) - if (senderTxns instanceof Error) throw senderTxns - if (!(senderTxns && senderTxns.length)) throw new Error() - const senderTxn = senderTxns[0] - expect(senderTxn.type).toBe(LedgerTransactionType.LnIntraLedger) + it("recordWalletIdIntraLedgerPayment", async () => { + const btcWalletDescriptor = BtcWalletDescriptor(crypto.randomUUID() as WalletId) + const usdWalletDescriptor = UsdWalletDescriptor(crypto.randomUUID() as WalletId) + + const res = await recordWalletIdIntraLedgerPayment({ + senderWalletDescriptor: btcWalletDescriptor, + recipientWalletDescriptor: usdWalletDescriptor, + paymentAmount: sendAmount, + senderDisplayAmounts: { + senderAmountDisplayCurrency: displaySendEurAmounts.amountDisplayCurrency, + senderFeeDisplayCurrency: displaySendEurAmounts.feeDisplayCurrency, + senderDisplayCurrency: displaySendEurAmounts.displayCurrency, + }, + recipientDisplayAmounts: { + recipientAmountDisplayCurrency: + displayReceiveUsdAmounts.amountDisplayCurrency, + recipientFeeDisplayCurrency: displayReceiveUsdAmounts.feeDisplayCurrency, + recipientDisplayCurrency: displayReceiveUsdAmounts.displayCurrency, + }, + }) + if (res instanceof Error) throw res + + const senderTxns = await LedgerService().getTransactionsByWalletId( + btcWalletDescriptor.id, + ) + if (senderTxns instanceof Error) throw senderTxns + if (!(senderTxns && senderTxns.length)) throw new Error() + const senderTxn = senderTxns[0] + expect(senderTxn.type).toBe(LedgerTransactionType.IntraLedger) + + const recipientTxns = await LedgerService().getTransactionsByWalletId( + usdWalletDescriptor.id, + ) + if (recipientTxns instanceof Error) throw recipientTxns + if (!(recipientTxns && recipientTxns.length)) throw new Error() + const recipientTxn = recipientTxns[0] + expect(recipientTxn.type).toBe(LedgerTransactionType.IntraLedger) + }) - const recipientTxns = await LedgerService().getTransactionsByWalletId( - usdWalletDescriptor.id, - ) - if (recipientTxns instanceof Error) throw recipientTxns - if (!(recipientTxns && recipientTxns.length)) throw new Error() - const recipientTxn = recipientTxns[0] - expect(recipientTxn.type).toBe(LedgerTransactionType.LnIntraLedger) + it("recordOnChainIntraLedgerPayment", async () => { + const btcWalletDescriptor = BtcWalletDescriptor(crypto.randomUUID() as WalletId) + const usdWalletDescriptor = UsdWalletDescriptor(crypto.randomUUID() as WalletId) + + const res = await recordOnChainIntraLedgerPayment({ + senderWalletDescriptor: btcWalletDescriptor, + recipientWalletDescriptor: usdWalletDescriptor, + paymentAmount: sendAmount, + senderDisplayAmounts: { + senderAmountDisplayCurrency: displaySendEurAmounts.amountDisplayCurrency, + senderFeeDisplayCurrency: displaySendEurAmounts.feeDisplayCurrency, + senderDisplayCurrency: displaySendEurAmounts.displayCurrency, + }, + recipientDisplayAmounts: { + recipientAmountDisplayCurrency: + displayReceiveUsdAmounts.amountDisplayCurrency, + recipientFeeDisplayCurrency: displayReceiveUsdAmounts.feeDisplayCurrency, + recipientDisplayCurrency: displayReceiveUsdAmounts.displayCurrency, + }, + }) + if (res instanceof Error) throw res + + const senderTxns = await LedgerService().getTransactionsByWalletId( + btcWalletDescriptor.id, + ) + if (senderTxns instanceof Error) throw senderTxns + if (!(senderTxns && senderTxns.length)) throw new Error() + const senderTxn = senderTxns[0] + expect(senderTxn.type).toBe(LedgerTransactionType.OnchainIntraLedger) + + const recipientTxns = await LedgerService().getTransactionsByWalletId( + usdWalletDescriptor.id, + ) + if (recipientTxns instanceof Error) throw recipientTxns + if (!(recipientTxns && recipientTxns.length)) throw new Error() + const recipientTxn = recipientTxns[0] + expect(recipientTxn.type).toBe(LedgerTransactionType.OnchainIntraLedger) + }) }) - it("recordWalletIdIntraLedgerPayment", async () => { - const btcWalletDescriptor = BtcWalletDescriptor(crypto.randomUUID() as WalletId) - const usdWalletDescriptor = UsdWalletDescriptor(crypto.randomUUID() as WalletId) - - const res = await recordWalletIdIntraLedgerPayment({ - senderWalletDescriptor: btcWalletDescriptor, - recipientWalletDescriptor: usdWalletDescriptor, - paymentAmount: sendAmount, - senderDisplayAmounts: { - senderAmountDisplayCurrency: displaySendEurAmounts.amountDisplayCurrency, - senderFeeDisplayCurrency: displaySendEurAmounts.feeDisplayCurrency, - senderDisplayCurrency: displaySendEurAmounts.displayCurrency, - }, - recipientDisplayAmounts: { - recipientAmountDisplayCurrency: displayReceiveUsdAmounts.amountDisplayCurrency, - recipientFeeDisplayCurrency: displayReceiveUsdAmounts.feeDisplayCurrency, - recipientDisplayCurrency: displayReceiveUsdAmounts.displayCurrency, - }, + describe("recordTradeIntraAccount", () => { + it("recordLnTradeIntraAccountTxn", async () => { + const btcWalletDescriptor = BtcWalletDescriptor(crypto.randomUUID() as WalletId) + const usdWalletDescriptor = UsdWalletDescriptor(crypto.randomUUID() as WalletId) + + const res = await recordLnTradeIntraAccountTxn({ + senderWalletDescriptor: btcWalletDescriptor, + recipientWalletDescriptor: usdWalletDescriptor, + paymentAmount: sendAmount, + senderDisplayAmounts: { + senderAmountDisplayCurrency: displaySendEurAmounts.amountDisplayCurrency, + senderFeeDisplayCurrency: displaySendEurAmounts.feeDisplayCurrency, + senderDisplayCurrency: displaySendEurAmounts.displayCurrency, + }, + recipientDisplayAmounts: { + recipientAmountDisplayCurrency: + displayReceiveUsdAmounts.amountDisplayCurrency, + recipientFeeDisplayCurrency: displayReceiveUsdAmounts.feeDisplayCurrency, + recipientDisplayCurrency: displayReceiveUsdAmounts.displayCurrency, + }, + }) + if (res instanceof Error) throw res + + const senderTxns = await LedgerService().getTransactionsByWalletId( + btcWalletDescriptor.id, + ) + if (senderTxns instanceof Error) throw senderTxns + if (!(senderTxns && senderTxns.length)) throw new Error() + const senderTxn = senderTxns[0] + expect(senderTxn.type).toBe(LedgerTransactionType.LnTradeIntraAccount) + + const recipientTxns = await LedgerService().getTransactionsByWalletId( + usdWalletDescriptor.id, + ) + if (recipientTxns instanceof Error) throw recipientTxns + if (!(recipientTxns && recipientTxns.length)) throw new Error() + const recipientTxn = recipientTxns[0] + expect(recipientTxn.type).toBe(LedgerTransactionType.LnTradeIntraAccount) }) - if (res instanceof Error) throw res - const senderTxns = await LedgerService().getTransactionsByWalletId( - btcWalletDescriptor.id, - ) - if (senderTxns instanceof Error) throw senderTxns - if (!(senderTxns && senderTxns.length)) throw new Error() - const senderTxn = senderTxns[0] - expect(senderTxn.type).toBe(LedgerTransactionType.IntraLedger) + it("recordWalletIdTradeIntraAccountTxn", async () => { + const btcWalletDescriptor = BtcWalletDescriptor(crypto.randomUUID() as WalletId) + const usdWalletDescriptor = UsdWalletDescriptor(crypto.randomUUID() as WalletId) + + const res = await recordWalletIdTradeIntraAccountTxn({ + senderWalletDescriptor: btcWalletDescriptor, + recipientWalletDescriptor: usdWalletDescriptor, + paymentAmount: sendAmount, + senderDisplayAmounts: { + senderAmountDisplayCurrency: displaySendEurAmounts.amountDisplayCurrency, + senderFeeDisplayCurrency: displaySendEurAmounts.feeDisplayCurrency, + senderDisplayCurrency: displaySendEurAmounts.displayCurrency, + }, + recipientDisplayAmounts: { + recipientAmountDisplayCurrency: + displayReceiveUsdAmounts.amountDisplayCurrency, + recipientFeeDisplayCurrency: displayReceiveUsdAmounts.feeDisplayCurrency, + recipientDisplayCurrency: displayReceiveUsdAmounts.displayCurrency, + }, + }) + if (res instanceof Error) throw res + + const senderTxns = await LedgerService().getTransactionsByWalletId( + btcWalletDescriptor.id, + ) + if (senderTxns instanceof Error) throw senderTxns + if (!(senderTxns && senderTxns.length)) throw new Error() + const senderTxn = senderTxns[0] + expect(senderTxn.type).toBe(LedgerTransactionType.WalletIdTradeIntraAccount) + + const recipientTxns = await LedgerService().getTransactionsByWalletId( + usdWalletDescriptor.id, + ) + if (recipientTxns instanceof Error) throw recipientTxns + if (!(recipientTxns && recipientTxns.length)) throw new Error() + const recipientTxn = recipientTxns[0] + expect(recipientTxn.type).toBe(LedgerTransactionType.WalletIdTradeIntraAccount) + }) - const recipientTxns = await LedgerService().getTransactionsByWalletId( - usdWalletDescriptor.id, - ) - if (recipientTxns instanceof Error) throw recipientTxns - if (!(recipientTxns && recipientTxns.length)) throw new Error() - const recipientTxn = recipientTxns[0] - expect(recipientTxn.type).toBe(LedgerTransactionType.IntraLedger) + it("recordOnChainTradeIntraAccountTxn", async () => { + const btcWalletDescriptor = BtcWalletDescriptor(crypto.randomUUID() as WalletId) + const usdWalletDescriptor = UsdWalletDescriptor(crypto.randomUUID() as WalletId) + + const res = await recordOnChainTradeIntraAccountTxn({ + senderWalletDescriptor: btcWalletDescriptor, + recipientWalletDescriptor: usdWalletDescriptor, + paymentAmount: sendAmount, + senderDisplayAmounts: { + senderAmountDisplayCurrency: displaySendEurAmounts.amountDisplayCurrency, + senderFeeDisplayCurrency: displaySendEurAmounts.feeDisplayCurrency, + senderDisplayCurrency: displaySendEurAmounts.displayCurrency, + }, + recipientDisplayAmounts: { + recipientAmountDisplayCurrency: + displayReceiveUsdAmounts.amountDisplayCurrency, + recipientFeeDisplayCurrency: displayReceiveUsdAmounts.feeDisplayCurrency, + recipientDisplayCurrency: displayReceiveUsdAmounts.displayCurrency, + }, + }) + if (res instanceof Error) throw res + + const senderTxns = await LedgerService().getTransactionsByWalletId( + btcWalletDescriptor.id, + ) + if (senderTxns instanceof Error) throw senderTxns + if (!(senderTxns && senderTxns.length)) throw new Error() + const senderTxn = senderTxns[0] + expect(senderTxn.type).toBe(LedgerTransactionType.OnChainTradeIntraAccount) + + const recipientTxns = await LedgerService().getTransactionsByWalletId( + usdWalletDescriptor.id, + ) + if (recipientTxns instanceof Error) throw recipientTxns + if (!(recipientTxns && recipientTxns.length)) throw new Error() + const recipientTxn = recipientTxns[0] + expect(recipientTxn.type).toBe(LedgerTransactionType.OnChainTradeIntraAccount) + }) }) - it("recordOnChainIntraLedgerPayment", async () => { - const btcWalletDescriptor = BtcWalletDescriptor(crypto.randomUUID() as WalletId) - const usdWalletDescriptor = UsdWalletDescriptor(crypto.randomUUID() as WalletId) - - const res = await recordOnChainIntraLedgerPayment({ - senderWalletDescriptor: btcWalletDescriptor, - recipientWalletDescriptor: usdWalletDescriptor, - paymentAmount: sendAmount, - senderDisplayAmounts: { - senderAmountDisplayCurrency: displaySendEurAmounts.amountDisplayCurrency, - senderFeeDisplayCurrency: displaySendEurAmounts.feeDisplayCurrency, - senderDisplayCurrency: displaySendEurAmounts.displayCurrency, - }, - recipientDisplayAmounts: { - recipientAmountDisplayCurrency: displayReceiveUsdAmounts.amountDisplayCurrency, - recipientFeeDisplayCurrency: displayReceiveUsdAmounts.feeDisplayCurrency, - recipientDisplayCurrency: displayReceiveUsdAmounts.displayCurrency, - }, + describe("recordReceiveOnChainFeeReconciliation", () => { + it("recordReceiveOnChainFeeReconciliation", async () => { + const lowerFee = { amount: 1000n, currency: WalletCurrency.Btc } + const higherFee = { amount: 2100n, currency: WalletCurrency.Btc } + + const res = await recordReceiveOnChainFeeReconciliation({ + estimatedFee: lowerFee, + actualFee: higherFee, + }) + if (res instanceof Error) throw res + + const { transactionIds } = res + expect(transactionIds).toHaveLength(2) + + const ledger = LedgerService() + + const tx0 = await ledger.getTransactionById(transactionIds[0]) + const tx1 = await ledger.getTransactionById(transactionIds[1]) + const liabilitiesTxn = [tx0, tx1].find( + (tx): tx is LedgerTransaction => + !(tx instanceof CouldNotFindError), + ) + if (liabilitiesTxn === undefined) throw new Error("Could not find transaction") + expect(liabilitiesTxn.type).toBe(LedgerTransactionType.OnchainPayment) }) - if (res instanceof Error) throw res - - const senderTxns = await LedgerService().getTransactionsByWalletId( - btcWalletDescriptor.id, - ) - if (senderTxns instanceof Error) throw senderTxns - if (!(senderTxns && senderTxns.length)) throw new Error() - const senderTxn = senderTxns[0] - expect(senderTxn.type).toBe(LedgerTransactionType.OnchainIntraLedger) - - const recipientTxns = await LedgerService().getTransactionsByWalletId( - usdWalletDescriptor.id, - ) - if (recipientTxns instanceof Error) throw recipientTxns - if (!(recipientTxns && recipientTxns.length)) throw new Error() - const recipientTxn = recipientTxns[0] - expect(recipientTxn.type).toBe(LedgerTransactionType.OnchainIntraLedger) }) }) - describe("recordTradeIntraAccount", () => { - it("recordLnTradeIntraAccountTxn", async () => { - const btcWalletDescriptor = BtcWalletDescriptor(crypto.randomUUID() as WalletId) - const usdWalletDescriptor = UsdWalletDescriptor(crypto.randomUUID() as WalletId) - - const res = await recordLnTradeIntraAccountTxn({ - senderWalletDescriptor: btcWalletDescriptor, - recipientWalletDescriptor: usdWalletDescriptor, - paymentAmount: sendAmount, - senderDisplayAmounts: { - senderAmountDisplayCurrency: displaySendEurAmounts.amountDisplayCurrency, - senderFeeDisplayCurrency: displaySendEurAmounts.feeDisplayCurrency, - senderDisplayCurrency: displaySendEurAmounts.displayCurrency, - }, - recipientDisplayAmounts: { - recipientAmountDisplayCurrency: displayReceiveUsdAmounts.amountDisplayCurrency, - recipientFeeDisplayCurrency: displayReceiveUsdAmounts.feeDisplayCurrency, - recipientDisplayCurrency: displayReceiveUsdAmounts.displayCurrency, + describe("volume", () => { + const remainingWithdrawalLimit = async ({ + priceRatio, + }: { + priceRatio: WalletPriceRatio + }) => { + const accountVolumeRemaining = AccountTxVolumeRemaining(accountLimitsLevelOne) + + const walletVolumes = await LedgerFacade.externalPaymentVolumeAmountForAccountSince( + { + accountWalletDescriptors, + period: ONE_DAY, }, - }) - if (res instanceof Error) throw res - - const senderTxns = await LedgerService().getTransactionsByWalletId( - btcWalletDescriptor.id, ) - if (senderTxns instanceof Error) throw senderTxns - if (!(senderTxns && senderTxns.length)) throw new Error() - const senderTxn = senderTxns[0] - expect(senderTxn.type).toBe(LedgerTransactionType.LnTradeIntraAccount) + if (walletVolumes instanceof Error) throw walletVolumes - const recipientTxns = await LedgerService().getTransactionsByWalletId( - usdWalletDescriptor.id, - ) - if (recipientTxns instanceof Error) throw recipientTxns - if (!(recipientTxns && recipientTxns.length)) throw new Error() - const recipientTxn = recipientTxns[0] - expect(recipientTxn.type).toBe(LedgerTransactionType.LnTradeIntraAccount) - }) - - it("recordWalletIdTradeIntraAccountTxn", async () => { - const btcWalletDescriptor = BtcWalletDescriptor(crypto.randomUUID() as WalletId) - const usdWalletDescriptor = UsdWalletDescriptor(crypto.randomUUID() as WalletId) - - const res = await recordWalletIdTradeIntraAccountTxn({ - senderWalletDescriptor: btcWalletDescriptor, - recipientWalletDescriptor: usdWalletDescriptor, - paymentAmount: sendAmount, - senderDisplayAmounts: { - senderAmountDisplayCurrency: displaySendEurAmounts.amountDisplayCurrency, - senderFeeDisplayCurrency: displaySendEurAmounts.feeDisplayCurrency, - senderDisplayCurrency: displaySendEurAmounts.displayCurrency, - }, - recipientDisplayAmounts: { - recipientAmountDisplayCurrency: displayReceiveUsdAmounts.amountDisplayCurrency, - recipientFeeDisplayCurrency: displayReceiveUsdAmounts.feeDisplayCurrency, - recipientDisplayCurrency: displayReceiveUsdAmounts.displayCurrency, - }, + const remainingAmount = await accountVolumeRemaining.withdrawal({ + priceRatio, + walletVolumes, }) - if (res instanceof Error) throw res + if (remainingAmount instanceof Error) throw remainingAmount - const senderTxns = await LedgerService().getTransactionsByWalletId( - btcWalletDescriptor.id, - ) - if (senderTxns instanceof Error) throw senderTxns - if (!(senderTxns && senderTxns.length)) throw new Error() - const senderTxn = senderTxns[0] - expect(senderTxn.type).toBe(LedgerTransactionType.WalletIdTradeIntraAccount) + return remainingAmount + } - const recipientTxns = await LedgerService().getTransactionsByWalletId( - usdWalletDescriptor.id, - ) - if (recipientTxns instanceof Error) throw recipientTxns - if (!(recipientTxns && recipientTxns.length)) throw new Error() - const recipientTxn = recipientTxns[0] - expect(recipientTxn.type).toBe(LedgerTransactionType.WalletIdTradeIntraAccount) - }) + describe("withdrawal", () => { + const sendPriceRatio = WalletPriceRatio(sendAmount) + if (sendPriceRatio instanceof Error) throw sendPriceRatio - it("recordOnChainTradeIntraAccountTxn", async () => { - const btcWalletDescriptor = BtcWalletDescriptor(crypto.randomUUID() as WalletId) - const usdWalletDescriptor = UsdWalletDescriptor(crypto.randomUUID() as WalletId) - - const res = await recordOnChainTradeIntraAccountTxn({ - senderWalletDescriptor: btcWalletDescriptor, - recipientWalletDescriptor: usdWalletDescriptor, - paymentAmount: sendAmount, - senderDisplayAmounts: { - senderAmountDisplayCurrency: displaySendEurAmounts.amountDisplayCurrency, - senderFeeDisplayCurrency: displaySendEurAmounts.feeDisplayCurrency, - senderDisplayCurrency: displaySendEurAmounts.displayCurrency, - }, - recipientDisplayAmounts: { - recipientAmountDisplayCurrency: displayReceiveUsdAmounts.amountDisplayCurrency, - recipientFeeDisplayCurrency: displayReceiveUsdAmounts.feeDisplayCurrency, - recipientDisplayCurrency: displayReceiveUsdAmounts.displayCurrency, - }, + it("returns 0 volume for no transactions", async () => { + const remaining = await remainingWithdrawalLimit({ priceRatio: sendPriceRatio }) + expect(remaining).toStrictEqual(accountLimitAmountsLevelOne.withdrawalLimit) }) - if (res instanceof Error) throw res - const senderTxns = await LedgerService().getTransactionsByWalletId( - btcWalletDescriptor.id, - ) - if (senderTxns instanceof Error) throw senderTxns - if (!(senderTxns && senderTxns.length)) throw new Error() - const senderTxn = senderTxns[0] - expect(senderTxn.type).toBe(LedgerTransactionType.OnChainTradeIntraAccount) - - const recipientTxns = await LedgerService().getTransactionsByWalletId( - usdWalletDescriptor.id, - ) - if (recipientTxns instanceof Error) throw recipientTxns - if (!(recipientTxns && recipientTxns.length)) throw new Error() - const recipientTxn = recipientTxns[0] - expect(recipientTxn.type).toBe(LedgerTransactionType.OnChainTradeIntraAccount) - }) - }) - - describe("recordReceiveOnChainFeeReconciliation", () => { - it("recordReceiveOnChainFeeReconciliation", async () => { - const lowerFee = { amount: 1000n, currency: WalletCurrency.Btc } - const higherFee = { amount: 2100n, currency: WalletCurrency.Btc } - - const res = await recordReceiveOnChainFeeReconciliation({ - estimatedFee: lowerFee, - actualFee: higherFee, + it("returns correct volume for a btc and usd transactions", async () => { + const resBtc = await recordSendLnPayment({ + walletDescriptor: accountWalletDescriptors.BTC, + paymentAmount: sendAmount, + bankFee, + displayAmounts: displaySendEurAmounts, + }) + if (resBtc instanceof Error) throw resBtc + + const resUsd = await recordSendLnPayment({ + walletDescriptor: accountWalletDescriptors.USD, + paymentAmount: sendAmount, + bankFee, + displayAmounts: displaySendEurAmounts, + }) + if (resUsd instanceof Error) throw resUsd + + const expectedRemaining = calc.sub( + accountLimitAmountsLevelOne.withdrawalLimit, + calc.mul(sendAmount.usd, 2n), + ) + + const remaining = await remainingWithdrawalLimit({ priceRatio: sendPriceRatio }) + expect(remaining).toStrictEqual(expectedRemaining) }) - if (res instanceof Error) throw res - const { transactionIds } = res - expect(transactionIds).toHaveLength(2) + it("returns 0 volume for a voided btc transaction", async () => { + const resBtc = await recordSendLnPayment({ + walletDescriptor: accountWalletDescriptors.BTC, + paymentAmount: sendAmount, + bankFee, + displayAmounts: displaySendEurAmounts, + }) + if (resBtc instanceof Error) throw resBtc + + const voided = await LedgerFacade.recordLnSendRevert({ + journalId: resBtc.journalId, + paymentHash: resBtc.paymentHash, + }) + if (voided instanceof Error) return voided + + const remaining = await remainingWithdrawalLimit({ priceRatio: sendPriceRatio }) + expect(remaining).toStrictEqual(accountLimitAmountsLevelOne.withdrawalLimit) + }) - const ledger = LedgerService() + it("returns 0 volume for a delayed voided btc transaction", async () => { + const resUsd = await recordSendLnPayment({ + walletDescriptor: accountWalletDescriptors.USD, + paymentAmount: sendAmount, + bankFee, + displayAmounts: displaySendEurAmounts, + }) + if (resUsd instanceof Error) throw resUsd + const { journalId, paymentHash } = resUsd + + const voided = await LedgerFacade.recordLnSendRevert({ + journalId, + paymentHash, + }) + if (voided instanceof Error) return voided + + const newDateTime = new Date(Date.now() - MS_PER_DAY * 2) + await Transaction.updateMany( + { _journal: toObjectId(journalId) }, + { timestamp: newDateTime, datetime: newDateTime }, + ) + + const remaining = await remainingWithdrawalLimit({ priceRatio: sendPriceRatio }) + expect(remaining).toStrictEqual(accountLimitAmountsLevelOne.withdrawalLimit) + }) - const tx0 = await ledger.getTransactionById(transactionIds[0]) - const tx1 = await ledger.getTransactionById(transactionIds[1]) - const liabilitiesTxn = [tx0, tx1].find( - (tx): tx is LedgerTransaction => - !(tx instanceof CouldNotFindError), - ) - if (liabilitiesTxn === undefined) throw new Error("Could not find transaction") - expect(liabilitiesTxn.type).toBe(LedgerTransactionType.OnchainPayment) + it("returns correct volume for a fee reimbursed btc transaction", async () => { + const resBtc = await recordSendLnPayment({ + walletDescriptor: accountWalletDescriptors.BTC, + paymentAmount: sendAmount, + bankFee, + displayAmounts: displaySendEurAmounts, + }) + if (resBtc instanceof Error) throw resBtc + + const reimbursed = await recordLnFeeReimbursement({ + walletDescriptor: accountWalletDescriptors.BTC, + paymentAmount: bankFee, + bankFee, + displayAmounts: displayReceiveEurAmounts, + }) + if (reimbursed instanceof Error) throw reimbursed + + const expectedRemaining = calc.sub( + accountLimitAmountsLevelOne.withdrawalLimit, + calc.sub(sendAmount.usd, bankFee.usd), + ) + + const remaining = await remainingWithdrawalLimit({ priceRatio: sendPriceRatio }) + expect(remaining).toStrictEqual(expectedRemaining) + }) }) }) })