From 49bd1867e8e6a273defbc92a768bbc56062ecf15 Mon Sep 17 00:00:00 2001 From: vindard <17693119+vindard@users.noreply.github.com> Date: Wed, 29 Nov 2023 17:06:47 -0400 Subject: [PATCH 01/16] test: move LnSendOffChain metadata check --- .../app/wallets/send-lightning.spec.ts | 59 ++++++++++++ .../02-user-wallet/02-tx-display.spec.ts | 89 ------------------- 2 files changed, 59 insertions(+), 89 deletions(-) diff --git a/core/api/test/integration/app/wallets/send-lightning.spec.ts b/core/api/test/integration/app/wallets/send-lightning.spec.ts index 10e083c45a..b819c77ce5 100644 --- a/core/api/test/integration/app/wallets/send-lightning.spec.ts +++ b/core/api/test/integration/app/wallets/send-lightning.spec.ts @@ -21,6 +21,7 @@ import { } from "@/domain/errors" import { AmountCalculator, WalletCurrency } from "@/domain/shared" import * as LnFeesImpl from "@/domain/payments" +import * as DisplayAmountsConverterImpl from "@/domain/fiat" import { AccountsRepository, @@ -33,6 +34,7 @@ import { WalletInvoice } from "@/services/mongoose/schema" import { LnPayment } from "@/services/lnd/schema" import * as LndImpl from "@/services/lnd" import * as PushNotificationsServiceImpl from "@/services/notifications/push-notifications" +import * as LedgerFacadeImpl from "@/services/ledger/facade" import { createMandatoryUsers, @@ -41,6 +43,7 @@ import { getBalanceHelper, recordReceiveLnPayment, } from "test/helpers" +import { LedgerTransactionType } from "@/domain/ledger" let lnInvoice: LnInvoice let noAmountLnInvoice: LnInvoice @@ -485,6 +488,62 @@ describe("initiated via lightning", () => { // Restore system state lndServiceSpy.mockRestore() }) + + it("records transaction with lightning metadata on ln send", async () => { + // Setup mocks + const { LndService: LnServiceOrig } = jest.requireActual("@/services/lnd") + const lndServiceSpy = jest.spyOn(LndImpl, "LndService").mockReturnValue({ + ...LnServiceOrig(), + defaultPubkey: (): Pubkey => DEFAULT_PUBKEY, + listAllPubkeys: () => [], + }) + + const displayAmountsConverterSpy = jest.spyOn( + DisplayAmountsConverterImpl, + "DisplayAmountsConverter", + ) + + const lnSendLedgerMetadataSpy = jest.spyOn(LedgerFacadeImpl, "LnSendLedgerMetadata") + const recordOffChainSendSpy = jest.spyOn(LedgerFacadeImpl, "recordSendOffChain") + + // Create users + const newWalletDescriptor = await createRandomUserAndBtcWallet() + const newAccount = await AccountsRepository().findById( + newWalletDescriptor.accountId, + ) + if (newAccount instanceof Error) throw newAccount + + // Fund balance for send + const receive = await recordReceiveLnPayment({ + walletDescriptor: newWalletDescriptor, + paymentAmount: receiveAmounts, + bankFee: receiveBankFee, + displayAmounts: receiveDisplayAmounts, + memo, + }) + if (receive instanceof Error) throw receive + + // Execute pay + await Payments.payNoAmountInvoiceByWalletIdForBtcWallet({ + uncheckedPaymentRequest: noAmountLnInvoice.paymentRequest, + memo, + senderWalletId: newWalletDescriptor.id, + senderAccount: newAccount, + amount, + }) + + // Check record function was called with right metadata + expect(displayAmountsConverterSpy).toHaveBeenCalledTimes(1) + expect(lnSendLedgerMetadataSpy).toHaveBeenCalledTimes(1) + const args = recordOffChainSendSpy.mock.calls[0][0] + expect(args.metadata.type).toBe(LedgerTransactionType.Payment) + + // Restore system state + lndServiceSpy.mockRestore() + displayAmountsConverterSpy.mockRestore() + lnSendLedgerMetadataSpy.mockRestore() + recordOffChainSendSpy.mockRestore() + }) }) describe("settles intraledger", () => { diff --git a/core/api/test/legacy-integration/02-user-wallet/02-tx-display.spec.ts b/core/api/test/legacy-integration/02-user-wallet/02-tx-display.spec.ts index f6c8593d80..abb4334f7b 100644 --- a/core/api/test/legacy-integration/02-user-wallet/02-tx-display.spec.ts +++ b/core/api/test/legacy-integration/02-user-wallet/02-tx-display.spec.ts @@ -427,95 +427,6 @@ describe("Display properties on transactions", () => { }) describe("send", () => { - it("(LnSendLedgerMetadata) pay zero amount invoice with amount less than 1 cent", async () => { - // TxMetadata: - // - LnSendLedgerMetadata - - const amountInvoice = toSats(1) - - const senderWalletId = walletIdB - - // Send payment - const { request, id } = await createInvoice({ lnd: lndOutside1 }) - const paymentHash = id as PaymentHash - - const { result: fee, error } = - await Payments.getNoAmountLightningFeeEstimationForBtcWallet({ - walletId: senderWalletId, - uncheckedPaymentRequest: request, - amount: amountInvoice, - }) - if (error instanceof Error) throw error - expect(fee).not.toBeNull() - - const paymentResult = await Payments.payNoAmountInvoiceByWalletIdForBtcWallet({ - uncheckedPaymentRequest: request, - memo: null, - amount: amountInvoice, - senderWalletId: walletIdB, - senderAccount: accountB, - }) - if (paymentResult instanceof Error) throw paymentResult - expect(paymentResult).toEqual({ - status: PaymentSendStatus.Success, - transaction: expect.objectContaining({ - walletId: walletIdB, - status: "success", - settlementAmount: amountInvoice * -1, - settlementCurrency: "BTC", - initiationVia: expect.objectContaining({ - type: "lightning", - paymentHash, - }), - settlementVia: expect.objectContaining({ - type: "lightning", - }), - }), - }) - - // Check entries - const txns = await getAllTransactionsByHash(paymentHash) - if (txns instanceof Error) throw txns - - const senderTxn = txns.find( - ({ walletId, type }) => - walletId === senderWalletId && type === LedgerTransactionType.Payment, - ) - if (senderTxn === undefined) throw new Error("'senderTxn' not found") - - const internalTxns = txns.filter(({ walletId }) => walletId !== senderWalletId) - expect(internalTxns.length).toBeGreaterThan(0) - - const { EUR: expectedSenderDisplayProps } = await getDisplayAmounts({ - satsAmount: senderTxn.satsAmount || toSats(0), - satsFee: senderTxn.satsFee || toSats(0), - }) - - expect(senderTxn).toEqual( - expect.objectContaining({ - ...expectedSenderDisplayProps, - type: LedgerTransactionType.Payment, - }), - ) - - for (const txn of internalTxns) { - expect(txn).toEqual( - expect.objectContaining({ - displayAmount: senderTxn.centsAmount, - displayFee: senderTxn.centsFee, - displayCurrency: UsdDisplayCurrency, - }), - ) - } - - const reimbursementTxn = txns.find( - ({ walletId, type }) => - walletId === senderWalletId && - type === LedgerTransactionType.LnFeeReimbursement, - ) - expect(reimbursementTxn).toBeUndefined() - }) - it("(LnFeeReimbursementReceiveLedgerMetadata) pay zero amount invoice", async () => { // TxMetadata: // - LnSendLedgerMetadata From 411556ab9e674c42697764128c940496205e6bb7 Mon Sep 17 00:00:00 2001 From: vindard <17693119+vindard@users.noreply.github.com> Date: Thu, 30 Nov 2023 09:10:19 -0400 Subject: [PATCH 02/16] test: move LnFeeReimbursementReceive metadata check --- .../app/wallets/send-lightning.spec.ts | 68 ++++++++++++ .../02-user-wallet/02-tx-display.spec.ts | 101 ------------------ 2 files changed, 68 insertions(+), 101 deletions(-) diff --git a/core/api/test/integration/app/wallets/send-lightning.spec.ts b/core/api/test/integration/app/wallets/send-lightning.spec.ts index b819c77ce5..b7da242511 100644 --- a/core/api/test/integration/app/wallets/send-lightning.spec.ts +++ b/core/api/test/integration/app/wallets/send-lightning.spec.ts @@ -544,6 +544,74 @@ describe("initiated via lightning", () => { lnSendLedgerMetadataSpy.mockRestore() recordOffChainSendSpy.mockRestore() }) + + it("records transaction with fee reimbursement metadata on ln send", async () => { + // Setup mocks + const { LndService: LnServiceOrig } = jest.requireActual("@/services/lnd") + const lndServiceSpy = jest.spyOn(LndImpl, "LndService").mockReturnValue({ + ...LnServiceOrig(), + defaultPubkey: (): Pubkey => DEFAULT_PUBKEY, + listAllPubkeys: () => [], + payInvoiceViaPaymentDetails: () => ({ + roundedUpFee: toSats(0), + revealedPreImage: "revealedPreImage" as RevealedPreImage, + sentFromPubkey: DEFAULT_PUBKEY, + }), + }) + + const displayAmountsConverterSpy = jest.spyOn( + DisplayAmountsConverterImpl, + "DisplayAmountsConverter", + ) + + const lnFeeReimbursementReceiveLedgerMetadataSpy = jest.spyOn( + LedgerFacadeImpl, + "LnFeeReimbursementReceiveLedgerMetadata", + ) + const recordOffChainReceiveSpy = jest.spyOn( + LedgerFacadeImpl, + "recordReceiveOffChain", + ) + + // Create users + const newWalletDescriptor = await createRandomUserAndBtcWallet() + const newAccount = await AccountsRepository().findById( + newWalletDescriptor.accountId, + ) + if (newAccount instanceof Error) throw newAccount + + // Fund balance for send + const receive = await recordReceiveLnPayment({ + walletDescriptor: newWalletDescriptor, + paymentAmount: receiveAmounts, + bankFee: receiveBankFee, + displayAmounts: receiveDisplayAmounts, + memo, + }) + if (receive instanceof Error) throw receive + + // Execute pay + await Payments.payNoAmountInvoiceByWalletIdForBtcWallet({ + uncheckedPaymentRequest: noAmountLnInvoice.paymentRequest, + memo, + senderWalletId: newWalletDescriptor.id, + senderAccount: newAccount, + amount, + }) + + // Check record function was called with right metadata + expect(displayAmountsConverterSpy).toHaveBeenCalledTimes(2) + expect(lnFeeReimbursementReceiveLedgerMetadataSpy).toHaveBeenCalledTimes(1) + // Note: 1st call is funding balance in test, 2nd call is fee reimbursement + const args = recordOffChainReceiveSpy.mock.calls[1][0] + expect(args.metadata.type).toBe(LedgerTransactionType.LnFeeReimbursement) + + // Restore system state + lndServiceSpy.mockRestore() + displayAmountsConverterSpy.mockRestore() + lnFeeReimbursementReceiveLedgerMetadataSpy.mockRestore() + recordOffChainReceiveSpy.mockRestore() + }) }) describe("settles intraledger", () => { diff --git a/core/api/test/legacy-integration/02-user-wallet/02-tx-display.spec.ts b/core/api/test/legacy-integration/02-user-wallet/02-tx-display.spec.ts index abb4334f7b..fe5a00c64d 100644 --- a/core/api/test/legacy-integration/02-user-wallet/02-tx-display.spec.ts +++ b/core/api/test/legacy-integration/02-user-wallet/02-tx-display.spec.ts @@ -427,107 +427,6 @@ describe("Display properties on transactions", () => { }) describe("send", () => { - it("(LnFeeReimbursementReceiveLedgerMetadata) pay zero amount invoice", async () => { - // TxMetadata: - // - LnSendLedgerMetadata - // - LnFeeReimbursementReceiveLedgerMetadata - - const amountInvoice = toSats(20_000) - - const senderWalletId = walletIdB - - // Send payment - const { request, id } = await createInvoice({ lnd: lndOutside1 }) - const paymentHash = id as PaymentHash - - const paymentResult = await Payments.payNoAmountInvoiceByWalletIdForBtcWallet({ - uncheckedPaymentRequest: request, - memo: null, - amount: amountInvoice, - senderWalletId: walletIdB, - senderAccount: accountB, - }) - if (paymentResult instanceof Error) throw paymentResult - expect(paymentResult).toEqual({ - status: PaymentSendStatus.Success, - transaction: expect.objectContaining({ - walletId: walletIdB, - status: "success", - settlementAmount: - (amountInvoice + paymentResult.transaction.settlementFee) * -1, - settlementCurrency: "BTC", - initiationVia: expect.objectContaining({ - type: "lightning", - paymentHash, - }), - settlementVia: expect.objectContaining({ - type: "lightning", - }), - }), - }) - - // Check entries - const txns = await getAllTransactionsByHash(paymentHash) - if (txns instanceof Error) throw txns - - const senderTxn = txns.find( - ({ walletId, type }) => - walletId === senderWalletId && type === LedgerTransactionType.Payment, - ) - if (senderTxn === undefined) throw new Error("'senderTxn' not found") - - const reimbursementTxn = txns.find( - ({ walletId, type }) => - walletId === senderWalletId && - type === LedgerTransactionType.LnFeeReimbursement, - ) - if (reimbursementTxn === undefined) - throw new Error("'reimbursementTxn' not found") - - const internalTxns = txns.filter(({ walletId }) => walletId !== senderWalletId) - expect(internalTxns.length).toBeGreaterThan(0) - - const { EUR: expectedSenderDisplayProps } = await getDisplayAmounts({ - satsAmount: senderTxn.satsAmount || toSats(0), - satsFee: senderTxn.satsFee || toSats(0), - }) - - const { EUR: expectedReimbursementDisplayProps } = await getDisplayAmounts({ - satsAmount: reimbursementTxn.satsAmount || toSats(0), - satsFee: reimbursementTxn.satsFee || toSats(0), - }) - - expect(senderTxn).toEqual( - expect.objectContaining({ - ...expectedSenderDisplayProps, - type: LedgerTransactionType.Payment, - }), - ) - expect(reimbursementTxn).toEqual( - expect.objectContaining({ - ...expectedReimbursementDisplayProps, - type: LedgerTransactionType.LnFeeReimbursement, - }), - ) - - expect(internalTxns).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - displayAmount: senderTxn.centsAmount, - displayFee: senderTxn.centsFee, - displayCurrency: UsdDisplayCurrency, - type: LedgerTransactionType.Payment, - }), - expect.objectContaining({ - displayAmount: reimbursementTxn.centsAmount, - displayFee: reimbursementTxn.centsFee, - displayCurrency: UsdDisplayCurrency, - type: LedgerTransactionType.LnFeeReimbursement, - }), - ]), - ) - }) - it("(LnFailedPaymentReceiveLedgerMetadata) pay zero amount invoice & revert txn when verifyMaxFee fails", async () => { // TxMetadata: // - LnFailedPaymentReceiveLedgerMetadata From 2d5550c24d3c06611735776a5af17cef01f093fe Mon Sep 17 00:00:00 2001 From: vindard <17693119+vindard@users.noreply.github.com> Date: Thu, 30 Nov 2023 10:06:11 -0400 Subject: [PATCH 03/16] test: move LnTradeIntraAccountLedger metadata check --- .../app/wallets/send-lightning.spec.ts | 73 +++++++++++++++ .../02-user-wallet/02-tx-display.spec.ts | 92 ------------------- 2 files changed, 73 insertions(+), 92 deletions(-) diff --git a/core/api/test/integration/app/wallets/send-lightning.spec.ts b/core/api/test/integration/app/wallets/send-lightning.spec.ts index b7da242511..28f7b68ebd 100644 --- a/core/api/test/integration/app/wallets/send-lightning.spec.ts +++ b/core/api/test/integration/app/wallets/send-lightning.spec.ts @@ -987,5 +987,78 @@ describe("initiated via lightning", () => { pushNotificationsServiceSpy.mockRestore() lndServiceSpy.mockRestore() }) + + it("records transaction with ln-trade-intra-account metadata on intraledger send", async () => { + // Setup mocks + const { LndService: LnServiceOrig } = jest.requireActual("@/services/lnd") + const lndServiceSpy = jest.spyOn(LndImpl, "LndService").mockReturnValue({ + ...LnServiceOrig(), + listAllPubkeys: () => [noAmountLnInvoice.destination], + cancelInvoice: () => true, + }) + + const displayAmountsConverterSpy = jest.spyOn( + DisplayAmountsConverterImpl, + "DisplayAmountsConverter", + ) + + const lnTradeIntraAccountLedgerMetadataSpy = jest.spyOn( + LedgerFacadeImpl, + "LnTradeIntraAccountLedgerMetadata", + ) + const recordIntraledgerSpy = jest.spyOn(LedgerFacadeImpl, "recordIntraledger") + + // Create users + const { btcWalletDescriptor: newWalletDescriptor, usdWalletDescriptor } = + await createRandomUserAndWallets() + const newAccount = await AccountsRepository().findById( + newWalletDescriptor.accountId, + ) + if (newAccount instanceof Error) throw newAccount + + // Persist invoice as self-invoice + const persisted = await WalletInvoicesRepository().persistNew({ + paymentHash: noAmountLnInvoice.paymentHash, + secret: "secret" as SecretPreImage, + selfGenerated: true, + pubkey: noAmountLnInvoice.destination, + recipientWalletDescriptor: usdWalletDescriptor, + paid: false, + lnInvoice, + processingCompleted: false, + }) + if (persisted instanceof Error) throw persisted + + // Fund balance for send + const receive = await recordReceiveLnPayment({ + walletDescriptor: newWalletDescriptor, + paymentAmount: receiveAmounts, + bankFee: receiveBankFee, + displayAmounts: receiveDisplayAmounts, + memo, + }) + if (receive instanceof Error) throw receive + + // Execute pay + await Payments.payNoAmountInvoiceByWalletIdForBtcWallet({ + uncheckedPaymentRequest: noAmountLnInvoice.paymentRequest, + memo, + senderWalletId: newWalletDescriptor.id, + senderAccount: newAccount, + amount, + }) + + // Check record function was called with right metadata + expect(displayAmountsConverterSpy).toHaveBeenCalledTimes(2) + expect(lnTradeIntraAccountLedgerMetadataSpy).toHaveBeenCalledTimes(1) + const args = recordIntraledgerSpy.mock.calls[0][0] + expect(args.metadata.type).toBe(LedgerTransactionType.LnTradeIntraAccount) + + // Restore system state + lnTradeIntraAccountLedgerMetadataSpy.mockRestore() + displayAmountsConverterSpy.mockRestore() + recordIntraledgerSpy.mockRestore() + lndServiceSpy.mockRestore() + }) }) }) diff --git a/core/api/test/legacy-integration/02-user-wallet/02-tx-display.spec.ts b/core/api/test/legacy-integration/02-user-wallet/02-tx-display.spec.ts index fe5a00c64d..fe18c5cd2e 100644 --- a/core/api/test/legacy-integration/02-user-wallet/02-tx-display.spec.ts +++ b/core/api/test/legacy-integration/02-user-wallet/02-tx-display.spec.ts @@ -332,98 +332,6 @@ describe("Display properties on transactions", () => { ) expect(internalTxns.length).toEqual(0) }) - - it("(LnTradeIntraAccountLedgerMetadata) pay self amountless invoice from btc wallet to usd wallet", async () => { - // TxMetadata: - // - LnIntraledgerLedgerMetadata - - const amountInvoice = toSats(100) - - const senderWalletId = walletIdB - const senderAccount = accountB - const recipientWalletId = walletIdUsdB - - // Send payment - const memo = "invoiceMemo #" + (Math.random() * 1_000_000).toFixed() - - const invoice = await Wallets.addInvoiceNoAmountForSelf({ - walletId: recipientWalletId, - memo, - }) - if (invoice instanceof Error) throw invoice - const { - paymentRequest: uncheckedPaymentRequest, - paymentHash, - destination, - } = invoice.lnInvoice - - const paymentResult = await Payments.payNoAmountInvoiceByWalletIdForBtcWallet({ - uncheckedPaymentRequest, - memo: null, - senderWalletId: senderWalletId, - senderAccount: senderAccount, - amount: amountInvoice, - }) - if (paymentResult instanceof Error) throw paymentResult - expect(paymentResult).toEqual({ - status: PaymentSendStatus.Success, - transaction: expect.objectContaining({ - walletId: senderWalletId, - status: "success", - settlementAmount: amountInvoice * -1, - settlementCurrency: "BTC", - initiationVia: expect.objectContaining({ - type: "lightning", - paymentHash, - pubkey: destination, - }), - settlementVia: expect.objectContaining({ - type: "intraledger", - }), - }), - }) - - // Check entries - const txns = await getAllTransactionsByHash(paymentHash) - if (txns instanceof Error) throw txns - - const senderTxn = txns.find(({ walletId }) => walletId === senderWalletId) - if (senderTxn === undefined) throw new Error("'senderTxn' not found") - const recipientTxn = txns.find(({ walletId }) => walletId === recipientWalletId) - - const internalTxns = txns.filter( - ({ walletId }) => walletId !== senderWalletId && walletId !== recipientWalletId, - ) - expect(internalTxns.length).toEqual(2) - - const { EUR: expectedSenderDisplayProps } = await getDisplayAmounts({ - satsAmount: senderTxn.satsAmount || toSats(0), - satsFee: senderTxn.satsFee || toSats(0), - }) - - expect(senderTxn).toEqual( - expect.objectContaining({ - ...expectedSenderDisplayProps, - type: LedgerTransactionType.LnTradeIntraAccount, - }), - ) - expect(recipientTxn).toEqual( - expect.objectContaining({ - ...expectedSenderDisplayProps, - type: LedgerTransactionType.LnTradeIntraAccount, - }), - ) - - for (const txn of internalTxns) { - expect(txn).toEqual( - expect.objectContaining({ - displayAmount: senderTxn.centsAmount, - displayFee: senderTxn.centsFee, - displayCurrency: UsdDisplayCurrency, - }), - ) - } - }) }) describe("send", () => { From 59195c2b69cd5b56250f51a7c337849ad8b257b1 Mon Sep 17 00:00:00 2001 From: vindard <17693119+vindard@users.noreply.github.com> Date: Thu, 30 Nov 2023 10:11:23 -0400 Subject: [PATCH 04/16] test: move LnIntraledgerLedger metadata check --- .../app/wallets/send-lightning.spec.ts | 74 +++++++++++++++++++ .../02-user-wallet/02-tx-display.spec.ts | 64 ---------------- 2 files changed, 74 insertions(+), 64 deletions(-) diff --git a/core/api/test/integration/app/wallets/send-lightning.spec.ts b/core/api/test/integration/app/wallets/send-lightning.spec.ts index 28f7b68ebd..9b63f89c34 100644 --- a/core/api/test/integration/app/wallets/send-lightning.spec.ts +++ b/core/api/test/integration/app/wallets/send-lightning.spec.ts @@ -1060,5 +1060,79 @@ describe("initiated via lightning", () => { recordIntraledgerSpy.mockRestore() lndServiceSpy.mockRestore() }) + + it("records transaction with ln-intraledger metadata on intraledger send", async () => { + // Setup mocks + const { LndService: LnServiceOrig } = jest.requireActual("@/services/lnd") + const lndServiceSpy = jest.spyOn(LndImpl, "LndService").mockReturnValue({ + ...LnServiceOrig(), + listAllPubkeys: () => [noAmountLnInvoice.destination], + cancelInvoice: () => true, + }) + + const displayAmountsConverterSpy = jest.spyOn( + DisplayAmountsConverterImpl, + "DisplayAmountsConverter", + ) + + const lnIntraledgerLedgerMetadataSpy = jest.spyOn( + LedgerFacadeImpl, + "LnIntraledgerLedgerMetadata", + ) + const recordIntraledgerSpy = jest.spyOn(LedgerFacadeImpl, "recordIntraledger") + + // Setup users and wallets + const newWalletDescriptor = await createRandomUserAndBtcWallet() + const newAccount = await AccountsRepository().findById( + newWalletDescriptor.accountId, + ) + if (newAccount instanceof Error) throw newAccount + + const recipientWalletDescriptor = await createRandomUserAndBtcWallet() + + // Persist invoice as self-invoice + const persisted = await WalletInvoicesRepository().persistNew({ + paymentHash: noAmountLnInvoice.paymentHash, + secret: "secret" as SecretPreImage, + selfGenerated: true, + pubkey: noAmountLnInvoice.destination, + recipientWalletDescriptor, + paid: false, + lnInvoice, + processingCompleted: false, + }) + if (persisted instanceof Error) throw persisted + + // Fund balance for send + const receive = await recordReceiveLnPayment({ + walletDescriptor: newWalletDescriptor, + paymentAmount: receiveAmounts, + bankFee: receiveBankFee, + displayAmounts: receiveDisplayAmounts, + memo, + }) + if (receive instanceof Error) throw receive + + // Execute pay + await Payments.payNoAmountInvoiceByWalletIdForBtcWallet({ + uncheckedPaymentRequest: noAmountLnInvoice.paymentRequest, + memo, + senderWalletId: newWalletDescriptor.id, + senderAccount: newAccount, + amount, + }) + + // Check record function was called with right metadata + expect(displayAmountsConverterSpy).toHaveBeenCalledTimes(2) + expect(lnIntraledgerLedgerMetadataSpy).toHaveBeenCalledTimes(1) + const args = recordIntraledgerSpy.mock.calls[0][0] + expect(args.metadata.type).toBe(LedgerTransactionType.LnIntraLedger) + + // Restore system state + lnIntraledgerLedgerMetadataSpy.mockRestore() + displayAmountsConverterSpy.mockRestore() + recordIntraledgerSpy.mockRestore() + lndServiceSpy.mockRestore() + }) }) }) diff --git a/core/api/test/legacy-integration/02-user-wallet/02-tx-display.spec.ts b/core/api/test/legacy-integration/02-user-wallet/02-tx-display.spec.ts index fe18c5cd2e..8aa4edb08b 100644 --- a/core/api/test/legacy-integration/02-user-wallet/02-tx-display.spec.ts +++ b/core/api/test/legacy-integration/02-user-wallet/02-tx-display.spec.ts @@ -270,70 +270,6 @@ describe("Display properties on transactions", () => { }) }) - describe("intraledger", () => { - it("(LnIntraledgerLedgerMetadata) sends to another Galoy user with memo", async () => { - // TxMetadata: - // - LnIntraledgerLedgerMetadata - - const amountInvoice = 1_001 - - const senderWalletId = walletIdB - const senderAccount = accountB - const recipientWalletId = walletIdC - - // Send payment - const memo = "invoiceMemo #" + (Math.random() * 1_000_000).toFixed() - - const invoice = await Wallets.addInvoiceForSelfForBtcWallet({ - walletId: recipientWalletId, - amount: amountInvoice, - memo, - }) - if (invoice instanceof Error) throw invoice - const { paymentRequest } = invoice.lnInvoice - - const paymentResult = await Payments.payInvoiceByWalletId({ - uncheckedPaymentRequest: paymentRequest, - memo: null, - senderWalletId: senderWalletId, - senderAccount, - }) - if (paymentResult instanceof Error) throw paymentResult - - // Check entries - const txns = await getAllTransactionsByHash(invoice.paymentHash) - if (txns instanceof Error) throw txns - - const senderTxn = txns.find(({ walletId }) => walletId === senderWalletId) - if (senderTxn === undefined) throw new Error("'senderTxn' not found") - const recipientTxn = txns.find(({ walletId }) => walletId === recipientWalletId) - - const { EUR: expectedSenderDisplayProps, CRC: expectedRecipientDisplayProps } = - await getDisplayAmounts({ - satsAmount: senderTxn.satsAmount || toSats(0), - satsFee: senderTxn.satsFee || toSats(0), - }) - - expect(senderTxn).toEqual( - expect.objectContaining({ - ...expectedSenderDisplayProps, - type: LedgerTransactionType.LnIntraLedger, - }), - ) - expect(recipientTxn).toEqual( - expect.objectContaining({ - ...expectedRecipientDisplayProps, - type: LedgerTransactionType.LnIntraLedger, - }), - ) - - const internalTxns = txns.filter( - ({ walletId }) => walletId !== senderWalletId && walletId !== recipientWalletId, - ) - expect(internalTxns.length).toEqual(0) - }) - }) - describe("send", () => { it("(LnFailedPaymentReceiveLedgerMetadata) pay zero amount invoice & revert txn when verifyMaxFee fails", async () => { // TxMetadata: From b31fa02620f4354108ab03e147ec5ea7b5675328 Mon Sep 17 00:00:00 2001 From: vindard <17693119+vindard@users.noreply.github.com> Date: Thu, 30 Nov 2023 10:33:30 -0400 Subject: [PATCH 05/16] test: move WalletIdTradeIntraAccountLedger metadata check --- .../app/wallets/send-intraledger.spec.ts | 53 ++++++++++++ .../02-user-wallet/02-tx-display.spec.ts | 86 ------------------- 2 files changed, 53 insertions(+), 86 deletions(-) diff --git a/core/api/test/integration/app/wallets/send-intraledger.spec.ts b/core/api/test/integration/app/wallets/send-intraledger.spec.ts index d584de6ed7..b11164633a 100644 --- a/core/api/test/integration/app/wallets/send-intraledger.spec.ts +++ b/core/api/test/integration/app/wallets/send-intraledger.spec.ts @@ -5,6 +5,7 @@ import { Accounts, Payments } from "@/app" import { AccountStatus } from "@/domain/accounts" import { toSats } from "@/domain/bitcoin" import { PaymentSendStatus } from "@/domain/bitcoin/lightning" +import { LedgerTransactionType } from "@/domain/ledger" import { UsdDisplayCurrency, toCents } from "@/domain/fiat" import { InactiveAccountError, @@ -12,9 +13,11 @@ import { SelfPaymentError, TradeIntraAccountLimitsExceededError, } from "@/domain/errors" +import * as DisplayAmountsConverterImpl from "@/domain/fiat" import { AccountsRepository } from "@/services/mongoose" import { Transaction } from "@/services/ledger/schema" +import * as LedgerFacadeImpl from "@/services/ledger/facade" import * as PushNotificationsServiceImpl from "@/services/notifications/push-notifications" import { AmountCalculator, WalletCurrency } from "@/domain/shared" @@ -317,4 +320,54 @@ describe("intraLedgerPay", () => { // Restore system state pushNotificationsServiceSpy.mockReset() }) + + it("records transaction with wallet-id-trade-intra-account metadata on intraledger send", async () => { + // Setup mocks + const displayAmountsConverterSpy = jest.spyOn( + DisplayAmountsConverterImpl, + "DisplayAmountsConverter", + ) + + const walletIdTradeIntraAccountLedgerMetadataSpy = jest.spyOn( + LedgerFacadeImpl, + "WalletIdTradeIntraAccountLedgerMetadata", + ) + const recordIntraledgerSpy = jest.spyOn(LedgerFacadeImpl, "recordIntraledger") + + // Create users + const { btcWalletDescriptor: newWalletDescriptor, usdWalletDescriptor } = + await createRandomUserAndWallets() + const newAccount = await AccountsRepository().findById(newWalletDescriptor.accountId) + if (newAccount instanceof Error) throw newAccount + + // Fund balance for send + const receive = await recordReceiveLnPayment({ + walletDescriptor: newWalletDescriptor, + paymentAmount: receiveAmounts, + bankFee: receiveBankFee, + displayAmounts: receiveDisplayAmounts, + memo, + }) + if (receive instanceof Error) throw receive + + // Pay intraledger + await Payments.intraledgerPaymentSendWalletIdForBtcWallet({ + recipientWalletId: usdWalletDescriptor.id, + memo, + amount, + senderWalletId: newWalletDescriptor.id, + senderAccount: newAccount, + }) + + // Check record function was called with right metadata + expect(displayAmountsConverterSpy).toHaveBeenCalledTimes(2) + expect(walletIdTradeIntraAccountLedgerMetadataSpy).toHaveBeenCalledTimes(1) + const args = recordIntraledgerSpy.mock.calls[0][0] + expect(args.metadata.type).toBe(LedgerTransactionType.WalletIdTradeIntraAccount) + + // Restore system state + displayAmountsConverterSpy.mockRestore() + walletIdTradeIntraAccountLedgerMetadataSpy.mockRestore() + recordIntraledgerSpy.mockRestore() + }) }) diff --git a/core/api/test/legacy-integration/02-user-wallet/02-tx-display.spec.ts b/core/api/test/legacy-integration/02-user-wallet/02-tx-display.spec.ts index 8aa4edb08b..e1ac59cde2 100644 --- a/core/api/test/legacy-integration/02-user-wallet/02-tx-display.spec.ts +++ b/core/api/test/legacy-integration/02-user-wallet/02-tx-display.spec.ts @@ -889,92 +889,6 @@ describe("Display properties on transactions", () => { }), ) }) - - it("(WalletIdTradeIntraAccountLedgerMetadata) sends to self WalletId", async () => { - // TxMetadata: - // - WalletIdTradeIntraAccountLedgerMetadata - - const amountSats = 10_000 - - const senderWalletId = walletIdB - const senderAccount = accountB - const recipientWalletId = walletIdUsdB - - // Send payment - const memo = "invoiceMemo #" + (Math.random() * 1_000_000).toFixed() - - const paymentResult = await Payments.intraledgerPaymentSendWalletIdForBtcWallet({ - senderWalletId, - senderAccount, - memo, - recipientWalletId, - amount: amountSats, - }) - expect(paymentResult).toEqual({ - status: PaymentSendStatus.Success, - transaction: expect.objectContaining({ - walletId: senderWalletId, - status: "success", - settlementAmount: amountSats * -1, - settlementCurrency: "BTC", - initiationVia: expect.objectContaining({ - type: "intraledger", - }), - settlementVia: expect.objectContaining({ - type: "intraledger", - }), - }), - }) - - // Check entries - const txns = await getAllTransactionsByMemo(memo) - if (txns instanceof Error) throw txns - - const senderTxn = txns.find( - ({ walletId, debit }) => walletId === senderWalletId && debit > 0, - ) - if (senderTxn === undefined) throw new Error("'senderTxn' not found") - const recipientTxn = txns.find( - ({ walletId, credit }) => walletId === recipientWalletId && credit > 0, - ) - if (recipientTxn === undefined) throw new Error("'recipientTxn' not found") - - const internalTxns = txns.filter( - ({ walletId }) => walletId !== senderWalletId && walletId !== recipientWalletId, - ) - expect(internalTxns.length).toEqual(2) - - const { EUR: expectedSenderDisplayProps } = await getDisplayAmounts({ - satsAmount: senderTxn.satsAmount || toSats(0), - satsFee: senderTxn.satsFee || toSats(0), - }) - - expect(senderTxn).toEqual( - expect.objectContaining({ - ...expectedSenderDisplayProps, - type: LedgerTransactionType.WalletIdTradeIntraAccount, - currency: WalletCurrency.Btc, - }), - ) - - expect(recipientTxn).toEqual( - expect.objectContaining({ - ...expectedSenderDisplayProps, - type: LedgerTransactionType.WalletIdTradeIntraAccount, - currency: WalletCurrency.Usd, - }), - ) - - for (const txn of internalTxns) { - expect(txn).toEqual( - expect.objectContaining({ - displayAmount: senderTxn.centsAmount, - displayFee: senderTxn.centsFee, - displayCurrency: UsdDisplayCurrency, - }), - ) - } - }) }) }) }) From e0b12da80952f5de5b46fd95c89f22120d62bfe6 Mon Sep 17 00:00:00 2001 From: vindard <17693119+vindard@users.noreply.github.com> Date: Thu, 30 Nov 2023 10:38:40 -0400 Subject: [PATCH 06/16] test: move WalletIdIntraledgerLedger metadata check --- .../app/wallets/send-intraledger.spec.ts | 51 ++++++++++++ .../02-user-wallet/02-tx-display.spec.ts | 77 ------------------- 2 files changed, 51 insertions(+), 77 deletions(-) diff --git a/core/api/test/integration/app/wallets/send-intraledger.spec.ts b/core/api/test/integration/app/wallets/send-intraledger.spec.ts index b11164633a..acbf27be7c 100644 --- a/core/api/test/integration/app/wallets/send-intraledger.spec.ts +++ b/core/api/test/integration/app/wallets/send-intraledger.spec.ts @@ -370,4 +370,55 @@ describe("intraLedgerPay", () => { walletIdTradeIntraAccountLedgerMetadataSpy.mockRestore() recordIntraledgerSpy.mockRestore() }) + + it("records transaction with wallet-id-intraledger metadata on intraledger send", async () => { + // Setup mocks + const displayAmountsConverterSpy = jest.spyOn( + DisplayAmountsConverterImpl, + "DisplayAmountsConverter", + ) + + const walletIdIntraledgerLedgerMetadataSpy = jest.spyOn( + LedgerFacadeImpl, + "WalletIdIntraledgerLedgerMetadata", + ) + const recordIntraledgerSpy = jest.spyOn(LedgerFacadeImpl, "recordIntraledger") + + // Setup users and wallets + const newWalletDescriptor = await createRandomUserAndBtcWallet() + const newAccount = await AccountsRepository().findById(newWalletDescriptor.accountId) + if (newAccount instanceof Error) throw newAccount + + const recipientWalletDescriptor = await createRandomUserAndBtcWallet() + + // Fund balance for send + const receive = await recordReceiveLnPayment({ + walletDescriptor: newWalletDescriptor, + paymentAmount: receiveAmounts, + bankFee: receiveBankFee, + displayAmounts: receiveDisplayAmounts, + memo, + }) + if (receive instanceof Error) throw receive + + // Pay intraledger + await Payments.intraledgerPaymentSendWalletIdForBtcWallet({ + recipientWalletId: recipientWalletDescriptor.id, + memo, + amount, + senderWalletId: newWalletDescriptor.id, + senderAccount: newAccount, + }) + + // Check record function was called with right metadata + expect(displayAmountsConverterSpy).toHaveBeenCalledTimes(2) + expect(walletIdIntraledgerLedgerMetadataSpy).toHaveBeenCalledTimes(1) + const args = recordIntraledgerSpy.mock.calls[0][0] + expect(args.metadata.type).toBe(LedgerTransactionType.IntraLedger) + + // Restore system state + displayAmountsConverterSpy.mockRestore() + walletIdIntraledgerLedgerMetadataSpy.mockRestore() + recordIntraledgerSpy.mockRestore() + }) }) diff --git a/core/api/test/legacy-integration/02-user-wallet/02-tx-display.spec.ts b/core/api/test/legacy-integration/02-user-wallet/02-tx-display.spec.ts index e1ac59cde2..56e561058f 100644 --- a/core/api/test/legacy-integration/02-user-wallet/02-tx-display.spec.ts +++ b/core/api/test/legacy-integration/02-user-wallet/02-tx-display.spec.ts @@ -814,81 +814,4 @@ describe("Display properties on transactions", () => { }) }) }) - - describe("wallet-id", () => { - describe("intraledger", () => { - it("(WalletIdIntraledgerLedgerMetadata) sends to an internal walletId", async () => { - // TxMetadata: - // - WalletIdIntraledgerLedgerMetadata - - const amountSats = 20_000 - - const senderWalletId = walletIdB - const senderAccount = accountB - const recipientWalletId = walletIdC - - // Send payment - const memo = "invoiceMemo #" + (Math.random() * 1_000_000).toFixed() - - const paymentResult = await Payments.intraledgerPaymentSendWalletIdForBtcWallet({ - senderWalletId, - senderAccount, - memo, - recipientWalletId, - amount: amountSats, - }) - expect(paymentResult).toEqual({ - status: PaymentSendStatus.Success, - transaction: expect.objectContaining({ - walletId: senderWalletId, - status: "success", - settlementAmount: amountSats * -1, - settlementCurrency: "BTC", - initiationVia: expect.objectContaining({ - type: "intraledger", - }), - settlementVia: expect.objectContaining({ - type: "intraledger", - }), - }), - }) - - // Check entries - const txns = await getAllTransactionsByMemo(memo) - if (txns instanceof Error) throw txns - - const senderTxn = txns.find(({ walletId }) => walletId === senderWalletId) - if (senderTxn === undefined) throw new Error("'senderTxn' not found") - const recipientTxn = txns.find(({ walletId }) => walletId === recipientWalletId) - if (recipientTxn === undefined) throw new Error("'recipientTxn' not found") - - const internalTxns = txns.filter( - ({ walletId }) => walletId !== senderWalletId && walletId !== recipientWalletId, - ) - expect(internalTxns.length).toEqual(0) - - const { EUR: expectedSenderDisplayProps, CRC: expectedRecipientDisplayProps } = - await getDisplayAmounts({ - satsAmount: senderTxn.satsAmount || toSats(0), - satsFee: senderTxn.satsFee || toSats(0), - }) - - expect(senderTxn).toEqual( - expect.objectContaining({ - ...expectedSenderDisplayProps, - type: LedgerTransactionType.IntraLedger, - currency: WalletCurrency.Btc, - }), - ) - - expect(recipientTxn).toEqual( - expect.objectContaining({ - ...expectedRecipientDisplayProps, - type: LedgerTransactionType.IntraLedger, - currency: WalletCurrency.Btc, - }), - ) - }) - }) - }) }) From 03816ff8dda58358fe2ce8e4f9cf2574a4604386 Mon Sep 17 00:00:00 2001 From: vindard <17693119+vindard@users.noreply.github.com> Date: Thu, 30 Nov 2023 15:10:46 -0400 Subject: [PATCH 07/16] test: move LnReceiveLedger metadata check --- .../wallets/update-pending-invoices.spec.ts | 92 +++++++++++++++++++ .../02-user-wallet/02-tx-display.spec.ts | 69 -------------- 2 files changed, 92 insertions(+), 69 deletions(-) diff --git a/core/api/test/integration/app/wallets/update-pending-invoices.spec.ts b/core/api/test/integration/app/wallets/update-pending-invoices.spec.ts index ffe45995d7..81de83ac78 100644 --- a/core/api/test/integration/app/wallets/update-pending-invoices.spec.ts +++ b/core/api/test/integration/app/wallets/update-pending-invoices.spec.ts @@ -2,6 +2,7 @@ import { randomUUID } from "crypto" import { MS_PER_DAY } from "@/config" +import * as LedgerFacadeImpl from "@/services/ledger/facade" import * as LndImpl from "@/services/lnd" import * as DeclinePendingInvoiceImpl from "@/app/wallets/decline-single-pending-invoice" @@ -13,7 +14,9 @@ import { updatePendingInvoice } from "@/app/wallets/update-single-pending-invoic import { toMilliSatsFromNumber, toSats } from "@/domain/bitcoin" import { decodeInvoice, getSecretAndPaymentHash } from "@/domain/bitcoin/lightning" import { DEFAULT_EXPIRATIONS } from "@/domain/bitcoin/lightning/invoice-expiration" +import { LedgerTransactionType } from "@/domain/ledger" import { WalletCurrency } from "@/domain/shared" +import * as DisplayAmountsConverterImpl from "@/domain/fiat" import { baseLogger } from "@/services/logger" import { WalletInvoicesRepository } from "@/services/mongoose" @@ -229,5 +232,94 @@ describe("update pending invoices", () => { // Restore system state lndServiceSpy.mockRestore() }) + + it("records transaction with ln-receive metadata on ln receive", async () => { + const invoiceAmount = toSats(1) + const { paymentHash } = getSecretAndPaymentHash() + const btcDelayMs = DEFAULT_EXPIRATIONS.BTC.delay * 1000 + const timeBuffer = 1000 // buffer for any time library discrepancies + const pastCreatedAt = new Date(Date.now() - (btcDelayMs + timeBuffer)) + + const walletInvoices = WalletInvoicesRepository() + + // Setup mocks + const lnInvoiceLookup: LnInvoiceLookup = { + paymentHash, + createdAt: pastCreatedAt, + confirmedAt: undefined, + isSettled: false, + isHeld: true, + heldAt: undefined, + roundedDownReceived: invoiceAmount, + milliSatsReceived: toMilliSatsFromNumber(1000), + secretPreImage: "secretPreImage" as SecretPreImage, + lnInvoice: { + description: "", + paymentRequest: undefined, + expiresAt: new Date(Date.now() + MS_PER_DAY), + roundedDownAmount: invoiceAmount, + }, + } + const { LndService: LnServiceOrig } = jest.requireActual("@/services/lnd") + const lndServiceSpy = jest.spyOn(LndImpl, "LndService").mockReturnValue({ + ...LnServiceOrig(), + defaultPubkey: () => DEFAULT_PUBKEY, + lookupInvoice: () => lnInvoiceLookup, + settleInvoice: () => true, + }) + + const displayAmountsConverterSpy = jest.spyOn( + DisplayAmountsConverterImpl, + "DisplayAmountsConverter", + ) + + const lnReceiveLedgerMetadataSpy = jest.spyOn( + LedgerFacadeImpl, + "LnReceiveLedgerMetadata", + ) + const recordReceiveOffChainSpy = jest.spyOn( + LedgerFacadeImpl, + "recordReceiveOffChain", + ) + + // Setup BTC wallet and invoice + const recipientWalletDescriptor = await createRandomUserAndBtcWallet() + + const btcWalletInvoice = { + paymentHash, + secret: "secretPreImage" as SecretPreImage, + selfGenerated: true, + pubkey: "pubkey" as Pubkey, + recipientWalletDescriptor, + paid: false, + lnInvoice: mockLnInvoice, + processingCompleted: false, + } + + const persisted = await walletInvoices.persistNew(btcWalletInvoice) + if (persisted instanceof Error) throw persisted + + await WalletInvoice.findOneAndUpdate( + { _id: paymentHash }, + { timestamp: pastCreatedAt }, + ) + + // Call invoice update + const walletInvoice = await walletInvoices.findByPaymentHash(paymentHash) + if (walletInvoice instanceof Error) throw walletInvoice + await updatePendingInvoice({ walletInvoice, logger: baseLogger }) + + // Check record function was called with right metadata + expect(displayAmountsConverterSpy).toHaveBeenCalledTimes(1) + expect(lnReceiveLedgerMetadataSpy).toHaveBeenCalledTimes(1) + const args = recordReceiveOffChainSpy.mock.calls[0][0] + expect(args.metadata.type).toBe(LedgerTransactionType.Invoice) + + // Restore system state + lndServiceSpy.mockRestore() + displayAmountsConverterSpy.mockRestore() + lnReceiveLedgerMetadataSpy.mockRestore() + recordReceiveOffChainSpy.mockRestore() + }) }) }) diff --git a/core/api/test/legacy-integration/02-user-wallet/02-tx-display.spec.ts b/core/api/test/legacy-integration/02-user-wallet/02-tx-display.spec.ts index 56e561058f..17232aa89c 100644 --- a/core/api/test/legacy-integration/02-user-wallet/02-tx-display.spec.ts +++ b/core/api/test/legacy-integration/02-user-wallet/02-tx-display.spec.ts @@ -201,75 +201,6 @@ describe("Display properties on transactions", () => { } describe("ln", () => { - describe("receive", () => { - it("(LnReceiveLedgerMetadata) receives payment from outside", async () => { - // larger amount to not fall below the escrow limit - const amountInvoice = toSats(100_000) - - const recipientWalletId = walletIdB - - // Receive payment - const memo = "invoiceMemo #" + (Math.random() * 1_000_000).toFixed() - - const invoice = await Wallets.addInvoiceForSelfForBtcWallet({ - walletId: recipientWalletId, - amount: amountInvoice, - memo, - }) - if (invoice instanceof Error) throw invoice - const { paymentRequest, paymentHash } = invoice.lnInvoice - - const [outsideLndpayResult, updateResult] = await Promise.all([ - safePay({ lnd: lndOutside1, request: paymentRequest }), - (async () => { - // TODO: we could use event instead of a sleep to lower test latency - await sleep(500) - return Wallets.handleHeldInvoiceByPaymentHash({ - paymentHash, - logger: baseLogger, - }) - })(), - ]) - expect(outsideLndpayResult?.is_confirmed).toBe(true) - expect(updateResult).not.toBeInstanceOf(Error) - - // Check entries - const txns = await getAllTransactionsByHash(paymentHash) - if (txns instanceof Error) throw txns - - const recipientTxn = txns.find( - ({ walletId, type }) => - walletId === recipientWalletId && type === LedgerTransactionType.Invoice, - ) - if (recipientTxn === undefined) throw new Error("'recipientTxn' not found") - - const internalTxns = txns.filter(({ walletId }) => walletId !== recipientWalletId) - expect(internalTxns.length).toBeGreaterThan(0) - - const { EUR: expectedRecipientDisplayProps } = await getDisplayAmounts({ - satsAmount: recipientTxn.satsAmount || toSats(0), - satsFee: recipientTxn.satsFee || toSats(0), - }) - - expect(recipientTxn).toEqual( - expect.objectContaining({ - ...expectedRecipientDisplayProps, - type: LedgerTransactionType.Invoice, - }), - ) - - for (const txn of internalTxns) { - expect(txn).toEqual( - expect.objectContaining({ - displayAmount: recipientTxn.centsAmount, - displayFee: recipientTxn.centsFee, - displayCurrency: UsdDisplayCurrency, - }), - ) - } - }) - }) - describe("send", () => { it("(LnFailedPaymentReceiveLedgerMetadata) pay zero amount invoice & revert txn when verifyMaxFee fails", async () => { // TxMetadata: From 824198647436899b4790dae346fd2c89b59c394a Mon Sep 17 00:00:00 2001 From: vindard <17693119+vindard@users.noreply.github.com> Date: Thu, 30 Nov 2023 15:17:01 -0400 Subject: [PATCH 08/16] test: move OnChainSendLedger metadata check --- .../app/wallets/send-onchain.spec.ts | 58 +++++++++- .../02-user-wallet/02-tx-display.spec.ts | 102 +----------------- 2 files changed, 58 insertions(+), 102 deletions(-) diff --git a/core/api/test/integration/app/wallets/send-onchain.spec.ts b/core/api/test/integration/app/wallets/send-onchain.spec.ts index 412df8b4aa..cea24c0db5 100644 --- a/core/api/test/integration/app/wallets/send-onchain.spec.ts +++ b/core/api/test/integration/app/wallets/send-onchain.spec.ts @@ -22,14 +22,16 @@ import { InvalidBtcPaymentAmountError, WalletCurrency, } from "@/domain/shared" +import * as DisplayAmountsConverterImpl from "@/domain/fiat" import { PayoutSpeed } from "@/domain/bitcoin/onchain" import { PaymentSendStatus } from "@/domain/bitcoin/lightning" +import { DealerPriceService } from "@/services/dealer-price" import { AccountsRepository } from "@/services/mongoose" import { Transaction, TransactionMetadata } from "@/services/ledger/schema" import * as PushNotificationsServiceImpl from "@/services/notifications/push-notifications" -import { DealerPriceService } from "@/services/dealer-price" +import * as LedgerFacadeImpl from "@/services/ledger/facade" import { timestampDaysAgo } from "@/utils" @@ -40,6 +42,7 @@ import { recordReceiveLnPayment, } from "test/helpers" import { getBalanceHelper } from "test/helpers/wallet" +import { LedgerTransactionType } from "@/domain/ledger" let outsideAddress: OnChainAddress let memo: string @@ -301,6 +304,59 @@ describe("onChainPay", () => { }) expect(res).toBeInstanceOf(InactiveAccountError) }) + + it("records transaction with send-onchain metadata on send", async () => { + // Setup mocks + const displayAmountsConverterSpy = jest.spyOn( + DisplayAmountsConverterImpl, + "DisplayAmountsConverter", + ) + + const onChainSendLedgerMetadataSpy = jest.spyOn( + LedgerFacadeImpl, + "OnChainSendLedgerMetadata", + ) + const recordOnChainSendSpy = jest.spyOn(LedgerFacadeImpl, "recordSendOnChain") + + // Create users + const newWalletDescriptor = await createRandomUserAndBtcWallet() + const newAccount = await AccountsRepository().findById( + newWalletDescriptor.accountId, + ) + if (newAccount instanceof Error) throw newAccount + + // Fund balance for send + const receive = await recordReceiveLnPayment({ + walletDescriptor: newWalletDescriptor, + paymentAmount: receiveAmounts, + bankFee: receiveBankFee, + displayAmounts: receiveDisplayAmounts, + memo, + }) + if (receive instanceof Error) throw receive + + // Send payment + await Payments.payOnChainByWalletIdForBtcWallet({ + senderWalletId: newWalletDescriptor.id, + senderAccount: newAccount, + amount, + address: outsideAddress, + + speed: PayoutSpeed.Fast, + memo, + }) + + // Check record function was called with right metadata + expect(displayAmountsConverterSpy).toHaveBeenCalledTimes(1) + expect(onChainSendLedgerMetadataSpy).toHaveBeenCalledTimes(1) + const args = recordOnChainSendSpy.mock.calls[0][0] + expect(args.metadata.type).toBe(LedgerTransactionType.OnchainPayment) + + // Restore system state + displayAmountsConverterSpy.mockRestore() + onChainSendLedgerMetadataSpy.mockRestore() + recordOnChainSendSpy.mockRestore() + }) }) describe("settles intraledger", () => { diff --git a/core/api/test/legacy-integration/02-user-wallet/02-tx-display.spec.ts b/core/api/test/legacy-integration/02-user-wallet/02-tx-display.spec.ts index 17232aa89c..dee2c79e34 100644 --- a/core/api/test/legacy-integration/02-user-wallet/02-tx-display.spec.ts +++ b/core/api/test/legacy-integration/02-user-wallet/02-tx-display.spec.ts @@ -16,24 +16,19 @@ import { updateDisplayCurrency } from "@/app/accounts" import { PayoutSpeed } from "@/domain/bitcoin/onchain" import { translateToLedgerTx } from "@/services/ledger" -import { MainBook, Transaction } from "@/services/ledger/books" -import * as BriaImpl from "@/services/bria" +import { MainBook } from "@/services/ledger/books" import { BriaPayloadType } from "@/services/bria" import { AccountsRepository } from "@/services/mongoose" import { toObjectId } from "@/services/mongoose/utils" -import { baseLogger } from "@/services/logger" import { utxoDetectedEventHandler, utxoSettledEventHandler, } from "@/servers/event-handlers/bria" -import { sleep } from "@/utils" - import { bitcoindClient, bitcoindOutside, - createChainAddress, createInvoice, createUserAndWalletFromPhone, getAccountByPhone, @@ -44,7 +39,6 @@ import { onceBriaSubscribe, RANDOM_ADDRESS, randomPhone, - safePay, sendToAddressAndConfirm, } from "test/helpers" @@ -650,99 +644,5 @@ describe("Display properties on transactions", () => { } }) }) - - describe("send", () => { - it("(OnChainSendLedgerMetadata) sends an onchain payment", async () => { - // TxMetadata: - // - OnChainSendLedgerMetadata - - const { OnChainService: OnChainServiceOrig } = - jest.requireActual("@/services/bria") - const briaSpy = jest.spyOn(BriaImpl, "OnChainService").mockReturnValue({ - ...OnChainServiceOrig(), - queuePayoutToAddress: async () => "payoutId" as PayoutId, - }) - - const amountSats = toSats(20_000) - - const senderWalletId = walletIdB - const senderAccount = accountB - - // Execute Send - const memo = "invoiceMemo #" + (Math.random() * 1_000_000).toFixed() - - const { address } = await createChainAddress({ - format: "p2wpkh", - lnd: lndOutside1, - }) - - const paymentResult = await Payments.payOnChainByWalletIdForBtcWallet({ - senderAccount, - senderWalletId, - address, - amount: amountSats, - speed: PayoutSpeed.Fast, - memo, - }) - if (paymentResult instanceof Error) throw paymentResult - expect(paymentResult).toEqual({ - status: PaymentSendStatus.Success, - transaction: expect.objectContaining({ - walletId: senderWalletId, - status: "pending", - settlementAmount: (amountSats + paymentResult.transaction.settlementFee) * -1, - settlementCurrency: "BTC", - initiationVia: expect.objectContaining({ - type: "onchain", - address, - }), - settlementVia: expect.objectContaining({ - type: "onchain", - transactionHash: undefined, - vout: undefined, - }), - }), - }) - - // Check entries - const txns = await getAllTransactionsByMemo(memo) - if (txns instanceof Error) throw txns - - const senderTxn = txns.find( - ({ walletId, debit }) => walletId === senderWalletId && debit > 0, - ) - if (senderTxn === undefined) throw new Error("'senderTxn' not found") - - const internalTxns = txns.filter(({ walletId }) => walletId !== senderWalletId) - expect(internalTxns.length).toEqual(2) - - const { EUR: expectedSenderDisplayProps } = await getDisplayAmounts({ - satsAmount: senderTxn.satsAmount || toSats(0), - satsFee: senderTxn.satsFee || toSats(0), - }) - - expect(senderTxn).toEqual( - expect.objectContaining({ - ...expectedSenderDisplayProps, - type: LedgerTransactionType.OnchainPayment, - currency: WalletCurrency.Btc, - }), - ) - - for (const txn of internalTxns) { - expect(txn).toEqual( - expect.objectContaining({ - displayAmount: senderTxn.centsAmount, - displayFee: senderTxn.centsFee, - displayCurrency: UsdDisplayCurrency, - }), - ) - } - - // Clean up after test - await Transaction.deleteMany({ memo }) - briaSpy.mockRestore() - }) - }) }) }) From 2daf4c831683a5048f1caa5d6943ab8af7aafcf9 Mon Sep 17 00:00:00 2001 From: vindard <17693119+vindard@users.noreply.github.com> Date: Thu, 30 Nov 2023 15:22:26 -0400 Subject: [PATCH 09/16] test: move OnChainReceiveLedger metadata check --- .../add-settled-on-chain-transaction.spec.ts | 47 ++++++++- .../02-user-wallet/02-tx-display.spec.ts | 98 ------------------- 2 files changed, 46 insertions(+), 99 deletions(-) diff --git a/core/api/test/integration/app/wallets/add-settled-on-chain-transaction.spec.ts b/core/api/test/integration/app/wallets/add-settled-on-chain-transaction.spec.ts index 2a5a65474d..a1505ff270 100644 --- a/core/api/test/integration/app/wallets/add-settled-on-chain-transaction.spec.ts +++ b/core/api/test/integration/app/wallets/add-settled-on-chain-transaction.spec.ts @@ -1,11 +1,14 @@ import { addSettledTransaction } from "@/app/wallets" +import { LedgerTransactionType } from "@/domain/ledger" import { WalletCurrency } from "@/domain/shared" -import { Transaction, TransactionMetadata } from "@/services/ledger/schema" +import * as DisplayAmountsConverterImpl from "@/domain/fiat" +import { Transaction, TransactionMetadata } from "@/services/ledger/schema" import { WalletOnChainAddressesRepository } from "@/services/mongoose" import { Wallet } from "@/services/mongoose/schema" import * as PushNotificationsServiceImpl from "@/services/notifications/push-notifications" +import * as LedgerFacadeImpl from "@/services/ledger/facade" import { createMandatoryUsers, createRandomUserAndWallets } from "test/helpers" @@ -62,4 +65,46 @@ describe("addSettledTransaction", () => { // Restore system state pushNotificationsServiceSpy.mockRestore() }) + + it("records transaction with receive-onchain metadata on receive", async () => { + // Setup mocks + const displayAmountsConverterSpy = jest.spyOn( + DisplayAmountsConverterImpl, + "DisplayAmountsConverter", + ) + + const onChainReceiveLedgerMetadataSpy = jest.spyOn( + LedgerFacadeImpl, + "OnChainReceiveLedgerMetadata", + ) + const recordOnChainReceiveSpy = jest.spyOn(LedgerFacadeImpl, "recordReceiveOnChain") + + // Create user + const { btcWalletDescriptor } = await createRandomUserAndWallets() + + // Add address to user wallet + await WalletOnChainAddressesRepository().persistNew({ + walletId: btcWalletDescriptor.id, + onChainAddress: { address }, + }) + + // Add settled transaction + await addSettledTransaction({ + txId: "txId" as OnChainTxHash, + vout: 0 as OnChainTxVout, + satoshis: btcAmount, + address, + }) + + // Check record function was called with right metadata + expect(displayAmountsConverterSpy).toHaveBeenCalledTimes(1) + expect(onChainReceiveLedgerMetadataSpy).toHaveBeenCalledTimes(1) + const args = recordOnChainReceiveSpy.mock.calls[0][0] + expect(args.metadata.type).toBe(LedgerTransactionType.OnchainReceipt) + + // Restore system state + displayAmountsConverterSpy.mockRestore() + onChainReceiveLedgerMetadataSpy.mockRestore() + recordOnChainReceiveSpy.mockRestore() + }) }) diff --git a/core/api/test/legacy-integration/02-user-wallet/02-tx-display.spec.ts b/core/api/test/legacy-integration/02-user-wallet/02-tx-display.spec.ts index dee2c79e34..46608699b8 100644 --- a/core/api/test/legacy-integration/02-user-wallet/02-tx-display.spec.ts +++ b/core/api/test/legacy-integration/02-user-wallet/02-tx-display.spec.ts @@ -39,7 +39,6 @@ import { onceBriaSubscribe, RANDOM_ADDRESS, randomPhone, - sendToAddressAndConfirm, } from "test/helpers" let accountB: Account @@ -272,104 +271,7 @@ describe("Display properties on transactions", () => { }) describe("onchain", () => { - const sendToWalletTestWrapper = async ({ - amountSats, - walletId, - }: { - amountSats: Satoshis - walletId: WalletId - }) => { - const address = await Wallets.createOnChainAddress({ - walletId, - }) - if (address instanceof Error) throw address - expect(address.substring(0, 4)).toBe("bcrt") - - const txId = await sendToAddressAndConfirm({ - walletClient: bitcoindOutside, - address, - amount: sat2btc(amountSats), - }) - if (txId instanceof Error) throw txId - - // Register confirmed txn in database - const detectedEvent = await onceBriaSubscribe({ - type: BriaPayloadType.UtxoDetected, - txId, - }) - if (detectedEvent?.payload.type !== BriaPayloadType.UtxoDetected) { - throw new Error(`Expected ${BriaPayloadType.UtxoDetected} event`) - } - const resultPending = await utxoDetectedEventHandler({ - event: detectedEvent.payload, - }) - if (resultPending instanceof Error) { - throw resultPending - } - - const settledEvent = await onceBriaSubscribe({ - type: BriaPayloadType.UtxoSettled, - txId, - }) - if (settledEvent?.payload.type !== BriaPayloadType.UtxoSettled) { - throw new Error(`Expected ${BriaPayloadType.UtxoSettled} event`) - } - const resultSettled = await utxoSettledEventHandler({ event: settledEvent.payload }) - if (resultSettled instanceof Error) { - throw resultSettled - } - - return txId - } - describe("receive", () => { - it("(OnChainReceiveLedgerMetadata) receives on-chain transaction", async () => { - // TxMetadata: - // - OnChainReceiveLedgerMetadata - - const amountSats = toSats(20_000) - - const recipientWalletId = walletIdB - - // Execute receive - const txId = await sendToWalletTestWrapper({ - walletId: recipientWalletId, - amountSats, - }) - - // Check entries - const txns = await getAllTransactionsByHash(txId) - if (txns instanceof Error) throw txns - - const recipientTxn = txns.find(({ walletId }) => walletId === recipientWalletId) - if (recipientTxn === undefined) throw new Error("'recipientTxn' not found") - - const internalTxns = txns.filter(({ walletId }) => walletId !== recipientWalletId) - expect(internalTxns.length).toBeGreaterThan(0) - - const { EUR: expectedRecipientDisplayProps } = await getDisplayAmounts({ - satsAmount: recipientTxn.satsAmount || toSats(0), - satsFee: recipientTxn.satsFee || toSats(0), - }) - - expect(recipientTxn).toEqual( - expect.objectContaining({ - ...expectedRecipientDisplayProps, - type: LedgerTransactionType.OnchainReceipt, - }), - ) - - for (const txn of internalTxns) { - expect(txn).toEqual( - expect.objectContaining({ - displayAmount: recipientTxn.centsAmount, - displayFee: recipientTxn.centsFee, - displayCurrency: UsdDisplayCurrency, - }), - ) - } - }) - it("(Pending, no metadata) identifies unconfirmed incoming on-chain transactions", async () => { const amountSats = toSats(20_000) From bb6ffe9f2b3db115d010b501ab0faaec645daed9 Mon Sep 17 00:00:00 2001 From: vindard <17693119+vindard@users.noreply.github.com> Date: Thu, 30 Nov 2023 22:59:56 -0400 Subject: [PATCH 10/16] test: move OnChainTradeIntraAccountLedger metadata check --- .../app/wallets/send-onchain.spec.ts | 75 +++++++++++++ .../02-user-wallet/02-tx-display.spec.ts | 102 ------------------ 2 files changed, 75 insertions(+), 102 deletions(-) diff --git a/core/api/test/integration/app/wallets/send-onchain.spec.ts b/core/api/test/integration/app/wallets/send-onchain.spec.ts index cea24c0db5..d3e0ceaf82 100644 --- a/core/api/test/integration/app/wallets/send-onchain.spec.ts +++ b/core/api/test/integration/app/wallets/send-onchain.spec.ts @@ -680,5 +680,80 @@ describe("onChainPay", () => { // Restore system state pushNotificationsServiceSpy.mockRestore() }) + + it("records transaction with onchain-trade-intra-account metadata on intraledger send", async () => { + // Setup mocks + const displayAmountsConverterSpy = jest.spyOn( + DisplayAmountsConverterImpl, + "DisplayAmountsConverter", + ) + + const onChainTradeIntraAccountLedgerMetadataSpy = jest.spyOn( + LedgerFacadeImpl, + "OnChainTradeIntraAccountLedgerMetadata", + ) + const recordIntraledgerSpy = jest.spyOn(LedgerFacadeImpl, "recordIntraledger") + + // Create users + const { btcWalletDescriptor: newWalletDescriptor, usdWalletDescriptor } = + await createRandomUserAndWallets() + const newAccount = await AccountsRepository().findById( + newWalletDescriptor.accountId, + ) + if (newAccount instanceof Error) throw newAccount + + // Fund balance for send + const receive = await recordReceiveLnPayment({ + walletDescriptor: newWalletDescriptor, + paymentAmount: receiveAmounts, + bankFee: receiveBankFee, + displayAmounts: receiveDisplayAmounts, + memo, + }) + if (receive instanceof Error) throw receive + + const recipientWalletIdAddress = await Wallets.createOnChainAddress({ + walletId: usdWalletDescriptor.id, + }) + if (recipientWalletIdAddress instanceof Error) throw recipientWalletIdAddress + + // Execute payment + const paymentResult = await Payments.payOnChainByWalletIdForBtcWallet({ + senderWalletId: newWalletDescriptor.id, + senderAccount: newAccount, + amount, + address: recipientWalletIdAddress, + speed: PayoutSpeed.Fast, + memo, + }) + if (paymentResult instanceof Error) throw paymentResult + expect(paymentResult).toEqual({ + status: PaymentSendStatus.Success, + transaction: expect.objectContaining({ + walletId: newWalletDescriptor.id, + status: "success", + settlementAmount: amount * -1, + settlementCurrency: "BTC", + initiationVia: expect.objectContaining({ + type: "onchain", + address: recipientWalletIdAddress, + }), + settlementVia: expect.objectContaining({ + type: "intraledger", + }), + }), + }) + + // Check record function was called with right metadata + expect(displayAmountsConverterSpy).toHaveBeenCalledTimes(2) + expect(onChainTradeIntraAccountLedgerMetadataSpy).toHaveBeenCalledTimes(1) + const args = recordIntraledgerSpy.mock.calls[0][0] + expect(args.metadata.type).toBe(LedgerTransactionType.OnChainTradeIntraAccount) + + // Restore system state + displayAmountsConverterSpy.mockRestore() + onChainTradeIntraAccountLedgerMetadataSpy.mockRestore() + recordIntraledgerSpy.mockRestore() + }) }) }) diff --git a/core/api/test/legacy-integration/02-user-wallet/02-tx-display.spec.ts b/core/api/test/legacy-integration/02-user-wallet/02-tx-display.spec.ts index 46608699b8..6b60356638 100644 --- a/core/api/test/legacy-integration/02-user-wallet/02-tx-display.spec.ts +++ b/core/api/test/legacy-integration/02-user-wallet/02-tx-display.spec.ts @@ -34,7 +34,6 @@ import { getAccountByPhone, getDefaultWalletIdByPhone, getPendingTransactionsForWalletId, - getUsdWalletIdByPhone, lndOutside1, onceBriaSubscribe, RANDOM_ADDRESS, @@ -45,7 +44,6 @@ let accountB: Account let accountC: Account let walletIdB: WalletId -let walletIdUsdB: WalletId let walletIdC: WalletId const phoneB = randomPhone() @@ -59,7 +57,6 @@ beforeAll(async () => { accountC = await getAccountByPhone(phoneC) walletIdB = await getDefaultWalletIdByPhone(phoneB) - walletIdUsdB = await getUsdWalletIdByPhone(phoneB) walletIdC = await getDefaultWalletIdByPhone(phoneC) await bitcoindClient.loadWallet({ filename: "outside" }) @@ -446,105 +443,6 @@ describe("Display properties on transactions", () => { }), ) }) - - it("(OnChainTradeIntraAccountLedgerMetadata) self trade via onchain address", async () => { - // TxMetadata: - // - OnChainTradeIntraAccountLedgerMetadata - - const amountSats = toSats(20_000) - - const senderWalletId = walletIdB - const senderAccount = accountB - const recipientWalletId = walletIdUsdB - - // Execute Send - const memo = "invoiceMemo #" + (Math.random() * 1_000_000).toFixed() - - const address = await Wallets.createOnChainAddress({ - walletId: recipientWalletId, - }) - if (address instanceof Error) throw address - - const paymentResult = await Payments.payOnChainByWalletIdForBtcWallet({ - senderAccount, - senderWalletId, - address, - amount: amountSats, - speed: PayoutSpeed.Fast, - memo, - }) - if (paymentResult instanceof Error) throw paymentResult - expect(paymentResult).toEqual({ - status: PaymentSendStatus.Success, - transaction: expect.objectContaining({ - walletId: senderWalletId, - status: "success", - settlementAmount: amountSats * -1, - settlementCurrency: "BTC", - initiationVia: expect.objectContaining({ - type: "onchain", - address, - }), - settlementVia: expect.objectContaining({ - type: "intraledger", - }), - }), - }) - - // Check entries - const memoTxns = await getAllTransactionsByMemo(memo) - if (memoTxns instanceof Error) throw memoTxns - expect(memoTxns.length).toEqual(1) - const { journalId } = memoTxns[0] - const txns = await getAllTransactionsByJournalId(journalId) - if (txns instanceof Error) throw txns - - const senderTxn = txns.find( - ({ walletId, debit }) => walletId === senderWalletId && debit > 0, - ) - if (senderTxn === undefined) throw new Error("'senderTxn' not found") - - const recipientTxn = txns.find( - ({ walletId, credit }) => walletId === recipientWalletId && credit > 0, - ) - if (recipientTxn === undefined) throw new Error("'recipientTxn' not found") - - const internalTxns = txns.filter( - ({ walletId }) => walletId !== senderWalletId && walletId !== recipientWalletId, - ) - expect(internalTxns.length).toEqual(2) - - const { EUR: expectedSenderDisplayProps } = await getDisplayAmounts({ - satsAmount: recipientTxn.satsAmount || toSats(0), - satsFee: recipientTxn.satsFee || toSats(0), - }) - - expect(senderTxn).toEqual( - expect.objectContaining({ - ...expectedSenderDisplayProps, - type: LedgerTransactionType.OnChainTradeIntraAccount, - currency: WalletCurrency.Btc, - }), - ) - - expect(recipientTxn).toEqual( - expect.objectContaining({ - ...expectedSenderDisplayProps, - type: LedgerTransactionType.OnChainTradeIntraAccount, - currency: WalletCurrency.Usd, - }), - ) - - for (const txn of internalTxns) { - expect(txn).toEqual( - expect.objectContaining({ - displayAmount: senderTxn.centsAmount, - displayFee: senderTxn.centsFee, - displayCurrency: UsdDisplayCurrency, - }), - ) - } - }) }) }) }) From 411485a6e8a4db0312a01f2b08625390536eb439 Mon Sep 17 00:00:00 2001 From: vindard <17693119+vindard@users.noreply.github.com> Date: Thu, 30 Nov 2023 23:06:33 -0400 Subject: [PATCH 11/16] test: move OnChainIntraledgerLedger metadata check --- .../app/wallets/send-onchain.spec.ts | 76 +++++++++++ .../02-user-wallet/02-tx-display.spec.ts | 129 +----------------- 2 files changed, 77 insertions(+), 128 deletions(-) diff --git a/core/api/test/integration/app/wallets/send-onchain.spec.ts b/core/api/test/integration/app/wallets/send-onchain.spec.ts index d3e0ceaf82..dc92faf7ae 100644 --- a/core/api/test/integration/app/wallets/send-onchain.spec.ts +++ b/core/api/test/integration/app/wallets/send-onchain.spec.ts @@ -755,5 +755,81 @@ describe("onChainPay", () => { onChainTradeIntraAccountLedgerMetadataSpy.mockRestore() recordIntraledgerSpy.mockRestore() }) + + it("records transaction with onchain-intraledger metadata on intraledger send", async () => { + // Setup mocks + const displayAmountsConverterSpy = jest.spyOn( + DisplayAmountsConverterImpl, + "DisplayAmountsConverter", + ) + + const onChainIntraledgerLedgerMetadataSpy = jest.spyOn( + LedgerFacadeImpl, + "OnChainIntraledgerLedgerMetadata", + ) + const recordIntraledgerSpy = jest.spyOn(LedgerFacadeImpl, "recordIntraledger") + + // Setup users and wallets + const newWalletDescriptor = await createRandomUserAndBtcWallet() + const newAccount = await AccountsRepository().findById( + newWalletDescriptor.accountId, + ) + if (newAccount instanceof Error) throw newAccount + + const recipientWalletDescriptor = await createRandomUserAndBtcWallet() + + // Fund balance for send + const receive = await recordReceiveLnPayment({ + walletDescriptor: newWalletDescriptor, + paymentAmount: receiveAmounts, + bankFee: receiveBankFee, + displayAmounts: receiveDisplayAmounts, + memo, + }) + if (receive instanceof Error) throw receive + + const recipientWalletIdAddress = await Wallets.createOnChainAddress({ + walletId: recipientWalletDescriptor.id, + }) + if (recipientWalletIdAddress instanceof Error) throw recipientWalletIdAddress + + // Execute payment + const paymentResult = await Payments.payOnChainByWalletIdForBtcWallet({ + senderWalletId: newWalletDescriptor.id, + senderAccount: newAccount, + amount, + address: recipientWalletIdAddress, + speed: PayoutSpeed.Fast, + memo, + }) + if (paymentResult instanceof Error) throw paymentResult + expect(paymentResult).toEqual({ + status: PaymentSendStatus.Success, + transaction: expect.objectContaining({ + walletId: newWalletDescriptor.id, + status: "success", + settlementAmount: amount * -1, + settlementCurrency: "BTC", + initiationVia: expect.objectContaining({ + type: "onchain", + address: recipientWalletIdAddress, + }), + settlementVia: expect.objectContaining({ + type: "intraledger", + }), + }), + }) + + // Check record function was called with right metadata + expect(displayAmountsConverterSpy).toHaveBeenCalledTimes(2) + expect(onChainIntraledgerLedgerMetadataSpy).toHaveBeenCalledTimes(1) + const args = recordIntraledgerSpy.mock.calls[0][0] + expect(args.metadata.type).toBe(LedgerTransactionType.OnchainIntraLedger) + + // Restore system state + displayAmountsConverterSpy.mockRestore() + onChainIntraledgerLedgerMetadataSpy.mockRestore() + recordIntraledgerSpy.mockRestore() + }) }) }) diff --git a/core/api/test/legacy-integration/02-user-wallet/02-tx-display.spec.ts b/core/api/test/legacy-integration/02-user-wallet/02-tx-display.spec.ts index 6b60356638..0b4fa80cb0 100644 --- a/core/api/test/legacy-integration/02-user-wallet/02-tx-display.spec.ts +++ b/core/api/test/legacy-integration/02-user-wallet/02-tx-display.spec.ts @@ -1,10 +1,7 @@ import { Payments, Wallets } from "@/app" import { getCurrentPriceAsDisplayPriceRatio } from "@/app/prices" -import { - MaxFeeTooLargeForRoutelessPaymentError, - PaymentSendStatus, -} from "@/domain/bitcoin/lightning" +import { MaxFeeTooLargeForRoutelessPaymentError } from "@/domain/bitcoin/lightning" import { sat2btc, toSats } from "@/domain/bitcoin" import { LedgerTransactionType, UnknownLedgerError } from "@/domain/ledger" import * as LnFeesImpl from "@/domain/payments" @@ -13,13 +10,10 @@ import { UsdDisplayCurrency, displayAmountFromNumber } from "@/domain/fiat" import { updateDisplayCurrency } from "@/app/accounts" -import { PayoutSpeed } from "@/domain/bitcoin/onchain" - import { translateToLedgerTx } from "@/services/ledger" import { MainBook } from "@/services/ledger/books" import { BriaPayloadType } from "@/services/bria" import { AccountsRepository } from "@/services/mongoose" -import { toObjectId } from "@/services/mongoose/utils" import { utxoDetectedEventHandler, @@ -44,7 +38,6 @@ let accountB: Account let accountC: Account let walletIdB: WalletId -let walletIdC: WalletId const phoneB = randomPhone() const phoneC = randomPhone() @@ -57,7 +50,6 @@ beforeAll(async () => { accountC = await getAccountByPhone(phoneC) walletIdB = await getDefaultWalletIdByPhone(phoneB) - walletIdC = await getDefaultWalletIdByPhone(phoneC) await bitcoindClient.loadWallet({ filename: "outside" }) @@ -109,36 +101,6 @@ describe("Display properties on transactions", () => { } } - const getAllTransactionsByJournalId = async ( - journalId: LedgerJournalId, - ): Promise[] | LedgerServiceError> => { - try { - const { results } = await MainBook.ledger({ - _journal: toObjectId(journalId), - }) - /* eslint @typescript-eslint/ban-ts-comment: "off" */ - // @ts-ignore-next-line no-implicit-any error - return results.map((tx) => translateToLedgerTx(tx)) - } catch (err) { - return new UnknownLedgerError(err) - } - } - - const getAllTransactionsByMemo = async ( - memoPayer: string, - ): Promise[] | LedgerServiceError> => { - try { - const { results } = await MainBook.ledger({ - memoPayer, - }) - /* eslint @typescript-eslint/ban-ts-comment: "off" */ - // @ts-ignore-next-line no-implicit-any error - return results.map((tx) => translateToLedgerTx(tx)) - } catch (err) { - return new UnknownLedgerError(err) - } - } - const getDisplayAmounts = async ({ satsAmount, satsFee, @@ -355,94 +317,5 @@ describe("Display properties on transactions", () => { } }) }) - - describe("intraledger", () => { - it("(OnChainIntraledgerLedgerMetadata) pays another galoy user via onchain address", async () => { - // TxMetadata: - // - OnChainIntraledgerLedgerMetadata - const amountSats = toSats(20_000) - - const senderWalletId = walletIdB - const senderAccount = accountB - const recipientWalletId = walletIdC - - // Execute Send - const memo = "invoiceMemo #" + (Math.random() * 1_000_000).toFixed() - - const address = await Wallets.createOnChainAddress({ - walletId: recipientWalletId, - }) - if (address instanceof Error) throw address - - const paymentResult = await Payments.payOnChainByWalletIdForBtcWallet({ - senderAccount, - senderWalletId, - address, - amount: amountSats, - speed: PayoutSpeed.Fast, - memo, - }) - if (paymentResult instanceof Error) throw paymentResult - expect(paymentResult).toEqual({ - status: PaymentSendStatus.Success, - transaction: expect.objectContaining({ - walletId: senderWalletId, - status: "success", - settlementAmount: amountSats * -1, - settlementCurrency: "BTC", - initiationVia: expect.objectContaining({ - type: "onchain", - address, - }), - settlementVia: expect.objectContaining({ - type: "intraledger", - }), - }), - }) - - // Check entries - const memoTxns = await getAllTransactionsByMemo(memo) - if (memoTxns instanceof Error) throw memoTxns - expect(memoTxns.length).toEqual(1) - const { journalId } = memoTxns[0] - const txns = await getAllTransactionsByJournalId(journalId) - if (txns instanceof Error) throw txns - - const senderTxn = txns.find( - ({ walletId, debit }) => walletId === senderWalletId && debit > 0, - ) - if (senderTxn === undefined) throw new Error("'senderTxn' not found") - - const recipientTxn = txns.find( - ({ walletId, credit }) => walletId === recipientWalletId && credit > 0, - ) - if (recipientTxn === undefined) throw new Error("'recipientTxn' not found") - - const internalTxns = txns.filter( - ({ walletId }) => walletId !== senderWalletId && walletId !== recipientWalletId, - ) - expect(internalTxns.length).toEqual(0) - - const { EUR: expectedSenderDisplayProps, CRC: expectedRecipientDisplayProps } = - await getDisplayAmounts({ - satsAmount: recipientTxn.satsAmount || toSats(0), - satsFee: recipientTxn.satsFee || toSats(0), - }) - - expect(senderTxn).toEqual( - expect.objectContaining({ - ...expectedSenderDisplayProps, - type: LedgerTransactionType.OnchainIntraLedger, - }), - ) - - expect(recipientTxn).toEqual( - expect.objectContaining({ - ...expectedRecipientDisplayProps, - type: LedgerTransactionType.OnchainIntraLedger, - }), - ) - }) - }) }) }) From 51805b611918cc2a642d03eab304d16bd043bcc2 Mon Sep 17 00:00:00 2001 From: vindard <17693119+vindard@users.noreply.github.com> Date: Fri, 1 Dec 2023 00:10:33 -0400 Subject: [PATCH 12/16] test: move LnFailedPaymentReceiveLedger metadata check --- .../wallets/update-pending-payments.spec.ts | 108 ++++++++++++++++++ .../02-user-wallet/02-tx-display.spec.ts | 103 +---------------- 2 files changed, 110 insertions(+), 101 deletions(-) create mode 100644 core/api/test/integration/app/wallets/update-pending-payments.spec.ts diff --git a/core/api/test/integration/app/wallets/update-pending-payments.spec.ts b/core/api/test/integration/app/wallets/update-pending-payments.spec.ts new file mode 100644 index 0000000000..c97abc4d50 --- /dev/null +++ b/core/api/test/integration/app/wallets/update-pending-payments.spec.ts @@ -0,0 +1,108 @@ +import { updatePendingPaymentByHash } from "@/app/payments" + +import { LedgerTransactionType } from "@/domain/ledger" +import { PaymentStatus } from "@/domain/bitcoin/lightning" +import { AmountCalculator, WalletCurrency } from "@/domain/shared" +import * as DisplayAmountsConverterImpl from "@/domain/fiat" + +import { baseLogger } from "@/services/logger" +import * as LedgerFacadeImpl from "@/services/ledger/facade" +import * as LndImpl from "@/services/lnd" +import * as MongooseImpl from "@/services/mongoose" + +import { createRandomUserAndBtcWallet, recordSendLnPayment } from "test/helpers" + +const calc = AmountCalculator() + +describe("update pending payments", () => { + const sendAmount = { + usd: { amount: 20n, currency: WalletCurrency.Usd }, + btc: { amount: 60n, currency: WalletCurrency.Btc }, + } + + const bankFee = { + usd: { amount: 10n, currency: WalletCurrency.Usd }, + btc: { amount: 30n, currency: WalletCurrency.Btc }, + } + + const displaySendEurAmounts = { + amountDisplayCurrency: 24 as DisplayCurrencyBaseAmount, + feeDisplayCurrency: 12 as DisplayCurrencyBaseAmount, + displayCurrency: "EUR" as DisplayCurrency, + } + + it("records transaction with ln-failed-payment metadata on ln update", async () => { + // Setup mocks + const { LndService: LnServiceOrig } = jest.requireActual("@/services/lnd") + const lndServiceSpy = jest.spyOn(LndImpl, "LndService").mockReturnValue({ + ...LnServiceOrig(), + lookupPayment: () => ({ + status: PaymentStatus.Failed, + }), + }) + + const displayAmountsConverterSpy = jest.spyOn( + DisplayAmountsConverterImpl, + "DisplayAmountsConverter", + ) + + const lnFailedPaymentReceiveLedgerMetadataSpy = jest.spyOn( + LedgerFacadeImpl, + "LnFailedPaymentReceiveLedgerMetadata", + ) + const recordOffChainReceiveSpy = jest.spyOn(LedgerFacadeImpl, "recordReceiveOffChain") + + // Setup users and wallets + const newWalletDescriptor = await createRandomUserAndBtcWallet() + + // Initiate pending ln payment + const { paymentHash } = await recordSendLnPayment({ + walletDescriptor: newWalletDescriptor, + paymentAmount: sendAmount, + bankFee, + displayAmounts: displaySendEurAmounts, + }) + + // Setup payment-flow mock + const mockedPaymentFlow = { + senderWalletCurrency: WalletCurrency.Usd, + paymentHashForFlow: () => paymentHash, + senderWalletDescriptor: () => newWalletDescriptor, + + btcPaymentAmount: sendAmount.btc, + usdPaymentAmount: sendAmount.usd, + btcProtocolAndBankFee: bankFee.btc, + usdProtocolAndBankFee: bankFee.usd, + totalAmountsForPayment: () => ({ + btc: calc.add(sendAmount.btc, bankFee.btc), + usd: calc.add(sendAmount.usd, bankFee.usd), + }), + } + + const { PaymentFlowStateRepository: PaymentFlowStateRepositoryOrig } = + jest.requireActual("@/services/mongoose") + const paymentFlowRepoSpy = jest + .spyOn(MongooseImpl, "PaymentFlowStateRepository") + .mockReturnValue({ + ...PaymentFlowStateRepositoryOrig(), + markLightningPaymentFlowNotPending: () => mockedPaymentFlow, + }) + + // Call update-pending function + await updatePendingPaymentByHash({ paymentHash, logger: baseLogger }) + + // Check record function was called with right metadata + expect(displayAmountsConverterSpy).toHaveBeenCalledTimes(0) + expect(lnFailedPaymentReceiveLedgerMetadataSpy).toHaveBeenCalledTimes(1) + const args = recordOffChainReceiveSpy.mock.calls[0][0] + expect(args.metadata.type).toBe(LedgerTransactionType.Payment) + expect(args.description).toBe("Usd payment canceled") + + // Restore system state + lndServiceSpy.mockRestore() + displayAmountsConverterSpy.mockRestore() + lnFailedPaymentReceiveLedgerMetadataSpy.mockRestore() + recordOffChainReceiveSpy.mockRestore() + paymentFlowRepoSpy.mockRestore() + }) +}) diff --git a/core/api/test/legacy-integration/02-user-wallet/02-tx-display.spec.ts b/core/api/test/legacy-integration/02-user-wallet/02-tx-display.spec.ts index 0b4fa80cb0..17225e05c2 100644 --- a/core/api/test/legacy-integration/02-user-wallet/02-tx-display.spec.ts +++ b/core/api/test/legacy-integration/02-user-wallet/02-tx-display.spec.ts @@ -1,17 +1,12 @@ -import { Payments, Wallets } from "@/app" +import { Wallets } from "@/app" import { getCurrentPriceAsDisplayPriceRatio } from "@/app/prices" -import { MaxFeeTooLargeForRoutelessPaymentError } from "@/domain/bitcoin/lightning" import { sat2btc, toSats } from "@/domain/bitcoin" -import { LedgerTransactionType, UnknownLedgerError } from "@/domain/ledger" -import * as LnFeesImpl from "@/domain/payments" import { paymentAmountFromNumber, WalletCurrency } from "@/domain/shared" -import { UsdDisplayCurrency, displayAmountFromNumber } from "@/domain/fiat" +import { displayAmountFromNumber } from "@/domain/fiat" import { updateDisplayCurrency } from "@/app/accounts" -import { translateToLedgerTx } from "@/services/ledger" -import { MainBook } from "@/services/ledger/books" import { BriaPayloadType } from "@/services/bria" import { AccountsRepository } from "@/services/mongoose" @@ -23,12 +18,10 @@ import { import { bitcoindClient, bitcoindOutside, - createInvoice, createUserAndWalletFromPhone, getAccountByPhone, getDefaultWalletIdByPhone, getPendingTransactionsForWalletId, - lndOutside1, onceBriaSubscribe, RANDOM_ADDRESS, randomPhone, @@ -86,21 +79,6 @@ afterAll(async () => { }) describe("Display properties on transactions", () => { - const getAllTransactionsByHash = async ( - hash: PaymentHash | OnChainTxHash, - ): Promise[] | LedgerServiceError> => { - try { - const { results } = await MainBook.ledger({ - hash, - }) - /* eslint @typescript-eslint/ban-ts-comment: "off" */ - // @ts-ignore-next-line no-implicit-any error - return results.map((tx) => translateToLedgerTx(tx)) - } catch (err) { - return new UnknownLedgerError(err) - } - } - const getDisplayAmounts = async ({ satsAmount, satsFee, @@ -152,83 +130,6 @@ describe("Display properties on transactions", () => { return result } - describe("ln", () => { - describe("send", () => { - it("(LnFailedPaymentReceiveLedgerMetadata) pay zero amount invoice & revert txn when verifyMaxFee fails", async () => { - // TxMetadata: - // - LnFailedPaymentReceiveLedgerMetadata - const { LnFees: LnFeesOrig } = jest.requireActual("@/domain/payments") - const lndFeesSpy = jest.spyOn(LnFeesImpl, "LnFees").mockReturnValue({ - ...LnFeesOrig(), - verifyMaxFee: () => new MaxFeeTooLargeForRoutelessPaymentError(), - }) - - const senderWalletId = walletIdB - const senderAccount = accountB - - const amountInvoice = toSats(20_000) - - const { request, id } = await createInvoice({ lnd: lndOutside1 }) - const paymentHash = id as PaymentHash - - const paymentResult = await Payments.payNoAmountInvoiceByWalletIdForBtcWallet({ - uncheckedPaymentRequest: request, - memo: null, - amount: amountInvoice, - senderWalletId, - senderAccount, - }) - expect(paymentResult).toBeInstanceOf(MaxFeeTooLargeForRoutelessPaymentError) - // Restore system state - lndFeesSpy.mockReset() - - const txns = await getAllTransactionsByHash(paymentHash) - if (txns instanceof Error) throw txns - - const senderTxn = txns.find( - ({ walletId, debit }) => walletId === senderWalletId && debit > 0, - ) - if (senderTxn === undefined) throw new Error("'senderTxn' not found") - - const repaidTxn = txns.find( - ({ walletId, credit }) => walletId === senderWalletId && credit > 0, - ) - if (repaidTxn === undefined) throw new Error("'repaidTxn' not found") - - const internalTxns = txns.filter(({ walletId }) => walletId !== senderWalletId) - expect(internalTxns.length).toEqual(2) - - const { EUR: expectedSenderDisplayProps } = await getDisplayAmounts({ - satsAmount: senderTxn.satsAmount || toSats(0), - satsFee: senderTxn.satsFee || toSats(0), - }) - - expect(senderTxn).toEqual( - expect.objectContaining({ - ...expectedSenderDisplayProps, - type: LedgerTransactionType.Payment, - }), - ) - expect(repaidTxn).toEqual( - expect.objectContaining({ - ...expectedSenderDisplayProps, - type: LedgerTransactionType.Payment, - }), - ) - - for (const txn of internalTxns) { - expect(txn).toEqual( - expect.objectContaining({ - displayAmount: senderTxn.centsAmount, - displayFee: senderTxn.centsFee, - displayCurrency: UsdDisplayCurrency, - }), - ) - } - }) - }) - }) - describe("onchain", () => { describe("receive", () => { it("(Pending, no metadata) identifies unconfirmed incoming on-chain transactions", async () => { From d5baf15a216d048313de707e6390f06d07c2e244 Mon Sep 17 00:00:00 2001 From: vindard <17693119+vindard@users.noreply.github.com> Date: Fri, 1 Dec 2023 10:01:06 -0400 Subject: [PATCH 13/16] test: move add-pending-onchain display-converter check --- .../add-pending-on-chain-transaction.ts | 2 +- .../add-pending-on-chain-transaction.spec.ts | 32 +++ .../02-user-wallet/02-tx-display.spec.ts | 222 ------------------ 3 files changed, 33 insertions(+), 223 deletions(-) delete mode 100644 core/api/test/legacy-integration/02-user-wallet/02-tx-display.spec.ts diff --git a/core/api/src/app/wallets/add-pending-on-chain-transaction.ts b/core/api/src/app/wallets/add-pending-on-chain-transaction.ts index c63e07a9f0..0b8f14e56a 100644 --- a/core/api/src/app/wallets/add-pending-on-chain-transaction.ts +++ b/core/api/src/app/wallets/add-pending-on-chain-transaction.ts @@ -23,7 +23,7 @@ import { baseLogger } from "@/services/logger" import { LedgerService } from "@/services/ledger" import { DealerPriceService } from "@/services/dealer-price" import { NotificationsService } from "@/services/notifications" -import { DisplayAmountsConverter } from "@/domain/fiat/display-amounts-converter" +import { DisplayAmountsConverter } from "@/domain/fiat" const dealer = DealerPriceService() const { dustThreshold } = getOnChainWalletConfig() diff --git a/core/api/test/integration/app/wallets/add-pending-on-chain-transaction.spec.ts b/core/api/test/integration/app/wallets/add-pending-on-chain-transaction.spec.ts index c19d683513..26eeff98eb 100644 --- a/core/api/test/integration/app/wallets/add-pending-on-chain-transaction.spec.ts +++ b/core/api/test/integration/app/wallets/add-pending-on-chain-transaction.spec.ts @@ -1,6 +1,7 @@ import { addPendingTransaction } from "@/app/wallets" import { WalletCurrency } from "@/domain/shared" +import * as DisplayAmountsConverterImpl from "@/domain/fiat" import { WalletOnChainAddressesRepository } from "@/services/mongoose" import { Wallet, WalletOnChainPendingReceive } from "@/services/mongoose/schema" @@ -56,4 +57,35 @@ describe("addPendingTransaction", () => { // Restore system state pushNotificationsServiceSpy.mockRestore() }) + + it("calls DisplayConverter on pending onchain receive", async () => { + // Setup mocks + const displayAmountsConverterSpy = jest.spyOn( + DisplayAmountsConverterImpl, + "DisplayAmountsConverter", + ) + + // Create user + const { btcWalletDescriptor } = await createRandomUserAndWallets() + + // Add address to user wallet + await WalletOnChainAddressesRepository().persistNew({ + walletId: btcWalletDescriptor.id, + onChainAddress: { address }, + }) + + // Add pending transaction + await addPendingTransaction({ + txId: "txId" as OnChainTxHash, + vout: 0 as OnChainTxVout, + satoshis: btcAmount, + address, + }) + + // Expect sent notification + expect(displayAmountsConverterSpy).toHaveBeenCalledTimes(1) + + // Restore system state + displayAmountsConverterSpy.mockRestore() + }) }) diff --git a/core/api/test/legacy-integration/02-user-wallet/02-tx-display.spec.ts b/core/api/test/legacy-integration/02-user-wallet/02-tx-display.spec.ts deleted file mode 100644 index 17225e05c2..0000000000 --- a/core/api/test/legacy-integration/02-user-wallet/02-tx-display.spec.ts +++ /dev/null @@ -1,222 +0,0 @@ -import { Wallets } from "@/app" -import { getCurrentPriceAsDisplayPriceRatio } from "@/app/prices" - -import { sat2btc, toSats } from "@/domain/bitcoin" -import { paymentAmountFromNumber, WalletCurrency } from "@/domain/shared" -import { displayAmountFromNumber } from "@/domain/fiat" - -import { updateDisplayCurrency } from "@/app/accounts" - -import { BriaPayloadType } from "@/services/bria" -import { AccountsRepository } from "@/services/mongoose" - -import { - utxoDetectedEventHandler, - utxoSettledEventHandler, -} from "@/servers/event-handlers/bria" - -import { - bitcoindClient, - bitcoindOutside, - createUserAndWalletFromPhone, - getAccountByPhone, - getDefaultWalletIdByPhone, - getPendingTransactionsForWalletId, - onceBriaSubscribe, - RANDOM_ADDRESS, - randomPhone, -} from "test/helpers" - -let accountB: Account -let accountC: Account - -let walletIdB: WalletId - -const phoneB = randomPhone() -const phoneC = randomPhone() - -beforeAll(async () => { - await createUserAndWalletFromPhone(phoneB) - await createUserAndWalletFromPhone(phoneC) - - accountB = await getAccountByPhone(phoneB) - accountC = await getAccountByPhone(phoneC) - - walletIdB = await getDefaultWalletIdByPhone(phoneB) - - await bitcoindClient.loadWallet({ filename: "outside" }) - - // Update account display currencies - const updatedAccountB = await updateDisplayCurrency({ - accountId: accountB.id, - currency: "EUR", - }) - if (updatedAccountB instanceof Error) throw updatedAccountB - - const updatedAccountC = await updateDisplayCurrency({ - accountId: accountC.id, - currency: "CRC", - }) - if (updatedAccountC instanceof Error) throw updatedAccountC - - const accountBRaw = await AccountsRepository().findById(accountB.id) - if (accountBRaw instanceof Error) throw accountBRaw - accountB = accountBRaw - if (accountB.displayCurrency !== "EUR") { - throw new Error("Error changing display currency for accountB") - } - - const accountCRaw = await AccountsRepository().findById(accountC.id) - if (accountCRaw instanceof Error) throw accountCRaw - accountC = accountCRaw - if (accountC.displayCurrency !== "CRC") { - throw new Error("Error changing display currency for accountC") - } -}) - -afterAll(async () => { - await bitcoindClient.unloadWallet({ walletName: "outside" }) -}) - -describe("Display properties on transactions", () => { - const getDisplayAmounts = async ({ - satsAmount, - satsFee, - }: { - satsAmount: Satoshis - satsFee: Satoshis - }) => { - const currencies = ["EUR" as DisplayCurrency, "CRC" as DisplayCurrency] - - const btcAmount = paymentAmountFromNumber({ - amount: satsAmount, - currency: WalletCurrency.Btc, - }) - if (btcAmount instanceof Error) throw btcAmount - const btcFee = paymentAmountFromNumber({ - amount: satsFee, - currency: WalletCurrency.Btc, - }) - if (btcFee instanceof Error) throw btcFee - - const result: Record< - string, - { - displayAmount: DisplayCurrencyBaseAmount - displayFee: DisplayCurrencyBaseAmount - displayCurrency: DisplayCurrency - } - > = {} - for (const currency of currencies) { - const displayPriceRatio = await getCurrentPriceAsDisplayPriceRatio({ - currency, - }) - if (displayPriceRatio instanceof Error) throw displayPriceRatio - - const displayAmount = displayPriceRatio.convertFromWallet(btcAmount) - if (satsAmount > 0 && displayAmount.amountInMinor === 0n) { - displayAmount.amountInMinor = 1n - } - - const displayFee = displayPriceRatio.convertFromWalletToCeil(btcFee) - - result[currency] = { - displayAmount: Number(displayAmount.amountInMinor) as DisplayCurrencyBaseAmount, - displayFee: Number(displayFee.amountInMinor) as DisplayCurrencyBaseAmount, - displayCurrency: currency, - } - } - - return result - } - - describe("onchain", () => { - describe("receive", () => { - it("(Pending, no metadata) identifies unconfirmed incoming on-chain transactions", async () => { - const amountSats = toSats(20_000) - - const recipientWalletId = walletIdB - - // Execute receive - const address = await Wallets.createOnChainAddress({ - walletId: recipientWalletId, - }) - if (address instanceof Error) throw address - - const txId = (await bitcoindOutside.sendToAddress({ - address, - amount: sat2btc(amountSats), - })) as OnChainTxHash - - const detectedEvent = await onceBriaSubscribe({ - type: BriaPayloadType.UtxoDetected, - txId, - }) - if (detectedEvent?.payload.type !== BriaPayloadType.UtxoDetected) { - throw new Error(`Expected ${BriaPayloadType.UtxoDetected} event`) - } - const resultPending = await utxoDetectedEventHandler({ - event: detectedEvent.payload, - }) - if (resultPending instanceof Error) { - throw resultPending - } - - // Check entries - const pendingTxs = await getPendingTransactionsForWalletId(recipientWalletId) - if (pendingTxs instanceof Error) throw pendingTxs - - expect(pendingTxs.length).toBe(1) - const recipientTxn = pendingTxs[0] - - const { EUR: expectedRecipientDisplayProps } = await getDisplayAmounts({ - satsAmount: toSats(recipientTxn.settlementAmount), - satsFee: toSats(recipientTxn.settlementFee), - }) - - const settlementDisplayAmountObj = displayAmountFromNumber({ - amount: expectedRecipientDisplayProps.displayAmount, - currency: expectedRecipientDisplayProps.displayCurrency, - }) - if (settlementDisplayAmountObj instanceof Error) throw settlementDisplayAmountObj - - const settlementDisplayFeeObj = displayAmountFromNumber({ - amount: expectedRecipientDisplayProps.displayFee, - currency: expectedRecipientDisplayProps.displayCurrency, - }) - if (settlementDisplayFeeObj instanceof Error) throw settlementDisplayFeeObj - - const expectedRecipientWalletTxnDisplayProps = { - settlementDisplayAmount: settlementDisplayAmountObj.displayInMajor, - settlementDisplayFee: settlementDisplayFeeObj.displayInMajor, - } - - expect(recipientTxn).toEqual( - expect.objectContaining(expectedRecipientWalletTxnDisplayProps), - ) - expect(recipientTxn.settlementDisplayPrice).toEqual( - expect.objectContaining({ - displayCurrency: expectedRecipientDisplayProps.displayCurrency, - }), - ) - - // Settle pending - await bitcoindOutside.generateToAddress({ nblocks: 3, address: RANDOM_ADDRESS }) - - const settledEvent = await onceBriaSubscribe({ - type: BriaPayloadType.UtxoSettled, - txId, - }) - if (settledEvent?.payload.type !== BriaPayloadType.UtxoSettled) { - throw new Error(`Expected ${BriaPayloadType.UtxoSettled} event`) - } - const resultSettled = await utxoSettledEventHandler({ - event: settledEvent.payload, - }) - if (resultSettled instanceof Error) { - throw resultSettled - } - }) - }) - }) -}) From dfdfa15bd27f28f9730d451576b172ffc4b89e0b Mon Sep 17 00:00:00 2001 From: vindard <17693119+vindard@users.noreply.github.com> Date: Fri, 1 Dec 2023 11:13:49 -0400 Subject: [PATCH 14/16] refactor: restore all mocks after each test --- .../add-pending-on-chain-transaction.spec.ts | 10 ++---- .../add-settled-on-chain-transaction.spec.ts | 7 ++-- .../app/wallets/send-intraledger.spec.ts | 12 ++----- .../app/wallets/send-lightning.spec.ts | 34 ++++--------------- .../app/wallets/send-onchain.spec.ts | 17 ++-------- .../wallets/update-pending-invoices.spec.ts | 10 ++---- .../wallets/update-pending-payments.spec.ts | 23 +++++-------- 7 files changed, 27 insertions(+), 86 deletions(-) diff --git a/core/api/test/integration/app/wallets/add-pending-on-chain-transaction.spec.ts b/core/api/test/integration/app/wallets/add-pending-on-chain-transaction.spec.ts index 26eeff98eb..cc13b5a58b 100644 --- a/core/api/test/integration/app/wallets/add-pending-on-chain-transaction.spec.ts +++ b/core/api/test/integration/app/wallets/add-pending-on-chain-transaction.spec.ts @@ -20,13 +20,15 @@ afterEach(async () => { { $pull: { onchain: { address } } }, { multi: true }, // This option updates all matching documents ) + + jest.restoreAllMocks() }) describe("addPendingTransaction", () => { it("calls sendFilteredNotification on pending onchain receive", async () => { // Setup mocks const sendFilteredNotification = jest.fn() - const pushNotificationsServiceSpy = jest + jest .spyOn(PushNotificationsServiceImpl, "PushNotificationsService") .mockImplementationOnce(() => ({ sendFilteredNotification, @@ -53,9 +55,6 @@ describe("addPendingTransaction", () => { // Expect sent notification expect(sendFilteredNotification.mock.calls.length).toBe(1) expect(sendFilteredNotification.mock.calls[0][0].title).toBeTruthy() - - // Restore system state - pushNotificationsServiceSpy.mockRestore() }) it("calls DisplayConverter on pending onchain receive", async () => { @@ -84,8 +83,5 @@ describe("addPendingTransaction", () => { // Expect sent notification expect(displayAmountsConverterSpy).toHaveBeenCalledTimes(1) - - // Restore system state - displayAmountsConverterSpy.mockRestore() }) }) diff --git a/core/api/test/integration/app/wallets/add-settled-on-chain-transaction.spec.ts b/core/api/test/integration/app/wallets/add-settled-on-chain-transaction.spec.ts index a1505ff270..eb069a78dc 100644 --- a/core/api/test/integration/app/wallets/add-settled-on-chain-transaction.spec.ts +++ b/core/api/test/integration/app/wallets/add-settled-on-chain-transaction.spec.ts @@ -28,6 +28,8 @@ afterEach(async () => { { $pull: { onchain: { address } } }, { multi: true }, // This option updates all matching documents ) + + jest.restoreAllMocks() }) describe("addSettledTransaction", () => { @@ -101,10 +103,5 @@ describe("addSettledTransaction", () => { expect(onChainReceiveLedgerMetadataSpy).toHaveBeenCalledTimes(1) const args = recordOnChainReceiveSpy.mock.calls[0][0] expect(args.metadata.type).toBe(LedgerTransactionType.OnchainReceipt) - - // Restore system state - displayAmountsConverterSpy.mockRestore() - onChainReceiveLedgerMetadataSpy.mockRestore() - recordOnChainReceiveSpy.mockRestore() }) }) diff --git a/core/api/test/integration/app/wallets/send-intraledger.spec.ts b/core/api/test/integration/app/wallets/send-intraledger.spec.ts index acbf27be7c..7fe193d48b 100644 --- a/core/api/test/integration/app/wallets/send-intraledger.spec.ts +++ b/core/api/test/integration/app/wallets/send-intraledger.spec.ts @@ -44,6 +44,8 @@ beforeEach(() => { afterEach(async () => { await Transaction.deleteMany({ memo }) await Transaction.deleteMany({ memoPayer: memo }) + + jest.restoreAllMocks() }) const amount = toSats(10040) @@ -364,11 +366,6 @@ describe("intraLedgerPay", () => { expect(walletIdTradeIntraAccountLedgerMetadataSpy).toHaveBeenCalledTimes(1) const args = recordIntraledgerSpy.mock.calls[0][0] expect(args.metadata.type).toBe(LedgerTransactionType.WalletIdTradeIntraAccount) - - // Restore system state - displayAmountsConverterSpy.mockRestore() - walletIdTradeIntraAccountLedgerMetadataSpy.mockRestore() - recordIntraledgerSpy.mockRestore() }) it("records transaction with wallet-id-intraledger metadata on intraledger send", async () => { @@ -415,10 +412,5 @@ describe("intraLedgerPay", () => { expect(walletIdIntraledgerLedgerMetadataSpy).toHaveBeenCalledTimes(1) const args = recordIntraledgerSpy.mock.calls[0][0] expect(args.metadata.type).toBe(LedgerTransactionType.IntraLedger) - - // Restore system state - displayAmountsConverterSpy.mockRestore() - walletIdIntraledgerLedgerMetadataSpy.mockRestore() - recordIntraledgerSpy.mockRestore() }) }) diff --git a/core/api/test/integration/app/wallets/send-lightning.spec.ts b/core/api/test/integration/app/wallets/send-lightning.spec.ts index 9b63f89c34..304fdc4d3a 100644 --- a/core/api/test/integration/app/wallets/send-lightning.spec.ts +++ b/core/api/test/integration/app/wallets/send-lightning.spec.ts @@ -87,6 +87,8 @@ afterEach(async () => { await TransactionMetadata.deleteMany({}) await WalletInvoice.deleteMany({}) await LnPayment.deleteMany({}) + + jest.restoreAllMocks() }) const amount = toSats(10040) @@ -492,7 +494,7 @@ describe("initiated via lightning", () => { it("records transaction with lightning metadata on ln send", async () => { // Setup mocks const { LndService: LnServiceOrig } = jest.requireActual("@/services/lnd") - const lndServiceSpy = jest.spyOn(LndImpl, "LndService").mockReturnValue({ + jest.spyOn(LndImpl, "LndService").mockReturnValue({ ...LnServiceOrig(), defaultPubkey: (): Pubkey => DEFAULT_PUBKEY, listAllPubkeys: () => [], @@ -537,18 +539,12 @@ describe("initiated via lightning", () => { expect(lnSendLedgerMetadataSpy).toHaveBeenCalledTimes(1) const args = recordOffChainSendSpy.mock.calls[0][0] expect(args.metadata.type).toBe(LedgerTransactionType.Payment) - - // Restore system state - lndServiceSpy.mockRestore() - displayAmountsConverterSpy.mockRestore() - lnSendLedgerMetadataSpy.mockRestore() - recordOffChainSendSpy.mockRestore() }) it("records transaction with fee reimbursement metadata on ln send", async () => { // Setup mocks const { LndService: LnServiceOrig } = jest.requireActual("@/services/lnd") - const lndServiceSpy = jest.spyOn(LndImpl, "LndService").mockReturnValue({ + jest.spyOn(LndImpl, "LndService").mockReturnValue({ ...LnServiceOrig(), defaultPubkey: (): Pubkey => DEFAULT_PUBKEY, listAllPubkeys: () => [], @@ -605,12 +601,6 @@ describe("initiated via lightning", () => { // Note: 1st call is funding balance in test, 2nd call is fee reimbursement const args = recordOffChainReceiveSpy.mock.calls[1][0] expect(args.metadata.type).toBe(LedgerTransactionType.LnFeeReimbursement) - - // Restore system state - lndServiceSpy.mockRestore() - displayAmountsConverterSpy.mockRestore() - lnFeeReimbursementReceiveLedgerMetadataSpy.mockRestore() - recordOffChainReceiveSpy.mockRestore() }) }) @@ -991,7 +981,7 @@ describe("initiated via lightning", () => { it("records transaction with ln-trade-intra-account metadata on intraledger send", async () => { // Setup mocks const { LndService: LnServiceOrig } = jest.requireActual("@/services/lnd") - const lndServiceSpy = jest.spyOn(LndImpl, "LndService").mockReturnValue({ + jest.spyOn(LndImpl, "LndService").mockReturnValue({ ...LnServiceOrig(), listAllPubkeys: () => [noAmountLnInvoice.destination], cancelInvoice: () => true, @@ -1053,18 +1043,12 @@ describe("initiated via lightning", () => { expect(lnTradeIntraAccountLedgerMetadataSpy).toHaveBeenCalledTimes(1) const args = recordIntraledgerSpy.mock.calls[0][0] expect(args.metadata.type).toBe(LedgerTransactionType.LnTradeIntraAccount) - - // Restore system state - lnTradeIntraAccountLedgerMetadataSpy.mockRestore() - displayAmountsConverterSpy.mockRestore() - recordIntraledgerSpy.mockRestore() - lndServiceSpy.mockRestore() }) it("records transaction with ln-intraledger metadata on intraledger send", async () => { // Setup mocks const { LndService: LnServiceOrig } = jest.requireActual("@/services/lnd") - const lndServiceSpy = jest.spyOn(LndImpl, "LndService").mockReturnValue({ + jest.spyOn(LndImpl, "LndService").mockReturnValue({ ...LnServiceOrig(), listAllPubkeys: () => [noAmountLnInvoice.destination], cancelInvoice: () => true, @@ -1127,12 +1111,6 @@ describe("initiated via lightning", () => { expect(lnIntraledgerLedgerMetadataSpy).toHaveBeenCalledTimes(1) const args = recordIntraledgerSpy.mock.calls[0][0] expect(args.metadata.type).toBe(LedgerTransactionType.LnIntraLedger) - - // Restore system state - lnIntraledgerLedgerMetadataSpy.mockRestore() - displayAmountsConverterSpy.mockRestore() - recordIntraledgerSpy.mockRestore() - lndServiceSpy.mockRestore() }) }) }) diff --git a/core/api/test/integration/app/wallets/send-onchain.spec.ts b/core/api/test/integration/app/wallets/send-onchain.spec.ts index dc92faf7ae..3fb644ca0a 100644 --- a/core/api/test/integration/app/wallets/send-onchain.spec.ts +++ b/core/api/test/integration/app/wallets/send-onchain.spec.ts @@ -64,6 +64,8 @@ beforeEach(async () => { afterEach(async () => { await Transaction.deleteMany() await TransactionMetadata.deleteMany() + + jest.restoreAllMocks() }) const amount = toSats(10040) @@ -351,11 +353,6 @@ describe("onChainPay", () => { expect(onChainSendLedgerMetadataSpy).toHaveBeenCalledTimes(1) const args = recordOnChainSendSpy.mock.calls[0][0] expect(args.metadata.type).toBe(LedgerTransactionType.OnchainPayment) - - // Restore system state - displayAmountsConverterSpy.mockRestore() - onChainSendLedgerMetadataSpy.mockRestore() - recordOnChainSendSpy.mockRestore() }) }) @@ -749,11 +746,6 @@ describe("onChainPay", () => { expect(onChainTradeIntraAccountLedgerMetadataSpy).toHaveBeenCalledTimes(1) const args = recordIntraledgerSpy.mock.calls[0][0] expect(args.metadata.type).toBe(LedgerTransactionType.OnChainTradeIntraAccount) - - // Restore system state - displayAmountsConverterSpy.mockRestore() - onChainTradeIntraAccountLedgerMetadataSpy.mockRestore() - recordIntraledgerSpy.mockRestore() }) it("records transaction with onchain-intraledger metadata on intraledger send", async () => { @@ -825,11 +817,6 @@ describe("onChainPay", () => { expect(onChainIntraledgerLedgerMetadataSpy).toHaveBeenCalledTimes(1) const args = recordIntraledgerSpy.mock.calls[0][0] expect(args.metadata.type).toBe(LedgerTransactionType.OnchainIntraLedger) - - // Restore system state - displayAmountsConverterSpy.mockRestore() - onChainIntraledgerLedgerMetadataSpy.mockRestore() - recordIntraledgerSpy.mockRestore() }) }) }) diff --git a/core/api/test/integration/app/wallets/update-pending-invoices.spec.ts b/core/api/test/integration/app/wallets/update-pending-invoices.spec.ts index 81de83ac78..4aa037aec9 100644 --- a/core/api/test/integration/app/wallets/update-pending-invoices.spec.ts +++ b/core/api/test/integration/app/wallets/update-pending-invoices.spec.ts @@ -44,6 +44,8 @@ afterEach(async () => { await WalletInvoice.deleteMany({}) await Transaction.deleteMany({}) await TransactionMetadata.deleteMany({}) + + jest.restoreAllMocks() }) describe("update pending invoices", () => { @@ -261,7 +263,7 @@ describe("update pending invoices", () => { }, } const { LndService: LnServiceOrig } = jest.requireActual("@/services/lnd") - const lndServiceSpy = jest.spyOn(LndImpl, "LndService").mockReturnValue({ + jest.spyOn(LndImpl, "LndService").mockReturnValue({ ...LnServiceOrig(), defaultPubkey: () => DEFAULT_PUBKEY, lookupInvoice: () => lnInvoiceLookup, @@ -314,12 +316,6 @@ describe("update pending invoices", () => { expect(lnReceiveLedgerMetadataSpy).toHaveBeenCalledTimes(1) const args = recordReceiveOffChainSpy.mock.calls[0][0] expect(args.metadata.type).toBe(LedgerTransactionType.Invoice) - - // Restore system state - lndServiceSpy.mockRestore() - displayAmountsConverterSpy.mockRestore() - lnReceiveLedgerMetadataSpy.mockRestore() - recordReceiveOffChainSpy.mockRestore() }) }) }) diff --git a/core/api/test/integration/app/wallets/update-pending-payments.spec.ts b/core/api/test/integration/app/wallets/update-pending-payments.spec.ts index c97abc4d50..276cd2dd98 100644 --- a/core/api/test/integration/app/wallets/update-pending-payments.spec.ts +++ b/core/api/test/integration/app/wallets/update-pending-payments.spec.ts @@ -14,6 +14,10 @@ import { createRandomUserAndBtcWallet, recordSendLnPayment } from "test/helpers" const calc = AmountCalculator() +afterEach(() => { + jest.restoreAllMocks() +}) + describe("update pending payments", () => { const sendAmount = { usd: { amount: 20n, currency: WalletCurrency.Usd }, @@ -34,7 +38,7 @@ describe("update pending payments", () => { it("records transaction with ln-failed-payment metadata on ln update", async () => { // Setup mocks const { LndService: LnServiceOrig } = jest.requireActual("@/services/lnd") - const lndServiceSpy = jest.spyOn(LndImpl, "LndService").mockReturnValue({ + jest.spyOn(LndImpl, "LndService").mockReturnValue({ ...LnServiceOrig(), lookupPayment: () => ({ status: PaymentStatus.Failed, @@ -81,12 +85,10 @@ describe("update pending payments", () => { const { PaymentFlowStateRepository: PaymentFlowStateRepositoryOrig } = jest.requireActual("@/services/mongoose") - const paymentFlowRepoSpy = jest - .spyOn(MongooseImpl, "PaymentFlowStateRepository") - .mockReturnValue({ - ...PaymentFlowStateRepositoryOrig(), - markLightningPaymentFlowNotPending: () => mockedPaymentFlow, - }) + jest.spyOn(MongooseImpl, "PaymentFlowStateRepository").mockReturnValue({ + ...PaymentFlowStateRepositoryOrig(), + markLightningPaymentFlowNotPending: () => mockedPaymentFlow, + }) // Call update-pending function await updatePendingPaymentByHash({ paymentHash, logger: baseLogger }) @@ -97,12 +99,5 @@ describe("update pending payments", () => { const args = recordOffChainReceiveSpy.mock.calls[0][0] expect(args.metadata.type).toBe(LedgerTransactionType.Payment) expect(args.description).toBe("Usd payment canceled") - - // Restore system state - lndServiceSpy.mockRestore() - displayAmountsConverterSpy.mockRestore() - lnFailedPaymentReceiveLedgerMetadataSpy.mockRestore() - recordOffChainReceiveSpy.mockRestore() - paymentFlowRepoSpy.mockRestore() }) }) From a5f5625bebbcee2d1927722bc1d3d2c6e657bae4 Mon Sep 17 00:00:00 2001 From: vindard <17693119+vindard@users.noreply.github.com> Date: Thu, 7 Dec 2023 10:40:24 -0400 Subject: [PATCH 15/16] test(core): bump initial balance to accommodate extremely high onchain fees --- core/api/test/integration/app/wallets/send-onchain.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/api/test/integration/app/wallets/send-onchain.spec.ts b/core/api/test/integration/app/wallets/send-onchain.spec.ts index 3fb644ca0a..83e43ace9e 100644 --- a/core/api/test/integration/app/wallets/send-onchain.spec.ts +++ b/core/api/test/integration/app/wallets/send-onchain.spec.ts @@ -80,7 +80,7 @@ const usdPaymentAmount: UsdPaymentAmount = { currency: WalletCurrency.Usd, } -const receiveAmounts = { btc: calc.mul(btcPaymentAmount, 3n), usd: usdPaymentAmount } +const receiveAmounts = { btc: calc.mul(btcPaymentAmount, 10n), usd: usdPaymentAmount } const receiveBankFee = { btc: { amount: 100n, currency: WalletCurrency.Btc }, From 10be57e1e79514688e04c70c9fde63e199363299 Mon Sep 17 00:00:00 2001 From: vindard <17693119+vindard@users.noreply.github.com> Date: Thu, 7 Dec 2023 10:46:11 -0400 Subject: [PATCH 16/16] test(core): add missing 'createMandatoryUsers' step --- .../app/wallets/update-pending-payments.spec.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/core/api/test/integration/app/wallets/update-pending-payments.spec.ts b/core/api/test/integration/app/wallets/update-pending-payments.spec.ts index 276cd2dd98..368c4dfd4b 100644 --- a/core/api/test/integration/app/wallets/update-pending-payments.spec.ts +++ b/core/api/test/integration/app/wallets/update-pending-payments.spec.ts @@ -10,10 +10,18 @@ import * as LedgerFacadeImpl from "@/services/ledger/facade" import * as LndImpl from "@/services/lnd" import * as MongooseImpl from "@/services/mongoose" -import { createRandomUserAndBtcWallet, recordSendLnPayment } from "test/helpers" +import { + createMandatoryUsers, + createRandomUserAndBtcWallet, + recordSendLnPayment, +} from "test/helpers" const calc = AmountCalculator() +beforeAll(async () => { + await createMandatoryUsers() +}) + afterEach(() => { jest.restoreAllMocks() })