Skip to content

Commit

Permalink
test: move 'decline held expired USD invoice' integration (#3268)
Browse files Browse the repository at this point in the history
* test: remove redundant trigger-restart legacy tests

* chore: remove unused WalletInvoiceValidator

* refactor: add new WalletInvoiceChecker

* refactor: swap WalletInvoiceChecker into trigger handler

* refactor: isolate 'updateOrDeclinePendingInvoice' method for held-invoices handler

* refactor: clean up invoiceUpdateHandler usage

* test: move decline-expired-usd test to new integration format

* test: move remaining invoice-expiration to new integration format
  • Loading branch information
vindard authored Sep 28, 2023
1 parent 54caf08 commit f0e66c7
Show file tree
Hide file tree
Showing 11 changed files with 398 additions and 616 deletions.
194 changes: 96 additions & 98 deletions src/app/wallets/update-pending-invoices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,7 @@ import {
} from "@domain/errors"
import { checkedToSats } from "@domain/bitcoin"
import { DisplayAmountsConverter } from "@domain/fiat"
import {
InvoiceNotFoundError,
invoiceExpirationForCurrency,
} from "@domain/bitcoin/lightning"
import { InvoiceNotFoundError } from "@domain/bitcoin/lightning"
import { paymentAmountFromNumber, WalletCurrency } from "@domain/shared"
import { WalletInvoiceReceiver } from "@domain/wallet-invoices/wallet-invoice-receiver"
import { DeviceTokensNotRegisteredNotificationsServiceError } from "@domain/notifications"
Expand Down Expand Up @@ -39,12 +36,10 @@ import { AccountLevel } from "@domain/accounts"
import { CallbackService } from "@services/svix"
import { getCallbackServiceConfig } from "@config"
import { toDisplayBaseAmount } from "@domain/payments"
import { WalletInvoiceChecker } from "@domain/wallet-invoices"

export const handleHeldInvoices = async (logger: Logger): Promise<void> => {
const invoicesRepo = WalletInvoicesRepository()

const pendingInvoices = invoicesRepo.yieldPending()

const pendingInvoices = WalletInvoicesRepository().yieldPending()
if (pendingInvoices instanceof Error) {
logger.error(
{ error: pendingInvoices },
Expand All @@ -59,20 +54,10 @@ export const handleHeldInvoices = async (logger: Logger): Promise<void> => {
processor: async (walletInvoice: WalletInvoice, index: number) => {
logger.trace("updating pending invoices %s in worker %d", index)

const { pubkey, paymentHash, recipientWalletDescriptor, createdAt } = walletInvoice
const expiresIn = invoiceExpirationForCurrency(
recipientWalletDescriptor.currency,
createdAt,
)
if (
recipientWalletDescriptor.currency === WalletCurrency.Usd &&
expiresIn.getTime() < Date.now()
) {
await declineHeldInvoice({ pubkey, paymentHash, logger })
return
}

await updatePendingInvoice({ walletInvoice, logger })
return updateOrDeclinePendingInvoice({
walletInvoice,
logger,
})
},
})

Expand All @@ -96,7 +81,93 @@ export const updatePendingInvoiceByPaymentHash = async ({
return updatePendingInvoice({ walletInvoice, logger })
}

const dealer = DealerPriceService()
export const declineHeldInvoice = wrapAsyncToRunInSpan({
namespace: "app.invoices",
fnName: "declineHeldInvoice",
fn: async ({
pubkey,
paymentHash,
logger,
}: {
pubkey: Pubkey
paymentHash: PaymentHash
logger: Logger
}): Promise<boolean | ApplicationError> => {
addAttributesToCurrentSpan({ paymentHash, pubkey })

const lndService = LndService()
if (lndService instanceof Error) return lndService

const walletInvoicesRepo = WalletInvoicesRepository()

const lnInvoiceLookup = await lndService.lookupInvoice({ pubkey, paymentHash })

const pendingInvoiceLogger = logger.child({
hash: paymentHash,
pubkey,
lnInvoiceLookup,
topic: "payment",
protocol: "lightning",
transactionType: "receipt",
onUs: false,
})

if (lnInvoiceLookup instanceof InvoiceNotFoundError) {
const isDeleted = await walletInvoicesRepo.deleteByPaymentHash(paymentHash)
if (isDeleted instanceof Error) {
pendingInvoiceLogger.error("impossible to delete WalletInvoice entry")
return isDeleted
}
return false
}
if (lnInvoiceLookup instanceof Error) return lnInvoiceLookup

if (lnInvoiceLookup.isSettled) {
return new InvalidNonHodlInvoiceError(
JSON.stringify({ paymentHash: lnInvoiceLookup.paymentHash }),
)
}

if (!lnInvoiceLookup.isHeld) {
pendingInvoiceLogger.info({ lnInvoiceLookup }, "invoice has not been paid yet")
return false
}

let heldForMsg = ""
if (lnInvoiceLookup.heldAt) {
heldForMsg = `for ${elapsedSinceTimestamp(lnInvoiceLookup.heldAt)}s `
}
pendingInvoiceLogger.error(
{ lnInvoiceLookup },
`invoice has been held ${heldForMsg}and is now been cancelled`,
)

const invoiceSettled = await lndService.cancelInvoice({ pubkey, paymentHash })
if (invoiceSettled instanceof Error) return invoiceSettled

const isDeleted = await walletInvoicesRepo.deleteByPaymentHash(paymentHash)
if (isDeleted instanceof Error) {
pendingInvoiceLogger.error("impossible to delete WalletInvoice entry")
}

return true
},
})

const updateOrDeclinePendingInvoice = async ({
walletInvoice,
logger,
}: {
walletInvoice: WalletInvoice
logger: Logger
}): Promise<boolean | ApplicationError> =>
WalletInvoiceChecker(walletInvoice).shouldDecline()
? declineHeldInvoice({
pubkey: walletInvoice.pubkey,
paymentHash: walletInvoice.paymentHash,
logger,
})
: updatePendingInvoice({ walletInvoice, logger })

const updatePendingInvoiceBeforeFinally = async ({
walletInvoice,
Expand Down Expand Up @@ -212,7 +283,7 @@ const updatePendingInvoiceBeforeFinally = async ({
recipientWalletDescriptors: accountWallets,
}).withConversion({
mid: { usdFromBtc: usdFromBtcMidPriceFn },
hedgeBuyUsd: { usdFromBtc: dealer.getCentsFromSatsForImmediateBuy },
hedgeBuyUsd: { usdFromBtc: DealerPriceService().getCentsFromSatsForImmediateBuy },
})
if (receivedWalletInvoice instanceof Error) return receivedWalletInvoice

Expand Down Expand Up @@ -341,7 +412,7 @@ const updatePendingInvoiceBeforeFinally = async ({
})
}

const updatePendingInvoice = wrapAsyncToRunInSpan({
export const updatePendingInvoice = wrapAsyncToRunInSpan({
namespace: "app.invoices",
fnName: "updatePendingInvoice",
fn: async ({
Expand Down Expand Up @@ -370,76 +441,3 @@ const updatePendingInvoice = wrapAsyncToRunInSpan({
return result
},
})

export const declineHeldInvoice = wrapAsyncToRunInSpan({
namespace: "app.invoices",
fnName: "declineHeldInvoice",
fn: async ({
pubkey,
paymentHash,
logger,
}: {
pubkey: Pubkey
paymentHash: PaymentHash
logger: Logger
}): Promise<boolean | ApplicationError> => {
addAttributesToCurrentSpan({ paymentHash, pubkey })

const lndService = LndService()
if (lndService instanceof Error) return lndService

const walletInvoicesRepo = WalletInvoicesRepository()

const lnInvoiceLookup = await lndService.lookupInvoice({ pubkey, paymentHash })

const pendingInvoiceLogger = logger.child({
hash: paymentHash,
pubkey,
lnInvoiceLookup,
topic: "payment",
protocol: "lightning",
transactionType: "receipt",
onUs: false,
})

if (lnInvoiceLookup instanceof InvoiceNotFoundError) {
const isDeleted = await walletInvoicesRepo.deleteByPaymentHash(paymentHash)
if (isDeleted instanceof Error) {
pendingInvoiceLogger.error("impossible to delete WalletInvoice entry")
return isDeleted
}
return false
}
if (lnInvoiceLookup instanceof Error) return lnInvoiceLookup

if (lnInvoiceLookup.isSettled) {
return new InvalidNonHodlInvoiceError(
JSON.stringify({ paymentHash: lnInvoiceLookup.paymentHash }),
)
}

if (!lnInvoiceLookup.isHeld) {
pendingInvoiceLogger.info({ lnInvoiceLookup }, "invoice has not been paid yet")
return false
}

let heldForMsg = ""
if (lnInvoiceLookup.heldAt) {
heldForMsg = `for ${elapsedSinceTimestamp(lnInvoiceLookup.heldAt)}s `
}
pendingInvoiceLogger.error(
{ lnInvoiceLookup },
`invoice has been held ${heldForMsg}and is now been cancelled`,
)

const invoiceSettled = await lndService.cancelInvoice({ pubkey, paymentHash })
if (invoiceSettled instanceof Error) return invoiceSettled

const isDeleted = await walletInvoicesRepo.deleteByPaymentHash(paymentHash)
if (isDeleted instanceof Error) {
pendingInvoiceLogger.error("impossible to delete WalletInvoice entry")
}

return true
},
})
2 changes: 1 addition & 1 deletion src/domain/wallet-invoices/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export * from "./wallet-invoice-validator"
export * from "./wallet-invoice-checker"
8 changes: 4 additions & 4 deletions src/domain/wallet-invoices/index.types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ type BtcFromUsdFn = (
amount: UsdPaymentAmount,
) => Promise<BtcPaymentAmount | DealerPriceServiceError>

type WalletInvoiceChecker = {
shouldDecline: () => boolean
}

type WalletInvoiceBuilderConfig = {
dealerBtcFromUsd: BtcFromUsdFn
lnRegisterInvoice: (
Expand Down Expand Up @@ -147,10 +151,6 @@ type WalletAddressReceiverArgs<S extends WalletCurrency> = {
walletAddress: WalletAddress<S>
}

type WalletInvoiceValidator = {
validateToSend(fromWalletId: WalletId): true | ValidationError
}

type WalletInvoicesPersistNewArgs = Omit<WalletInvoice, "createdAt">

interface IWalletInvoicesRepository {
Expand Down
31 changes: 31 additions & 0 deletions src/domain/wallet-invoices/wallet-invoice-checker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { invoiceExpirationForCurrency } from "@domain/bitcoin/lightning"
import { CouldNotFindWalletInvoiceError } from "@domain/errors"
import { WalletCurrency } from "@domain/shared"

export const WalletInvoiceChecker = (
walletInvoice: WalletInvoice | RepositoryError,
): WalletInvoiceChecker => {
const shouldDecline = (): boolean => {
if (walletInvoice instanceof CouldNotFindWalletInvoiceError) {
return true
}

if (walletInvoice instanceof Error) {
return false
}

const expiresAtWithDelay = invoiceExpirationForCurrency(
walletInvoice.recipientWalletDescriptor.currency,
walletInvoice.createdAt,
)
const isUsdRecipient =
walletInvoice.recipientWalletDescriptor.currency === WalletCurrency.Usd
if (isUsdRecipient && expiresAtWithDelay.getTime() < Date.now()) {
return true
}

return false
}

return { shouldDecline }
}
16 changes: 0 additions & 16 deletions src/domain/wallet-invoices/wallet-invoice-validator.ts

This file was deleted.

Loading

0 comments on commit f0e66c7

Please sign in to comment.