gnunet-svn
[Top][All Lists]
Advanced

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

[taler-wallet-core] branch master updated: test recoup, fix bug in reser


From: gnunet
Subject: [taler-wallet-core] branch master updated: test recoup, fix bug in reserve state machine, fix bug in recoup-refresh
Date: Thu, 03 Sep 2020 22:50:25 +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 9ec6018e test recoup, fix bug in reserve state machine, fix bug in 
recoup-refresh
9ec6018e is described below

commit 9ec6018efef9b45ee42ccda33ed7093881534141
Author: Florian Dold <florian.dold@gmail.com>
AuthorDate: Fri Sep 4 02:20:20 2020 +0530

    test recoup, fix bug in reserve state machine, fix bug in recoup-refresh
---
 packages/taler-integrationtests/src/harness.ts     | 44 +++++++++++
 packages/taler-integrationtests/src/helpers.ts     | 85 +++++++++++++++++++++-
 .../taler-integrationtests/src/test-payment.ts     | 57 +++------------
 packages/taler-wallet-core/src/db.ts               |  2 +-
 .../taler-wallet-core/src/operations/recoup.ts     |  9 ++-
 .../taler-wallet-core/src/operations/reserves.ts   | 31 ++++++--
 packages/taler-wallet-core/src/types/dbTypes.ts    |  6 ++
 .../taler-wallet-core/src/types/notifications.ts   |  2 +
 .../taler-wallet-core/src/types/walletTypes.ts     | 33 +++++++++
 packages/taler-wallet-core/src/wallet.ts           | 54 ++++++++++++--
 10 files changed, 261 insertions(+), 62 deletions(-)

diff --git a/packages/taler-integrationtests/src/harness.ts 
b/packages/taler-integrationtests/src/harness.ts
index cc30df61..dbb416b4 100644
--- a/packages/taler-integrationtests/src/harness.ts
+++ b/packages/taler-integrationtests/src/harness.ts
@@ -69,6 +69,9 @@ import {
   ApplyRefundRequest,
   codecForApplyRefundResponse,
   codecForAny,
+  CoinDumpJson,
+  ForceExchangeUpdateRequest,
+  ForceRefreshRequest,
 } from "taler-wallet-core";
 import { URL } from "url";
 import axios, { AxiosError } from "axios";
@@ -1077,6 +1080,23 @@ export class ExchangeService implements 
ExchangeServiceInterface {
     );
   }
 
+  async revokeDenomination(denomPubHash: string) {
+    if (this.isRunning()) {
+      throw Error("exchange must be stopped when revoking denominations");
+    }
+    await runCommand(
+      this.globalState,
+      "exchange-keyup",
+      "taler-exchange-keyup",
+      [
+        "-c", this.configFilename,
+        ...this.timetravelArgArr,
+        "--revoke",
+        denomPubHash,
+      ],
+    );
+  }
+
   async start(): Promise<void> {
     if (this.isRunning()) {
       throw Error("exchange is already running");
@@ -1540,6 +1560,14 @@ export class WalletCli {
     throw new OperationFailedError(resp.error);
   }
 
+  async dumpCoins(): Promise<CoinDumpJson> {
+    const resp = await this.apiRequest("dumpCoins", {});
+    if (resp.type === "response") {
+      return codecForAny().decode(resp.result);
+    }
+    throw new OperationFailedError(resp.error);
+  }
+
   async addExchange(req: AddExchangeRequest): Promise<void> {
     const resp = await this.apiRequest("addExchange", req);
     if (resp.type === "response") {
@@ -1548,6 +1576,22 @@ export class WalletCli {
     throw new OperationFailedError(resp.error);
   }
 
+  async forceUpdateExchange(req: ForceExchangeUpdateRequest): Promise<void> {
+    const resp = await this.apiRequest("forceUpdateExchange", req);
+    if (resp.type === "response") {
+      return;
+    }
+    throw new OperationFailedError(resp.error);
+  }
+
+  async forceRefresh(req: ForceRefreshRequest): Promise<void> {
+    const resp = await this.apiRequest("forceRefresh", req);
+    if (resp.type === "response") {
+      return;
+    }
+    throw new OperationFailedError(resp.error);
+  }
+
   async listExchanges(): Promise<ExchangesListRespose> {
     const resp = await this.apiRequest("listExchanges", {});
     if (resp.type === "response") {
diff --git a/packages/taler-integrationtests/src/helpers.ts 
b/packages/taler-integrationtests/src/helpers.ts
index d47d5f7b..ca9b57ab 100644
--- a/packages/taler-integrationtests/src/helpers.ts
+++ b/packages/taler-integrationtests/src/helpers.ts
@@ -36,8 +36,9 @@ import {
   MerchantServiceInterface,
   BankApi,
   BankAccessApi,
+  MerchantPrivateApi,
 } from "./harness";
-import { AmountString } from "taler-wallet-core";
+import { AmountString, Duration, PreparePayResultType, ConfirmPayResultType, 
ContractTerms } from "taler-wallet-core";
 import { FaultInjectedMerchantService } from "./faultInjection";
 
 export interface SimpleTestEnvironment {
@@ -280,3 +281,85 @@ export async function withdrawViaBank(
   const balApiResp = await wallet.apiRequest("getBalances", {});
   t.assertTrue(balApiResp.type === "response");
 }
+
+export async function applyTimeTravel(
+  timetravelDuration: Duration,
+  s: {
+    exchange?: ExchangeService;
+    merchant?: MerchantService;
+    wallet?: WalletCli;
+  },
+): Promise<void> {
+  if (s.exchange) {
+    await s.exchange.stop();
+    s.exchange.setTimetravel(timetravelDuration);
+    await s.exchange.start();
+    await s.exchange.pingUntilAvailable();
+  }
+
+  if (s.merchant) {
+    await s.merchant.stop();
+    s.merchant.setTimetravel(timetravelDuration);
+    await s.merchant.start();
+    await s.merchant.pingUntilAvailable();
+  }
+
+  if (s.wallet) {
+    s.wallet.setTimetravel(timetravelDuration);
+  }
+}
+
+
+/**
+ * Make a simple payment and check that it succeeded.
+ */
+export async function makeTestPayment(t: GlobalTestState, args: {
+  merchant: MerchantServiceInterface,
+  wallet: WalletCli,
+  order: Partial<ContractTerms>,
+  instance?: string
+}): Promise<void> {
+    // Set up order.
+
+    const { wallet, merchant } = args;
+    const instance = args.instance ?? "default";
+
+    const orderResp = await MerchantPrivateApi.createOrder(merchant, instance, 
{
+      order: {
+        summary: "Buy me!",
+        amount: "TESTKUDOS:5",
+        fulfillment_url: "taler://fulfillment-success/thx",
+      },
+    });
+
+    let orderStatus = await 
MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
+      orderId: orderResp.order_id,
+    });
+
+    t.assertTrue(orderStatus.order_status === "unpaid");
+
+    // Make wallet pay for the order
+
+    const preparePayResult = await wallet.preparePay({
+      talerPayUri: orderStatus.taler_pay_uri,
+    });
+
+    t.assertTrue(
+      preparePayResult.status === PreparePayResultType.PaymentPossible,
+    );
+
+    const r2 = await wallet.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,
+    });
+
+    t.assertTrue(orderStatus.order_status === "paid");
+}
\ No newline at end of file
diff --git a/packages/taler-integrationtests/src/test-payment.ts 
b/packages/taler-integrationtests/src/test-payment.ts
index a099e9f2..4f44fc14 100644
--- a/packages/taler-integrationtests/src/test-payment.ts
+++ b/packages/taler-integrationtests/src/test-payment.ts
@@ -20,14 +20,15 @@
 import {
   runTest,
   GlobalTestState,
-  MerchantPrivateApi,
-  WalletCli,
 } from "./harness";
-import { createSimpleTestkudosEnvironment, withdrawViaBank } from "./helpers";
-import { PreparePayResultType } from "taler-wallet-core";
+import {
+  createSimpleTestkudosEnvironment,
+  withdrawViaBank,
+  makeTestPayment,
+} from "./helpers";
 
 /**
- * Run test for basic, bank-integrated withdrawal.
+ * Run test for basic, bank-integrated withdrawal and payment.
  */
 runTest(async (t: GlobalTestState) => {
   // Set up test environment
@@ -43,45 +44,11 @@ runTest(async (t: GlobalTestState) => {
 
   await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" });
 
-  // Set up order.
-
-  const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", {
-    order: {
-      summary: "Buy me!",
-      amount: "TESTKUDOS:5",
-      fulfillment_url: "taler://fulfillment-success/thx",
-    },
-  });
-
-  let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, 
{
-    orderId: orderResp.order_id,
-  });
-
-  t.assertTrue(orderStatus.order_status === "unpaid");
-
-  // Make wallet pay for the order
-
-  const preparePayResult = await wallet.preparePay({
-    talerPayUri: orderStatus.taler_pay_uri,
-  });
-
-  t.assertTrue(
-    preparePayResult.status === PreparePayResultType.PaymentPossible,
-  );
-
-  const r2 = await wallet.apiRequest("confirmPay", {
-    // FIXME: should be validated, don't cast!
-    proposalId: preparePayResult.proposalId,
-  });
-  t.assertTrue(r2.type === "response");
-
-  // Check if payment was successful.
-
-  orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
-    orderId: orderResp.order_id,
-  });
-
-  t.assertTrue(orderStatus.order_status === "paid");
+  const order = {
+    summary: "Buy me!",
+    amount: "TESTKUDOS:5",
+    fulfillment_url: "taler://fulfillment-success/thx",
+  };
 
-  await t.shutdown();
+  await makeTestPayment(t, { wallet, merchant, order });
 });
diff --git a/packages/taler-wallet-core/src/db.ts 
b/packages/taler-wallet-core/src/db.ts
index d5ebdb6c..b3203935 100644
--- a/packages/taler-wallet-core/src/db.ts
+++ b/packages/taler-wallet-core/src/db.ts
@@ -8,7 +8,7 @@ import { IDBFactory, IDBDatabase } from "idb-bridge";
  * with each major change.  When incrementing the major version,
  * the wallet should import data from the previous version.
  */
-const TALER_DB_NAME = "taler-walletdb-v9";
+const TALER_DB_NAME = "taler-walletdb-v10";
 
 /**
  * Current database minor version, should be incremented
diff --git a/packages/taler-wallet-core/src/operations/recoup.ts 
b/packages/taler-wallet-core/src/operations/recoup.ts
index 0e4ce18d..91579f60 100644
--- a/packages/taler-wallet-core/src/operations/recoup.ts
+++ b/packages/taler-wallet-core/src/operations/recoup.ts
@@ -201,6 +201,7 @@ async function recoupWithdrawCoin(
       const currency = updatedCoin.currentAmount.currency;
       updatedCoin.currentAmount = Amounts.getZero(currency);
       updatedReserve.reserveStatus = ReserveRecordStatus.QUERYING_STATUS;
+      updatedReserve.retryInfo = initRetryInfo();
       await tx.put(Stores.coins, updatedCoin);
       await tx.put(Stores.reserves, updatedReserve);
       await putGroupAsFinished(ws, tx, recoupGroup, coinIdx);
@@ -253,7 +254,13 @@ async function recoupRefreshCoin(
   }
 
   await ws.db.runWithWriteTransaction(
-    [Stores.coins, Stores.reserves, Stores.recoupGroups, Stores.refreshGroups],
+    [
+      Stores.coins,
+      Stores.denominations,
+      Stores.reserves,
+      Stores.recoupGroups,
+      Stores.refreshGroups,
+    ],
     async (tx) => {
       const recoupGroup = await tx.get(Stores.recoupGroups, recoupGroupId);
       if (!recoupGroup) {
diff --git a/packages/taler-wallet-core/src/operations/reserves.ts 
b/packages/taler-wallet-core/src/operations/reserves.ts
index 439eb34a..a28c2e0c 100644
--- a/packages/taler-wallet-core/src/operations/reserves.ts
+++ b/packages/taler-wallet-core/src/operations/reserves.ts
@@ -74,6 +74,7 @@ import {
 import {
   reconcileReserveHistory,
   summarizeReserveHistory,
+  ReserveHistorySummary,
 } from "../util/reserveHistoryUtil";
 import { TransactionHandle } from "../util/query";
 import { addPaytoQueryParams } from "../util/payto";
@@ -162,6 +163,7 @@ export async function createReserve(
     retryInfo: initRetryInfo(),
     lastError: undefined,
     currency: req.amount.currency,
+    requestedQuery: false,
   };
 
   const reserveHistoryRecord: ReserveHistoryRecord = {
@@ -285,13 +287,12 @@ export async function forceQueryReserve(
     // Only force status query where it makes sense
     switch (reserve.reserveStatus) {
       case ReserveRecordStatus.DORMANT:
-      case ReserveRecordStatus.WITHDRAWING:
-      case ReserveRecordStatus.QUERYING_STATUS:
+        reserve.reserveStatus = ReserveRecordStatus.QUERYING_STATUS;
         break;
       default:
+        reserve.requestedQuery = true;
         return;
     }
-    reserve.reserveStatus = ReserveRecordStatus.QUERYING_STATUS;
     reserve.retryInfo = initRetryInfo();
     await tx.put(Stores.reserves, reserve);
   });
@@ -551,6 +552,7 @@ async function updateReserve(
 
   const balance = Amounts.parseOrThrow(reserveInfo.balance);
   const currency = balance.currency;
+  let updateSummary: ReserveHistorySummary | undefined;
   await ws.db.runWithWriteTransaction(
     [Stores.reserves, Stores.reserveUpdatedEvents, Stores.reserveHistory],
     async (tx) => {
@@ -578,7 +580,7 @@ async function updateReserve(
         reserveInfo.history,
       );
 
-      const summary = summarizeReserveHistory(
+      updateSummary = summarizeReserveHistory(
         reconciled.updatedLocalHistory,
         currency,
       );
@@ -591,16 +593,24 @@ async function updateReserve(
           reservePub: r.reservePub,
           timestamp: getTimestampNow(),
           amountReserveBalance: Amounts.stringify(balance),
-          amountExpected: Amounts.stringify(summary.awaitedReserveAmount),
+          amountExpected: 
Amounts.stringify(updateSummary.awaitedReserveAmount),
           newHistoryTransactions,
           reserveUpdateId,
         };
         await tx.put(Stores.reserveUpdatedEvents, reserveUpdate);
+        logger.trace("setting reserve status to 'withdrawing' after query");
         r.reserveStatus = ReserveRecordStatus.WITHDRAWING;
         r.retryInfo = initRetryInfo();
       } else {
-        r.reserveStatus = ReserveRecordStatus.DORMANT;
-        r.retryInfo = initRetryInfo(false);
+        logger.trace("setting reserve status to 'dormant' after query");
+        if (r.requestedQuery) {
+          r.reserveStatus = ReserveRecordStatus.QUERYING_STATUS;
+          r.requestedQuery = false;
+          r.retryInfo = initRetryInfo();
+        } else {
+          r.reserveStatus = ReserveRecordStatus.DORMANT;
+          r.retryInfo = initRetryInfo(false);
+        }
       }
       r.lastSuccessfulStatusQuery = getTimestampNow();
       hist.reserveTransactions = reconciled.updatedLocalHistory;
@@ -609,7 +619,11 @@ async function updateReserve(
       await tx.put(Stores.reserveHistory, hist);
     },
   );
-  ws.notify({ type: NotificationType.ReserveUpdated });
+  ws.notify({ type: NotificationType.ReserveUpdated, updateSummary });
+  const reserve2 = await ws.db.get(Stores.reserves, reservePub);
+  if (reserve2) {
+    logger.trace(`after db transaction, reserve status is 
${reserve2.reserveStatus}`);
+  }
   return { ready: true };
 }
 
@@ -782,6 +796,7 @@ async function depleteReserve(
           });
         }
       }
+      logger.trace("setting reserve status to dormant after depletion");
       newReserve.reserveStatus = ReserveRecordStatus.DORMANT;
       newReserve.retryInfo = initRetryInfo(false);
 
diff --git a/packages/taler-wallet-core/src/types/dbTypes.ts 
b/packages/taler-wallet-core/src/types/dbTypes.ts
index 30a56282..45c19cbd 100644
--- a/packages/taler-wallet-core/src/types/dbTypes.ts
+++ b/packages/taler-wallet-core/src/types/dbTypes.ts
@@ -320,6 +320,12 @@ export interface ReserveRecord {
 
   reserveStatus: ReserveRecordStatus;
 
+  /**
+   * Was a reserve query requested?  If so, query again instead
+   * of going into dormant status.
+   */
+  requestedQuery: boolean;
+
   /**
    * Time of the last successful status query.
    */
diff --git a/packages/taler-wallet-core/src/types/notifications.ts 
b/packages/taler-wallet-core/src/types/notifications.ts
index 7d3795a6..7a51f0d8 100644
--- a/packages/taler-wallet-core/src/types/notifications.ts
+++ b/packages/taler-wallet-core/src/types/notifications.ts
@@ -24,6 +24,7 @@
  */
 import { TalerErrorDetails } from "./walletTypes";
 import { WithdrawalSource } from "./dbTypes";
+import { ReserveHistorySummary } from "../util/reserveHistoryUtil";
 
 export enum NotificationType {
   CoinWithdrawn = "coin-withdrawn",
@@ -126,6 +127,7 @@ export interface RefreshRefusedNotification {
 
 export interface ReserveUpdatedNotification {
   type: NotificationType.ReserveUpdated;
+  updateSummary?: ReserveHistorySummary;
 }
 
 export interface ReserveConfirmedNotification {
diff --git a/packages/taler-wallet-core/src/types/walletTypes.ts 
b/packages/taler-wallet-core/src/types/walletTypes.ts
index 5686ee61..82f29c39 100644
--- a/packages/taler-wallet-core/src/types/walletTypes.ts
+++ b/packages/taler-wallet-core/src/types/walletTypes.ts
@@ -691,6 +691,17 @@ export const codecForAddExchangeRequest = (): 
Codec<AddExchangeRequest> =>
     .property("exchangeBaseUrl", codecForString())
     .build("AddExchangeRequest");
 
+export interface ForceExchangeUpdateRequest {
+  exchangeBaseUrl: string;
+}
+
+export const codecForForceExchangeUpdateRequest = (): Codec<
+  AddExchangeRequest
+> =>
+  buildCodecForObject<AddExchangeRequest>()
+    .property("exchangeBaseUrl", codecForString())
+    .build("AddExchangeRequest");
+
 export interface GetExchangeTosRequest {
   exchangeBaseUrl: string;
 }
@@ -870,3 +881,25 @@ export const codecForApplyRefundResponse = (): 
Codec<ApplyRefundResponse> =>
     .property("pendingAtExchange", codecForBoolean())
     .property("proposalId", codecForString())
     .build("ApplyRefundResponse");
+
+export interface SetCoinSuspendedRequest {
+  coinPub: string;
+  suspended: boolean;
+}
+
+export const codecForSetCoinSuspendedRequest = (): Codec<
+  SetCoinSuspendedRequest
+> =>
+  buildCodecForObject<SetCoinSuspendedRequest>()
+    .property("coinPub", codecForString())
+    .property("suspended", codecForBoolean())
+    .build("SetCoinSuspendedRequest");
+
+export interface ForceRefreshRequest {
+  coinPubList: string[];
+}
+
+export const codecForForceRefreshRequest = (): Codec<ForceRefreshRequest> =>
+  buildCodecForObject<ForceRefreshRequest>()
+    .property("coinPubList", codecForList(codecForString()))
+    .build("ForceRefreshRequest");
diff --git a/packages/taler-wallet-core/src/wallet.ts 
b/packages/taler-wallet-core/src/wallet.ts
index 21de541e..9666665a 100644
--- a/packages/taler-wallet-core/src/wallet.ts
+++ b/packages/taler-wallet-core/src/wallet.ts
@@ -90,6 +90,9 @@ import {
   withdrawTestBalanceDefaults,
   codecForWithdrawTestBalance,
   codecForTestPayArgs,
+  codecForSetCoinSuspendedRequest,
+  codecForForceExchangeUpdateRequest,
+  codecForForceRefreshRequest,
 } from "./types/walletTypes";
 import { Logger } from "./util/logging";
 
@@ -110,7 +113,11 @@ import {
 
 import { InternalWalletState } from "./operations/state";
 import { createReserve } from "./operations/reserves";
-import { processRefreshGroup, createRefreshGroup, autoRefresh } from 
"./operations/refresh";
+import {
+  processRefreshGroup,
+  createRefreshGroup,
+  autoRefresh,
+} from "./operations/refresh";
 import { processWithdrawGroup } from "./operations/withdraw";
 import { getPendingOperations } from "./operations/pending";
 import { getBalances } from "./operations/balance";
@@ -268,7 +275,7 @@ export class Wallet {
         await processRecoupGroup(this.ws, pending.recoupGroupId, forceNow);
         break;
       case PendingOperationType.ExchangeCheckRefresh:
-        await autoRefresh(this.ws, pending.exchangeBaseUrl)
+        await autoRefresh(this.ws, pending.exchangeBaseUrl);
         break;
       default:
         assertUnreachable(pending);
@@ -371,7 +378,8 @@ export class Wallet {
   }
 
   private async runRetryLoopImpl(): Promise<void> {
-    while (!this.stopped) {
+    let iteration = 0;
+    for (; !this.stopped; iteration++) {
       const pending = await this.getPendingOperations({ onlyDue: true });
       let numDueAndLive = 0;
       for (const p of pending.pendingOperations) {
@@ -379,7 +387,9 @@ export class Wallet {
           numDueAndLive++;
         }
       }
-      if (numDueAndLive === 0) {
+      // Make sure that we run tasks that don't give lifeness at least
+      // one time.
+      if (iteration !== 0 && numDueAndLive === 0) {
         const allPending = await this.getPendingOperations({ onlyDue: false });
         let numPending = 0;
         let numGivingLiveness = 0;
@@ -406,11 +416,12 @@ export class Wallet {
           numPending,
         });
         await Promise.race([timeout, this.latch.wait()]);
-        logger.trace("timeout done");
       } else {
         // FIXME: maybe be a bit smarter about executing these
         // operations in parallel?
-        logger.trace(`running ${pending.pendingOperations.length} pending 
operations`);
+        logger.trace(
+          `running ${pending.pendingOperations.length} pending operations`,
+        );
         for (const p of pending.pendingOperations) {
           try {
             await this.processOnePendingOperation(p);
@@ -985,6 +996,11 @@ export class Wallet {
         await this.updateExchangeFromUrl(req.exchangeBaseUrl);
         return {};
       }
+      case "forceUpdateExchange": {
+        const req = codecForForceExchangeUpdateRequest().decode(payload);
+        await this.updateExchangeFromUrl(req.exchangeBaseUrl, true);
+        return {};
+      }
       case "listExchanges": {
         return await this.getExchanges();
       }
@@ -1054,6 +1070,32 @@ export class Wallet {
         const req = codecForConfirmPayRequest().decode(payload);
         return await this.confirmPay(req.proposalId, req.sessionId);
       }
+      case "dumpCoins": {
+        return await this.dumpCoins();
+      }
+      case "setCoinSuspended": {
+        const req = codecForSetCoinSuspendedRequest().decode(payload);
+        await this.setCoinSuspended(req.coinPub, req.suspended);
+        return {};
+      }
+      case "forceRefresh": {
+        const req = codecForForceRefreshRequest().decode(payload);
+        const coinPubs = req.coinPubList.map((x) => ({ coinPub: x }));
+        const refreshGroupId = await this.db.runWithWriteTransaction(
+          [Stores.refreshGroups, Stores.denominations, Stores.coins],
+          async (tx) => {
+            return await createRefreshGroup(
+              this.ws,
+              tx,
+              coinPubs,
+              RefreshReason.Manual,
+            );
+          },
+        );
+        return {
+          refreshGroupId,
+        };
+      }
     }
     throw OperationFailedError.fromCode(
       TalerErrorCode.WALLET_CORE_API_OPERATION_UNKNOWN,

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