From 5695ae0a9f469ddbcd86e675f8f74b30032be457 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Sat, 1 Jul 2023 01:43:29 +0200 Subject: wallet-core: use testingWaitTransactionsFinal to wait for transactions --- packages/taler-harness/src/harness/harness.ts | 32 +++++++++- packages/taler-harness/src/harness/helpers.ts | 70 ++++++++++++++++++++++ .../src/integrationtests/test-payment.ts | 29 ++++----- .../taler-wallet-core/src/operations/refresh.ts | 28 ++++++--- .../taler-wallet-core/src/operations/testing.ts | 14 +++-- packages/taler-wallet-core/src/wallet-api-types.ts | 11 ++++ packages/taler-wallet-core/src/wallet.ts | 3 + 7 files changed, 157 insertions(+), 30 deletions(-) (limited to 'packages') diff --git a/packages/taler-harness/src/harness/harness.ts b/packages/taler-harness/src/harness/harness.ts index 7b2f980cc..1120eae84 100644 --- a/packages/taler-harness/src/harness/harness.ts +++ b/packages/taler-harness/src/harness/harness.ts @@ -41,17 +41,21 @@ import { Logger, MerchantReserveCreateConfirmation, MerchantTemplateAddDetails, + NotificationType, parsePaytoUri, stringToBytes, TalerError, TalerProtocolDuration, + TransactionMajorState, WalletNotification, } from "@gnu-taler/taler-util"; import { BankApi, BankServiceHandle, HarnessExchangeBankAccount, + OpenedPromise, openPromise, + WalletApiOperation, WalletCoreApiClient, WalletCoreRequestType, WalletCoreResponseType, @@ -934,7 +938,12 @@ export class FakebankService ); await this.pingUntilAvailable(); for (const acc of this.accounts) { - await BankApi.registerAccount(this, acc.accountName, acc.accountPassword, {}); + await BankApi.registerAccount( + this, + acc.accountName, + acc.accountPassword, + {}, + ); } } @@ -2246,9 +2255,26 @@ export interface WalletClientArgs { onNotification?(n: WalletNotification): void; } +export type CancelFn = () => void; +export type NotificationHandler = (n: WalletNotification) => void; + +/** + * Convenience wrapper around a (remote) wallet handle. + */ export class WalletClient { remoteWallet: RemoteWallet | undefined = undefined; private waiter: WalletNotificationWaiter = makeNotificationWaiter(); + notificationHandlers: NotificationHandler[] = []; + + addNotificationListener(f: NotificationHandler): CancelFn { + this.notificationHandlers.push(f); + return () => { + const idx = this.notificationHandlers.indexOf(f); + if (idx >= 0) { + this.notificationHandlers.splice(idx, 1); + } + }; + } async call( operation: Op, @@ -2260,6 +2286,7 @@ export class WalletClient { const client = getClientFromRemoteWallet(this.remoteWallet); return client.call(operation, payload); } + constructor(private args: WalletClientArgs) {} async connect(): Promise { @@ -2272,6 +2299,9 @@ export class WalletClient { walletClient.args.onNotification(n); } waiter.notify(n); + for (const h of walletClient.notificationHandlers) { + h(n); + } }, }); this.remoteWallet = w; diff --git a/packages/taler-harness/src/harness/helpers.ts b/packages/taler-harness/src/harness/helpers.ts index fd6e9aa2e..8c62aef37 100644 --- a/packages/taler-harness/src/harness/helpers.ts +++ b/packages/taler-harness/src/harness/helpers.ts @@ -689,3 +689,73 @@ export async function makeTestPayment( t.assertTrue(orderStatus.order_status === "paid"); } + +/** + * Make a simple payment and check that it succeeded. + */ +export async function makeTestPaymentV2( + t: GlobalTestState, + args: { + merchant: MerchantServiceInterface; + walletClient: WalletClient; + order: Partial; + instance?: string; + }, + auth: WithAuthorization = {}, +): Promise { + // Set up order. + + const { walletClient, merchant } = args; + const instance = args.instance ?? "default"; + + const orderResp = await MerchantPrivateApi.createOrder( + merchant, + instance, + { + order: args.order, + }, + auth, + ); + + let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus( + merchant, + { + orderId: orderResp.order_id, + }, + auth, + ); + + t.assertTrue(orderStatus.order_status === "unpaid"); + + // Make wallet pay for the order + + const preparePayResult = await walletClient.call( + WalletApiOperation.PreparePayForUri, + { + talerPayUri: orderStatus.taler_pay_uri, + }, + ); + + t.assertTrue( + preparePayResult.status === PreparePayResultType.PaymentPossible, + ); + + const r2 = await walletClient.call(WalletApiOperation.ConfirmPay, { + proposalId: preparePayResult.proposalId, + }); + + t.assertTrue(r2.type === ConfirmPayResultType.Done); + + // Check if payment was successful. + + orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus( + merchant, + { + orderId: orderResp.order_id, + instance, + }, + auth, + ); + + t.assertTrue(orderStatus.order_status === "paid"); +} diff --git a/packages/taler-harness/src/integrationtests/test-payment.ts b/packages/taler-harness/src/integrationtests/test-payment.ts index f184e57e7..9d1ce0e22 100644 --- a/packages/taler-harness/src/integrationtests/test-payment.ts +++ b/packages/taler-harness/src/integrationtests/test-payment.ts @@ -20,9 +20,9 @@ import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { GlobalTestState } from "../harness/harness.js"; import { - createSimpleTestkudosEnvironment, - withdrawViaBank, - makeTestPayment, + createSimpleTestkudosEnvironmentV2, + withdrawViaBankV2, + makeTestPaymentV2, } from "../harness/helpers.js"; import { j2s } from "@gnu-taler/taler-util"; @@ -32,12 +32,14 @@ import { j2s } from "@gnu-taler/taler-util"; export async function runPaymentTest(t: GlobalTestState) { // Set up test environment - const { wallet, bank, exchange, merchant } = - await createSimpleTestkudosEnvironment(t); + const { walletClient, bank, exchange, merchant } = + await createSimpleTestkudosEnvironmentV2(t); // Withdraw digital cash into the wallet. - await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" }); + await withdrawViaBankV2(t, { walletClient, bank, exchange, amount: "TESTKUDOS:20" }); + + await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {}); const order = { summary: "Buy me!", @@ -45,8 +47,8 @@ export async function runPaymentTest(t: GlobalTestState) { fulfillment_url: "taler://fulfillment-success/thx", }; - await makeTestPayment(t, { wallet, merchant, order }); - await wallet.runUntilDone(); + await makeTestPaymentV2(t, { walletClient, merchant, order }); + await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {}); // Test JSON normalization of contract terms: Does the wallet // agree with the merchant? @@ -56,8 +58,8 @@ export async function runPaymentTest(t: GlobalTestState) { fulfillment_url: "taler://fulfillment-success/thx", }; - await makeTestPayment(t, { wallet, merchant, order: order2 }); - await wallet.runUntilDone(); + await makeTestPaymentV2(t, { walletClient, merchant, order: order2 }); + await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {}); // Test JSON normalization of contract terms: Does the wallet // agree with the merchant? @@ -67,11 +69,10 @@ export async function runPaymentTest(t: GlobalTestState) { fulfillment_url: "taler://fulfillment-success/thx", }; - await makeTestPayment(t, { wallet, merchant, order: order3 }); - - await wallet.runUntilDone(); + await makeTestPaymentV2(t, { walletClient, merchant, order: order3 }); + await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {}); - const bal = await wallet.client.call(WalletApiOperation.GetBalances, {}); + const bal = await walletClient.call(WalletApiOperation.GetBalances, {}); console.log(`balance after 3 payments: ${j2s(bal)}`); t.assertAmountEquals(bal.balances[0].available, "TESTKUDOS:3.8"); t.assertAmountEquals(bal.balances[0].pendingIncoming, "TESTKUDOS:0"); diff --git a/packages/taler-wallet-core/src/operations/refresh.ts b/packages/taler-wallet-core/src/operations/refresh.ts index fd6281eda..6eb221c1c 100644 --- a/packages/taler-wallet-core/src/operations/refresh.ts +++ b/packages/taler-wallet-core/src/operations/refresh.ts @@ -263,22 +263,25 @@ async function refreshCreateSession( availableAmount, )} too small`, ); - // FIXME: State transition notification missing. - await ws.db + const transitionInfo = await ws.db .mktx((x) => [x.coins, x.coinAvailability, x.refreshGroups]) .runReadWrite(async (tx) => { const rg = await tx.refreshGroups.get(refreshGroupId); if (!rg) { return; } + const oldTxState = computeRefreshTransactionState(rg); rg.statusPerCoin[coinIndex] = RefreshCoinStatus.Finished; const updateRes = updateGroupStatus(rg); if (updateRes.final) { await makeCoinsVisible(ws, tx, transactionId); } await tx.refreshGroups.put(rg); + const newTxState = computeRefreshTransactionState(rg); + return { oldTxState, newTxState }; }); ws.notify({ type: NotificationType.BalanceChange }); + notifyTransition(ws, transactionId, transitionInfo); return; } @@ -438,7 +441,7 @@ async function refreshMelt( if (resp.status === HttpStatusCode.NotFound) { const errDetails = await readUnexpectedResponseDetails(resp); - await ws.db + const transitionInfo = await ws.db .mktx((x) => [x.refreshGroups, x.coins, x.coinAvailability]) .runReadWrite(async (tx) => { const rg = await tx.refreshGroups.get(refreshGroupId); @@ -451,6 +454,7 @@ async function refreshMelt( if (rg.statusPerCoin[coinIndex] !== RefreshCoinStatus.Pending) { return; } + const oldTxState = computeRefreshTransactionState(rg); rg.statusPerCoin[coinIndex] = RefreshCoinStatus.Failed; rg.lastErrorPerCoin[coinIndex] = errDetails; const updateRes = updateGroupStatus(rg); @@ -458,8 +462,14 @@ async function refreshMelt( await makeCoinsVisible(ws, tx, transactionId); } await tx.refreshGroups.put(rg); + const newTxState = computeRefreshTransactionState(rg); + return { + oldTxState, + newTxState, + }; }); ws.notify({ type: NotificationType.BalanceChange }); + notifyTransition(ws, transactionId, transitionInfo); return; } @@ -739,7 +749,7 @@ async function refreshReveal( } } - await ws.db + const transitionInfo = await ws.db .mktx((x) => [ x.coins, x.denominations, @@ -756,6 +766,7 @@ async function refreshReveal( if (!rs) { return; } + const oldTxState = computeRefreshTransactionState(rg); rg.statusPerCoin[coinIndex] = RefreshCoinStatus.Finished; updateGroupStatus(rg); for (const coin of coins) { @@ -763,7 +774,10 @@ async function refreshReveal( } await makeCoinsVisible(ws, tx, transactionId); await tx.refreshGroups.put(rg); + const newTxState = computeRefreshTransactionState(rg); + return { oldTxState, newTxState }; }); + notifyTransition(ws, transactionId, transitionInfo); logger.trace("refresh finished (end of reveal)"); } @@ -778,7 +792,7 @@ export async function processRefreshGroup( .mktx((x) => [x.refreshGroups]) .runReadOnly(async (tx) => tx.refreshGroups.get(refreshGroupId)); if (!refreshGroup) { - return TaskRunResult.finished() + return TaskRunResult.finished(); } if (refreshGroup.timestampFinished) { return TaskRunResult.finished(); @@ -1235,10 +1249,6 @@ export async function suspendRefreshGroup( tag: TransactionType.Refresh, refreshGroupId, }); - const retryTag = constructTaskIdentifier({ - tag: PendingTaskType.Refresh, - refreshGroupId, - }); let res = await ws.db .mktx((x) => [x.refreshGroups]) .runReadWrite(async (tx) => { diff --git a/packages/taler-wallet-core/src/operations/testing.ts b/packages/taler-wallet-core/src/operations/testing.ts index 8c84702b8..ea373e914 100644 --- a/packages/taler-wallet-core/src/operations/testing.ts +++ b/packages/taler-wallet-core/src/operations/testing.ts @@ -450,7 +450,7 @@ export async function runIntegrationTest( logger.trace("integration test: all done!"); } -async function waitUntilDone(ws: InternalWalletState): Promise { +export async function waitUntilDone(ws: InternalWalletState): Promise { logger.info("waiting until all transactions are in a final state"); ws.ensureTaskLoopRunning(); let p: OpenedPromise | undefined = undefined; @@ -459,11 +459,13 @@ async function waitUntilDone(ws: InternalWalletState): Promise { return; } if (notif.type === NotificationType.TransactionStateTransition) { - p.resolve(); - } - // Work-around, refresh transactions don't properly emit transition notifications yet. - if (notif.type === NotificationType.PendingOperationProcessed) { - p.resolve(); + switch (notif.newTxState.major) { + case TransactionMajorState.Pending: + case TransactionMajorState.Aborting: + break; + default: + p.resolve(); + } } }); while (1) { diff --git a/packages/taler-wallet-core/src/wallet-api-types.ts b/packages/taler-wallet-core/src/wallet-api-types.ts index 6bcee0299..cea548db6 100644 --- a/packages/taler-wallet-core/src/wallet-api-types.ts +++ b/packages/taler-wallet-core/src/wallet-api-types.ts @@ -206,6 +206,7 @@ export enum WalletApiOperation { Recycle = "recycle", ApplyDevExperiment = "applyDevExperiment", ValidateIban = "validateIban", + TestingWaitTransactionsFinal = "testingWaitTransactionsFinal", } // group: Initialization @@ -949,6 +950,15 @@ export type DumpCoinsOp = { response: CoinDumpJson; }; +/** + * Wait until all transactions are in a final state. + */ +export type TestingWaitTransactionsFinal = { + op: WalletApiOperation.TestingWaitTransactionsFinal; + request: EmptyObject; + response: EmptyObject; +}; + /** * Set a coin as (un-)suspended. * Suspended coins won't be used for payments. @@ -1051,6 +1061,7 @@ export type WalletOperations = { [WalletApiOperation.Recycle]: RecycleOp; [WalletApiOperation.ApplyDevExperiment]: ApplyDevExperimentOp; [WalletApiOperation.ValidateIban]: ValidateIbanOp; + [WalletApiOperation.TestingWaitTransactionsFinal]: TestingWaitTransactionsFinal; }; export type WalletCoreRequestType< diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts index 8f11a3d28..11030af2b 100644 --- a/packages/taler-wallet-core/src/wallet.ts +++ b/packages/taler-wallet-core/src/wallet.ts @@ -242,6 +242,7 @@ import { runIntegrationTest, runIntegrationTest2, testPay, + waitUntilDone, withdrawTestBalance, } from "./operations/testing.js"; import { @@ -1550,6 +1551,8 @@ async function dispatchRequestInternal( case WalletApiOperation.GetVersion: { return getVersion(ws); } + case WalletApiOperation.TestingWaitTransactionsFinal: + return await waitUntilDone(ws); // default: // assertUnreachable(operation); } -- cgit v1.2.3