diff --git a/src/app/wallets/get-balance-for-wallet.ts b/src/app/wallets/get-balance-for-wallet.ts index d1abc9bbf3..5f53566c4d 100644 --- a/src/app/wallets/get-balance-for-wallet.ts +++ b/src/app/wallets/get-balance-for-wallet.ts @@ -1,6 +1,8 @@ import { LedgerService } from "@services/ledger" import { updatePendingPaymentsByWalletId } from "@app/payments" +import { updatePendingInvoicesByWalletId } from "./update-pending-invoices" + export const getBalanceForWallet = async ({ walletId, logger, @@ -8,10 +10,16 @@ export const getBalanceForWallet = async ({ walletId: WalletId logger: Logger }): Promise => { - const updatePaymentsResult = await updatePendingPaymentsByWalletId({ - walletId, - logger, - }) + const [, updatePaymentsResult] = await Promise.all([ + updatePendingInvoicesByWalletId({ + walletId, + logger, + }), + updatePendingPaymentsByWalletId({ + walletId, + logger, + }), + ]) if (updatePaymentsResult instanceof Error) return updatePaymentsResult return LedgerService().getWalletBalance(walletId) diff --git a/src/app/wallets/update-pending-invoices.ts b/src/app/wallets/update-pending-invoices.ts index 1e688d4349..5f00d225d8 100644 --- a/src/app/wallets/update-pending-invoices.ts +++ b/src/app/wallets/update-pending-invoices.ts @@ -12,36 +12,57 @@ import { WalletsRepository, } from "@services/mongoose" import { NotificationsService } from "@services/notifications" -import { elapsedSinceTimestamp, runInParallel } from "@utils" +import { runInParallel } from "@utils" import { WalletInvoiceReceiver } from "@domain/wallet-invoices/wallet-invoice-receiver" import * as LedgerFacade from "@services/ledger/facade" import { usdFromBtcMidPriceFn } from "@app/shared" -export const declineHeldInvoices = async (logger: Logger): Promise => { +export const updatePendingInvoices = async (logger: Logger): Promise => { const invoicesRepo = WalletInvoicesRepository() - const pendingInvoices = invoicesRepo.yieldPending() + const walletIdsWithPendingInvoices = invoicesRepo.listWalletIdsWithPendingInvoices() - if (pendingInvoices instanceof Error) { + if (walletIdsWithPendingInvoices instanceof Error) { logger.error( - { error: pendingInvoices }, + { error: walletIdsWithPendingInvoices }, "finish updating pending invoices with error", ) return } await runInParallel({ - iterator: pendingInvoices, + iterator: walletIdsWithPendingInvoices, logger, - processor: async (walletInvoice: WalletInvoice, index: number) => { - logger.trace("updating pending invoices %s in worker %d", index) - await declineHeldInvoice({ walletInvoice, logger }) + processor: async (walletId: WalletId, index: number) => { + logger.trace( + "updating pending invoices for wallet %s in worker %d", + walletId, + index, + ) + await updatePendingInvoicesByWalletId({ walletId, logger }) }, }) logger.info("finish updating pending invoices") } +export const updatePendingInvoicesByWalletId = async ({ + walletId, + logger, +}: { + walletId: WalletId + logger: Logger +}) => { + const invoicesRepo = WalletInvoicesRepository() + + const invoices = invoicesRepo.findPendingByWalletId(walletId) + if (invoices instanceof Error) return invoices + + for await (const walletInvoice of invoices) { + await updatePendingInvoice({ walletInvoice, logger }) + } +} + export const updatePendingInvoiceByPaymentHash = async ({ paymentHash, logger, @@ -73,43 +94,46 @@ const updatePendingInvoice = async ({ const walletInvoicesRepo = WalletInvoicesRepository() - const { pubkey, paymentHash, secret, recipientWalletDescriptor } = walletInvoice - - const pendingInvoiceLogger = logger.child({ - hash: paymentHash, - walletId: recipientWalletDescriptor.id, - topic: "payment", - protocol: "lightning", - transactionType: "receipt", - onUs: false, - }) + const { pubkey, paymentHash, recipientWalletDescriptor } = walletInvoice const lnInvoiceLookup = await lndService.lookupInvoice({ pubkey, paymentHash }) if (lnInvoiceLookup instanceof InvoiceNotFoundError) { const isDeleted = await walletInvoicesRepo.deleteByPaymentHash(paymentHash) if (isDeleted instanceof Error) { - pendingInvoiceLogger.error("impossible to delete WalletInvoice entry") + logger.error( + { walletInvoice, error: isDeleted }, + "impossible to delete WalletInvoice entry", + ) return isDeleted } return false } if (lnInvoiceLookup instanceof Error) return lnInvoiceLookup + if (!lnInvoiceLookup.isSettled) { + logger.debug({ invoice: lnInvoiceLookup }, "invoice has not been paid") + return false + } + const { lnInvoice: { description }, roundedDownReceived, } = lnInvoiceLookup + const pendingInvoiceLogger = logger.child({ + hash: paymentHash, + walletId: recipientWalletDescriptor.id, + topic: "payment", + protocol: "lightning", + transactionType: "receipt", + onUs: false, + }) + if (walletInvoice.paid) { pendingInvoiceLogger.info("invoice has already been processed") return true } - if (!lnInvoiceLookup.isHeld) { - pendingInvoiceLogger.info("invoice has not been paid yet") - return false - } - const receivedBtc = paymentAmountFromNumber({ amount: roundedDownReceived, currency: WalletCurrency.Btc, @@ -133,7 +157,7 @@ const updatePendingInvoice = async ({ return false } if (invoiceToUpdate instanceof Error) return invoiceToUpdate - if (walletInvoice.paid) { + if (invoiceToUpdate.paid) { pendingInvoiceLogger.info("invoice has already been processed") return true } @@ -141,17 +165,14 @@ const updatePendingInvoice = async ({ const displayCurrencyPerSat = await getCurrentPrice() if (displayCurrencyPerSat instanceof Error) return displayCurrencyPerSat - const invoiceSettled = await lndService.settleInvoice({ pubkey, secret }) - if (invoiceSettled instanceof Error) return invoiceSettled - - const invoicePaid = await walletInvoicesRepo.markAsPaid(paymentHash) - if (invoicePaid instanceof Error) return invoicePaid - // TODO: this should be a in a mongodb transaction session with the ledger transaction below // markAsPaid could be done after the transaction, but we should in that case not only look // for walletInvoicesRepo, but also in the ledger to make sure in case the process crash in this // loop that an eventual consistency doesn't lead to a double credit + const invoicePaid = await walletInvoicesRepo.markAsPaid(paymentHash) + if (invoicePaid instanceof Error) return invoicePaid + const metadata = LedgerFacade.LnReceiveLedgerMetadata({ paymentHash, fee: walletInvoiceReceiver.btcBankFee, @@ -217,64 +238,3 @@ const updatePendingInvoice = async ({ return true }) } - -const declineHeldInvoice = async ({ - walletInvoice, - logger, -}: { - walletInvoice: WalletInvoice - logger: Logger -}): Promise => { - const lndService = LndService() - if (lndService instanceof Error) return lndService - - const walletInvoicesRepo = WalletInvoicesRepository() - - const { pubkey, paymentHash } = walletInvoice - - const lnInvoiceLookup = await lndService.lookupInvoice({ pubkey, paymentHash }) - - const pendingInvoiceLogger = logger.child({ - hash: paymentHash, - lnInvoiceLookup, - walletInvoice, - 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.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 -} diff --git a/src/domain/bitcoin/lightning/errors.ts b/src/domain/bitcoin/lightning/errors.ts index 385683d113..ab1f548e43 100644 --- a/src/domain/bitcoin/lightning/errors.ts +++ b/src/domain/bitcoin/lightning/errors.ts @@ -13,10 +13,6 @@ export class CouldNotDecodeReturnedPaymentRequest extends LightningServiceError export class UnknownLightningServiceError extends LightningServiceError { level = ErrorLevel.Critical } -export class SecretDoesNotMatchAnyExistingHodlInvoiceError extends LightningServiceError { - level = ErrorLevel.Critical -} - export class InvoiceNotFoundError extends LightningServiceError {} export class LnPaymentPendingError extends LightningServiceError {} export class LnAlreadyPaidError extends LightningServiceError {} diff --git a/src/domain/bitcoin/lightning/index.ts b/src/domain/bitcoin/lightning/index.ts index ef272c139b..ed887ac23c 100644 --- a/src/domain/bitcoin/lightning/index.ts +++ b/src/domain/bitcoin/lightning/index.ts @@ -1,5 +1,3 @@ -import { createHash, randomBytes } from "crypto" - import { InvalidPubKeyError } from "@domain/errors" export { decodeInvoice } from "./ln-invoice" @@ -29,14 +27,3 @@ export const checkedToPubkey = (pubkey: string): Pubkey | InvalidPubKeyError => } return new InvalidPubKeyError("Pubkey conversion error") } - -export const sha256 = (buffer: Buffer) => - createHash("sha256").update(buffer).digest("hex") -const randomSecret = () => randomBytes(32) - -export const getSecretAndPaymentHash = () => { - const secret = randomSecret() - const paymentHash = sha256(secret) as PaymentHash - - return { secret: secret.toString("hex") as SecretPreImage, paymentHash } -} diff --git a/src/domain/bitcoin/lightning/index.types.d.ts b/src/domain/bitcoin/lightning/index.types.d.ts index 493e7a39ac..6a0d53e45d 100644 --- a/src/domain/bitcoin/lightning/index.types.d.ts +++ b/src/domain/bitcoin/lightning/index.types.d.ts @@ -50,8 +50,6 @@ type LnInvoiceLookup = { readonly createdAt: Date readonly confirmedAt: Date | undefined readonly isSettled: boolean - readonly isHeld: boolean - readonly heldAt: Date | undefined readonly roundedDownReceived: Satoshis readonly milliSatsReceived: MilliSatoshis readonly secretPreImage: SecretPreImage @@ -111,7 +109,6 @@ type LnInvoice = { } type RegisterInvoiceArgs = { - paymentHash: PaymentHash description: string descriptionHash?: string sats: Satoshis @@ -119,7 +116,6 @@ type RegisterInvoiceArgs = { } type NewRegisterInvoiceArgs = { - paymentHash: PaymentHash description: string descriptionHash?: string btcPaymentAmount: BtcPaymentAmount @@ -223,14 +219,6 @@ interface ILightningService { pubkey?: Pubkey }): Promise - settleInvoice({ - pubkey, - secret, - }: { - pubkey: Pubkey - secret: SecretPreImage - }): Promise - cancelInvoice({ pubkey, paymentHash, diff --git a/src/domain/wallet-invoices/errors.ts b/src/domain/wallet-invoices/errors.ts deleted file mode 100644 index 30fcba59b0..0000000000 --- a/src/domain/wallet-invoices/errors.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { ValidationError, ErrorLevel } from "@domain/shared" - -export class InvalidWalletInvoiceBuilderStateError extends ValidationError { - level = ErrorLevel.Critical -} diff --git a/src/domain/wallet-invoices/index.types.d.ts b/src/domain/wallet-invoices/index.types.d.ts index 65bf86c048..87ba152eaa 100644 --- a/src/domain/wallet-invoices/index.types.d.ts +++ b/src/domain/wallet-invoices/index.types.d.ts @@ -68,7 +68,6 @@ type WIBWithAmount = { type WalletInvoice = { paymentHash: PaymentHash - secret: SecretPreImage selfGenerated: boolean pubkey: Pubkey usdAmount?: UsdPaymentAmount @@ -109,7 +108,11 @@ interface IWalletInvoicesRepository { paymentHash: PaymentHash, ) => Promise - yieldPending: () => AsyncGenerator | RepositoryError + findPendingByWalletId: ( + walletId: WalletId, + ) => AsyncGenerator | RepositoryError + + listWalletIdsWithPendingInvoices: () => AsyncGenerator | RepositoryError deleteByPaymentHash: (paymentHash: PaymentHash) => Promise diff --git a/src/domain/wallet-invoices/wallet-invoice-builder.ts b/src/domain/wallet-invoices/wallet-invoice-builder.ts index 695a15eefa..c6013e0d0d 100644 --- a/src/domain/wallet-invoices/wallet-invoice-builder.ts +++ b/src/domain/wallet-invoices/wallet-invoice-builder.ts @@ -1,12 +1,7 @@ -import { - getSecretAndPaymentHash, - invoiceExpirationForCurrency, -} from "@domain/bitcoin/lightning" +import { invoiceExpirationForCurrency } from "@domain/bitcoin/lightning" import { checkedToBtcPaymentAmount, checkedToUsdPaymentAmount } from "@domain/payments" import { WalletCurrency, ZERO_SATS } from "@domain/shared" -import { InvalidWalletInvoiceBuilderStateError } from "./errors" - export const WalletInvoiceBuilder = ( config: WalletInvoiceBuilderConfig, ): WalletInvoiceBuilder => { @@ -17,11 +12,7 @@ export const WalletInvoiceBuilder = ( description: string descriptionHash?: string }) => { - return WIBWithDescription({ - ...config, - description, - descriptionHash, - }) + return WIBWithDescription({ ...config, description, descriptionHash }) } return { @@ -98,23 +89,17 @@ export const WIBWithRecipient = (state: WIBWithRecipientState): WIBWithRecipient export const WIBWithAmount = (state: WIBWithAmountState): WIBWithAmount => { const registerInvoice = async () => { - const { secret, paymentHash } = getSecretAndPaymentHash() - const registeredInvoice = await state.lnRegisterInvoice({ - paymentHash, description: state.description, descriptionHash: state.descriptionHash, btcPaymentAmount: state.btcAmount, expiresAt: state.invoiceExpiration, }) + if (registeredInvoice instanceof Error) return registeredInvoice - if (paymentHash !== registeredInvoice.invoice.paymentHash) { - return new InvalidWalletInvoiceBuilderStateError() - } const walletInvoice: WalletInvoice = { - paymentHash, - secret, + paymentHash: registeredInvoice.invoice.paymentHash, selfGenerated: state.selfGenerated, pubkey: registeredInvoice.pubkey, usdAmount: state.usdAmount, diff --git a/src/graphql/error-map.ts b/src/graphql/error-map.ts index 4dc848d1dd..8c21f7e55f 100644 --- a/src/graphql/error-map.ts +++ b/src/graphql/error-map.ts @@ -415,7 +415,6 @@ export const mapError = (error: ApplicationError): CustomApolloError => { case "SafeWrapperError": case "InvalidFeeProbeStateError": case "InvalidPubKeyError": - case "SecretDoesNotMatchAnyExistingHodlInvoiceError": message = `Unknown error occurred (code: ${error.name}${ error.message ? ": " + error.message : "" })` diff --git a/src/servers/cron.ts b/src/servers/cron.ts index f1a67b8620..993e3039ca 100644 --- a/src/servers/cron.ts +++ b/src/servers/cron.ts @@ -26,7 +26,7 @@ const main = async () => { if (result instanceof Error) throw result } - const updatePendingLightningInvoices = () => Wallets.declineHeldInvoices(logger) + const updatePendingLightningInvoices = () => Wallets.updatePendingInvoices(logger) const updatePendingLightningPayments = () => Payments.updatePendingPayments(logger) diff --git a/src/servers/trigger.ts b/src/servers/trigger.ts index 9e016b6c6e..f566456a2e 100644 --- a/src/servers/trigger.ts +++ b/src/servers/trigger.ts @@ -181,7 +181,7 @@ export const onchainBlockEventHandler = async (height: number) => { export const invoiceUpdateEventHandler = async (invoice: GetInvoiceResult) => { logger.info({ invoice }, "invoiceUpdateEventHandler") - if (!invoice.is_held) { + if (!invoice.is_confirmed) { return } diff --git a/src/services/lnd/index.ts b/src/services/lnd/index.ts index f3d3f7d124..415f77b2de 100644 --- a/src/services/lnd/index.ts +++ b/src/services/lnd/index.ts @@ -1,6 +1,6 @@ import { cancelHodlInvoice, - createHodlInvoice, + createInvoice, getChannelBalance, getClosedChannels, getFailedPayments, @@ -17,7 +17,6 @@ import { payViaRoutes, PayViaRoutesResult, deletePayment, - settleHodlInvoice, } from "lightning" import lnService from "ln-service" @@ -42,7 +41,6 @@ import { ProbeForRouteTimedOutError, ProbeForRouteTimedOutFromApplicationError, RouteNotFoundError, - SecretDoesNotMatchAnyExistingHodlInvoiceError, UnknownLightningServiceError, UnknownRouteNotFoundError, } from "@domain/bitcoin/lightning" @@ -256,7 +254,6 @@ export const LndService = (): ILightningService | LightningServiceError => { } const registerInvoice = async ({ - paymentHash, sats, description, descriptionHash, @@ -264,7 +261,6 @@ export const LndService = (): ILightningService | LightningServiceError => { }: RegisterInvoiceArgs): Promise => { const input = { lnd: defaultLnd, - id: paymentHash, description, description_hash: descriptionHash, tokens: sats as number, @@ -272,7 +268,7 @@ export const LndService = (): ILightningService | LightningServiceError => { } try { - const result = await createHodlInvoice(input) + const result = await createInvoice(input) const request = result.request as EncodedPaymentRequest const returnedInvoice = decodeInvoice(request) if (returnedInvoice instanceof Error) { @@ -312,11 +308,6 @@ export const LndService = (): ILightningService | LightningServiceError => { createdAt: new Date(invoice.created_at), confirmedAt: invoice.confirmed_at ? new Date(invoice.confirmed_at) : undefined, isSettled: !!invoice.is_confirmed, - isHeld: !!invoice.is_held, - heldAt: - invoice.payments && invoice.payments.length - ? new Date(invoice.payments[0].created_at) - : undefined, roundedDownReceived: toSats(invoice.received), milliSatsReceived: toMilliSatsFromString(invoice.received_mtokens), secretPreImage: invoice.secret as SecretPreImage, @@ -447,31 +438,6 @@ export const LndService = (): ILightningService | LightningServiceError => { } } - const settleInvoice = async ({ - pubkey, - secret, - }: { - pubkey: Pubkey - secret: SecretPreImage - }): Promise => { - try { - const lnd = getLndFromPubkey({ pubkey }) - if (lnd instanceof Error) return lnd - - // Use the secret to claim the funds - await settleHodlInvoice({ lnd, secret }) - return true - } catch (err) { - const errDetails = parseLndErrorDetails(err) - switch (errDetails) { - case KnownLndErrorDetails.SecretDoesNotMatchAnyExistingHodlInvoice: - return new SecretDoesNotMatchAnyExistingHodlInvoiceError(err) - default: - return new UnknownLightningServiceError(err) - } - } - } - const cancelInvoice = async ({ pubkey, paymentHash, @@ -611,7 +577,6 @@ export const LndService = (): ILightningService | LightningServiceError => { listPendingPayments: listPaymentsFactory(getPendingPayments), listFailedPayments, deletePaymentByHash, - settleInvoice, cancelInvoice, payInvoiceViaRoutes, payInvoiceViaPaymentDetails, @@ -696,7 +661,6 @@ const KnownLndErrorDetails = { SentPaymentNotFound: "SentPaymentNotFound", PaymentInTransition: "payment is in transition", PaymentForDeleteNotFound: "non bucket element in payments bucket", - SecretDoesNotMatchAnyExistingHodlInvoice: "SecretDoesNotMatchAnyExistingHodlInvoice", } as const /* eslint @typescript-eslint/ban-ts-comment: "off" */ diff --git a/src/services/mongoose/schema.ts b/src/services/mongoose/schema.ts index 437a3d93b1..5d1cc5dca9 100644 --- a/src/services/mongoose/schema.ts +++ b/src/services/mongoose/schema.ts @@ -44,12 +44,6 @@ const walletInvoiceSchema = new Schema({ }, }, - secret: { - required: true, - type: String, - length: 64, - }, - currency: { required: true, type: String, diff --git a/src/services/mongoose/schema.types.d.ts b/src/services/mongoose/schema.types.d.ts index e9539a5ccd..0f9b8d8f51 100644 --- a/src/services/mongoose/schema.types.d.ts +++ b/src/services/mongoose/schema.types.d.ts @@ -65,7 +65,6 @@ interface WalletInvoiceRecord { _id: string walletId: string cents: number - secret: string currency: string timestamp: Date selfGenerated: boolean diff --git a/src/services/mongoose/wallet-invoices.ts b/src/services/mongoose/wallet-invoices.ts index bc9681a070..5f90b39a94 100644 --- a/src/services/mongoose/wallet-invoices.ts +++ b/src/services/mongoose/wallet-invoices.ts @@ -10,7 +10,6 @@ import { WalletInvoice } from "./schema" export const WalletInvoicesRepository = (): IWalletInvoicesRepository => { const persistNew = async ({ paymentHash, - secret, recipientWalletDescriptor, selfGenerated, pubkey, @@ -20,7 +19,6 @@ export const WalletInvoicesRepository = (): IWalletInvoicesRepository => { try { const walletInvoice = await new WalletInvoice({ _id: paymentHash, - secret, walletId: recipientWalletDescriptor.id, selfGenerated, pubkey, @@ -68,10 +66,12 @@ export const WalletInvoicesRepository = (): IWalletInvoicesRepository => { } } - async function* yieldPending(): AsyncGenerator | RepositoryError { + async function* findPendingByWalletId( + walletId: WalletId, + ): AsyncGenerator | RepositoryError { let pending try { - pending = WalletInvoice.find({ paid: false }).cursor({ + pending = WalletInvoice.find({ walletId, paid: false }).cursor({ batchSize: 100, }) } catch (error) { @@ -83,6 +83,25 @@ export const WalletInvoicesRepository = (): IWalletInvoicesRepository => { } } + async function* listWalletIdsWithPendingInvoices(): + | AsyncGenerator + | RepositoryError { + let pending + try { + // select distinct user ids from pending invoices + pending = WalletInvoice.aggregate([ + { $match: { paid: false } }, + { $group: { _id: "$walletId" } }, + ]).cursor({ batchSize: 100 }) + } catch (error) { + return new RepositoryError(error) + } + + for await (const { _id } of pending) { + yield _id as WalletId + } + } + const deleteByPaymentHash = async ( paymentHash: PaymentHash, ): Promise => { @@ -115,7 +134,8 @@ export const WalletInvoicesRepository = (): IWalletInvoicesRepository => { persistNew, markAsPaid, findByPaymentHash, - yieldPending, + findPendingByWalletId, + listWalletIdsWithPendingInvoices, deleteByPaymentHash, deleteUnpaidOlderThan, } @@ -123,7 +143,6 @@ export const WalletInvoicesRepository = (): IWalletInvoicesRepository => { const walletInvoiceFromRaw = (result: WalletInvoiceRecord): WalletInvoice => ({ paymentHash: result._id as PaymentHash, - secret: result.secret as SecretPreImage, recipientWalletDescriptor: { id: result.walletId as WalletId, currency: result.currency as WalletCurrency, diff --git a/test/e2e/servers/trigger.spec.ts b/test/e2e/servers/trigger.spec.ts index b395f718be..f0622aa803 100644 --- a/test/e2e/servers/trigger.spec.ts +++ b/test/e2e/servers/trigger.spec.ts @@ -202,9 +202,7 @@ describe("onchainBlockEventHandler", () => { expect(lnInvoice).not.toBeInstanceOf(Error) const { paymentRequest: request } = lnInvoice as LnInvoice - pay({ lnd: lndOutside1, request }) - - await sleep(250) + await pay({ lnd: lndOutside1, request }) const hash = getHash(request) const invoice = await getInvoice({ id: hash, lnd: lnd1 }) diff --git a/test/helpers/bitcoin-core.ts b/test/helpers/bitcoin-core.ts index 69c367d977..09e6f9ab1e 100644 --- a/test/helpers/bitcoin-core.ts +++ b/test/helpers/bitcoin-core.ts @@ -1,14 +1,20 @@ import BitcoindClient from "bitcoin-core-ts" -import { createOnChainAddress } from "@app/wallets" +import { + addInvoiceForSelf, + createOnChainAddress, + getBalanceForWallet, +} from "@app/wallets" import { getBitcoinCoreRPCConfig } from "@config" import { bitcoindDefaultClient, BitcoindWalletClient } from "@services/bitcoind" +import { baseLogger } from "@services/logger" import { LedgerService } from "@services/ledger" +import { pay } from "lightning" import { toSats } from "@domain/bitcoin" import { descriptors } from "./multisig-wallet" -import { checkIsBalanced, waitUntilBlockHeight } from "." +import { checkIsBalanced, lndOutside1, waitUntilBlockHeight } from "." export const RANDOM_ADDRESS = "2N1AdXp9qihogpSmSBXSSfgeUFgTYyjVWqo" export const bitcoindClient = bitcoindDefaultClient // no wallet @@ -96,6 +102,22 @@ export const fundWalletIdFromOnchain = async ({ return toSats(balance) } +export const fundWalletIdFromLightning = async ({ + walletId, + amount, +}: { + walletId: WalletId + amount: Satoshis | UsdCents +}) => { + const invoice = await addInvoiceForSelf({ walletId, amount }) + if (invoice instanceof Error) return invoice + + await pay({ lnd: lndOutside1, request: invoice.paymentRequest }) + + const balance = await getBalanceForWallet({ walletId, logger: baseLogger }) + if (balance instanceof Error) throw balance +} + export const createColdStorageWallet = async (walletName: string) => { const client = await getBitcoindClient() const wallet = await client.createWallet({ diff --git a/test/helpers/check-is-balanced.ts b/test/helpers/check-is-balanced.ts index c4d52ab813..5d91252fe1 100644 --- a/test/helpers/check-is-balanced.ts +++ b/test/helpers/check-is-balanced.ts @@ -1,5 +1,5 @@ import { updatePendingPayments } from "@app/payments" -import { declineHeldInvoices, updateOnChainReceipt } from "@app/wallets" +import { updatePendingInvoices, updateOnChainReceipt } from "@app/wallets" import { baseLogger } from "@services/logger" import { getBalance as getBitcoindBalance } from "@services/bitcoind" @@ -12,7 +12,7 @@ const logger = baseLogger.child({ module: "test" }) export const checkIsBalanced = async () => { await Promise.all([ - declineHeldInvoices(logger), + updatePendingInvoices(logger), updatePendingPayments(logger), updateOnChainReceipt({ logger }), ]) diff --git a/test/helpers/lightning.ts b/test/helpers/lightning.ts index de4f06c239..5a6fda5ea5 100644 --- a/test/helpers/lightning.ts +++ b/test/helpers/lightning.ts @@ -1,8 +1,5 @@ import { once } from "events" -import { Wallets } from "@app" -import { addInvoiceForSelf, getBalanceForWallet } from "@app/wallets" - import { offchainLnds, onchainLnds, @@ -10,8 +7,6 @@ import { updateEscrows, } from "@services/lnd/utils" import { baseLogger } from "@services/logger" -import { sleep } from "@utils" - import { authenticatedLndGrpc, closeChannel, @@ -22,7 +17,6 @@ import { getInvoice, getWalletInfo, openChannel, - pay, sendToChainAddress, subscribeToChannels, subscribeToGraph, @@ -31,6 +25,8 @@ import { import { parsePaymentRequest } from "invoices" +import { sleep } from "@utils" + import { bitcoindClient, bitcoindOutside, @@ -52,9 +48,6 @@ export const getAmount = (request: EncodedPaymentRequest) => { return parsePaymentRequest({ request }).tokens as Satoshis } -export const getPubKey = (request: EncodedPaymentRequest) => { - return parsePaymentRequest({ request }).destination as Pubkey -} export const getInvoiceAttempt = async ({ lnd, id }) => { try { const result = await getInvoice({ lnd, id }) @@ -299,31 +292,3 @@ export const waitFor = async (f) => { while (!(res = await f())) await sleep(500) return res } - -export const fundWalletIdFromLightning = async ({ - walletId, - amount, -}: { - walletId: WalletId - amount: number -}) => { - const invoice = await addInvoiceForSelf({ walletId, amount }) - if (invoice instanceof Error) return invoice - - pay({ lnd: lndOutside1, request: invoice.paymentRequest }) - - // TODO: we could use an event instead of a sleep - await sleep(500) - - const hash = getHash(invoice.paymentRequest) - - expect( - await Wallets.updatePendingInvoiceByPaymentHash({ - paymentHash: hash as PaymentHash, - logger: baseLogger, - }), - ).not.toBeInstanceOf(Error) - - const balance = await getBalanceForWallet({ walletId, logger: baseLogger }) - if (balance instanceof Error) throw balance -} diff --git a/test/integration/02-user-wallet/02-receive-lightning.spec.ts b/test/integration/02-user-wallet/02-receive-lightning.spec.ts index 730bbfd08a..eb40497a20 100644 --- a/test/integration/02-user-wallet/02-receive-lightning.spec.ts +++ b/test/integration/02-user-wallet/02-receive-lightning.spec.ts @@ -1,29 +1,19 @@ -import { MEMO_SHARING_SATS_THRESHOLD } from "@config" - import { Lightning } from "@app" +import { getDealerUsdWalletId } from "@services/ledger/caching" import * as Wallets from "@app/wallets" -import { declineHeldInvoices } from "@app/wallets" - +import { MEMO_SHARING_SATS_THRESHOLD } from "@config" import { toSats } from "@domain/bitcoin" -import { InvoiceNotFoundError } from "@domain/bitcoin/lightning" import { defaultTimeToExpiryInSeconds } from "@domain/bitcoin/lightning/invoice-expiration" import { toCents } from "@domain/fiat" import { PaymentInitiationMethod, WithdrawalFeePriceMethod } from "@domain/wallets" import { WalletCurrency } from "@domain/shared" -import { CouldNotFindWalletInvoiceError } from "@domain/errors" - -import { WalletInvoicesRepository } from "@services/mongoose" -import { getDealerUsdWalletId } from "@services/ledger/caching" import { DealerPriceService } from "@services/dealer-price" import { LedgerService } from "@services/ledger" import { TransactionsMetadataRepository } from "@services/ledger/services" -import { LndService } from "@services/lnd" import { baseLogger } from "@services/logger" import { ImbalanceCalculator } from "@domain/ledger/imbalance-calculator" -import { sleep } from "@utils" - import { checkIsBalanced, createUserAndWalletFromUserRef, @@ -31,7 +21,6 @@ import { getBalanceHelper, getDefaultWalletIdByTestUserRef, getHash, - getPubKey, getUsdWalletIdByTestUserRef, lndOutside1, pay, @@ -60,93 +49,6 @@ afterEach(async () => { }) describe("UserWallet - Lightning", () => { - it("if trigger is missing the invoice, then it should be denied", async () => { - /* - the reason we are doing this behavior is to limit the discrepancy between our books, - and the state of lnd. - if we get invoices that lnd has been settled because we were not using holdinvoice, - then there would be discrepancy between the time lnd settled the invoice - and the time it's being settle in our ledger - the reason this could happen is because trigger has to restart - the discrepancy in ledger is an okish behavior for bitcoin invoice, because there - are no price risk, but it's an unbearable risk for non bitcoin wallets, - because of the associated price risk exposure - */ - - const sats = 50000 - const memo = "myMemo" - - const lnInvoice = await Wallets.addInvoiceForSelf({ - walletId: walletIdB as WalletId, - amount: toSats(sats), - memo, - }) - if (lnInvoice instanceof Error) throw lnInvoice - const { paymentRequest: invoice } = lnInvoice - - const checker = await Lightning.PaymentStatusChecker(invoice) - if (checker instanceof Error) throw checker - - const isPaidBeforePay = await checker.invoiceIsPaid() - expect(isPaidBeforePay).not.toBeInstanceOf(Error) - expect(isPaidBeforePay).toBe(false) - - const paymentHash = getHash(invoice) - const pubkey = getPubKey(invoice) - - await Promise.all([ - (async () => { - try { - await pay({ lnd: lndOutside1, request: invoice }) - } catch (err) { - expect(err[1]).toBe("PaymentRejectedByDestination") - } - })(), - (async () => { - await sleep(500) - - // make sure invoice is held - - const lndService = LndService() - if (lndService instanceof Error) throw lndService - - { - const lnInvoiceLookup = await lndService.lookupInvoice({ pubkey, paymentHash }) - if (lnInvoiceLookup instanceof Error) throw lnInvoiceLookup - - expect(lnInvoiceLookup.isHeld).toBe(true) - } - - // declining invoice - await declineHeldInvoices(baseLogger) - - const ledger = LedgerService() - const ledgerTxs = await ledger.getTransactionsByHash(paymentHash) - if (ledgerTxs instanceof Error) throw ledgerTxs - expect(ledgerTxs).toStrictEqual([]) - - const isPaidAfterPay = await checker.invoiceIsPaid() - expect(isPaidAfterPay).not.toBeInstanceOf(Error) - expect(isPaidAfterPay).toBe(false) - - const finalBalance = await getBalanceHelper(walletIdB) - expect(finalBalance).toBe(initBalanceB) - - const lnInvoiceLookup = await lndService.lookupInvoice({ pubkey, paymentHash }) - expect(lnInvoiceLookup).toBeInstanceOf(InvoiceNotFoundError) - - { - const walletInvoiceRepo = WalletInvoicesRepository() - const result = await walletInvoiceRepo.findByPaymentHash(paymentHash) - expect(result).toBeInstanceOf(CouldNotFindWalletInvoiceError) - } - - // making sure relooping is a no-op and doesn't throw - await declineHeldInvoices(baseLogger) - })(), - ]) - }) - it("receives payment from outside", async () => { // larger amount to not fall below the escrow limit const sats = 50000 @@ -170,32 +72,21 @@ describe("UserWallet - Lightning", () => { const hash = getHash(invoice) - const updateInvoice = () => - Wallets.updatePendingInvoiceByPaymentHash({ + await pay({ lnd: lndOutside1, request: invoice }) + + expect( + await Wallets.updatePendingInvoiceByPaymentHash({ paymentHash: hash as PaymentHash, logger: baseLogger, - }) - - const promises = Promise.all([ - pay({ lnd: lndOutside1, request: invoice }), - (async () => { - // TODO: we could use event instead of a sleep to lower test latency - await sleep(500) - return updateInvoice() - })(), - ]) - - { - // first arg is the outsideLndpayResult - const [, result] = await promises - expect(result).not.toBeInstanceOf(Error) - } - + }), + ).not.toBeInstanceOf(Error) // should be idempotent (not return error when called again) - { - const result = await updateInvoice() - expect(result).not.toBeInstanceOf(Error) - } + expect( + await Wallets.updatePendingInvoiceByPaymentHash({ + paymentHash: hash as PaymentHash, + logger: baseLogger, + }), + ).not.toBeInstanceOf(Error) const ledger = LedgerService() const ledgerMetadata = TransactionsMetadataRepository() @@ -277,10 +168,7 @@ describe("UserWallet - Lightning", () => { expect(amount).toBe(sats) - pay({ lnd: lndOutside1, request: invoice }) - - // TODO: we could use an event instead of a sleep - await sleep(500) + await pay({ lnd: lndOutside1, request: invoice }) expect( await Wallets.updatePendingInvoiceByPaymentHash({ @@ -346,10 +234,7 @@ describe("UserWallet - Lightning", () => { const hash = getHash(invoice) - pay({ lnd: lndOutside1, request: invoice, tokens: sats }) - - // TODO: we could use an event instead of a sleep - await sleep(500) + await pay({ lnd: lndOutside1, request: invoice, tokens: sats }) expect( await Wallets.updatePendingInvoiceByPaymentHash({ @@ -412,10 +297,7 @@ describe("UserWallet - Lightning", () => { const hash = getHash(invoice) - pay({ lnd: lndOutside1, request: invoice, tokens: sats }) - - // TODO: we could use an event instead of a sleep - await sleep(500) + await pay({ lnd: lndOutside1, request: invoice, tokens: sats }) expect( await Wallets.updatePendingInvoiceByPaymentHash({ @@ -473,11 +355,7 @@ describe("UserWallet - Lightning", () => { const { paymentRequest: invoice } = lnInvoice const hash = getHash(invoice) - pay({ lnd: lndOutside1, request: invoice }) - - // TODO: we could use an event instead of a sleep - await sleep(500) - + await pay({ lnd: lndOutside1, request: invoice }) expect( await Wallets.updatePendingInvoiceByPaymentHash({ paymentHash: hash as PaymentHash, diff --git a/test/integration/services/mongoose/wallet-invoices.spec.ts b/test/integration/services/mongoose/wallet-invoices.spec.ts index 6550d345b6..b418eef290 100644 --- a/test/integration/services/mongoose/wallet-invoices.spec.ts +++ b/test/integration/services/mongoose/wallet-invoices.spec.ts @@ -1,18 +1,28 @@ import crypto from "crypto" +import { Wallets } from "@app" +import { toSats } from "@domain/bitcoin" import { WalletCurrency } from "@domain/shared" -import { getSecretAndPaymentHash } from "@domain/bitcoin/lightning" import { WalletInvoicesRepository } from "@services/mongoose" +import { WalletInvoice } from "@services/mongoose/schema" -import { createUserAndWalletFromUserRef } from "test/helpers" +import { + createUserAndWalletFromUserRef, + getDefaultWalletIdByTestUserRef, +} from "test/helpers" + +let walletB: WalletId beforeAll(async () => { await createUserAndWalletFromUserRef("B") + + walletB = await getDefaultWalletIdByTestUserRef("B") }) -const createTestWalletInvoice = (): WalletInvoice => { +const createTestWalletInvoice = () => { + const randomPaymentHash = crypto.randomBytes(32).toString("hex") as PaymentHash return { - ...getSecretAndPaymentHash(), + paymentHash: randomPaymentHash, selfGenerated: false, pubkey: "pubkey" as Pubkey, paid: false, @@ -24,7 +34,7 @@ const createTestWalletInvoice = (): WalletInvoice => { currency: WalletCurrency.Usd, amount: 10n, }, - } + } as WalletInvoice } describe("WalletInvoices", () => { @@ -69,4 +79,36 @@ describe("WalletInvoices", () => { expect(isDeleted).not.toBeInstanceOf(Error) expect(isDeleted).toEqual(true) }) + + it("find pending invoices by wallet id", async () => { + for (let i = 0; i < 2; i++) { + await Wallets.addInvoiceForSelf({ + walletId: walletB, + amount: toSats(1000), + }) + } + + const invoicesCount = await WalletInvoice.countDocuments({ + walletId: walletB, + paid: false, + }) + + const repo = WalletInvoicesRepository() + const invoices = repo.findPendingByWalletId(walletB) + expect(invoices).not.toBeInstanceOf(Error) + + const pendingInvoices = invoices as AsyncGenerator + + let count = 0 + for await (const invoice of pendingInvoices) { + count++ + expect(invoice).toBeDefined() + expect(invoice).toHaveProperty("paymentHash") + expect(invoice).toHaveProperty("pubkey") + expect(invoice.paid).toBe(false) + } + + expect(count).toBeGreaterThan(0) + expect(count).toBe(invoicesCount) + }) }) diff --git a/test/jest-e2e.setup.js b/test/jest-e2e.setup.js index 7a95d0cf8e..f9a47d62cc 100644 --- a/test/jest-e2e.setup.js +++ b/test/jest-e2e.setup.js @@ -29,4 +29,4 @@ afterAll(async () => { } }) -jest.setTimeout(process.env.JEST_TIMEOUT || 90000) +jest.setTimeout(process.env.JEST_TIMEOUT || 30000) diff --git a/test/unit/domain/wallet-invoices/wallet-invoice-builder.spec.ts b/test/unit/domain/wallet-invoices/wallet-invoice-builder.spec.ts index 0a1de0d128..4fc56af74a 100644 --- a/test/unit/domain/wallet-invoices/wallet-invoice-builder.spec.ts +++ b/test/unit/domain/wallet-invoices/wallet-invoice-builder.spec.ts @@ -1,5 +1,4 @@ import { toSats } from "@domain/bitcoin" -import { sha256 } from "@domain/bitcoin/lightning" import { BtcPaymentAmount, WalletCurrency } from "@domain/shared" import { WalletInvoiceBuilder } from "@domain/wallet-invoices/wallet-invoice-builder" @@ -28,7 +27,7 @@ describe("WalletInvoiceBuilder", () => { const lnInvoice = { destination: "pubkey" as Pubkey, - paymentHash: args.paymentHash, + paymentHash: "paymenthash" as PaymentHash, paymentRequest: "paymentRequest" as EncodedPaymentRequest, milliSatsAmount: (amount * 1000) as MilliSatoshis, description: args.description, @@ -48,7 +47,7 @@ describe("WalletInvoiceBuilder", () => { descriptionHash: args.descriptionHash, } - return invoice + return Promise.resolve(invoice) } const WIB = WalletInvoiceBuilder({ @@ -56,18 +55,8 @@ describe("WalletInvoiceBuilder", () => { lnRegisterInvoice: registerInvoice, }) - const checkSecretAndHash = ({ lnInvoice, walletInvoice }: LnAndWalletInvoice) => { - const { secret } = walletInvoice - const hashFromSecret = sha256(Buffer.from(secret, "hex")) - expect(hashFromSecret).toEqual(walletInvoice.paymentHash) - expect(walletInvoice.paymentHash).toEqual(lnInvoice.paymentHash) - expect(lnInvoice).not.toHaveProperty("secret") - } - const testDescription = "testdescription" - const WIBWithDescription = WIB.withDescription({ - description: testDescription, - }) + const WIBWithDescription = WIB.withDescription({ description: testDescription }) const checkDescription = ({ lnInvoice }: LnAndWalletInvoice) => { expect(lnInvoice.description).toEqual(testDescription) } @@ -108,7 +97,6 @@ describe("WalletInvoiceBuilder", () => { if (invoices instanceof Error) throw invoices - checkSecretAndHash(invoices) checkAmount(invoices) checkDescription(invoices) checkCreator(invoices) @@ -140,7 +128,6 @@ describe("WalletInvoiceBuilder", () => { if (invoices instanceof Error) throw invoices - checkSecretAndHash(invoices) checkAmount(invoices) checkDescription(invoices) checkCreator(invoices) @@ -183,7 +170,6 @@ describe("WalletInvoiceBuilder", () => { if (invoices instanceof Error) throw invoices - checkSecretAndHash(invoices) checkAmount(invoices) checkDescription(invoices) checkCreator(invoices) @@ -216,7 +202,6 @@ describe("WalletInvoiceBuilder", () => { if (invoices instanceof Error) throw invoices - checkSecretAndHash(invoices) checkAmount(invoices) checkDescription(invoices) checkCreator(invoices) diff --git a/test/unit/domain/wallet-invoices/wallet-invoice-receiver.spec.ts b/test/unit/domain/wallet-invoices/wallet-invoice-receiver.spec.ts index e76dc21ab2..2ac0bc807f 100644 --- a/test/unit/domain/wallet-invoices/wallet-invoice-receiver.spec.ts +++ b/test/unit/domain/wallet-invoices/wallet-invoice-receiver.spec.ts @@ -40,7 +40,6 @@ describe("WalletInvoiceReceiver", () => { describe("for btc invoice", () => { const btcInvoice: WalletInvoice = { paymentHash: "paymentHash" as PaymentHash, - secret: "secret" as SecretPreImage, selfGenerated: false, pubkey: "pubkey" as Pubkey, usdAmount: undefined, @@ -77,7 +76,6 @@ describe("WalletInvoiceReceiver", () => { describe("with cents amount", () => { const amountUsdInvoice: WalletInvoice = { paymentHash: "paymentHash" as PaymentHash, - secret: "secret" as SecretPreImage, recipientWalletDescriptor: recipientUsdWallet, selfGenerated: false, pubkey: "pubkey" as Pubkey, @@ -111,7 +109,6 @@ describe("WalletInvoiceReceiver", () => { describe("with no amount", () => { const noAmountUsdInvoice: WalletInvoice = { paymentHash: "paymentHash" as PaymentHash, - secret: "secret" as SecretPreImage, recipientWalletDescriptor: recipientUsdWallet, selfGenerated: false, pubkey: "pubkey" as Pubkey, diff --git a/test/unit/domain/wallet-invoices/wallet-invoice-validator.spec.ts b/test/unit/domain/wallet-invoices/wallet-invoice-validator.spec.ts index 11085fe139..65701736d3 100644 --- a/test/unit/domain/wallet-invoices/wallet-invoice-validator.spec.ts +++ b/test/unit/domain/wallet-invoices/wallet-invoice-validator.spec.ts @@ -5,7 +5,6 @@ import { WalletCurrency } from "@domain/shared" describe("WalletInvoiceValidator", () => { const walletInvoice: WalletInvoice = { paymentHash: "paymentHash" as PaymentHash, - secret: "secret" as SecretPreImage, recipientWalletDescriptor: { id: "toWalletId" as WalletId, currency: WalletCurrency.Btc,