diff --git a/core/api/dev/apollo-federation/supergraph.graphql b/core/api/dev/apollo-federation/supergraph.graphql index 5d0371e22a..227e5c1314 100644 --- a/core/api/dev/apollo-federation/supergraph.graphql +++ b/core/api/dev/apollo-federation/supergraph.graphql @@ -621,11 +621,12 @@ input IntraLedgerPaymentSendInput type IntraLedgerUpdate @join__type(graph: PUBLIC) { - amount: SatAmount! - displayCurrencyPerSat: Float! + amount: SatAmount! @deprecated(reason: "Deprecated in favor of transaction") + displayCurrencyPerSat: Float! @deprecated(reason: "Deprecated in favor of transaction") + transaction: Transaction! txNotificationType: TxNotificationType! usdPerSat: Float! @deprecated(reason: "updated over displayCurrencyPerSat") - walletId: WalletId! + walletId: WalletId! @deprecated(reason: "Deprecated in favor of transaction") } input IntraLedgerUsdPaymentSendInput @@ -917,9 +918,10 @@ scalar LnPaymentSecret type LnUpdate @join__type(graph: PUBLIC) { - paymentHash: PaymentHash! + paymentHash: PaymentHash! @deprecated(reason: "Deprecated in favor of transaction") status: InvoicePaymentStatus! - walletId: WalletId! + transaction: Transaction! + walletId: WalletId! @deprecated(reason: "Deprecated in favor of transaction") } input LnUsdInvoiceBtcDenominatedCreateOnBehalfOfRecipientInput @@ -1240,12 +1242,13 @@ scalar OnChainTxHash type OnChainUpdate @join__type(graph: PUBLIC) { - amount: SatAmount! - displayCurrencyPerSat: Float! - txHash: OnChainTxHash! + amount: SatAmount! @deprecated(reason: "Deprecated in favor of transaction") + displayCurrencyPerSat: Float! @deprecated(reason: "Deprecated in favor of transaction") + transaction: Transaction! + txHash: OnChainTxHash! @deprecated(reason: "Deprecated in favor of transaction") txNotificationType: TxNotificationType! usdPerSat: Float! @deprecated(reason: "updated over displayCurrencyPerSat") - walletId: WalletId! + walletId: WalletId! @deprecated(reason: "Deprecated in favor of transaction") } input OnChainUsdPaymentSendAsBtcDenominatedInput @@ -1321,6 +1324,7 @@ type PaymentSendPayload { errors: [Error!]! status: PaymentSendResult + transaction: Transaction } enum PaymentSendResult diff --git a/core/api/src/app/payments/index.types.d.ts b/core/api/src/app/payments/index.types.d.ts new file mode 100644 index 0000000000..1eb5ba0c2e --- /dev/null +++ b/core/api/src/app/payments/index.types.d.ts @@ -0,0 +1,4 @@ +type PaymentSendResult = { + status: PaymentSendStatus + transaction: WalletTransaction +} diff --git a/core/api/src/app/payments/send-intraledger.ts b/core/api/src/app/payments/send-intraledger.ts index 6428d39fd5..9f34b62fb9 100644 --- a/core/api/src/app/payments/send-intraledger.ts +++ b/core/api/src/app/payments/send-intraledger.ts @@ -51,7 +51,7 @@ const intraledgerPaymentSendWalletId = async ({ amount: uncheckedAmount, memo, senderWalletId: uncheckedSenderWalletId, -}: IntraLedgerPaymentSendWalletIdArgs): Promise => { +}: IntraLedgerPaymentSendWalletIdArgs): Promise => { const validatedPaymentInputs = await validateIntraledgerPaymentInputs({ uncheckedSenderWalletId, uncheckedRecipientWalletId, @@ -117,7 +117,7 @@ const intraledgerPaymentSendWalletId = async ({ "payment.finalRecipient": JSON.stringify(paymentFlow.recipientWalletDescriptor()), }) - const paymentSendStatus = await executePaymentViaIntraledger({ + const paymentSendResult = await executePaymentViaIntraledger({ paymentFlow, senderAccount, senderWallet, @@ -125,7 +125,7 @@ const intraledgerPaymentSendWalletId = async ({ recipientWallet, memo, }) - if (paymentSendStatus instanceof Error) return paymentSendStatus + if (paymentSendResult instanceof Error) return paymentSendResult if (senderAccount.id !== recipientAccount.id) { const addContactResult = await addContactsAfterSend({ @@ -137,19 +137,19 @@ const intraledgerPaymentSendWalletId = async ({ } } - return paymentSendStatus + return paymentSendResult } export const intraledgerPaymentSendWalletIdForBtcWallet = async ( args: IntraLedgerPaymentSendWalletIdArgs, -): Promise => { +): Promise => { const validated = await validateIsBtcWallet(args.senderWalletId) return validated instanceof Error ? validated : intraledgerPaymentSendWalletId(args) } export const intraledgerPaymentSendWalletIdForUsdWallet = async ( args: IntraLedgerPaymentSendWalletIdArgs, -): Promise => { +): Promise => { const validated = await validateIsUsdWallet(args.senderWalletId) return validated instanceof Error ? validated : intraledgerPaymentSendWalletId(args) } @@ -219,7 +219,7 @@ const executePaymentViaIntraledger = async < recipientAccount: Account recipientWallet: WalletDescriptor memo: string | null -}): Promise => { +}): Promise => { addAttributesToCurrentSpan({ "payment.settlement_method": SettlementMethod.IntraLedger, }) @@ -398,6 +398,9 @@ const executePaymentViaIntraledger = async < }) } - return PaymentSendStatus.Success + return { + status: PaymentSendStatus.Success, + transaction: senderWalletTransaction, + } }) } diff --git a/core/api/src/app/payments/send-lightning.ts b/core/api/src/app/payments/send-lightning.ts index cbc42cfb29..e2d3a7797b 100644 --- a/core/api/src/app/payments/send-lightning.ts +++ b/core/api/src/app/payments/send-lightning.ts @@ -67,6 +67,7 @@ import { getCurrentPriceAsDisplayPriceRatio } from "@/app/prices" import { removeDeviceTokens } from "@/app/users/remove-device-tokens" import { getTransactionForWalletByJournalId, + getTransactionsForWalletByPaymentHash, validateIsBtcWallet, validateIsUsdWallet, } from "@/app/wallets" @@ -82,7 +83,7 @@ export const payInvoiceByWalletId = async ({ memo, senderWalletId: uncheckedSenderWalletId, senderAccount, -}: PayInvoiceByWalletIdArgs): Promise => { +}: PayInvoiceByWalletIdArgs): Promise => { addAttributesToCurrentSpan({ "payment.initiation_method": PaymentInitiationMethod.Lightning, }) @@ -92,7 +93,12 @@ export const payInvoiceByWalletId = async ({ uncheckedSenderWalletId, }) if (validatedPaymentInputs instanceof AlreadyPaidError) { - return PaymentSendStatus.AlreadyPaid + const decodedInvoice = decodeInvoice(uncheckedPaymentRequest) + if (decodedInvoice instanceof Error) return decodedInvoice + return getAlreadyPaidResponse({ + walletId: uncheckedSenderWalletId, + paymentHash: decodedInvoice.paymentHash, + }) } if (validatedPaymentInputs instanceof Error) { return validatedPaymentInputs @@ -126,7 +132,7 @@ const payNoAmountInvoiceByWalletId = async ({ memo, senderWalletId: uncheckedSenderWalletId, senderAccount, -}: PayNoAmountInvoiceByWalletIdArgs): Promise => { +}: PayNoAmountInvoiceByWalletIdArgs): Promise => { addAttributesToCurrentSpan({ "payment.initiation_method": PaymentInitiationMethod.Lightning, }) @@ -138,7 +144,12 @@ const payNoAmountInvoiceByWalletId = async ({ senderAccount, }) if (validatedNoAmountPaymentInputs instanceof AlreadyPaidError) { - return PaymentSendStatus.AlreadyPaid + const decodedInvoice = decodeInvoice(uncheckedPaymentRequest) + if (decodedInvoice instanceof Error) return decodedInvoice + return getAlreadyPaidResponse({ + walletId: uncheckedSenderWalletId, + paymentHash: decodedInvoice.paymentHash, + }) } if (validatedNoAmountPaymentInputs instanceof Error) { return validatedNoAmountPaymentInputs @@ -168,14 +179,14 @@ const payNoAmountInvoiceByWalletId = async ({ export const payNoAmountInvoiceByWalletIdForBtcWallet = async ( args: PayNoAmountInvoiceByWalletIdArgs, -): Promise => { +): Promise => { const validated = await validateIsBtcWallet(args.senderWalletId) return validated instanceof Error ? validated : payNoAmountInvoiceByWalletId(args) } export const payNoAmountInvoiceByWalletIdForUsdWallet = async ( args: PayNoAmountInvoiceByWalletIdArgs, -): Promise => { +): Promise => { const validated = await validateIsUsdWallet(args.senderWalletId) return validated instanceof Error ? validated : payNoAmountInvoiceByWalletId(args) } @@ -358,7 +369,7 @@ const executePaymentViaIntraledger = async < senderWallet: WalletDescriptor senderAccount: Account memo: string | null -}): Promise => { +}): Promise => { addAttributesToCurrentSpan({ "payment.settlement_method": SettlementMethod.IntraLedger, }) @@ -407,7 +418,11 @@ const executePaymentViaIntraledger = async < const recorded = await ledgerService.isLnTxRecorded(paymentHash) if (recorded instanceof Error) return recorded - if (recorded) return PaymentSendStatus.AlreadyPaid + if (recorded) + return getAlreadyPaidResponse({ + walletId: senderWallet.id, + paymentHash, + }) const balance = await ledgerService.getWalletBalanceAmount(senderWallet) if (balance instanceof Error) return balance @@ -492,13 +507,14 @@ const executePaymentViaIntraledger = async < })) } + const senderWalletDescriptor = paymentFlow.senderWalletDescriptor() const journal = await LedgerFacade.recordIntraledger({ description: paymentFlow.descriptionFromInvoice, amount: { btc: paymentFlow.btcPaymentAmount, usd: paymentFlow.usdPaymentAmount, }, - senderWalletDescriptor: paymentFlow.senderWalletDescriptor(), + senderWalletDescriptor, recipientWalletDescriptor, metadata, additionalDebitMetadata, @@ -522,13 +538,13 @@ const executePaymentViaIntraledger = async < const recipientUser = await UsersRepository().findById(recipientUserId) if (recipientUser instanceof Error) return recipientUser - const walletTransaction = await getTransactionForWalletByJournalId({ + const recipientWalletTransaction = await getTransactionForWalletByJournalId({ walletId: recipientWalletDescriptor.id, journalId: journal.journalId, }) - if (walletTransaction instanceof Error) return walletTransaction + if (recipientWalletTransaction instanceof Error) return recipientWalletTransaction - const result = await NotificationsService().sendTransaction({ + const recipientResult = await NotificationsService().sendTransaction({ recipient: { accountId: recipientAccount.id, walletId: recipientWalletDescriptor.id, @@ -537,11 +553,42 @@ const executePaymentViaIntraledger = async < notificationSettings: recipientAccount.notificationSettings, level: recipientAccount.level, }, - transaction: walletTransaction, + transaction: recipientWalletTransaction, }) - if (result instanceof DeviceTokensNotRegisteredNotificationsServiceError) { - await removeDeviceTokens({ userId: recipientUser.id, deviceTokens: result.tokens }) + if (recipientResult instanceof DeviceTokensNotRegisteredNotificationsServiceError) { + await removeDeviceTokens({ + userId: recipientUser.id, + deviceTokens: recipientResult.tokens, + }) + } + + const senderUser = await UsersRepository().findById(senderAccount.kratosUserId) + if (senderUser instanceof Error) return senderUser + + const senderWalletTransaction = await getTransactionForWalletByJournalId({ + walletId: senderWalletDescriptor.id, + journalId: journal.journalId, + }) + if (senderWalletTransaction instanceof Error) return senderWalletTransaction + + const senderResult = await NotificationsService().sendTransaction({ + recipient: { + accountId: senderAccount.id, + walletId: senderWalletDescriptor.id, + deviceTokens: senderUser.deviceTokens, + language: senderUser.language, + notificationSettings: senderAccount.notificationSettings, + level: senderAccount.level, + }, + transaction: senderWalletTransaction, + }) + + if (senderResult instanceof DeviceTokensNotRegisteredNotificationsServiceError) { + await removeDeviceTokens({ + userId: senderUser.id, + deviceTokens: senderResult.tokens, + }) } if (senderAccount.id !== recipientAccount.id) { @@ -550,11 +597,17 @@ const executePaymentViaIntraledger = async < recipientAccount, }) if (addContactResult instanceof Error) { - recordExceptionInCurrentSpan({ error: addContactResult, level: ErrorLevel.Warn }) + recordExceptionInCurrentSpan({ + error: addContactResult, + level: ErrorLevel.Warn, + }) } } - return PaymentSendStatus.Success + return { + status: PaymentSendStatus.Success, + transaction: senderWalletTransaction, + } }) } @@ -572,7 +625,7 @@ const executePaymentViaLn = async ({ senderAccount: Account senderDisplayCurrency: DisplayCurrency memo: string | null -}): Promise => { +}): Promise => { addAttributesToCurrentSpan({ "payment.settlement_method": SettlementMethod.Lightning, }) @@ -714,7 +767,10 @@ const executePaymentViaLn = async ({ if (updateResult instanceof Error) { recordExceptionInCurrentSpan({ error: updateResult }) } - return PaymentSendStatus.Pending + return getPendingPaymentResponse({ + walletId: senderWallet.id, + paymentHash, + }) } const settled = await LedgerFacade.settlePendingLnSend(paymentHash) @@ -728,7 +784,10 @@ const executePaymentViaLn = async ({ if (voided instanceof Error) return voided if (payResult instanceof LnAlreadyPaidError) { - return PaymentSendStatus.AlreadyPaid + return getAlreadyPaidResponse({ + walletId: senderWallet.id, + paymentHash, + }) } return payResult @@ -771,6 +830,55 @@ const executePaymentViaLn = async ({ await removeDeviceTokens({ userId: senderUser.id, deviceTokens: result.tokens }) } - return PaymentSendStatus.Success + return { + status: PaymentSendStatus.Success, + transaction: walletTransaction, + } + }) +} + +const getAlreadyPaidResponse = async ({ + walletId, + paymentHash, +}: { + walletId: WalletId + paymentHash: PaymentHash +}): Promise => + getPaymentSendResponse({ + walletId, + paymentHash, + status: PaymentSendStatus.AlreadyPaid, + }) + +const getPendingPaymentResponse = async ({ + walletId, + paymentHash, +}: { + walletId: WalletId + paymentHash: PaymentHash +}): Promise => + getPaymentSendResponse({ + walletId, + paymentHash, + status: PaymentSendStatus.Pending, + }) + +const getPaymentSendResponse = async ({ + walletId, + paymentHash, + status, +}: { + walletId: WalletId + paymentHash: PaymentHash + status: PaymentSendStatus +}): Promise => { + const transactions = await getTransactionsForWalletByPaymentHash({ + walletId, + paymentHash, }) + if (transactions instanceof Error) return transactions + return { + status, + transaction: transactions.find((t) => t.settlementAmount < 0) || transactions[0], + } } diff --git a/core/api/src/app/payments/send-on-chain.ts b/core/api/src/app/payments/send-on-chain.ts index c5ac140c4c..6a48dfc0c6 100644 --- a/core/api/src/app/payments/send-on-chain.ts +++ b/core/api/src/app/payments/send-on-chain.ts @@ -67,7 +67,7 @@ const payOnChainByWalletId = async ({ speed, memo, sendAll, -}: PayOnChainByWalletIdArgs): Promise => { +}: PayOnChainByWalletIdArgs): Promise => { const latestAccountState = await AccountsRepository().findById(senderAccount.id) if (latestAccountState instanceof Error) return latestAccountState const accountValidator = AccountValidator(latestAccountState) @@ -177,6 +177,7 @@ const payOnChainByWalletId = async ({ return executePaymentViaIntraledger({ builder, + senderAccount, senderWallet, senderUsername: senderAccount.username, senderDisplayCurrency: senderAccount.displayCurrency, @@ -202,7 +203,7 @@ const payOnChainByWalletId = async ({ export const payOnChainByWalletIdForBtcWallet = async ( args: PayOnChainByWalletIdWithoutCurrencyArgs, -): Promise => { +): Promise => { const validated = await validateIsBtcWallet(args.senderWalletId) return validated instanceof Error ? validated @@ -215,7 +216,7 @@ export const payOnChainByWalletIdForBtcWallet = async ( export const payOnChainByWalletIdForUsdWallet = async ( args: PayOnChainByWalletIdWithoutCurrencyArgs, -): Promise => { +): Promise => { const validated = await validateIsUsdWallet(args.senderWalletId) return validated instanceof Error ? validated @@ -228,7 +229,7 @@ export const payOnChainByWalletIdForUsdWallet = async ( export const payOnChainByWalletIdForUsdWalletAndBtcAmount = async ( args: PayOnChainByWalletIdWithoutCurrencyArgs, -): Promise => { +): Promise => { const validated = await validateIsUsdWallet(args.senderWalletId) return validated instanceof Error ? validated @@ -241,7 +242,7 @@ export const payOnChainByWalletIdForUsdWalletAndBtcAmount = async ( export const payAllOnChainByWalletId = async ( args: PayAllOnChainByWalletIdArgs, -): Promise => +): Promise => payOnChainByWalletId({ ...args, amount: 0, amountCurrency: undefined, sendAll: true }) const executePaymentViaIntraledger = async < @@ -249,6 +250,7 @@ const executePaymentViaIntraledger = async < R extends WalletCurrency, >({ builder, + senderAccount, senderWallet, senderUsername, senderDisplayCurrency, @@ -256,12 +258,13 @@ const executePaymentViaIntraledger = async < sendAll, }: { builder: OPFBWithConversion | OPFBWithError + senderAccount: Account senderWallet: WalletDescriptor senderUsername: Username | undefined senderDisplayCurrency: DisplayCurrency memo: string | null sendAll: boolean -}): Promise => { +}): Promise => { const paymentFlow = await builder.withoutMinerFee() if (paymentFlow instanceof Error) return paymentFlow @@ -404,6 +407,7 @@ const executePaymentViaIntraledger = async < })) } + const senderWalletDescriptor = paymentFlow.senderWalletDescriptor() // Record transaction const journal = await LedgerFacade.recordIntraledger({ description: "", @@ -411,7 +415,7 @@ const executePaymentViaIntraledger = async < btc: paymentFlow.btcPaymentAmount, usd: paymentFlow.usdPaymentAmount, }, - senderWalletDescriptor: paymentFlow.senderWalletDescriptor(), + senderWalletDescriptor, recipientWalletDescriptor, metadata, additionalDebitMetadata, @@ -423,14 +427,14 @@ const executePaymentViaIntraledger = async < const recipientUser = await UsersRepository().findById(recipientUserId) if (recipientUser instanceof Error) return recipientUser - const walletTransaction = await getTransactionForWalletByJournalId({ + const recipientWalletTransaction = await getTransactionForWalletByJournalId({ walletId: recipientWallet.id, journalId: journal.journalId, }) - if (walletTransaction instanceof Error) return walletTransaction + if (recipientWalletTransaction instanceof Error) return recipientWalletTransaction // Send 'received'-side intraledger notification - const result = await NotificationsService().sendTransaction({ + const recipientResult = await NotificationsService().sendTransaction({ recipient: { accountId: recipientWallet.accountId, walletId: recipientWallet.id, @@ -439,14 +443,45 @@ const executePaymentViaIntraledger = async < notificationSettings: recipientAccount.notificationSettings, level: recipientAccount.level, }, - transaction: walletTransaction, + transaction: recipientWalletTransaction, }) - if (result instanceof DeviceTokensNotRegisteredNotificationsServiceError) { - await removeDeviceTokens({ userId: recipientUser.id, deviceTokens: result.tokens }) + if (recipientResult instanceof DeviceTokensNotRegisteredNotificationsServiceError) { + await removeDeviceTokens({ + userId: recipientUser.id, + deviceTokens: recipientResult.tokens, + }) } - return { status: PaymentSendStatus.Success, payoutId: undefined } + const senderUser = await UsersRepository().findById(senderAccount.kratosUserId) + if (senderUser instanceof Error) return senderUser + + const senderWalletTransaction = await getTransactionForWalletByJournalId({ + walletId: senderWalletDescriptor.id, + journalId: journal.journalId, + }) + if (senderWalletTransaction instanceof Error) return senderWalletTransaction + + const senderResult = await NotificationsService().sendTransaction({ + recipient: { + accountId: senderAccount.id, + walletId: senderWalletDescriptor.id, + deviceTokens: senderUser.deviceTokens, + language: senderUser.language, + notificationSettings: senderAccount.notificationSettings, + level: senderAccount.level, + }, + transaction: senderWalletTransaction, + }) + + if (senderResult instanceof DeviceTokensNotRegisteredNotificationsServiceError) { + await removeDeviceTokens({ + userId: senderUser.id, + deviceTokens: senderResult.tokens, + }) + } + + return { status: PaymentSendStatus.Success, transaction: senderWalletTransaction } }) } @@ -467,7 +502,7 @@ const executePaymentViaOnChain = async < memo: string | null sendAll: boolean logger: Logger -}): Promise => { +}): Promise => { const senderWalletDescriptor = await builder.senderWalletDescriptor() if (senderWalletDescriptor instanceof Error) return senderWalletDescriptor @@ -599,6 +634,12 @@ const executePaymentViaOnChain = async < return payoutId } - return { status: PaymentSendStatus.Success, payoutId } + const walletTransaction = await getTransactionForWalletByJournalId({ + walletId: senderWalletDescriptor.id, + journalId: journal.journalId, + }) + if (walletTransaction instanceof Error) return walletTransaction + + return { status: PaymentSendStatus.Success, transaction: walletTransaction } }) } diff --git a/core/api/src/app/wallets/index.types.d.ts b/core/api/src/app/wallets/index.types.d.ts index 71b07dd4a8..10c2f97568 100644 --- a/core/api/src/app/wallets/index.types.d.ts +++ b/core/api/src/app/wallets/index.types.d.ts @@ -117,8 +117,3 @@ type PayOnChainByWalletIdArgs = PayOnChainByWalletIdWithoutCurrencyArgs & { amountCurrency: WalletCurrency | undefined sendAll: boolean } - -type PayOnChainByWalletIdResult = { - status: PaymentSendStatus - payoutId: PayoutId | undefined -} diff --git a/core/api/src/graphql/public/root/mutation/intraledger-payment-send.ts b/core/api/src/graphql/public/root/mutation/intraledger-payment-send.ts index 2143f49fcf..0744d46af0 100644 --- a/core/api/src/graphql/public/root/mutation/intraledger-payment-send.ts +++ b/core/api/src/graphql/public/root/mutation/intraledger-payment-send.ts @@ -48,20 +48,21 @@ const IntraLedgerPaymentSendMutation = GT.Field( return { errors: [mapAndParseErrorForGqlResponse(recipientWalletIdChecked)] } } - const status = await Payments.intraledgerPaymentSendWalletIdForBtcWallet({ + const result = await Payments.intraledgerPaymentSendWalletIdForBtcWallet({ recipientWalletId, memo, amount, senderWalletId: walletId, senderAccount: domainAccount, }) - if (status instanceof Error) { - return { status: "failed", errors: [mapAndParseErrorForGqlResponse(status)] } + if (result instanceof Error) { + return { status: "failed", errors: [mapAndParseErrorForGqlResponse(result)] } } return { errors: [], - status: status.value, + status: result.status.value, + transaction: result.transaction, } }, }) diff --git a/core/api/src/graphql/public/root/mutation/intraledger-usd-payment-send.ts b/core/api/src/graphql/public/root/mutation/intraledger-usd-payment-send.ts index 65dc406e72..0aa7dd3a3d 100644 --- a/core/api/src/graphql/public/root/mutation/intraledger-usd-payment-send.ts +++ b/core/api/src/graphql/public/root/mutation/intraledger-usd-payment-send.ts @@ -48,20 +48,21 @@ const IntraLedgerUsdPaymentSendMutation = GT.Field ({ txNotificationType: { type: GT.NonNull(TxNotificationType) }, - amount: { type: GT.NonNull(SatAmount) }, - displayCurrencyPerSat: { type: GT.NonNull(GT.Float) }, + amount: { + type: GT.NonNull(SatAmount), + deprecationReason: "Deprecated in favor of transaction", + }, + displayCurrencyPerSat: { + type: GT.NonNull(GT.Float), + deprecationReason: "Deprecated in favor of transaction", + }, usdPerSat: { type: GT.NonNull(GT.Float), deprecationReason: "updated over displayCurrencyPerSat", }, - walletId: { type: GT.NonNull(WalletId) }, + walletId: { + type: GT.NonNull(WalletId), + deprecationReason: "Deprecated in favor of transaction", + }, + transaction: { type: GT.NonNull(Transaction) }, }), }) const LnUpdate = GT.Object({ name: "LnUpdate", fields: () => ({ - paymentHash: { type: GT.NonNull(PaymentHash) }, + paymentHash: { + type: GT.NonNull(PaymentHash), + deprecationReason: "Deprecated in favor of transaction", + }, status: { type: GT.NonNull(InvoicePaymentStatus) }, - walletId: { type: GT.NonNull(WalletId) }, + walletId: { + type: GT.NonNull(WalletId), + deprecationReason: "Deprecated in favor of transaction", + }, + transaction: { type: GT.NonNull(Transaction) }, }), }) @@ -53,14 +70,27 @@ const OnChainUpdate = GT.Object({ name: "OnChainUpdate", fields: () => ({ txNotificationType: { type: GT.NonNull(TxNotificationType) }, - txHash: { type: GT.NonNull(OnChainTxHash) }, - amount: { type: GT.NonNull(SatAmount) }, - displayCurrencyPerSat: { type: GT.NonNull(GT.Float) }, + txHash: { + type: GT.NonNull(OnChainTxHash), + deprecationReason: "Deprecated in favor of transaction", + }, + amount: { + type: GT.NonNull(SatAmount), + deprecationReason: "Deprecated in favor of transaction", + }, + displayCurrencyPerSat: { + type: GT.NonNull(GT.Float), + deprecationReason: "Deprecated in favor of transaction", + }, usdPerSat: { type: GT.NonNull(GT.Float), deprecationReason: "updated over displayCurrencyPerSat", }, - walletId: { type: GT.NonNull(WalletId) }, + walletId: { + type: GT.NonNull(WalletId), + deprecationReason: "Deprecated in favor of transaction", + }, + transaction: { type: GT.NonNull(Transaction) }, }), }) diff --git a/core/api/src/graphql/public/schema.graphql b/core/api/src/graphql/public/schema.graphql index 1679a58f1f..53b58225ad 100644 --- a/core/api/src/graphql/public/schema.graphql +++ b/core/api/src/graphql/public/schema.graphql @@ -453,11 +453,12 @@ input IntraLedgerPaymentSendInput { } type IntraLedgerUpdate { - amount: SatAmount! - displayCurrencyPerSat: Float! + amount: SatAmount! @deprecated(reason: "Deprecated in favor of transaction") + displayCurrencyPerSat: Float! @deprecated(reason: "Deprecated in favor of transaction") + transaction: Transaction! txNotificationType: TxNotificationType! usdPerSat: Float! @deprecated(reason: "updated over displayCurrencyPerSat") - walletId: WalletId! + walletId: WalletId! @deprecated(reason: "Deprecated in favor of transaction") } input IntraLedgerUsdPaymentSendInput { @@ -678,9 +679,10 @@ scalar LnPaymentRequest scalar LnPaymentSecret type LnUpdate { - paymentHash: PaymentHash! + paymentHash: PaymentHash! @deprecated(reason: "Deprecated in favor of transaction") status: InvoicePaymentStatus! - walletId: WalletId! + transaction: Transaction! + walletId: WalletId! @deprecated(reason: "Deprecated in favor of transaction") } input LnUsdInvoiceBtcDenominatedCreateOnBehalfOfRecipientInput { @@ -953,12 +955,13 @@ type OnChainTxFee { scalar OnChainTxHash type OnChainUpdate { - amount: SatAmount! - displayCurrencyPerSat: Float! - txHash: OnChainTxHash! + amount: SatAmount! @deprecated(reason: "Deprecated in favor of transaction") + displayCurrencyPerSat: Float! @deprecated(reason: "Deprecated in favor of transaction") + transaction: Transaction! + txHash: OnChainTxHash! @deprecated(reason: "Deprecated in favor of transaction") txNotificationType: TxNotificationType! usdPerSat: Float! @deprecated(reason: "updated over displayCurrencyPerSat") - walletId: WalletId! + walletId: WalletId! @deprecated(reason: "Deprecated in favor of transaction") } input OnChainUsdPaymentSendAsBtcDenominatedInput { @@ -1019,6 +1022,7 @@ scalar PaymentHash type PaymentSendPayload { errors: [Error!]! status: PaymentSendResult + transaction: Transaction } enum PaymentSendResult { diff --git a/core/api/src/graphql/public/types/payload/payment-send.ts b/core/api/src/graphql/public/types/payload/payment-send.ts index 92f03b4b52..7752395d0b 100644 --- a/core/api/src/graphql/public/types/payload/payment-send.ts +++ b/core/api/src/graphql/public/types/payload/payment-send.ts @@ -1,8 +1,8 @@ -import IError from "../../../shared/types/abstract/error" - import PaymentSendResult from "../scalar/payment-send-result" import { GT } from "@/graphql/index" +import IError from "@/graphql/shared/types/abstract/error" +import Transaction from "@/graphql/shared/types/object/transaction" const PaymentSendPayload = GT.Object({ name: "PaymentSendPayload", @@ -11,6 +11,7 @@ const PaymentSendPayload = GT.Object({ type: GT.NonNullList(IError), }, status: { type: PaymentSendResult }, + transaction: { type: Transaction }, }), }) diff --git a/core/api/src/services/notifications/index.ts b/core/api/src/services/notifications/index.ts index da7a5ddfe7..47c0c9eab4 100644 --- a/core/api/src/services/notifications/index.ts +++ b/core/api/src/services/notifications/index.ts @@ -66,6 +66,7 @@ export const NotificationsService = (): INotificationsService => { walletId: recipient.walletId, paymentHash, status: WalletInvoiceStatus.Paid, + transaction, }, }, }), @@ -89,6 +90,7 @@ export const NotificationsService = (): INotificationsService => { currency: intraLedgerTx.settlementCurrency, displayAmount: intraLedgerTx.settlementDisplayAmount, displayCurrency: intraLedgerTx.settlementDisplayPrice.displayCurrency, + transaction, } // TODO: remove deprecated fields @@ -126,6 +128,7 @@ export const NotificationsService = (): INotificationsService => { displayAmount: onchainTx.settlementDisplayAmount, displayCurrency: onchainTx.settlementDisplayPrice.displayCurrency, txHash: onchainTx.settlementVia.transactionHash, + transaction, } // TODO: remove deprecated fields diff --git a/core/api/src/services/notifications/index.types.d.ts b/core/api/src/services/notifications/index.types.d.ts index 5b947dbf9f..b83d411abb 100644 --- a/core/api/src/services/notifications/index.types.d.ts +++ b/core/api/src/services/notifications/index.types.d.ts @@ -9,4 +9,5 @@ type NotificationsDataObject = { sats?: Satoshis cents?: UsdCents txHash?: OnChainTxHash + transaction: WalletTransaction } 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 ac54788d94..d584de6ed7 100644 --- a/core/api/test/integration/app/wallets/send-intraledger.spec.ts +++ b/core/api/test/integration/app/wallets/send-intraledger.spec.ts @@ -294,7 +294,21 @@ describe("intraLedgerPay", () => { senderWalletId: newWalletDescriptor.id, senderAccount: newAccount, }) - expect(paymentResult).toEqual(PaymentSendStatus.Success) + expect(paymentResult).toEqual({ + status: PaymentSendStatus.Success, + transaction: expect.objectContaining({ + walletId: newWalletDescriptor.id, + status: "success", + settlementAmount: amount * -1, + settlementCurrency: "BTC", + initiationVia: expect.objectContaining({ + type: "intraledger", + }), + settlementVia: expect.objectContaining({ + type: "intraledger", + }), + }), + }) // Expect sent notification expect(sendFilteredNotification.mock.calls.length).toBe(1) 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 7d73696c0f..10e083c45a 100644 --- a/core/api/test/integration/app/wallets/send-lightning.spec.ts +++ b/core/api/test/integration/app/wallets/send-lightning.spec.ts @@ -456,7 +456,24 @@ describe("initiated via lightning", () => { senderAccount: newAccount, amount, }) - expect(paymentResult).toEqual(PaymentSendStatus.Success) + if (paymentResult instanceof Error) throw paymentResult + expect(paymentResult).toEqual({ + status: PaymentSendStatus.Success, + transaction: expect.objectContaining({ + walletId: newWalletDescriptor.id, + status: "success", + settlementAmount: (amount + paymentResult.transaction.settlementFee) * -1, + settlementCurrency: "BTC", + initiationVia: expect.objectContaining({ + type: "lightning", + paymentHash: noAmountLnInvoice.paymentHash, + pubkey: DEFAULT_PUBKEY, + }), + settlementVia: expect.objectContaining({ + type: "lightning", + }), + }), + }) // Check lnPayment collection after const lnPaymentAfter = await LnPaymentsRepository().findByPaymentHash( @@ -817,7 +834,23 @@ describe("initiated via lightning", () => { senderAccount: newAccount, amount, }) - expect(paymentResult).toEqual(PaymentSendStatus.Success) + expect(paymentResult).toEqual({ + status: PaymentSendStatus.Success, + transaction: expect.objectContaining({ + walletId: newWalletDescriptor.id, + status: "success", + settlementAmount: amount * -1, + settlementCurrency: "BTC", + initiationVia: expect.objectContaining({ + type: "lightning", + paymentHash: noAmountLnInvoice.paymentHash, + pubkey: noAmountLnInvoice.destination, + }), + settlementVia: expect.objectContaining({ + type: "intraledger", + }), + }), + }) // Expect sent notification expect(sendFilteredNotification.mock.calls.length).toBe(1) 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 b9429cf325..412df8b4aa 100644 --- a/core/api/test/integration/app/wallets/send-onchain.spec.ts +++ b/core/api/test/integration/app/wallets/send-onchain.spec.ts @@ -596,12 +596,26 @@ describe("onChainPay", () => { senderAccount: newAccount, amount, address: recipientWalletIdAddress, - speed: PayoutSpeed.Fast, memo, }) if (paymentResult instanceof Error) throw paymentResult - expect(paymentResult.status).toEqual(PaymentSendStatus.Success) + 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", + }), + }), + }) // Expect sent notification expect(sendFilteredNotification.mock.calls.length).toBe(1) 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 38c917f8f1..45d839efda 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 @@ -352,7 +352,11 @@ describe("Display properties on transactions", () => { memo, }) if (invoice instanceof Error) throw invoice - const { paymentRequest: uncheckedPaymentRequest, paymentHash } = invoice.lnInvoice + const { + paymentRequest: uncheckedPaymentRequest, + paymentHash, + destination, + } = invoice.lnInvoice const paymentResult = await Payments.payNoAmountInvoiceByWalletIdForBtcWallet({ uncheckedPaymentRequest, @@ -362,7 +366,23 @@ describe("Display properties on transactions", () => { amount: amountInvoice, }) if (paymentResult instanceof Error) throw paymentResult - expect(paymentResult).toBe(PaymentSendStatus.Success) + 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) @@ -437,7 +457,22 @@ describe("Display properties on transactions", () => { senderAccount: accountB, }) if (paymentResult instanceof Error) throw paymentResult - expect(paymentResult).toBe(PaymentSendStatus.Success) + 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) @@ -503,7 +538,23 @@ describe("Display properties on transactions", () => { senderAccount: accountB, }) if (paymentResult instanceof Error) throw paymentResult - expect(paymentResult).toBe(PaymentSendStatus.Success) + 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) @@ -848,7 +899,7 @@ describe("Display properties on transactions", () => { }) if (address instanceof Error) throw address - const paid = await Payments.payOnChainByWalletIdForBtcWallet({ + const paymentResult = await Payments.payOnChainByWalletIdForBtcWallet({ senderAccount, senderWalletId, address, @@ -856,8 +907,23 @@ describe("Display properties on transactions", () => { speed: PayoutSpeed.Fast, memo, }) - if (paid instanceof Error) throw paid - expect(paid.status).toBe(PaymentSendStatus.Success) + 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) @@ -921,7 +987,7 @@ describe("Display properties on transactions", () => { }) if (address instanceof Error) throw address - const paid = await Payments.payOnChainByWalletIdForBtcWallet({ + const paymentResult = await Payments.payOnChainByWalletIdForBtcWallet({ senderAccount, senderWalletId, address, @@ -929,8 +995,23 @@ describe("Display properties on transactions", () => { speed: PayoutSpeed.Fast, memo, }) - if (paid instanceof Error) throw paid - expect(paid.status).toBe(PaymentSendStatus.Success) + 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) @@ -1013,7 +1094,7 @@ describe("Display properties on transactions", () => { lnd: lndOutside1, }) - const paid = await Payments.payOnChainByWalletIdForBtcWallet({ + const paymentResult = await Payments.payOnChainByWalletIdForBtcWallet({ senderAccount, senderWalletId, address, @@ -1021,8 +1102,25 @@ describe("Display properties on transactions", () => { speed: PayoutSpeed.Fast, memo, }) - if (paid instanceof Error) throw paid - expect(paid.status).toBe(PaymentSendStatus.Success) + 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) @@ -1081,14 +1179,28 @@ describe("Display properties on transactions", () => { // Send payment const memo = "invoiceMemo #" + (Math.random() * 1_000_000).toFixed() - const paid = await Payments.intraledgerPaymentSendWalletIdForBtcWallet({ + const paymentResult = await Payments.intraledgerPaymentSendWalletIdForBtcWallet({ senderWalletId, senderAccount, memo, recipientWalletId, amount: amountSats, }) - expect(paid).toBe(PaymentSendStatus.Success) + 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) @@ -1131,7 +1243,7 @@ describe("Display properties on transactions", () => { // TxMetadata: // - WalletIdTradeIntraAccountLedgerMetadata - const amountSats = 20_000 + const amountSats = 10_000 const senderWalletId = walletIdB const senderAccount = accountB @@ -1140,14 +1252,28 @@ describe("Display properties on transactions", () => { // Send payment const memo = "invoiceMemo #" + (Math.random() * 1_000_000).toFixed() - const paid = await Payments.intraledgerPaymentSendWalletIdForBtcWallet({ + const paymentResult = await Payments.intraledgerPaymentSendWalletIdForBtcWallet({ senderWalletId, senderAccount, memo, recipientWalletId, amount: amountSats, }) - expect(paid).toBe(PaymentSendStatus.Success) + 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) diff --git a/core/api/test/legacy-integration/app/cron/delete-payments.spec.ts b/core/api/test/legacy-integration/app/cron/delete-payments.spec.ts index 6a2f4df51b..dc92ec8738 100644 --- a/core/api/test/legacy-integration/app/cron/delete-payments.spec.ts +++ b/core/api/test/legacy-integration/app/cron/delete-payments.spec.ts @@ -57,16 +57,32 @@ describe("Delete payments from Lnd - Lightning Pay", () => { const { request, secret, id } = await createInvoice({ lnd: lndOutside1 }) const paymentHash = id as PaymentHash const revealedPreImage = secret as RevealedPreImage + const amount = toSats(1000) const paymentResult = await Payments.payNoAmountInvoiceByWalletIdForBtcWallet({ uncheckedPaymentRequest: request, memo: null, - amount: toSats(1000), + amount, senderWalletId: walletIdB, senderAccount: accountB, }) if (paymentResult instanceof Error) throw paymentResult - expect(paymentResult).toBe(PaymentSendStatus.Success) + expect(paymentResult).toEqual({ + status: PaymentSendStatus.Success, + transaction: expect.objectContaining({ + walletId: walletIdB, + status: "success", + settlementAmount: (amount + paymentResult.transaction.settlementFee) * -1, + settlementCurrency: "BTC", + initiationVia: expect.objectContaining({ + type: "lightning", + paymentHash, + }), + settlementVia: expect.objectContaining({ + type: "lightning", + }), + }), + }) const lndService = LndService() if (lndService instanceof Error) return lndService diff --git a/dev/config/apollo-federation/supergraph.graphql b/dev/config/apollo-federation/supergraph.graphql index 5d0371e22a..227e5c1314 100644 --- a/dev/config/apollo-federation/supergraph.graphql +++ b/dev/config/apollo-federation/supergraph.graphql @@ -621,11 +621,12 @@ input IntraLedgerPaymentSendInput type IntraLedgerUpdate @join__type(graph: PUBLIC) { - amount: SatAmount! - displayCurrencyPerSat: Float! + amount: SatAmount! @deprecated(reason: "Deprecated in favor of transaction") + displayCurrencyPerSat: Float! @deprecated(reason: "Deprecated in favor of transaction") + transaction: Transaction! txNotificationType: TxNotificationType! usdPerSat: Float! @deprecated(reason: "updated over displayCurrencyPerSat") - walletId: WalletId! + walletId: WalletId! @deprecated(reason: "Deprecated in favor of transaction") } input IntraLedgerUsdPaymentSendInput @@ -917,9 +918,10 @@ scalar LnPaymentSecret type LnUpdate @join__type(graph: PUBLIC) { - paymentHash: PaymentHash! + paymentHash: PaymentHash! @deprecated(reason: "Deprecated in favor of transaction") status: InvoicePaymentStatus! - walletId: WalletId! + transaction: Transaction! + walletId: WalletId! @deprecated(reason: "Deprecated in favor of transaction") } input LnUsdInvoiceBtcDenominatedCreateOnBehalfOfRecipientInput @@ -1240,12 +1242,13 @@ scalar OnChainTxHash type OnChainUpdate @join__type(graph: PUBLIC) { - amount: SatAmount! - displayCurrencyPerSat: Float! - txHash: OnChainTxHash! + amount: SatAmount! @deprecated(reason: "Deprecated in favor of transaction") + displayCurrencyPerSat: Float! @deprecated(reason: "Deprecated in favor of transaction") + transaction: Transaction! + txHash: OnChainTxHash! @deprecated(reason: "Deprecated in favor of transaction") txNotificationType: TxNotificationType! usdPerSat: Float! @deprecated(reason: "updated over displayCurrencyPerSat") - walletId: WalletId! + walletId: WalletId! @deprecated(reason: "Deprecated in favor of transaction") } input OnChainUsdPaymentSendAsBtcDenominatedInput @@ -1321,6 +1324,7 @@ type PaymentSendPayload { errors: [Error!]! status: PaymentSendResult + transaction: Transaction } enum PaymentSendResult