diff --git a/src/_locales/en/messages.json b/src/_locales/en/messages.json index bb084cc4..bddd981f 100755 --- a/src/_locales/en/messages.json +++ b/src/_locales/en/messages.json @@ -51,6 +51,13 @@ "pay_error_notEnoughFunds": { "message": "Insufficient funds to complete the payment." }, + "pay_error_outgoingPaymentFailed": { + "message": "Payment failed." + }, + "pay_error_outgoingPaymentCompletionLimitReached": { + "message": "Payment not finished yet.", + "description": "We were polling for completion, but it's not finished yet" + }, "pay_error_invalidReceivers": { "message": "At the moment, you cannot pay this website.", "description": "We cannot send money (probable cause: un-peered wallets)" diff --git a/src/background/services/monetization.ts b/src/background/services/monetization.ts index c2c578d2..25e7e2ec 100644 --- a/src/background/services/monetization.ts +++ b/src/background/services/monetization.ts @@ -265,17 +265,38 @@ export class MonetizationService { const splitAmount = Number(amount) / payableSessions.length; // TODO: handle paying across two grants (when one grant doesn't have enough funds) - const results = await Promise.allSettled( + let results = await Promise.allSettled( payableSessions.map((session) => session.pay(splitAmount)), ); + const signal = AbortSignal.timeout(8_000); // can use other signals as well, such as popup closed etc. + results = await Promise.allSettled( + results.map((e) => { + if (e.status !== 'fulfilled') throw e.reason; + if (!e.value) return e.value; + const outgoingPaymentId = e.value.id; + return this.openPaymentsService.outgoingPaymentWaitForCompletion( + outgoingPaymentId, + { signal, maxAttempts: 10 }, + ); + }), + ); const totalSentAmount = results .filter((e) => e.status === 'fulfilled') - .reduce((acc, curr) => acc + BigInt(curr.value?.value ?? 0), 0n); + .reduce( + (acc, curr) => acc + BigInt(curr.value?.debitAmount?.value ?? 0), + 0n, + ); if (totalSentAmount === 0n) { const isNotEnoughFunds = results .filter((e) => e.status === 'rejected') .some((e) => isOutOfBalanceError(e.reason)); + // TODO: If sentAmount is zero in all outgoing payments, and + // pay_error_outgoingPaymentCompletionLimitReached, it also likely means + // we don't have enough funds. + // + // TODO: If sentAmount is non-zero but not equal to debitAmount, show + // warning that not entire payment went through (yet?) if (isNotEnoughFunds) { throw new Error(this.t('pay_error_notEnoughFunds')); } diff --git a/src/background/services/openPayments.ts b/src/background/services/openPayments.ts index 635306f9..43dca4d8 100644 --- a/src/background/services/openPayments.ts +++ b/src/background/services/openPayments.ts @@ -32,6 +32,7 @@ import { errorWithKeyToJSON, getWalletInformation, isErrorWithKey, + sleep, withResolvers, type ErrorWithKeyLike, } from '@/shared/helpers'; @@ -694,7 +695,7 @@ export class OpenPaymentsService { }, { type: 'outgoing-payment', - actions: ['create'], + actions: ['create', 'read'], identifier: walletAddress.id, limits: { debitAmount: { @@ -881,6 +882,43 @@ export class OpenPaymentsService { return outgoingPayment; } + async outgoingPaymentWaitForCompletion( + outgoingPaymentId: OutgoingPayment['id'], + { + signal, + maxAttempts = 10, + }: Partial<{ signal: AbortSignal; maxAttempts: number }> = {}, + ) { + let attempt = 0; + let outgoingPayment: undefined | OutgoingPayment; + while (++attempt <= maxAttempts) { + signal?.throwIfAborted(); + try { + outgoingPayment = await this.client!.outgoingPayment.get({ + url: outgoingPaymentId, + accessToken: this.token.value, + }); + if (outgoingPayment.failed) { + throw new ErrorWithKey('pay_error_outgoingPaymentFailed'); + } + if ( + outgoingPayment.debitAmount.value === outgoingPayment.sentAmount.value + ) { + return outgoingPayment; + } + signal?.throwIfAborted(); + await sleep(1500); + } catch (error) { + if (isTokenExpiredError(error)) { + await this.rotateToken(); + } else { + throw error; + } + } + } + throw new ErrorWithKey('pay_error_outgoingPaymentCompletionLimitReached'); + } + async probeDebitAmount( amount: AmountValue, incomingPayment: IncomingPayment['id'], diff --git a/src/background/services/paymentSession.ts b/src/background/services/paymentSession.ts index a9207185..aaaf1dcb 100644 --- a/src/background/services/paymentSession.ts +++ b/src/background/services/paymentSession.ts @@ -421,7 +421,7 @@ export class PaymentSession { } } - return outgoingPayment?.debitAmount; + return outgoingPayment; } private setAmount(amount: bigint): void {