taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit 03b8a0058c7030bd23336112320d879e080bc753
parent 50f4866cc0a435c22be3c99832a1febce6951bc1
Author: Florian Dold <florian@dold.me>
Date:   Wed, 30 Oct 2024 12:33:01 +0100

wallet-core: use generic helper to wait for events

Diffstat:
Mpackages/taler-wallet-core/src/pay-merchant.ts | 148++++++++++++++++++++++++++++++++-----------------------------------------------
Mpackages/taler-wallet-core/src/withdraw.ts | 98++++++++++++++++++++++++++++---------------------------------------------------
2 files changed, 95 insertions(+), 151 deletions(-)

diff --git a/packages/taler-wallet-core/src/pay-merchant.ts b/packages/taler-wallet-core/src/pay-merchant.ts @@ -32,7 +32,6 @@ import { Amounts, AmountString, assertUnreachable, - AsyncFlag, checkDbInvariant, CheckPayTemplateReponse, CheckPayTemplateRequest, @@ -115,6 +114,7 @@ import { import { constructTaskIdentifier, genericWaitForState, + genericWaitForStateVal, PendingTaskType, spendCoins, TaskIdentifiers, @@ -2060,108 +2060,80 @@ export async function generateDepositPermissions( return depositPermissions; } -async function internalWaitPaymentResult( - ctx: PayMerchantTransactionContext, - purchaseNotifFlag: AsyncFlag, +/** + * Wait until either: + * a) the payment succeeded (if provided under the {@param waitSessionId}), or + * b) the attempt to pay failed (merchant unavailable, etc.) + */ +async function waitPaymentResult( + wex: WalletExecutionContext, + proposalId: string, waitSessionId?: string, ): Promise<ConfirmPayResult> { - while (true) { - const txRes = await ctx.wex.db.runReadOnlyTx( - { storeNames: ["purchases", "operationRetries"] }, - async (tx) => { - const purchase = await tx.purchases.get(ctx.proposalId); - const retryRecord = await tx.operationRetries.get(ctx.taskId); - return { purchase, retryRecord }; - }, - ); + const ctx = new PayMerchantTransactionContext(wex, proposalId); + wex.taskScheduler.startShepherdTask(ctx.taskId); - if (!txRes.purchase) { - throw Error("purchase gone"); - } + return await genericWaitForStateVal<ConfirmPayResult>(wex, { + filterNotification(notif) { + return ( + notif.type === NotificationType.TransactionStateTransition && + notif.transactionId === ctx.transactionId + ); + }, + async checkState() { + const txRes = await ctx.wex.db.runReadOnlyTx( + { storeNames: ["purchases", "operationRetries"] }, + async (tx) => { + const purchase = await tx.purchases.get(ctx.proposalId); + const retryRecord = await tx.operationRetries.get(ctx.taskId); + return { purchase, retryRecord }; + }, + ); - const purchase = txRes.purchase; + if (!txRes.purchase) { + throw Error("purchase gone"); + } - logger.info( - `purchase is in state ${PurchaseStatus[purchase.purchaseStatus]}`, - ); + const purchase = txRes.purchase; - const d = await expectProposalDownload(ctx.wex, purchase); + logger.info( + `purchase is in state ${PurchaseStatus[purchase.purchaseStatus]}`, + ); - if (txRes.purchase.timestampFirstSuccessfulPay) { - if ( - waitSessionId == null || - txRes.purchase.lastSessionId === waitSessionId - ) { + const d = await expectProposalDownload(ctx.wex, purchase); + + if (txRes.purchase.timestampFirstSuccessfulPay) { + if ( + waitSessionId == null || + txRes.purchase.lastSessionId === waitSessionId + ) { + return { + type: ConfirmPayResultType.Done, + contractTerms: d.contractTermsRaw, + transactionId: ctx.transactionId, + }; + } + } + + if (txRes.retryRecord) { + return { + type: ConfirmPayResultType.Pending, + lastError: txRes.retryRecord.lastError, + transactionId: ctx.transactionId, + }; + } + + if (txRes.purchase.purchaseStatus >= PurchaseStatus.Done) { return { type: ConfirmPayResultType.Done, contractTerms: d.contractTermsRaw, transactionId: ctx.transactionId, }; } - } - if (txRes.retryRecord) { - return { - type: ConfirmPayResultType.Pending, - lastError: txRes.retryRecord.lastError, - transactionId: ctx.transactionId, - }; - } - - if (txRes.purchase.purchaseStatus >= PurchaseStatus.Done) { - return { - type: ConfirmPayResultType.Done, - contractTerms: d.contractTermsRaw, - transactionId: ctx.transactionId, - }; - } - - await purchaseNotifFlag.wait(); - purchaseNotifFlag.reset(); - } -} - -/** - * Wait until either: - * a) the payment succeeded (if provided under the {@param waitSessionId}), or - * b) the attempt to pay failed (merchant unavailable, etc.) - */ -async function waitPaymentResult( - wex: WalletExecutionContext, - proposalId: string, - waitSessionId?: string, -): Promise<ConfirmPayResult> { - // FIXME: We don't support cancelletion yet! - const ctx = new PayMerchantTransactionContext(wex, proposalId); - wex.taskScheduler.startShepherdTask(ctx.taskId); - - // FIXME: Clean up using the new JS "using" / Symbol.dispose syntax. - const purchaseNotifFlag = new AsyncFlag(); - // Raise purchaseNotifFlag whenever we get a notification - // about our purchase. - const cancelNotif = wex.ws.addNotificationListener((notif) => { - if ( - notif.type === NotificationType.TransactionStateTransition && - notif.transactionId === ctx.transactionId - ) { - purchaseNotifFlag.raise(); - } + return undefined; + }, }); - - try { - logger.info(`waiting for first payment success on ${ctx.transactionId}`); - const res = await internalWaitPaymentResult( - ctx, - purchaseNotifFlag, - waitSessionId, - ); - logger.info( - `done waiting for first payment success on ${ctx.transactionId}, result ${res.type}`, - ); - return res; - } finally { - cancelNotif(); - } } /** diff --git a/packages/taler-wallet-core/src/withdraw.ts b/packages/taler-wallet-core/src/withdraw.ts @@ -33,7 +33,6 @@ import { AmountLike, AmountString, Amounts, - AsyncFlag, BankWithdrawDetails, CancellationToken, CoinStatus, @@ -3956,7 +3955,7 @@ export async function createManualWithdrawal( } /** - * Wait until a refresh operation is final. + * Wait until a withdrawal operation is final. */ export async function waitWithdrawalFinal( wex: WalletExecutionContext, @@ -3965,68 +3964,41 @@ export async function waitWithdrawalFinal( const ctx = new WithdrawTransactionContext(wex, withdrawalGroupId); wex.taskScheduler.startShepherdTask(ctx.taskId); - // FIXME: Clean up using the new JS "using" / Symbol.dispose syntax. - const withdrawalNotifFlag = new AsyncFlag(); - // Raise purchaseNotifFlag whenever we get a notification - // about our refresh. - const cancelNotif = wex.ws.addNotificationListener((notif) => { - if ( - notif.type === NotificationType.TransactionStateTransition && - notif.transactionId === ctx.transactionId - ) { - withdrawalNotifFlag.raise(); - } - }); - const unregisterOnCancelled = wex.cancellationToken.onCancelled(() => { - cancelNotif(); - withdrawalNotifFlag.raise(); - }); - - try { - await internalWaitWithdrawalFinal(ctx, withdrawalNotifFlag); - } catch (e) { - unregisterOnCancelled(); - cancelNotif(); - } -} - -async function internalWaitWithdrawalFinal( - ctx: WithdrawTransactionContext, - flag: AsyncFlag, -): Promise<void> { - while (true) { - if (ctx.wex.cancellationToken.isCancelled) { - throw Error("cancelled"); - } - - // Check if refresh is final - const res = await ctx.wex.db.runReadOnlyTx( - { storeNames: ["withdrawalGroups"] }, - async (tx) => { - return { - wg: await tx.withdrawalGroups.get(ctx.withdrawalGroupId), - }; - }, - ); - const { wg } = res; - if (!wg) { - // Must've been deleted, we consider that final. - return; - } - switch (wg.status) { - case WithdrawalGroupStatus.AbortedBank: - case WithdrawalGroupStatus.AbortedExchange: - case WithdrawalGroupStatus.Done: - case WithdrawalGroupStatus.FailedAbortingBank: - case WithdrawalGroupStatus.FailedBankAborted: - // Transaction is final - return; - } + await genericWaitForState(wex, { + filterNotification(notif) { + return ( + notif.type === NotificationType.TransactionStateTransition && + notif.transactionId === ctx.transactionId + ); + }, + async checkState() { + // Check if withdrawal is final + const res = await ctx.wex.db.runReadOnlyTx( + { storeNames: ["withdrawalGroups"] }, + async (tx) => { + return { + wg: await tx.withdrawalGroups.get(ctx.withdrawalGroupId), + }; + }, + ); + const { wg } = res; + if (!wg) { + // Must've been deleted, we consider that final. + return true; + } + switch (wg.status) { + case WithdrawalGroupStatus.AbortedBank: + case WithdrawalGroupStatus.AbortedExchange: + case WithdrawalGroupStatus.Done: + case WithdrawalGroupStatus.FailedAbortingBank: + case WithdrawalGroupStatus.FailedBankAborted: + // Transaction is final + return true; + } - // Wait for the next transition - await flag.wait(); - flag.reset(); - } + return false; + }, + }); } export async function getWithdrawalDetailsForAmount(