gnunet-svn
[Top][All Lists]
Advanced

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

[taler-wallet-core] branch master updated (13f83586 -> 599c8380)


From: gnunet
Subject: [taler-wallet-core] branch master updated (13f83586 -> 599c8380)
Date: Thu, 13 Aug 2020 11:45:07 +0200

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

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

    from 13f83586 Remove old tests from CI runs
     new 61ee1efb logging
     new 599c8380 make withdrawal requests sequentially, clean up withdrawal 
logic a bit

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../src/test-paywall-flow.ts                       |   7 +-
 packages/taler-wallet-core/src/TalerErrorCode.ts   |  71 +++++-
 .../taler-wallet-core/src/operations/reserves.ts   |   1 -
 packages/taler-wallet-core/src/operations/tip.ts   |   2 +-
 .../taler-wallet-core/src/operations/withdraw.ts   | 273 +++++++++++++--------
 packages/taler-wallet-core/src/types/dbTypes.ts    |   8 +-
 6 files changed, 251 insertions(+), 111 deletions(-)

diff --git a/packages/taler-integrationtests/src/test-paywall-flow.ts 
b/packages/taler-integrationtests/src/test-paywall-flow.ts
index b329a9c6..0671fecb 100644
--- a/packages/taler-integrationtests/src/test-paywall-flow.ts
+++ b/packages/taler-integrationtests/src/test-paywall-flow.ts
@@ -121,6 +121,7 @@ runTest(async (t: GlobalTestState) => {
   });
 
   if (publicOrderStatusResp.status != 410) {
+    console.log(publicOrderStatusResp.data);
     throw Error(
       `expected status 410 (after paying), but got 
${publicOrderStatusResp.status}`,
     );
@@ -167,8 +168,10 @@ runTest(async (t: GlobalTestState) => {
     },
   });
 
+  const secondOrderId = orderResp.order_id;
+
   orderStatus = await merchant.queryPrivateOrderStatus({
-    orderId: orderResp.order_id,
+    orderId: secondOrderId,
     sessionId: "mysession-three",
   });
 
@@ -187,6 +190,8 @@ runTest(async (t: GlobalTestState) => {
   t.assertTrue(preparePayResp.status === 
PreparePayResultType.AlreadyConfirmed);
   t.assertTrue(preparePayResp.paid);
 
+  console.log("requesting public status", publicOrderStatusUrl);
+
   // Ask the order status of the claimed-but-unpaid order
   publicOrderStatusResp = await axios.get(publicOrderStatusUrl, {
     validateStatus: () => true,
diff --git a/packages/taler-wallet-core/src/TalerErrorCode.ts 
b/packages/taler-wallet-core/src/TalerErrorCode.ts
index d45b1064..1557007f 100644
--- a/packages/taler-wallet-core/src/TalerErrorCode.ts
+++ b/packages/taler-wallet-core/src/TalerErrorCode.ts
@@ -22,6 +22,8 @@
  */
 
 export enum TalerErrorCode {
+
+
   /**
    * Special code to indicate no error (or no "code" present).
    * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -73,21 +75,21 @@ export enum TalerErrorCode {
 
   /**
    * Exchange failed to allocate memory for building JSON reply.
-   * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+   * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR 
(500).
    * (A value of 0 indicates that the error is generated client-side).
    */
   JSON_ALLOCATION_FAILURE = 7,
 
   /**
    * HTTP method invalid for this URL.
-   * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+   * Returned with an HTTP status code of #MHD_HTTP_METHOD_NOT_ALLOWED (405).
    * (A value of 0 indicates that the error is generated client-side).
    */
   METHOD_INVALID = 8,
 
   /**
-   * Operation specified invalid for this URL (resulting in a "NOT FOUND" for 
the overall response).
-   * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+   * Operation specified invalid for this endpoint.
+   * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
    * (A value of 0 indicates that the error is generated client-side).
    */
   OPERATION_INVALID = 9,
@@ -148,6 +150,20 @@ export enum TalerErrorCode {
    */
   PAYTO_MALFORMED = 17,
 
+  /**
+   * Operation specified unknown for this endpoint.
+   * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+   * (A value of 0 indicates that the error is generated client-side).
+   */
+  OPERATION_UNKNOWN = 18,
+
+  /**
+   * Exchange failed to allocate memory.
+   * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR 
(500).
+   * (A value of 0 indicates that the error is generated client-side).
+   */
+  ALLOCATION_FAILURE = 19,
+
   /**
    * The exchange failed to even just initialize its connection to the 
database.
    * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR 
(500).
@@ -842,11 +858,18 @@ export enum TalerErrorCode {
   REFUND_COIN_NOT_FOUND = 1500,
 
   /**
-   * We could not process the refund request as the coin's transaction history 
does not permit the requested refund at this time.  The "history" in the 
response proves this.
+   * We could not process the refund request as the coin's transaction history 
does not permit the requested refund because then refunds would exceed the 
deposit amount.  The "history" in the response proves this.
    * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
    * (A value of 0 indicates that the error is generated client-side).
    */
-  REFUND_CONFLICT = 1501,
+  REFUND_CONFLICT_DEPOSIT_INSUFFICIENT = 1501,
+
+  /**
+   * We could not process the refund request as the same refund transaction ID 
was already used with a different amount. Retrying with a different refund 
transaction ID may work. The "history" in the response proves this by providing 
the conflicting entry.
+   * Returned with an HTTP status code of #MHD_HTTP_PRECONDITION_FAILED (412).
+   * (A value of 0 indicates that the error is generated client-side).
+   */
+  REFUND_INCONSITENT_AMOUNT = 1502,
 
   /**
    * The exchange knows about the coin we were asked to refund, but not about 
the specific /deposit operation.  Hence, we cannot issue a refund (as we do not 
know if this merchant public key is authorized to do a refund).
@@ -939,6 +962,13 @@ export enum TalerErrorCode {
    */
   REFUND_INVALID_SIGNATURE_BY_EXCHANGE = 1515,
 
+  /**
+   * The failure proof returned by the exchange is incorrect. Error code 
generated client-side.
+   * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+   * (A value of 0 indicates that the error is generated client-side).
+   */
+  REFUND_INVALID_FAILURE_PROOF_BY_EXCHANGE = 1516,
+
   /**
    * The wire format specified in the "sender_account_details" is not 
understood or not supported by this exchange.
    * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -1163,6 +1193,20 @@ export enum TalerErrorCode {
    */
   PROPOSAL_INSTANCE_CONFIGURATION_LACKS_WIRE = 2002,
 
+  /**
+   * The backend could not locate a required template to generate an HTML 
reply.
+   * Returned with an HTTP status code of #MHD_HTTP_NOT_ACCEPTABLE (406).
+   * (A value of 0 indicates that the error is generated client-side).
+   */
+  MERCHANT_FAILED_TO_LOAD_TEMPLATE = 2003,
+
+  /**
+   * The backend could not expand the template to generate an HTML reply.
+   * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR 
(500).
+   * (A value of 0 indicates that the error is generated client-side).
+   */
+  MERCHANT_FAILED_TO_EXPAND_TEMPLATE = 2004,
+
   /**
    * The merchant failed to provide a meaningful response to a /pay request.  
This error is created client-side.
    * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -2885,6 +2929,13 @@ export enum TalerErrorCode {
    */
   BANK_TRANSFER_REQUEST_UID_REUSED = 5500,
 
+  /**
+   * The withdrawal operation already has a reserve selected.  The current 
request conflicts with the existing selection.
+   * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+   * (A value of 0 indicates that the error is generated client-side).
+   */
+  BANK_WITHDRAWAL_OPERATION_RESERVE_SELECTION_CONFLICT = 5600,
+
   /**
    * The sync service failed to access its database.
    * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR 
(500).
@@ -3081,6 +3132,13 @@ export enum TalerErrorCode {
    */
   WALLET_INVALID_TALER_PAY_URI = 7008,
 
+  /**
+   * The signature on a coin by the exchange's denomination key is invalid 
after unblinding it.
+   * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+   * (A value of 0 indicates that the error is generated client-side).
+   */
+  WALLET_EXCHANGE_COIN_SIGNATURE_INVALID = 7009,
+
   /**
    * The exchange does not know about the reserve (yet), and thus withdrawal 
can't progress.
    * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -3094,4 +3152,5 @@ export enum TalerErrorCode {
    * (A value of 0 indicates that the error is generated client-side).
    */
   END = 9999,
+
 }
diff --git a/packages/taler-wallet-core/src/operations/reserves.ts 
b/packages/taler-wallet-core/src/operations/reserves.ts
index 58095aff..060226ca 100644
--- a/packages/taler-wallet-core/src/operations/reserves.ts
+++ b/packages/taler-wallet-core/src/operations/reserves.ts
@@ -758,7 +758,6 @@ async function depleteReserve(
         rawWithdrawalAmount: withdrawAmount,
         timestampStart: getTimestampNow(),
         retryInfo: initRetryInfo(),
-        lastErrorPerCoin: {},
         lastError: undefined,
         denomsSel: denomSelectionInfoToState(denomsForWithdraw),
       };
diff --git a/packages/taler-wallet-core/src/operations/tip.ts 
b/packages/taler-wallet-core/src/operations/tip.ts
index d6768bdb..84cfa570 100644
--- a/packages/taler-wallet-core/src/operations/tip.ts
+++ b/packages/taler-wallet-core/src/operations/tip.ts
@@ -281,6 +281,7 @@ async function processTipImpl(
       coinIdx: i,
       withdrawalDone: false,
       withdrawalGroupId: withdrawalGroupId,
+      lastError: undefined,
     };
     planchets.push(planchet);
   }
@@ -294,7 +295,6 @@ async function processTipImpl(
     timestampStart: getTimestampNow(),
     withdrawalGroupId: withdrawalGroupId,
     rawWithdrawalAmount: tipRecord.amount,
-    lastErrorPerCoin: {},
     retryInfo: initRetryInfo(),
     timestampFinish: undefined,
     lastError: undefined,
diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts 
b/packages/taler-wallet-core/src/operations/withdraw.ts
index 9719772a..a72a7082 100644
--- a/packages/taler-wallet-core/src/operations/withdraw.ts
+++ b/packages/taler-wallet-core/src/operations/withdraw.ts
@@ -39,6 +39,7 @@ import {
   codecForWithdrawOperationStatusResponse,
   codecForWithdrawResponse,
   WithdrawUriInfoResponse,
+  WithdrawResponse,
 } from "../types/talerTypes";
 import { InternalWalletState } from "./state";
 import { parseWithdrawUri } from "../util/taleruri";
@@ -47,7 +48,7 @@ import { updateExchangeFromUrl, getExchangeTrust } from 
"./exchanges";
 import { WALLET_EXCHANGE_PROTOCOL_VERSION } from "./versions";
 
 import * as LibtoolVersion from "../util/libtoolVersion";
-import { guardOperationException } from "./errors";
+import { guardOperationException, makeErrorDetails, OperationFailedError } 
from "./errors";
 import { NotificationType } from "../types/notifications";
 import {
   getTimestampNow,
@@ -57,6 +58,7 @@ import {
 } from "../util/time";
 import { readSuccessResponseJsonOrThrow } from "../util/http";
 import { URL } from "../util/url";
+import { TalerErrorCode } from "../TalerErrorCode";
 
 const logger = new Logger("withdraw.ts");
 
@@ -184,9 +186,13 @@ async function getPossibleDenoms(
 }
 
 /**
- * Given a planchet, withdraw a coin from the exchange.
+ * Generate a planchet for a coin index in a withdrawal group.
+ * Does not actually withdraw the coin yet.
+ *
+ * Split up so that we can parallelize the crypto, but serialize
+ * the exchange requests per reserve.
  */
-async function processPlanchet(
+async function processPlanchetGenerate(
   ws: InternalWalletState,
   withdrawalGroupId: string,
   coinIdx: number,
@@ -259,6 +265,7 @@ async function processPlanchet(
       withdrawalDone: false,
       withdrawSig: r.withdrawSig,
       withdrawalGroupId: withdrawalGroupId,
+      lastError: undefined,
     };
     await ws.db.runWithWriteTransaction([Stores.planchets], async (tx) => {
       const p = await tx.getIndexed(Stores.planchets.byGroupAndIndex, [
@@ -273,8 +280,31 @@ async function processPlanchet(
       planchet = newPlanchet;
     });
   }
+}
+
+/**
+ * Send the withdrawal request for a generated planchet to the exchange.
+ *
+ * The verification of the response is done asynchronously to enable 
parallelism.
+ */
+async function processPlanchetExchangeRequest(
+  ws: InternalWalletState,
+  withdrawalGroupId: string,
+  coinIdx: number,
+): Promise<WithdrawResponse | undefined> {
+  const withdrawalGroup = await ws.db.get(
+    Stores.withdrawalGroups,
+    withdrawalGroupId,
+  );
+  if (!withdrawalGroup) {
+    return;
+  }
+  let planchet = await ws.db.getIndexed(Stores.planchets.byGroupAndIndex, [
+    withdrawalGroupId,
+    coinIdx,
+  ]);
   if (!planchet) {
-    throw Error("invariant violated");
+    return;
   }
   if (planchet.withdrawalDone) {
     logger.warn("processPlanchet: planchet already withdrawn");
@@ -313,16 +343,62 @@ async function processPlanchet(
     exchange.baseUrl,
   ).href;
 
-  const resp = await ws.http.postJson(reqUrl, wd);
-  const r = await readSuccessResponseJsonOrThrow(
-    resp,
-    codecForWithdrawResponse(),
-  );
+  try {
+    const resp = await ws.http.postJson(reqUrl, wd);
+    const r = await readSuccessResponseJsonOrThrow(
+      resp,
+      codecForWithdrawResponse(),
+    );
+  
+    logger.trace(`got response for /withdraw`);
+    return r;
+  } catch (e) {
+    if (!(e instanceof OperationFailedError)) {
+      throw e;
+    }
+    const errDetails = e.operationError;
+    await ws.db.runWithWriteTransaction([Stores.planchets], async (tx) => {
+      let planchet = await ws.db.getIndexed(Stores.planchets.byGroupAndIndex, [
+        withdrawalGroupId,
+        coinIdx,
+      ]);
+      if (!planchet) {
+        return;
+      }
+      planchet.lastError = errDetails;
+      await tx.put(Stores.planchets, planchet);
+    });
+    return;
+  }
+}
 
-  logger.trace(`got response for /withdraw`);
+async function processPlanchetVerifyAndStoreCoin(
+  ws: InternalWalletState,
+  withdrawalGroupId: string,
+  coinIdx: number,
+  resp: WithdrawResponse,
+): Promise<void> {
+  const withdrawalGroup = await ws.db.get(
+    Stores.withdrawalGroups,
+    withdrawalGroupId,
+  );
+  if (!withdrawalGroup) {
+    return;
+  }
+  let planchet = await ws.db.getIndexed(Stores.planchets.byGroupAndIndex, [
+    withdrawalGroupId,
+    coinIdx,
+  ]);
+  if (!planchet) {
+    return;
+  }
+  if (planchet.withdrawalDone) {
+    logger.warn("processPlanchet: planchet already withdrawn");
+    return;
+  }
 
   const denomSig = await ws.cryptoApi.rsaUnblind(
-    r.ev_sig,
+    resp.ev_sig,
     planchet.blindingKey,
     planchet.denomPub,
   );
@@ -334,11 +410,24 @@ async function processPlanchet(
   );
 
   if (!isValid) {
-    throw Error("invalid RSA signature by the exchange");
+    await ws.db.runWithWriteTransaction([Stores.planchets], async (tx) => {
+      let planchet = await ws.db.getIndexed(Stores.planchets.byGroupAndIndex, [
+        withdrawalGroupId,
+        coinIdx,
+      ]);
+      if (!planchet) {
+        return;
+      }
+      planchet.lastError = makeErrorDetails(
+        TalerErrorCode.WALLET_EXCHANGE_COIN_SIGNATURE_INVALID,
+        "invalid signature from the exchange after unblinding",
+        {},
+      );
+      await tx.put(Stores.planchets, planchet);
+    });
+    return;
   }
 
-  logger.trace(`unblinded and verified`);
-
   const coin: CoinRecord = {
     blindingKey: planchet.blindingKey,
     coinPriv: planchet.coinPriv,
@@ -358,11 +447,9 @@ async function processPlanchet(
     suspended: false,
   };
 
-  let withdrawalGroupFinished = false;
-
   const planchetCoinPub = planchet.coinPub;
 
-  const success = await ws.db.runWithWriteTransaction(
+  const firstSuccess = await ws.db.runWithWriteTransaction(
     [Stores.coins, Stores.withdrawalGroups, Stores.reserves, Stores.planchets],
     async (tx) => {
       const ws = await tx.get(Stores.withdrawalGroups, withdrawalGroupId);
@@ -370,64 +457,21 @@ async function processPlanchet(
         return false;
       }
       const p = await tx.get(Stores.planchets, planchetCoinPub);
-      if (!p) {
-        return false;
-      }
-      if (p.withdrawalDone) {
-        // Already withdrawn
+      if (!p || p.withdrawalDone) {
         return false;
       }
       p.withdrawalDone = true;
       await tx.put(Stores.planchets, p);
-
-      let numTotal = 0;
-
-      for (const ds of ws.denomsSel.selectedDenoms) {
-        numTotal += ds.count;
-      }
-
-      let numDone = 0;
-
-      await tx
-        .iterIndexed(Stores.planchets.byGroup, withdrawalGroupId)
-        .forEach((x) => {
-          if (x.withdrawalDone) {
-            numDone++;
-          }
-        });
-
-      if (numDone > numTotal) {
-        throw Error(
-          "invariant violated (created more planchets than expected)",
-        );
-      }
-
-      if (numDone == numTotal) {
-        ws.timestampFinish = getTimestampNow();
-        ws.lastError = undefined;
-        ws.retryInfo = initRetryInfo(false);
-        withdrawalGroupFinished = true;
-      }
-      await tx.put(Stores.withdrawalGroups, ws);
       await tx.add(Stores.coins, coin);
       return true;
     },
   );
 
-  logger.trace(`withdrawal result stored in DB`);
-
-  if (success) {
+  if (firstSuccess) {
     ws.notify({
       type: NotificationType.CoinWithdrawn,
     });
   }
-
-  if (withdrawalGroupFinished) {
-    ws.notify({
-      type: NotificationType.WithdrawGroupFinished,
-      withdrawalSource: withdrawalGroup.source,
-    });
-  }
 }
 
 export function denomSelectionInfoToState(
@@ -552,27 +596,6 @@ async function resetWithdrawalGroupRetry(
   });
 }
 
-async function processInBatches(
-  workGen: Iterator<Promise<void>>,
-  batchSize: number,
-): Promise<void> {
-  for (;;) {
-    const batch: Promise<void>[] = [];
-    for (let i = 0; i < batchSize; i++) {
-      const wn = workGen.next();
-      if (wn.done) {
-        break;
-      }
-      batch.push(wn.value);
-    }
-    if (batch.length == 0) {
-      break;
-    }
-    logger.trace(`processing withdrawal batch of ${batch.length} elements`);
-    await Promise.all(batch);
-  }
-}
-
 async function processWithdrawGroupImpl(
   ws: InternalWalletState,
   withdrawalGroupId: string,
@@ -591,21 +614,79 @@ async function processWithdrawGroupImpl(
     return;
   }
 
-  const numDenoms = withdrawalGroup.denomsSel.selectedDenoms.length;
-  const genWork = function* (): Iterator<Promise<void>> {
-    let coinIdx = 0;
-    for (let i = 0; i < numDenoms; i++) {
-      const count = withdrawalGroup.denomsSel.selectedDenoms[i].count;
-      for (let j = 0; j < count; j++) {
-        yield processPlanchet(ws, withdrawalGroupId, coinIdx);
-        coinIdx++;
-      }
+  const numTotalCoins = withdrawalGroup.denomsSel.selectedDenoms
+    .map((x) => x.count)
+    .reduce((a, b) => a + b);
+
+  let work: Promise<void>[] = [];
+
+  for (let i = 0; i < numTotalCoins; i++) {
+    work.push(processPlanchetGenerate(ws, withdrawalGroupId, i));
+  }
+
+  // Generate coins concurrently (parallelism only happens in the crypto API 
workers)
+  await Promise.all(work);
+
+  work = [];
+
+  for (let coinIdx = 0; coinIdx < numTotalCoins; coinIdx++) {
+    const resp = await processPlanchetExchangeRequest(
+      ws,
+      withdrawalGroupId,
+      coinIdx,
+    );
+    if (!resp) {
+      continue;
     }
-  };
+    work.push(
+      processPlanchetVerifyAndStoreCoin(ws, withdrawalGroupId, coinIdx, resp),
+    );
+  }
 
-  // Withdraw coins in batches.
-  // The batch size is relatively large
-  await processInBatches(genWork(), 10);
+  await Promise.all(work);
+
+  let numFinished = 0;
+  let finishedForFirstTime = false;
+
+  await ws.db.runWithWriteTransaction(
+    [Stores.coins, Stores.withdrawalGroups, Stores.reserves, Stores.planchets],
+    async (tx) => {
+      const ws = await tx.get(Stores.withdrawalGroups, withdrawalGroupId);
+      if (!ws) {
+        return;
+      }
+
+      await tx
+        .iterIndexed(Stores.planchets.byGroup, withdrawalGroupId)
+        .forEach((x) => {
+          if (x.withdrawalDone) {
+            numFinished++;
+          }
+        });
+
+      if (ws.timestampFinish === undefined && numFinished == numTotalCoins) {
+        finishedForFirstTime = true;
+        ws.timestampFinish = getTimestampNow();
+        ws.lastError = undefined;
+        ws.retryInfo = initRetryInfo(false);
+      }
+      await tx.put(Stores.withdrawalGroups, ws);
+    },
+  );
+
+  if (numFinished != numTotalCoins) {
+    // FIXME: aggregate individual problems into the big error message here.
+    throw Error(
+      `withdrawal did not finish (${numFinished} / ${numTotalCoins} coins 
withdrawn)`,
+    );
+  }
+
+  if (finishedForFirstTime) {
+    ws.notify({
+      type: NotificationType.WithdrawGroupFinished,
+      withdrawalSource: withdrawalGroup.source,
+    });
+  }
 }
 
 export async function getExchangeWithdrawalInfo(
diff --git a/packages/taler-wallet-core/src/types/dbTypes.ts 
b/packages/taler-wallet-core/src/types/dbTypes.ts
index 26cf6915..3c4c2a25 100644
--- a/packages/taler-wallet-core/src/types/dbTypes.ts
+++ b/packages/taler-wallet-core/src/types/dbTypes.ts
@@ -662,6 +662,8 @@ export interface PlanchetRecord {
 
   withdrawalDone: boolean;
 
+  lastError: OperationErrorDetails | undefined;
+
   /**
    * Public key of the reserve, this might be a reserve not
    * known to the wallet if the planchet is from a tip.
@@ -1504,12 +1506,6 @@ export interface WithdrawalGroupRecord {
    */
   retryInfo: RetryInfo;
 
-  /**
-   * Last error per coin/planchet, or undefined if no error occured for
-   * the coin/planchet.
-   */
-  lastErrorPerCoin: { [coinIndex: number]: OperationErrorDetails };
-
   lastError: OperationErrorDetails | undefined;
 }
 

-- 
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]