diff --git a/packages/taler-harness/src/harness/harness.ts b/packages/taler-harness/src/harness/harness.ts index 65a19959a..c16200933 100644 --- a/packages/taler-harness/src/harness/harness.ts +++ b/packages/taler-harness/src/harness/harness.ts @@ -654,6 +654,9 @@ export class FakebankService return this.baseUrl; } + // FIXME: Why do we have this function at all? + // We now have a unified corebank API, we should just use that + // to create bank accounts, also for the exchange. async createExchangeAccount( accountName: string, password: string, diff --git a/packages/taler-harness/src/integrationtests/test-timetravel-autorefresh.ts b/packages/taler-harness/src/integrationtests/test-timetravel-autorefresh.ts index 449142809..def2462e0 100644 --- a/packages/taler-harness/src/integrationtests/test-timetravel-autorefresh.ts +++ b/packages/taler-harness/src/integrationtests/test-timetravel-autorefresh.ts @@ -136,20 +136,22 @@ export async function runTimetravelAutorefreshTest(t: GlobalTestState) { }, ); - await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {}); - let p: PendingOperationsResponse; p = await walletClient.call(WalletApiOperation.GetPendingOperations, {}); console.log("pending operations after first time travel"); console.log(JSON.stringify(p, undefined, 2)); - await withdrawViaBankV2(t, { + await walletClient.call(WalletApiOperation.TestingWaitTasksProcessed, {}); + await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {}); + + const wres2 = await withdrawViaBankV2(t, { walletClient, bank, exchange, amount: "TESTKUDOS:20", }); + await wres2.withdrawalFinishedCond; await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {}); @@ -165,12 +167,13 @@ export async function runTimetravelAutorefreshTest(t: GlobalTestState) { }, ); + await walletClient.call(WalletApiOperation.TestingWaitTasksProcessed, {}); + await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {}); + // At this point, the original coins should've been refreshed. // It would be too late to refresh them now, as we're past // the two year deposit expiration. - await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {}); - const orderResp = await merchantClient.createOrder({ order: { fulfillment_url: "http://example.com", @@ -195,7 +198,7 @@ export async function runTimetravelAutorefreshTest(t: GlobalTestState) { t.assertTrue(r.status === PreparePayResultType.PaymentPossible); const cpr = await walletClient.call(WalletApiOperation.ConfirmPay, { - proposalId: r.proposalId, + transactionId: r.transactionId, }); t.assertTrue(cpr.type === ConfirmPayResultType.Done); diff --git a/packages/taler-harness/src/integrationtests/test-withdrawal-fakebank.ts b/packages/taler-harness/src/integrationtests/test-withdrawal-fakebank.ts index e3057451e..e26d9f964 100644 --- a/packages/taler-harness/src/integrationtests/test-withdrawal-fakebank.ts +++ b/packages/taler-harness/src/integrationtests/test-withdrawal-fakebank.ts @@ -55,12 +55,14 @@ export async function runWithdrawalFakebankTest(t: GlobalTestState) { accountName: "exchange", accountPassword: "x", wireGatewayApiBaseUrl: new URL( - "/accounts/exchange/taler-wire-gateway", + "/accounts/exchange/taler-wire-gateway/", bank.baseUrl, ).href, accountPaytoUri: "payto://x-taler-bank/localhost/exchange", }); + await bank.createExchangeAccount("exchange", "x"); + await bank.start(); await bank.pingUntilAvailable(); @@ -93,8 +95,6 @@ export async function runWithdrawalFakebankTest(t: GlobalTestState) { const balResp = await wallet.client.call(WalletApiOperation.GetBalances, {}); t.assertAmountEquals("TESTKUDOS:9.72", balResp.balances[0].available); - - await t.shutdown(); } runWithdrawalFakebankTest.suites = ["wallet"]; diff --git a/packages/taler-util/src/taler-types.ts b/packages/taler-util/src/taler-types.ts index 551b0652f..4b3a426f5 100644 --- a/packages/taler-util/src/taler-types.ts +++ b/packages/taler-util/src/taler-types.ts @@ -1585,7 +1585,7 @@ export const codecForWithdrawResponse = (): Codec => .property("ev_sig", codecForBlindedDenominationSignature()) .build("WithdrawResponse"); -export const codecForWithdrawBatchResponse = +export const codecForExchangeWithdrawBatchResponse = (): Codec => buildCodecForObject() .property("ev_sigs", codecForList(codecForWithdrawResponse())) diff --git a/packages/taler-wallet-core/src/dbless.ts b/packages/taler-wallet-core/src/dbless.ts index 4fc890788..b6b80009f 100644 --- a/packages/taler-wallet-core/src/dbless.ts +++ b/packages/taler-wallet-core/src/dbless.ts @@ -49,6 +49,9 @@ import { Logger, parsePaytoUri, UnblindedSignature, + ExchangeBatchWithdrawRequest, + ExchangeWithdrawBatchResponse, + codecForExchangeWithdrawBatchResponse, } from "@gnu-taler/taler-util"; import { HttpRequestLibrary, @@ -165,25 +168,29 @@ export async function withdrawCoin(args: { value: Amounts.parseOrThrow(denom.value), }); - const reqBody: ExchangeWithdrawRequest = { - denom_pub_hash: planchet.denomPubHash, - reserve_sig: planchet.withdrawSig, - coin_ev: planchet.coinEv, + const reqBody: ExchangeBatchWithdrawRequest = { + planchets: [ + { + denom_pub_hash: planchet.denomPubHash, + reserve_sig: planchet.withdrawSig, + coin_ev: planchet.coinEv, + }, + ], }; const reqUrl = new URL( - `reserves/${planchet.reservePub}/withdraw`, + `reserves/${planchet.reservePub}/batch-withdraw`, exchangeBaseUrl, ).href; - const resp = await http.postJson(reqUrl, reqBody); - const r = await readSuccessResponseJsonOrThrow( + const resp = await http.fetch(reqUrl, { method: "POST", body: reqBody }); + const rBatch = await readSuccessResponseJsonOrThrow( resp, - codecForWithdrawResponse(), + codecForExchangeWithdrawBatchResponse(), ); const ubSig = await cryptoApi.unblindDenominationSignature({ planchet, - evSig: r.ev_sig, + evSig: rBatch.ev_sigs[0].ev_sig, }); return { diff --git a/packages/taler-wallet-core/src/operations/pending.ts b/packages/taler-wallet-core/src/operations/pending.ts index 1819aa1b8..7590280bc 100644 --- a/packages/taler-wallet-core/src/operations/pending.ts +++ b/packages/taler-wallet-core/src/operations/pending.ts @@ -101,14 +101,14 @@ async function gatherExchangePending( case ExchangeEntryDbUpdateStatus.Failed: return; } - const opTag = TaskIdentifiers.forExchangeUpdate(exch); - let opr = await tx.operationRetries.get(opTag); + const opUpdateExchangeTag = TaskIdentifiers.forExchangeUpdate(exch); + let opr = await tx.operationRetries.get(opUpdateExchangeTag); const timestampDue = opr?.retryInfo.nextRetry ?? exch.nextRefreshCheckStamp; resp.pendingOperations.push({ type: PendingTaskType.ExchangeUpdate, ...getPendingCommon( ws, - opTag, + opUpdateExchangeTag, AbsoluteTime.fromPreciseTimestamp(timestampPreciseFromDb(timestampDue)), ), givesLifeness: false, @@ -119,11 +119,12 @@ async function gatherExchangePending( // We only schedule a check for auto-refresh if the exchange update // was successful. if (!opr?.lastError) { + const opCheckRefreshTag = TaskIdentifiers.forExchangeCheckRefresh(exch); resp.pendingOperations.push({ type: PendingTaskType.ExchangeCheckRefresh, ...getPendingCommon( ws, - opTag, + opCheckRefreshTag, AbsoluteTime.fromPreciseTimestamp( timestampPreciseFromDb(timestampDue), ), diff --git a/packages/taler-wallet-core/src/operations/testing.ts b/packages/taler-wallet-core/src/operations/testing.ts index 607d03470..f5bed13dd 100644 --- a/packages/taler-wallet-core/src/operations/testing.ts +++ b/packages/taler-wallet-core/src/operations/testing.ts @@ -74,6 +74,7 @@ import { import { initiatePeerPushDebit } from "./pay-peer-push-debit.js"; import { OpenedPromise, openPromise } from "../index.js"; import { getTransactionById, getTransactions } from "./transactions.js"; +import { getPendingOperations } from "./pending.js"; const logger = new Logger("operations/testing.ts"); @@ -290,7 +291,7 @@ export async function runIntegrationTest( corebankApiBaseUrl: args.corebankApiBaseUrl, exchangeBaseUrl: args.exchangeBaseUrl, }); - await waitUntilDone(ws); + await waitUntilTransactionsFinal(ws); logger.info("done withdrawing test balance"); const balance = await getBalances(ws); @@ -305,7 +306,7 @@ export async function runIntegrationTest( await makePayment(ws, myMerchant, args.amountToSpend, "hello world"); // Wait until the refresh is done - await waitUntilDone(ws); + await waitUntilTransactionsFinal(ws); logger.trace("withdrawing test balance for refund"); const withdrawAmountTwo = Amounts.parseOrThrow(`${currency}:18`); @@ -320,7 +321,7 @@ export async function runIntegrationTest( }); // Wait until the withdraw is done - await waitUntilDone(ws); + await waitUntilTransactionsFinal(ws); const { orderId: refundOrderId } = await makePayment( ws, @@ -344,7 +345,7 @@ export async function runIntegrationTest( logger.trace("integration test: applied refund"); // Wait until the refund is done - await waitUntilDone(ws); + await waitUntilTransactionsFinal(ws); logger.trace("integration test: making payment after refund"); @@ -357,12 +358,17 @@ export async function runIntegrationTest( logger.trace("integration test: make payment done"); - await waitUntilDone(ws); + await waitUntilTransactionsFinal(ws); logger.trace("integration test: all done!"); } -export async function waitUntilDone(ws: InternalWalletState): Promise { +/** + * Wait until all transactions are in a final state. + */ +export async function waitUntilTransactionsFinal( + ws: InternalWalletState, +): Promise { logger.info("waiting until all transactions are in a final state"); ws.ensureTaskLoopRunning(); let p: OpenedPromise | undefined = undefined; @@ -410,6 +416,44 @@ export async function waitUntilDone(ws: InternalWalletState): Promise { logger.info("done waiting until all transactions are in a final state"); } +/** + * Wait until pending work is processed. + */ +export async function waitUntilTasksProcessed( + ws: InternalWalletState, +): Promise { + logger.info("waiting until pending work is processed"); + ws.ensureTaskLoopRunning(); + let p: OpenedPromise | undefined = undefined; + const cancelNotifs = ws.addNotificationListener((notif) => { + if (!p) { + return; + } + if (notif.type === NotificationType.PendingOperationProcessed) { + p.resolve(); + } + }); + while (1) { + p = openPromise(); + const pendingTasksResp = await getPendingOperations(ws); + logger.info(`waiting on pending ops: ${j2s(pendingTasksResp)}`); + let finished = true; + for (const task of pendingTasksResp.pendingOperations) { + if (task.isDue) { + finished = false; + } + logger.info(`continuing waiting for task ${task.id}`); + } + if (finished) { + break; + } + // Wait until task is done + await p.promise; + } + logger.info("done waiting until pending work is processed"); + cancelNotifs(); +} + export async function waitUntilRefreshesDone( ws: InternalWalletState, ): Promise { @@ -463,7 +507,7 @@ export async function waitUntilRefreshesDone( logger.info("done waiting until all refreshes are in a final state"); } -async function waitUntilPendingReady( +async function waitUntilTransactionPendingReady( ws: InternalWalletState, transactionId: string, ): Promise { @@ -560,7 +604,7 @@ export async function runIntegrationTest2( corebankApiBaseUrl: args.corebankApiBaseUrl, exchangeBaseUrl: args.exchangeBaseUrl, }); - await waitUntilDone(ws); + await waitUntilTransactionsFinal(ws); logger.info("done withdrawing test balance"); const balance = await getBalances(ws); @@ -580,7 +624,7 @@ export async function runIntegrationTest2( ); // Wait until the refresh is done - await waitUntilDone(ws); + await waitUntilTransactionsFinal(ws); logger.trace("withdrawing test balance for refund"); const withdrawAmountTwo = Amounts.parseOrThrow(`${currency}:18`); @@ -595,7 +639,7 @@ export async function runIntegrationTest2( }); // Wait until the withdraw is done - await waitUntilDone(ws); + await waitUntilTransactionsFinal(ws); const { orderId: refundOrderId } = await makePayment( ws, @@ -619,7 +663,7 @@ export async function runIntegrationTest2( logger.trace("integration test: applied refund"); // Wait until the refund is done - await waitUntilDone(ws); + await waitUntilTransactionsFinal(ws); logger.trace("integration test: making payment after refund"); @@ -632,7 +676,7 @@ export async function runIntegrationTest2( logger.trace("integration test: make payment done"); - await waitUntilDone(ws); + await waitUntilTransactionsFinal(ws); const peerPushInit = await initiatePeerPushDebit(ws, { partialContractTerms: { @@ -647,7 +691,7 @@ export async function runIntegrationTest2( }, }); - await waitUntilPendingReady(ws, peerPushInit.transactionId); + await waitUntilTransactionPendingReady(ws, peerPushInit.transactionId); const peerPushCredit = await preparePeerPushCredit(ws, { talerUri: peerPushInit.talerUri, @@ -670,7 +714,7 @@ export async function runIntegrationTest2( }, }); - await waitUntilPendingReady(ws, peerPullInit.transactionId); + await waitUntilTransactionPendingReady(ws, peerPullInit.transactionId); const peerPullInc = await preparePeerPullDebit(ws, { talerUri: peerPullInit.talerUri, @@ -680,7 +724,7 @@ export async function runIntegrationTest2( peerPullDebitId: peerPullInc.peerPullDebitId, }); - await waitUntilDone(ws); + await waitUntilTransactionsFinal(ws); logger.trace("integration test: all done!"); } diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts b/packages/taler-wallet-core/src/operations/withdraw.ts index 2c9c95d4c..5f728b6f5 100644 --- a/packages/taler-wallet-core/src/operations/withdraw.ts +++ b/packages/taler-wallet-core/src/operations/withdraw.ts @@ -33,7 +33,7 @@ import { codecForReserveStatus, codecForTalerConfigResponse, codecForWalletKycUuid, - codecForWithdrawBatchResponse, + codecForExchangeWithdrawBatchResponse, codecForWithdrawOperationStatusResponse, codecForWithdrawResponse, CoinStatus, @@ -939,7 +939,7 @@ async function processPlanchetExchangeBatchRequest( } const r = await readSuccessResponseJsonOrThrow( resp, - codecForWithdrawBatchResponse(), + codecForExchangeWithdrawBatchResponse(), ); return { coinIdxs: requestCoinIdxs, diff --git a/packages/taler-wallet-core/src/wallet-api-types.ts b/packages/taler-wallet-core/src/wallet-api-types.ts index fadc7aa7f..a8de9ac03 100644 --- a/packages/taler-wallet-core/src/wallet-api-types.ts +++ b/packages/taler-wallet-core/src/wallet-api-types.ts @@ -224,6 +224,7 @@ export enum WalletApiOperation { DeleteStoredBackup = "deleteStoredBackup", RecoverStoredBackup = "recoverStoredBackup", UpdateExchangeEntry = "updateExchangeEntry", + TestingWaitTasksProcessed = "testingWaitTasksProcessed", } // group: Initialization @@ -1007,7 +1008,7 @@ export type TestingSetTimetravelOp = { /** * Wait until all transactions are in a final state. */ -export type TestingWaitTransactionsFinal = { +export type TestingWaitTransactionsFinalOp = { op: WalletApiOperation.TestingWaitTransactionsFinal; request: EmptyObject; response: EmptyObject; @@ -1016,12 +1017,21 @@ export type TestingWaitTransactionsFinal = { /** * Wait until all refresh transactions are in a final state. */ -export type TestingWaitRefreshesFinal = { +export type TestingWaitRefreshesFinalOp = { op: WalletApiOperation.TestingWaitRefreshesFinal; request: EmptyObject; response: EmptyObject; }; +/** + * Wait until all tasks have been processed and the wallet is idle. + */ +export type TestingWaitTasksProcessedOp = { + op: WalletApiOperation.TestingWaitTasksProcessed; + request: EmptyObject; + response: EmptyObject; +}; + /** * Wait until a transaction is in a particular state. */ @@ -1132,8 +1142,9 @@ export type WalletOperations = { [WalletApiOperation.Recycle]: RecycleOp; [WalletApiOperation.ApplyDevExperiment]: ApplyDevExperimentOp; [WalletApiOperation.ValidateIban]: ValidateIbanOp; - [WalletApiOperation.TestingWaitTransactionsFinal]: TestingWaitTransactionsFinal; - [WalletApiOperation.TestingWaitRefreshesFinal]: TestingWaitRefreshesFinal; + [WalletApiOperation.TestingWaitTransactionsFinal]: TestingWaitTransactionsFinalOp; + [WalletApiOperation.TestingWaitRefreshesFinal]: TestingWaitRefreshesFinalOp; + [WalletApiOperation.TestingWaitTasksProcessed]: TestingWaitTasksProcessedOp; [WalletApiOperation.TestingSetTimetravel]: TestingSetTimetravelOp; [WalletApiOperation.TestingWaitTransactionState]: TestingWaitTransactionStateOp; [WalletApiOperation.GetCurrencySpecification]: GetCurrencySpecificationOp; diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts index a8c2895f8..06d9bb9e8 100644 --- a/packages/taler-wallet-core/src/wallet.ts +++ b/packages/taler-wallet-core/src/wallet.ts @@ -252,9 +252,10 @@ import { runIntegrationTest2, testPay, waitTransactionState, - waitUntilDone, + waitUntilTransactionsFinal, waitUntilRefreshesDone, withdrawTestBalance, + waitUntilTasksProcessed, } from "./operations/testing.js"; import { acceptTip, @@ -927,9 +928,9 @@ async function dumpCoins(ws: InternalWalletState): Promise { ageCommitmentProof: c.ageCommitmentProof, spend_allocation: c.spendAllocation ? { - amount: c.spendAllocation.amount, - id: c.spendAllocation.id, - } + amount: c.spendAllocation.amount, + id: c.spendAllocation.id, + } : undefined, }); } @@ -1427,6 +1428,10 @@ async function dispatchRequestInternal( await waitTransactionState(ws, req.transactionId, req.txState); return {}; } + case WalletApiOperation.TestingWaitTasksProcessed: { + await waitUntilTasksProcessed(ws); + return {}; + } case WalletApiOperation.GetCurrencySpecification: { // Ignore result, just validate in this mock implementation const req = codecForGetCurrencyInfoRequest().decode(payload); @@ -1600,7 +1605,7 @@ async function dispatchRequestInternal( return getVersion(ws); } case WalletApiOperation.TestingWaitTransactionsFinal: - return await waitUntilDone(ws); + return await waitUntilTransactionsFinal(ws); case WalletApiOperation.TestingWaitRefreshesFinal: return await waitUntilRefreshesDone(ws); case WalletApiOperation.TestingSetTimetravel: {