gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[taler-wallet-core] branch master updated: wallet-core: implement and te


From: gnunet
Subject: [taler-wallet-core] branch master updated: wallet-core: implement and test forced coin/denom selection
Date: Fri, 10 Jun 2022 13:03:51 +0200

This is an automated email from the git hooks/post-receive script.

dold pushed a commit to branch master
in repository wallet-core.

The following commit(s) were added to refs/heads/master by this push:
     new f57dc7bf wallet-core: implement and test forced coin/denom selection
f57dc7bf is described below

commit f57dc7bf7a1e3a14c67512ba67d92fa350c95c0e
Author: Florian Dold <florian@dold.me>
AuthorDate: Fri Jun 10 13:03:47 2022 +0200

    wallet-core: implement and test forced coin/denom selection
---
 packages/taler-util/src/walletTypes.ts             |  80 +++++++++---
 .../src/integrationtests/test-forced-selection.ts  |  94 ++++++++++++++
 .../src/integrationtests/testrunner.ts             |   2 +
 packages/taler-wallet-core/src/db.ts               |   2 +-
 .../src/operations/backup/import.ts                |   3 +-
 .../taler-wallet-core/src/operations/deposits.ts   |   3 +-
 packages/taler-wallet-core/src/operations/pay.ts   |  37 ++++--
 .../taler-wallet-core/src/operations/reserves.ts   | 108 +++++++++-------
 .../taler-wallet-core/src/operations/testing.ts    |  76 ++++++++----
 .../src/util/coinSelection.test.ts                 |   2 +
 .../taler-wallet-core/src/util/coinSelection.ts    | 138 ++++++++++++++++-----
 packages/taler-wallet-core/src/util/retries.ts     |   2 +-
 packages/taler-wallet-core/src/wallet-api-types.ts |  11 +-
 packages/taler-wallet-core/src/wallet.ts           |  78 ++++++------
 14 files changed, 463 insertions(+), 173 deletions(-)

diff --git a/packages/taler-util/src/walletTypes.ts 
b/packages/taler-util/src/walletTypes.ts
index 00a48986..2e5dd418 100644
--- a/packages/taler-util/src/walletTypes.ts
+++ b/packages/taler-util/src/walletTypes.ts
@@ -33,7 +33,6 @@ import {
   codecForAmountString,
 } from "./amounts.js";
 import {
-  AbsoluteTime,
   codecForTimestamp,
   TalerProtocolTimestamp,
 } from "./time.js";
@@ -231,6 +230,7 @@ export const codecForCreateReserveRequest = (): 
Codec<CreateReserveRequest> =>
     .property("exchangePaytoUri", codecForString())
     .property("senderWire", codecOptional(codecForString()))
     .property("bankWithdrawStatusUrl", codecOptional(codecForString()))
+    .property("forcedDenomSel", codecForAny())
     .build("CreateReserveRequest");
 
 /**
@@ -674,6 +674,7 @@ export interface TestPayArgs {
   merchantAuthToken?: string;
   amount: string;
   summary: string;
+  forcedCoinSel?: ForcedCoinSel;
 }
 
 export const codecForTestPayArgs = (): Codec<TestPayArgs> =>
@@ -682,6 +683,7 @@ export const codecForTestPayArgs = (): Codec<TestPayArgs> =>
     .property("merchantAuthToken", codecOptional(codecForString()))
     .property("amount", codecForString())
     .property("summary", codecForString())
+    .property("forcedCoinSel", codecForAny())
     .build("TestPayArgs");
 
 export interface IntegrationTestArgs {
@@ -738,7 +740,7 @@ export const codecForGetExchangeTosRequest = (): 
Codec<GetExchangeTosRequest> =>
 export interface AcceptManualWithdrawalRequest {
   exchangeBaseUrl: string;
   amount: string;
-  restrictAge?: number,
+  restrictAge?: number;
 }
 
 export const codecForAcceptManualWithdrawalRequet =
@@ -803,10 +805,11 @@ export interface ApplyRefundFromPurchaseIdRequest {
   purchaseId: string;
 }
 
-export const codecForApplyRefundFromPurchaseIdRequest = (): 
Codec<ApplyRefundFromPurchaseIdRequest> =>
-  buildCodecForObject<ApplyRefundFromPurchaseIdRequest>()
-    .property("purchaseId", codecForString())
-    .build("ApplyRefundFromPurchaseIdRequest");
+export const codecForApplyRefundFromPurchaseIdRequest =
+  (): Codec<ApplyRefundFromPurchaseIdRequest> =>
+    buildCodecForObject<ApplyRefundFromPurchaseIdRequest>()
+      .property("purchaseId", codecForString())
+      .build("ApplyRefundFromPurchaseIdRequest");
 
 export interface GetWithdrawalDetailsForUriRequest {
   talerWithdrawUri: string;
@@ -866,12 +869,14 @@ export const codecForPreparePayRequest = (): 
Codec<PreparePayRequest> =>
 export interface ConfirmPayRequest {
   proposalId: string;
   sessionId?: string;
+  forcedCoinSel?: ForcedCoinSel;
 }
 
 export const codecForConfirmPayRequest = (): Codec<ConfirmPayRequest> =>
   buildCodecForObject<ConfirmPayRequest>()
     .property("proposalId", codecForString())
     .property("sessionId", codecOptional(codecForString()))
+    .property("forcedCoinSel", codecForAny())
     .build("ConfirmPay");
 
 export type CoreApiResponse = CoreApiResponseSuccess | CoreApiResponseError;
@@ -903,6 +908,7 @@ export interface WithdrawTestBalanceRequest {
   amount: string;
   bankBaseUrl: string;
   exchangeBaseUrl: string;
+  forcedDenomSel?: ForcedDenomSel;
 }
 
 export const withdrawTestBalanceDefaults = {
@@ -976,6 +982,7 @@ export const codecForWithdrawTestBalance =
       .property("amount", codecForString())
       .property("bankBaseUrl", codecForString())
       .property("exchangeBaseUrl", codecForString())
+      .property("forcedDenomSel", codecForAny())
       .build("WithdrawTestBalanceRequest");
 
 export interface ApplyRefundResponse {
@@ -1026,8 +1033,6 @@ export const codecForForceRefreshRequest = (): 
Codec<ForceRefreshRequest> =>
     .property("coinPubList", codecForList(codecForString()))
     .build("ForceRefreshRequest");
 
-
-
 export interface PrepareRefundRequest {
   talerRefundUri: string;
 }
@@ -1084,14 +1089,12 @@ export const codecForGetFeeForDeposit = (): 
Codec<GetFeeForDepositRequest> =>
 export interface PrepareDepositRequest {
   depositPaytoUri: string;
   amount: AmountString;
-
 }
-export const codecForPrepareDepositRequest =
-  (): Codec<PrepareDepositRequest> =>
-    buildCodecForObject<PrepareDepositRequest>()
-      .property("amount", codecForAmountString())
-      .property("depositPaytoUri", codecForString())
-      .build("PrepareDepositRequest");
+export const codecForPrepareDepositRequest = (): Codec<PrepareDepositRequest> 
=>
+  buildCodecForObject<PrepareDepositRequest>()
+    .property("amount", codecForAmountString())
+    .property("depositPaytoUri", codecForString())
+    .build("PrepareDepositRequest");
 
 export interface PrepareDepositResponse {
   totalDepositCost: AmountJson;
@@ -1203,6 +1206,7 @@ export const codecForWithdrawFakebankRequest =
 export interface ImportDb {
   dump: any;
 }
+
 export const codecForImportDbRequest = (): Codec<ImportDb> =>
   buildCodecForObject<ImportDb>()
     .property("dump", codecForAny())
@@ -1214,3 +1218,49 @@ export interface ForcedDenomSel {
     count: number;
   }[];
 }
+
+/**
+ * Forced coin selection for deposits/payments.
+ */
+export interface ForcedCoinSel {
+  coins: {
+    value: AmountString;
+    contribution: AmountString;
+  }[];
+}
+
+export interface TestPayResult {
+  payCoinSelection: PayCoinSelection,
+}
+
+
+/**
+ * Result of selecting coins, contains the exchange, and selected
+ * coins with their denomination.
+ */
+ export interface PayCoinSelection {
+  /**
+   * Amount requested by the merchant.
+   */
+  paymentAmount: AmountJson;
+
+  /**
+   * Public keys of the coins that were selected.
+   */
+  coinPubs: string[];
+
+  /**
+   * Amount that each coin contributes.
+   */
+  coinContributions: AmountJson[];
+
+  /**
+   * How much of the wire fees is the customer paying?
+   */
+  customerWireFees: AmountJson;
+
+  /**
+   * How much of the deposit fees is the customer paying?
+   */
+  customerDepositFees: AmountJson;
+}
\ No newline at end of file
diff --git 
a/packages/taler-wallet-cli/src/integrationtests/test-forced-selection.ts 
b/packages/taler-wallet-cli/src/integrationtests/test-forced-selection.ts
new file mode 100644
index 00000000..0fe5f639
--- /dev/null
+++ b/packages/taler-wallet-cli/src/integrationtests/test-forced-selection.ts
@@ -0,0 +1,94 @@
+/*
+ This file is part of GNU Taler
+ (C) 2020 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Imports.
+ */
+import {
+  ConfirmPayResultType,
+  j2s,
+  PreparePayResultType,
+} from "@gnu-taler/taler-util";
+import { Wallet, WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import {
+  GlobalTestState,
+  MerchantPrivateApi,
+  WithAuthorization,
+} from "../harness/harness.js";
+import { createSimpleTestkudosEnvironment } from "../harness/helpers.js";
+
+/**
+ * Run test for forced denom/coin selection.
+ */
+export async function runForcedSelectionTest(t: GlobalTestState) {
+  // Set up test environment
+
+  const { wallet, bank, exchange, merchant } =
+    await createSimpleTestkudosEnvironment(t);
+
+  await wallet.client.call(WalletApiOperation.AddExchange, {
+    exchangeBaseUrl: exchange.baseUrl,
+  });
+
+  await wallet.client.call(WalletApiOperation.WithdrawTestBalance, {
+    exchangeBaseUrl: exchange.baseUrl,
+    amount: "TESTKUDOS:10",
+    bankBaseUrl: bank.baseUrl,
+    forcedDenomSel: {
+      denoms: [
+        {
+          value: "TESTKUDOS:2",
+          count: 3,
+        },
+      ],
+    },
+  });
+
+  await wallet.runUntilDone();
+
+  const coinDump = await wallet.client.call(WalletApiOperation.DumpCoins, {});
+  console.log(coinDump);
+  t.assertDeepEqual(coinDump.coins.length, 3);
+
+  const payResp = await wallet.client.call(WalletApiOperation.TestPay, {
+    amount: "TESTKUDOS:3",
+    merchantBaseUrl: merchant.makeInstanceBaseUrl(),
+    summary: "bla",
+    forcedCoinSel: {
+      coins: [
+        {
+          value: "TESTKUDOS:2",
+          contribution: "TESTKUDOS:1",
+        },
+        {
+          value: "TESTKUDOS:2",
+          contribution: "TESTKUDOS:1",
+        },
+        {
+          value: "TESTKUDOS:2",
+          contribution: "TESTKUDOS:1",
+        },
+      ],
+    },
+  });
+
+  console.log(j2s(payResp));
+
+  // Without forced selection, we would only use 2 coins.
+  t.assertDeepEqual(payResp.payCoinSelection.coinContributions.length, 3);
+}
+
+runForcedSelectionTest.suites = ["wallet"];
diff --git a/packages/taler-wallet-cli/src/integrationtests/testrunner.ts 
b/packages/taler-wallet-cli/src/integrationtests/testrunner.ts
index db66aa7d..e8aef513 100644
--- a/packages/taler-wallet-cli/src/integrationtests/testrunner.ts
+++ b/packages/taler-wallet-cli/src/integrationtests/testrunner.ts
@@ -34,6 +34,7 @@ import { runDepositTest } from "./test-deposit";
 import { runExchangeManagementTest } from "./test-exchange-management";
 import { runExchangeTimetravelTest } from "./test-exchange-timetravel.js";
 import { runFeeRegressionTest } from "./test-fee-regression";
+import { runForcedSelectionTest } from "./test-forced-selection.js";
 import { runLibeufinApiBankaccountTest } from 
"./test-libeufin-api-bankaccount";
 import { runLibeufinApiBankconnectionTest } from 
"./test-libeufin-api-bankconnection";
 import { runLibeufinApiFacadeTest } from "./test-libeufin-api-facade";
@@ -113,6 +114,7 @@ const allTests: TestMainFunction[] = [
   runExchangeManagementTest,
   runExchangeTimetravelTest,
   runFeeRegressionTest,
+  runForcedSelectionTest,
   runLibeufinBasicTest,
   runLibeufinKeyrotationTest,
   runLibeufinTutorialTest,
diff --git a/packages/taler-wallet-core/src/db.ts 
b/packages/taler-wallet-core/src/db.ts
index 8fe1937a..b22bc585 100644
--- a/packages/taler-wallet-core/src/db.ts
+++ b/packages/taler-wallet-core/src/db.ts
@@ -41,9 +41,9 @@ import {
   TalerProtocolTimestamp,
   TalerProtocolDuration,
   AgeCommitmentProof,
+  PayCoinSelection,
 } from "@gnu-taler/taler-util";
 import { RetryInfo } from "./util/retries.js";
-import { PayCoinSelection } from "./util/coinSelection.js";
 import { Event, IDBDatabase } from "@gnu-taler/idb-bridge";
 
 /**
diff --git a/packages/taler-wallet-core/src/operations/backup/import.ts 
b/packages/taler-wallet-core/src/operations/backup/import.ts
index 16a88fe7..3a912150 100644
--- a/packages/taler-wallet-core/src/operations/backup/import.ts
+++ b/packages/taler-wallet-core/src/operations/backup/import.ts
@@ -18,7 +18,7 @@ import {
   AmountJson,
   Amounts, BackupCoinSourceType, BackupDenomSel, BackupProposalStatus,
   BackupPurchase, BackupRefreshReason, BackupRefundState, 
codecForContractTerms,
-  DenomKeyType, j2s, Logger, RefreshReason, TalerProtocolTimestamp,
+  DenomKeyType, j2s, Logger, PayCoinSelection, RefreshReason, 
TalerProtocolTimestamp,
   WalletBackupContentV1
 } from "@gnu-taler/taler-util";
 import {
@@ -29,7 +29,6 @@ import {
   ReserveRecordStatus, WalletContractData, WalletRefundItem, WalletStoresV1, 
WireInfo
 } from "../../db.js";
 import { InternalWalletState } from "../../internal-wallet-state.js";
-import { PayCoinSelection } from "../../util/coinSelection.js";
 import {
   checkDbInvariant,
   checkLogicInvariant
diff --git a/packages/taler-wallet-core/src/operations/deposits.ts 
b/packages/taler-wallet-core/src/operations/deposits.ts
index 41f051cb..a016cb8e 100644
--- a/packages/taler-wallet-core/src/operations/deposits.ts
+++ b/packages/taler-wallet-core/src/operations/deposits.ts
@@ -35,6 +35,7 @@ import {
   Logger,
   NotificationType,
   parsePaytoUri,
+  PayCoinSelection,
   PrepareDepositRequest,
   PrepareDepositResponse,
   TalerErrorDetail,
@@ -45,7 +46,7 @@ import {
 } from "@gnu-taler/taler-util";
 import { DepositGroupRecord, OperationStatus, WireFee } from "../db.js";
 import { InternalWalletState } from "../internal-wallet-state.js";
-import { PayCoinSelection, selectPayCoins } from "../util/coinSelection.js";
+import { selectPayCoins } from "../util/coinSelection.js";
 import { readSuccessResponseJsonOrThrow } from "../util/http.js";
 import { RetryInfo } from "../util/retries.js";
 import { guardOperationException } from "./common.js";
diff --git a/packages/taler-wallet-core/src/operations/pay.ts 
b/packages/taler-wallet-core/src/operations/pay.ts
index f22d51a9..b6bae751 100644
--- a/packages/taler-wallet-core/src/operations/pay.ts
+++ b/packages/taler-wallet-core/src/operations/pay.ts
@@ -40,12 +40,14 @@ import {
   durationMin,
   durationMul,
   encodeCrock,
+  ForcedCoinSel,
   getRandomBytes,
   HttpStatusCode,
   j2s,
   Logger,
   NotificationType,
   parsePayUri,
+  PayCoinSelection,
   PreparePayResult,
   PreparePayResultType,
   RefreshReason,
@@ -81,8 +83,8 @@ import {
 import {
   AvailableCoinInfo,
   CoinCandidateSelection,
-  PayCoinSelection,
   PreviousPayCoins,
+  selectForcedPayCoins,
   selectPayCoins,
 } from "../util/coinSelection.js";
 import { ContractTermsUtil } from "../util/contractTerms.js";
@@ -305,6 +307,7 @@ export async function getCandidatePayCoins(
           }
           candidateCoins.push({
             availableAmount: coin.currentAmount,
+            value: denom.value,
             coinPub: coin.coinPub,
             denomPub: denom.denomPub,
             feeDeposit: denom.feeDeposit,
@@ -1423,6 +1426,7 @@ export async function confirmPay(
   ws: InternalWalletState,
   proposalId: string,
   sessionIdOverride?: string,
+  forcedCoinSel?: ForcedCoinSel,
 ): Promise<ConfirmPayResult> {
   logger.trace(
     `executing confirmPay with proposalId ${proposalId} and sessionIdOverride 
${sessionIdOverride}`,
@@ -1479,15 +1483,28 @@ export async function confirmPay(
     wireMethod: contractData.wireMethod,
   });
 
-  const res = selectPayCoins({
-    candidates,
-    contractTermsAmount: contractData.amount,
-    depositFeeLimit: contractData.maxDepositFee,
-    wireFeeAmortization: contractData.wireFeeAmortization ?? 1,
-    wireFeeLimit: contractData.maxWireFee,
-    prevPayCoins: [],
-    requiredMinimumAge: contractData.minimumAge,
-  });
+  let res: PayCoinSelection | undefined = undefined;
+
+  if (forcedCoinSel) {
+    res = selectForcedPayCoins(forcedCoinSel, {
+      candidates,
+      contractTermsAmount: contractData.amount,
+      depositFeeLimit: contractData.maxDepositFee,
+      wireFeeAmortization: contractData.wireFeeAmortization ?? 1,
+      wireFeeLimit: contractData.maxWireFee,
+      requiredMinimumAge: contractData.minimumAge,
+    });
+  } else {
+    res = selectPayCoins({
+      candidates,
+      contractTermsAmount: contractData.amount,
+      depositFeeLimit: contractData.maxDepositFee,
+      wireFeeAmortization: contractData.wireFeeAmortization ?? 1,
+      wireFeeLimit: contractData.maxWireFee,
+      prevPayCoins: [],
+      requiredMinimumAge: contractData.minimumAge,
+    });
+  }
 
   logger.trace("coin selection result", res);
 
diff --git a/packages/taler-wallet-core/src/operations/reserves.ts 
b/packages/taler-wallet-core/src/operations/reserves.ts
index d9fc8cf4..b33f574f 100644
--- a/packages/taler-wallet-core/src/operations/reserves.ts
+++ b/packages/taler-wallet-core/src/operations/reserves.ts
@@ -15,6 +15,7 @@
  */
 
 import {
+  AbsoluteTime,
   AcceptWithdrawalResponse,
   addPaytoQueryParams,
   Amounts,
@@ -28,6 +29,7 @@ import {
   durationMax,
   durationMin,
   encodeCrock,
+  ForcedDenomSel,
   getRandomBytes,
   j2s,
   Logger,
@@ -35,13 +37,10 @@ import {
   randomBytes,
   TalerErrorCode,
   TalerErrorDetail,
-  AbsoluteTime,
   URL,
-  AmountString,
-  ForcedDenomSel,
 } from "@gnu-taler/taler-util";
-import { InternalWalletState } from "../internal-wallet-state.js";
 import {
+  DenomSelectionState,
   OperationStatus,
   ReserveBankInfo,
   ReserveRecord,
@@ -50,6 +49,7 @@ import {
   WithdrawalGroupRecord,
 } from "../db.js";
 import { TalerError } from "../errors.js";
+import { InternalWalletState } from "../internal-wallet-state.js";
 import { assertUnreachable } from "../util/assertUnreachable.js";
 import {
   readSuccessResponseJsonOrErrorCode,
@@ -57,9 +57,8 @@ import {
   throwUnexpectedRequestError,
 } from "../util/http.js";
 import { GetReadOnlyAccess } from "../util/query.js";
-import {
-  RetryInfo,
-} from "../util/retries.js";
+import { RetryInfo } from "../util/retries.js";
+import { guardOperationException } from "./common.js";
 import {
   getExchangeDetails,
   getExchangePaytoUri,
@@ -70,10 +69,10 @@ import {
   getBankWithdrawalInfo,
   getCandidateWithdrawalDenoms,
   processWithdrawGroup,
+  selectForcedWithdrawalDenominations,
   selectWithdrawalDenominations,
   updateWithdrawalDenoms,
 } from "./withdraw.js";
-import { guardOperationException } from "./common.js";
 
 const logger = new Logger("taler-wallet-core:reserves.ts");
 
@@ -178,7 +177,18 @@ export async function createReserve(
 
   await updateWithdrawalDenoms(ws, canonExchange);
   const denoms = await getCandidateWithdrawalDenoms(ws, canonExchange);
-  const initialDenomSel = selectWithdrawalDenominations(req.amount, denoms);
+
+  let initialDenomSel: DenomSelectionState;
+  if (req.forcedDenomSel) {
+    logger.warn("using forced denom selection");
+    initialDenomSel = selectForcedWithdrawalDenominations(
+      req.amount,
+      denoms,
+      req.forcedDenomSel,
+    );
+  } else {
+    initialDenomSel = selectWithdrawalDenominations(req.amount, denoms);
+  }
 
   const reserveRecord: ReserveRecord = {
     instructedAmount: req.amount,
@@ -436,7 +446,7 @@ async function processReserveBankStatus(
   );
 
   if (status.aborted) {
-    logger.trace("bank aborted the withdrawal");
+    logger.info("bank aborted the withdrawal");
     await ws.db
       .mktx((x) => ({
         reserves: x.reserves,
@@ -463,12 +473,14 @@ async function processReserveBankStatus(
     return;
   }
 
-  if (status.selection_done) {
-    if (reserve.reserveStatus === ReserveRecordStatus.RegisteringBank) {
-      await registerReserveWithBank(ws, reservePub);
-      return await processReserveBankStatus(ws, reservePub);
-    }
-  } else {
+  // Bank still needs to know our reserve info
+  if (!status.selection_done) {
+    await registerReserveWithBank(ws, reservePub);
+    return await processReserveBankStatus(ws, reservePub);
+  }
+
+  // FIXME: Why do we do this?!
+  if (reserve.reserveStatus === ReserveRecordStatus.RegisteringBank) {
     await registerReserveWithBank(ws, reservePub);
     return await processReserveBankStatus(ws, reservePub);
   }
@@ -482,29 +494,26 @@ async function processReserveBankStatus(
       if (!r) {
         return;
       }
+      // Re-check reserve status within transaction
+      switch (r.reserveStatus) {
+        case ReserveRecordStatus.RegisteringBank:
+        case ReserveRecordStatus.WaitConfirmBank:
+          break;
+        default:
+          return;
+      }
       if (status.transfer_done) {
-        switch (r.reserveStatus) {
-          case ReserveRecordStatus.RegisteringBank:
-          case ReserveRecordStatus.WaitConfirmBank:
-            break;
-          default:
-            return;
-        }
         const now = AbsoluteTime.toTimestamp(AbsoluteTime.now());
         r.timestampBankConfirmed = now;
         r.reserveStatus = ReserveRecordStatus.QueryingStatus;
         r.operationStatus = OperationStatus.Pending;
         r.retryInfo = RetryInfo.reset();
       } else {
-        switch (r.reserveStatus) {
-          case ReserveRecordStatus.WaitConfirmBank:
-            break;
-          default:
-            return;
-        }
+        logger.info("Withdrawal operation not yet confirmed by bank");
         if (r.bankInfo) {
           r.bankInfo.confirmUrl = status.confirm_transfer_url;
         }
+        r.retryInfo = RetryInfo.increment(r.retryInfo);
       }
       await tx.reserves.put(r);
     });
@@ -540,6 +549,8 @@ async function updateReserve(
   const reserveUrl = new URL(`reserves/${reservePub}`, 
reserve.exchangeBaseUrl);
   reserveUrl.searchParams.set("timeout_ms", "30000");
 
+  logger.info(`querying reserve status via ${reserveUrl}`);
+
   const resp = await ws.http.get(reserveUrl.href, {
     timeout: getReserveRequestTimeout(reserve),
   });
@@ -553,7 +564,7 @@ async function updateReserve(
     if (
       resp.status === 404 &&
       result.talerErrorResponse.code ===
-      TalerErrorCode.EXCHANGE_RESERVES_STATUS_UNKNOWN
+        TalerErrorCode.EXCHANGE_RESERVES_STATUS_UNKNOWN
     ) {
       ws.notify({
         type: NotificationType.ReserveNotYetFound,
@@ -589,6 +600,7 @@ async function updateReserve(
       if (!newReserve) {
         return;
       }
+
       let amountReservePlus = reserveBalance;
       let amountReserveMinus = Amounts.getZero(currency);
 
@@ -628,30 +640,33 @@ async function updateReserve(
         amountReservePlus,
         amountReserveMinus,
       ).amount;
-      const denomSel = selectWithdrawalDenominations(remainingAmount, denoms);
-
-      logger.trace(
-        `Remaining unclaimed amount in reseve is ${Amounts.stringify(
-          remainingAmount,
-        )} and can be withdrawn with ${denomSel.selectedDenoms.length} coins`,
-      );
-
-      if (denomSel.selectedDenoms.length === 0) {
-        newReserve.reserveStatus = ReserveRecordStatus.Dormant;
-        newReserve.operationStatus = OperationStatus.Finished;
-        delete newReserve.lastError;
-        delete newReserve.retryInfo;
-        await tx.reserves.put(newReserve);
-        return;
-      }
 
       let withdrawalGroupId: string;
+      let denomSel: DenomSelectionState;
 
       if (!newReserve.initialWithdrawalStarted) {
         withdrawalGroupId = newReserve.initialWithdrawalGroupId;
         newReserve.initialWithdrawalStarted = true;
+        denomSel = newReserve.initialDenomSel;
       } else {
         withdrawalGroupId = encodeCrock(randomBytes(32));
+
+        denomSel = selectWithdrawalDenominations(remainingAmount, denoms);
+
+        logger.trace(
+          `Remaining unclaimed amount in reseve is ${Amounts.stringify(
+            remainingAmount,
+          )} and can be withdrawn with ${denomSel.selectedDenoms.length} 
coins`,
+        );
+  
+        if (denomSel.selectedDenoms.length === 0) {
+          newReserve.reserveStatus = ReserveRecordStatus.Dormant;
+          newReserve.operationStatus = OperationStatus.Finished;
+          delete newReserve.lastError;
+          delete newReserve.retryInfo;
+          await tx.reserves.put(newReserve);
+          return;
+        }
       }
 
       const withdrawalRecord: WithdrawalGroupRecord = {
@@ -768,6 +783,7 @@ export async function createTalerWithdrawReserve(
     senderWire: withdrawInfo.senderWire,
     exchangePaytoUri: exchangePaytoUri,
     restrictAge: options.restrictAge,
+    forcedDenomSel: options.forcedDenomSel,
   });
   // We do this here, as the reserve should be registered before we return,
   // so that we can redirect the user to the bank's status page.
diff --git a/packages/taler-wallet-core/src/operations/testing.ts 
b/packages/taler-wallet-core/src/operations/testing.ts
index 555e2d73..d609011c 100644
--- a/packages/taler-wallet-core/src/operations/testing.ts
+++ b/packages/taler-wallet-core/src/operations/testing.ts
@@ -17,7 +17,12 @@
 /**
  * Imports.
  */
-import { Logger } from "@gnu-taler/taler-util";
+import {
+  ConfirmPayResultType,
+  Logger,
+  TestPayResult,
+  WithdrawTestBalanceRequest,
+} from "@gnu-taler/taler-util";
 import {
   HttpRequestLibrary,
   readSuccessResponseJsonOrThrow,
@@ -39,6 +44,7 @@ import { InternalWalletState } from 
"../internal-wallet-state.js";
 import { confirmPay, preparePayForUri } from "./pay.js";
 import { getBalances } from "./balance.js";
 import { applyRefund } from "./refund.js";
+import { checkLogicInvariant } from "../util/invariants.js";
 
 const logger = new Logger("operations/testing.ts");
 
@@ -82,10 +88,12 @@ function makeBasicAuthHeader(username: string, password: 
string): string {
 
 export async function withdrawTestBalance(
   ws: InternalWalletState,
-  amount = "TESTKUDOS:10",
-  bankBaseUrl = "https://bank.test.taler.net/";,
-  exchangeBaseUrl = "https://exchange.test.taler.net/";,
+  req: WithdrawTestBalanceRequest,
 ): Promise<void> {
+  const bankBaseUrl = req.bankBaseUrl;
+  const amount = req.amount;
+  const exchangeBaseUrl = req.exchangeBaseUrl;
+
   const bankUser = await registerRandomBankUser(ws.http, bankBaseUrl);
   logger.trace(`Registered bank user ${JSON.stringify(bankUser)}`);
 
@@ -100,6 +108,9 @@ export async function withdrawTestBalance(
     ws,
     wresp.taler_withdraw_uri,
     exchangeBaseUrl,
+    {
+      forcedDenomSel: req.forcedDenomSel,
+    },
   );
 
   await confirmBankWithdrawalUri(
@@ -140,7 +151,10 @@ export async function createDemoBankWithdrawalUri(
     },
     {
       headers: {
-        Authorization: makeBasicAuthHeader(bankUser.username, 
bankUser.password),
+        Authorization: makeBasicAuthHeader(
+          bankUser.username,
+          bankUser.password,
+        ),
       },
     },
   );
@@ -163,7 +177,10 @@ async function confirmBankWithdrawalUri(
     {},
     {
       headers: {
-        Authorization: makeBasicAuthHeader(bankUser.username, 
bankUser.password),
+        Authorization: makeBasicAuthHeader(
+          bankUser.username,
+          bankUser.password,
+        ),
       },
     },
   );
@@ -331,12 +348,11 @@ export async function runIntegrationTest(
   const currency = parsedSpendAmount.currency;
 
   logger.info("withdrawing test balance");
-  await withdrawTestBalance(
-    ws,
-    args.amountToWithdraw,
-    args.bankBaseUrl,
-    args.exchangeBaseUrl,
-  );
+  await withdrawTestBalance(ws, {
+    amount: args.amountToWithdraw,
+    bankBaseUrl: args.bankBaseUrl,
+    exchangeBaseUrl: args.exchangeBaseUrl,
+  });
   await ws.runUntilDone();
   logger.info("done withdrawing test balance");
 
@@ -360,12 +376,11 @@ export async function runIntegrationTest(
   const refundAmount = Amounts.parseOrThrow(`${currency}:6`);
   const spendAmountThree = Amounts.parseOrThrow(`${currency}:3`);
 
-  await withdrawTestBalance(
-    ws,
-    Amounts.stringify(withdrawAmountTwo),
-    args.bankBaseUrl,
-    args.exchangeBaseUrl,
-  );
+  await withdrawTestBalance(ws, {
+    amount: Amounts.stringify(withdrawAmountTwo),
+    bankBaseUrl: args.bankBaseUrl,
+    exchangeBaseUrl: args.exchangeBaseUrl,
+  });
 
   // Wait until the withdraw is done
   await ws.runUntilDone();
@@ -410,7 +425,10 @@ export async function runIntegrationTest(
   logger.trace("integration test: all done!");
 }
 
-export async function testPay(ws: InternalWalletState, args: TestPayArgs) {
+export async function testPay(
+  ws: InternalWalletState,
+  args: TestPayArgs,
+): Promise<TestPayResult> {
   logger.trace("creating order");
   const merchant = {
     authToken: args.merchantAuthToken,
@@ -429,12 +447,28 @@ export async function testPay(ws: InternalWalletState, 
args: TestPayArgs) {
   if (!talerPayUri) {
     console.error("fatal: no taler pay URI received from backend");
     process.exit(1);
-    return;
   }
   logger.trace("taler pay URI:", talerPayUri);
   const result = await preparePayForUri(ws, talerPayUri);
   if (result.status !== PreparePayResultType.PaymentPossible) {
     throw Error(`unexpected prepare pay status: ${result.status}`);
   }
-  await confirmPay(ws, result.proposalId, undefined);
+  const r = await confirmPay(
+    ws,
+    result.proposalId,
+    undefined,
+    args.forcedCoinSel,
+  );
+  if (r.type != ConfirmPayResultType.Done) {
+    throw Error("payment not done");
+  }
+  const purchase = await ws.db
+    .mktx((x) => ({ purchases: x.purchases }))
+    .runReadOnly(async (tx) => {
+      return tx.purchases.get(result.proposalId);
+    });
+  checkLogicInvariant(!!purchase);
+  return {
+    payCoinSelection: purchase.payCoinSelection,
+  };
 }
diff --git a/packages/taler-wallet-core/src/util/coinSelection.test.ts 
b/packages/taler-wallet-core/src/util/coinSelection.test.ts
index ca7b76eb..55c007bb 100644
--- a/packages/taler-wallet-core/src/util/coinSelection.test.ts
+++ b/packages/taler-wallet-core/src/util/coinSelection.test.ts
@@ -31,6 +31,7 @@ function a(x: string): AmountJson {
 
 function fakeAci(current: string, feeDeposit: string): AvailableCoinInfo {
   return {
+    value: a(current),
     availableAmount: a(current),
     coinPub: "foobar",
     denomPub: {
@@ -45,6 +46,7 @@ function fakeAci(current: string, feeDeposit: string): 
AvailableCoinInfo {
 
 function fakeAciWithAgeRestriction(current: string, feeDeposit: string): 
AvailableCoinInfo {
   return {
+    value: a(current),
     availableAmount: a(current),
     coinPub: "foobar",
     denomPub: {
diff --git a/packages/taler-wallet-core/src/util/coinSelection.ts 
b/packages/taler-wallet-core/src/util/coinSelection.ts
index 080a5049..b3439067 100644
--- a/packages/taler-wallet-core/src/util/coinSelection.ts
+++ b/packages/taler-wallet-core/src/util/coinSelection.ts
@@ -29,42 +29,14 @@ import {
   AmountJson,
   Amounts,
   DenominationPubKey,
+  ForcedCoinSel,
   Logger,
+  PayCoinSelection,
 } from "@gnu-taler/taler-util";
+import { checkLogicInvariant } from "./invariants.js";
 
 const logger = new Logger("coinSelection.ts");
 
-/**
- * Result of selecting coins, contains the exchange, and selected
- * coins with their denomination.
- */
-export interface PayCoinSelection {
-  /**
-   * Amount requested by the merchant.
-   */
-  paymentAmount: AmountJson;
-
-  /**
-   * Public keys of the coins that were selected.
-   */
-  coinPubs: string[];
-
-  /**
-   * Amount that each coin contributes.
-   */
-  coinContributions: AmountJson[];
-
-  /**
-   * How much of the wire fees is the customer paying?
-   */
-  customerWireFees: AmountJson;
-
-  /**
-   * How much of the deposit fees is the customer paying?
-   */
-  customerDepositFees: AmountJson;
-}
-
 /**
  * Structure to describe a coin that is available to be
  * used in a payment.
@@ -82,6 +54,11 @@ export interface AvailableCoinInfo {
    */
   denomPub: DenominationPubKey;
 
+  /**
+   * Full value of the coin.
+   */
+  value: AmountJson;
+
   /**
    * Amount still remaining (typically the full amount,
    * as coins are always refreshed after use.)
@@ -356,3 +333,102 @@ export function selectPayCoins(
   }
   return undefined;
 }
+
+export function selectForcedPayCoins(
+  forcedCoinSel: ForcedCoinSel,
+  req: SelectPayCoinRequest,
+): PayCoinSelection | undefined {
+  const {
+    candidates,
+    contractTermsAmount,
+    depositFeeLimit,
+    wireFeeLimit,
+    wireFeeAmortization,
+  } = req;
+
+  if (candidates.candidateCoins.length === 0) {
+    return undefined;
+  }
+  const coinPubs: string[] = [];
+  const coinContributions: AmountJson[] = [];
+  const currency = contractTermsAmount.currency;
+
+  let tally: CoinSelectionTally = {
+    amountPayRemaining: contractTermsAmount,
+    amountWireFeeLimitRemaining: wireFeeLimit,
+    amountDepositFeeLimitRemaining: depositFeeLimit,
+    customerDepositFees: Amounts.getZero(currency),
+    customerWireFees: Amounts.getZero(currency),
+    wireFeeCoveredForExchange: new Set(),
+  };
+
+  // Not supported by forced coin selection
+  checkLogicInvariant(!req.prevPayCoins);
+
+  // Sort by available amount (descending),  deposit fee (ascending) and
+  // denomPub (ascending) if deposit fee is the same
+  // (to guarantee deterministic results)
+  const candidateCoins = [...candidates.candidateCoins].sort(
+    (o1, o2) =>
+      -Amounts.cmp(o1.availableAmount, o2.availableAmount) ||
+      Amounts.cmp(o1.feeDeposit, o2.feeDeposit) ||
+      DenominationPubKey.cmp(o1.denomPub, o2.denomPub),
+  );
+
+  // FIXME:  Here, we should select coins in a smarter way.
+  // Instead of always spending the next-largest coin,
+  // we should try to find the smallest coin that covers the
+  // amount.
+
+  // Set of spent coin indices from candidate coins
+  const spentSet: Set<number> = new Set();
+
+  for (const forcedCoin of forcedCoinSel.coins) {
+    let aci: AvailableCoinInfo | undefined = undefined;
+    for (let i = 0; i < candidateCoins.length; i++) {
+      if (spentSet.has(i)) {
+        continue;
+      }
+      if (
+        Amounts.cmp(forcedCoin.value, candidateCoins[i].availableAmount) != 0
+      ) {
+        continue;
+      }
+      spentSet.add(i);
+      aci = candidateCoins[i];
+      break;
+    }
+
+    if (!aci) {
+      throw Error("can't find coin for forced coin selection");
+    }
+
+    tally = tallyFees(
+      tally,
+      candidates.wireFeesPerExchange,
+      wireFeeAmortization,
+      aci.exchangeBaseUrl,
+      aci.feeDeposit,
+    );
+
+    let coinSpend = Amounts.parseOrThrow(forcedCoin.contribution);
+
+    tally.amountPayRemaining = Amounts.sub(
+      tally.amountPayRemaining,
+      coinSpend,
+    ).amount;
+    coinPubs.push(aci.coinPub);
+    coinContributions.push(coinSpend);
+  }
+
+  if (Amounts.isZero(tally.amountPayRemaining)) {
+    return {
+      paymentAmount: contractTermsAmount,
+      coinContributions,
+      coinPubs,
+      customerDepositFees: tally.customerDepositFees,
+      customerWireFees: tally.customerWireFees,
+    };
+  }
+  return undefined;
+}
diff --git a/packages/taler-wallet-core/src/util/retries.ts 
b/packages/taler-wallet-core/src/util/retries.ts
index 2fe18cb2..13a05b38 100644
--- a/packages/taler-wallet-core/src/util/retries.ts
+++ b/packages/taler-wallet-core/src/util/retries.ts
@@ -37,7 +37,7 @@ export interface RetryPolicy {
 
 const defaultRetryPolicy: RetryPolicy = {
   backoffBase: 1.5,
-  backoffDelta: Duration.fromSpec({ seconds: 30 }),
+  backoffDelta: Duration.fromSpec({ seconds: 1 }),
   maxTimeout: Duration.fromSpec({ minutes: 2 }),
 };
 
diff --git a/packages/taler-wallet-core/src/wallet-api-types.ts 
b/packages/taler-wallet-core/src/wallet-api-types.ts
index 0555b0ce..9acfbf10 100644
--- a/packages/taler-wallet-core/src/wallet-api-types.ts
+++ b/packages/taler-wallet-core/src/wallet-api-types.ts
@@ -57,6 +57,7 @@ import {
   SetCoinSuspendedRequest,
   SetWalletDeviceIdRequest,
   TestPayArgs,
+  TestPayResult,
   TrackDepositGroupRequest,
   TrackDepositGroupResponse,
   TransactionsRequest,
@@ -270,7 +271,7 @@ export type WalletOperations = {
   };
   [WalletApiOperation.TestPay]: {
     request: TestPayArgs;
-    response: {};
+    response: TestPayResult;
   };
   [WalletApiOperation.ExportDb]: {
     request: {};
@@ -279,12 +280,12 @@ export type WalletOperations = {
 };
 
 export type RequestType<
-  Op extends WalletApiOperation & keyof WalletOperations
-  > = WalletOperations[Op] extends { request: infer T } ? T : never;
+  Op extends WalletApiOperation & keyof WalletOperations,
+> = WalletOperations[Op] extends { request: infer T } ? T : never;
 
 export type ResponseType<
-  Op extends WalletApiOperation & keyof WalletOperations
-  > = WalletOperations[Op] extends { response: infer T } ? T : never;
+  Op extends WalletApiOperation & keyof WalletOperations,
+> = WalletOperations[Op] extends { response: infer T } ? T : never;
 
 export interface WalletCoreApiClient {
   call<Op extends WalletApiOperation & keyof WalletOperations>(
diff --git a/packages/taler-wallet-core/src/wallet.ts 
b/packages/taler-wallet-core/src/wallet.ts
index a0eaca2e..c7b94138 100644
--- a/packages/taler-wallet-core/src/wallet.ts
+++ b/packages/taler-wallet-core/src/wallet.ts
@@ -23,7 +23,9 @@
  * Imports.
  */
 import {
-  AbsoluteTime, AcceptManualWithdrawalResult, AmountJson,
+  AbsoluteTime,
+  AcceptManualWithdrawalResult,
+  AmountJson,
   Amounts,
   BalancesResponse,
   codecForAbortPayWithRefundRequest,
@@ -48,7 +50,9 @@ import {
   codecForIntegrationTestArgs,
   codecForListKnownBankAccounts,
   codecForPrepareDepositRequest,
-  codecForPreparePayRequest, codecForPrepareRefundRequest, 
codecForPrepareTipRequest,
+  codecForPreparePayRequest,
+  codecForPrepareRefundRequest,
+  codecForPrepareTipRequest,
   codecForRetryTransactionRequest,
   codecForSetCoinSuspendedRequest,
   codecForSetWalletDeviceIdRequest,
@@ -58,7 +62,9 @@ import {
   codecForWithdrawFakebankRequest,
   codecForWithdrawTestBalance,
   CoinDumpJson,
-  CoreApiResponse, Duration, durationFromSpec,
+  CoreApiResponse,
+  Duration,
+  durationFromSpec,
   durationMin,
   ExchangeListItem,
   ExchangesListRespose,
@@ -71,13 +77,14 @@ import {
   parsePaytoUri,
   PaytoUri,
   RefreshReason,
-  TalerErrorCode, URL,
-  WalletNotification
+  TalerErrorCode,
+  URL,
+  WalletNotification,
 } from "@gnu-taler/taler-util";
 import { TalerCryptoInterface } from "./crypto/cryptoImplementation.js";
 import {
   CryptoDispatcher,
-  CryptoWorkerFactory
+  CryptoWorkerFactory,
 } from "./crypto/workers/cryptoDispatcher.js";
 import {
   AuditorTrustRecord,
@@ -85,7 +92,7 @@ import {
   exportDb,
   importDb,
   ReserveRecordStatus,
-  WalletStoresV1
+  WalletStoresV1,
 } from "./db.js";
 import { getErrorDetailFromException, TalerError } from "./errors.js";
 import {
@@ -96,7 +103,7 @@ import {
   MerchantOperations,
   NotificationListener,
   RecoupOperations,
-  ReserveOperations
+  ReserveOperations,
 } from "./internal-wallet-state.js";
 import { exportBackup } from "./operations/backup/export.js";
 import {
@@ -109,7 +116,7 @@ import {
   loadBackupRecovery,
   processBackupForProvider,
   removeBackupProvider,
-  runBackupCycle
+  runBackupCycle,
 } from "./operations/backup/index.js";
 import { setWalletDeviceId } from "./operations/backup/state.js";
 import { getBalances } from "./operations/balance.js";
@@ -118,7 +125,7 @@ import {
   getFeeForDeposit,
   prepareDepositGroup,
   processDepositGroup,
-  trackDepositGroup
+  trackDepositGroup,
 } from "./operations/deposits.js";
 import {
   acceptExchangeTermsOfService,
@@ -127,66 +134,66 @@ import {
   getExchangeRequestTimeout,
   getExchangeTrust,
   updateExchangeFromUrl,
-  updateExchangeTermsOfService
+  updateExchangeTermsOfService,
 } from "./operations/exchanges.js";
 import { getMerchantInfo } from "./operations/merchants.js";
 import {
   confirmPay,
   preparePayForUri,
   processDownloadProposal,
-  processPurchasePay
+  processPurchasePay,
 } from "./operations/pay.js";
 import { getPendingOperations } from "./operations/pending.js";
 import { createRecoupGroup, processRecoupGroup } from "./operations/recoup.js";
 import {
   autoRefresh,
   createRefreshGroup,
-  processRefreshGroup
+  processRefreshGroup,
 } from "./operations/refresh.js";
 import {
   abortFailedPayWithRefund,
   applyRefund,
   applyRefundFromPurchaseId,
   prepareRefund,
-  processPurchaseQueryRefund
+  processPurchaseQueryRefund,
 } from "./operations/refund.js";
 import {
   createReserve,
   createTalerWithdrawReserve,
   getFundingPaytoUris,
-  processReserve
+  processReserve,
 } from "./operations/reserves.js";
 import {
   runIntegrationTest,
   testPay,
-  withdrawTestBalance
+  withdrawTestBalance,
 } from "./operations/testing.js";
 import { acceptTip, prepareTip, processTip } from "./operations/tip.js";
 import {
   deleteTransaction,
   getTransactions,
-  retryTransaction
+  retryTransaction,
 } from "./operations/transactions.js";
 import {
   getExchangeWithdrawalInfo,
   getWithdrawalDetailsForUri,
-  processWithdrawGroup
+  processWithdrawGroup,
 } from "./operations/withdraw.js";
 import {
   PendingOperationsResponse,
   PendingTaskInfo,
-  PendingTaskType
+  PendingTaskType,
 } from "./pending-types.js";
 import { assertUnreachable } from "./util/assertUnreachable.js";
 import { AsyncOpMemoMap, AsyncOpMemoSingle } from "./util/asyncMemo.js";
 import {
   HttpRequestLibrary,
-  readSuccessResponseJsonOrThrow
+  readSuccessResponseJsonOrThrow,
 } from "./util/http.js";
 import {
   AsyncCondition,
   OpenedPromise,
-  openPromise
+  openPromise,
 } from "./util/promiseUtils.js";
 import { DbAccess, GetReadWriteAccess } from "./util/query.js";
 import { TimerAPI, TimerGroup } from "./util/timer.js";
@@ -355,7 +362,6 @@ async function runTaskLoop(
       if (p.givesLifeness) {
         numGivingLiveness++;
       }
-
     }
 
     if (opts.stopWhenDone && numGivingLiveness === 0 && iteration !== 0) {
@@ -459,13 +465,12 @@ async function acceptManualWithdrawal(
   exchangeBaseUrl: string,
   amount: AmountJson,
   restrictAge?: number,
-
 ): Promise<AcceptManualWithdrawalResult> {
   try {
     const resp = await createReserve(ws, {
       amount,
       exchange: exchangeBaseUrl,
-      restrictAge
+      restrictAge,
     });
     const exchangePaytoUris = await ws.db
       .mktx((x) => ({
@@ -688,7 +693,7 @@ async function dumpCoins(ws: InternalWalletState): 
Promise<CoinDumpJson> {
           c.denomPubHash,
         );
         if (!denomInfo) {
-          console.error("no denomination found for coin")
+          console.error("no denomination found for coin");
           continue;
         }
         coinsJson.coins.push({
@@ -749,22 +754,16 @@ async function dispatchRequestInternal(
       return {};
     }
     case "withdrawTestkudos": {
-      await withdrawTestBalance(
-        ws,
-        "TESTKUDOS:10",
-        "https://bank.test.taler.net/";,
-        "https://exchange.test.taler.net/";,
-      );
+      await withdrawTestBalance(ws, {
+        amount: "TESTKUDOS:10",
+        bankBaseUrl: "https://bank.test.taler.net/";,
+        exchangeBaseUrl: "https://exchange.test.taler.net/";,
+      });
       return {};
     }
     case "withdrawTestBalance": {
       const req = codecForWithdrawTestBalance().decode(payload);
-      await withdrawTestBalance(
-        ws,
-        req.amount,
-        req.bankBaseUrl,
-        req.exchangeBaseUrl,
-      );
+      await withdrawTestBalance(ws, req);
       return {};
     }
     case "runIntegrationTest": {
@@ -774,8 +773,7 @@ async function dispatchRequestInternal(
     }
     case "testPay": {
       const req = codecForTestPayArgs().decode(payload);
-      await testPay(ws, req);
-      return {};
+      return await testPay(ws, req);
     }
     case "getTransactions": {
       const req = codecForTransactionsRequest().decode(payload);
@@ -813,7 +811,7 @@ async function dispatchRequestInternal(
         ws,
         req.exchangeBaseUrl,
         Amounts.parseOrThrow(req.amount),
-        req.restrictAge
+        req.restrictAge,
       );
       return res;
     }

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]