gnunet-svn
[Top][All Lists]
Advanced

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

[taler-wallet-core] branch master updated (02ac6ca0b -> 5e8bd098b)


From: gnunet
Subject: [taler-wallet-core] branch master updated (02ac6ca0b -> 5e8bd098b)
Date: Tue, 03 Dec 2024 15:13:10 +0100

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

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

    from 02ac6ca0b add kyc form, testing vqf
     new 0b9ea45bf fix program input interface
     new 5e8bd098b test using multiple form in kyc measures

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:
 packages/taler-harness/src/harness/environments.ts |  13 +-
 packages/taler-harness/src/index.ts                |   8 +
 .../src/integrationtests/test-kyc-two-forms.ts     | 399 +++++++++++++++++++++
 .../src/integrationtests/testrunner.ts             |   2 +
 packages/taler-util/src/types-taler-exchange.ts    |   6 +-
 packages/taler-util/src/types-taler-kyc-aml.ts     |  15 +-
 6 files changed, 429 insertions(+), 14 deletions(-)
 create mode 100644 
packages/taler-harness/src/integrationtests/test-kyc-two-forms.ts

diff --git a/packages/taler-harness/src/harness/environments.ts 
b/packages/taler-harness/src/harness/environments.ts
index d5a4b1376..fa5d4a395 100644
--- a/packages/taler-harness/src/harness/environments.ts
+++ b/packages/taler-harness/src/harness/environments.ts
@@ -1101,9 +1101,14 @@ export async function postAmlDecision(
   t.assertDeepEqual(resp.status, HttpStatusCode.NoContent);
 }
 
+function defaultOnNotification(n: WalletNotification):void {
+  console.log("wallet-core notification", n);
+}
+
+
 export interface KycEnvOptions {
   coinConfig?: CoinConfig[];
-
+  onWalletNotification?: (n: WalletNotification) => void;
   adjustExchangeConfig?(config: Configuration): void;
 }
 
@@ -1118,6 +1123,7 @@ export interface KycTestEnv {
   merchant: MerchantService;
 }
 
+
 export async function createKycTestkudosEnvironment(
   t: GlobalTestState,
   opts: KycEnvOptions = {},
@@ -1206,12 +1212,11 @@ export async function createKycTestkudosEnvironment(
   await walletService.start();
   await walletService.pingUntilAvailable();
 
+
   const walletClient = new WalletClient({
     name: "wallet",
     unixPath: walletService.socketPath,
-    onNotification(n) {
-      console.log("wallet-core notification", n);
-    },
+    onNotification: opts.onWalletNotification ?? defaultOnNotification,
   });
   await walletClient.connect();
   await walletClient.client.call(WalletApiOperation.InitWallet, {
diff --git a/packages/taler-harness/src/index.ts 
b/packages/taler-harness/src/index.ts
index 4de5b1527..de2b27a39 100644
--- a/packages/taler-harness/src/index.ts
+++ b/packages/taler-harness/src/index.ts
@@ -83,6 +83,7 @@ import {
 import { getTestInfo, runTests } from "./integrationtests/testrunner.js";
 import { lintExchangeDeployment } from "./lint.js";
 import { AML_PROGRAM_FROM_ATTRIBUTES_TO_CONTEXT } from 
"integrationtests/test-kyc-skip-expiration.js";
+import { AML_PROGRAM_NEXT_MEASURE_FORM } from 
"integrationtests/test-kyc-two-forms.js";
 
 const logger = new Logger("taler-harness:index.ts");
 
@@ -1451,12 +1452,14 @@ const allAmlPrograms: 
TalerKycAml.AmlProgramDefinition[] = [
     requiredContext: [],
   },
   AML_PROGRAM_FROM_ATTRIBUTES_TO_CONTEXT,
+  AML_PROGRAM_NEXT_MEASURE_FORM,
 ];
 
 amlProgramCli
   .subcommand("run", "run-program")
   .requiredOption("name", ["-n", "--name"], clk.STRING)
   .flag("requires", ["-r"])
+  .flag("inputs", ["-i"])
   .flag("attributes", ["-a"])
   .maybeOption("config", ["-c", "--config"], clk.STRING)
   .action(async (args) => {
@@ -1473,6 +1476,11 @@ amlProgramCli
       console.log(found.requiredContext.join("\n"));
       return;
     }
+    if (args.run.inputs) {
+      logger.info("Reporting requirements");
+      console.log(found.requiredInputs.join("\n"));
+      return;
+    }
 
     if (args.run.attributes) {
       logger.info("reporting attributes");
diff --git a/packages/taler-harness/src/integrationtests/test-kyc-two-forms.ts 
b/packages/taler-harness/src/integrationtests/test-kyc-two-forms.ts
new file mode 100644
index 000000000..b3fb2fcf8
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-kyc-two-forms.ts
@@ -0,0 +1,399 @@
+/*
+ This file is part of GNU Taler
+ (C) 2024 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 {
+  AbsoluteTime,
+  AmountString,
+  amountToBuffer,
+  buildSigPS,
+  codecForAccountKycStatus,
+  codecForAny,
+  codecForKycProcessClientInformation,
+  codecForLegitimizationNeededResponse,
+  codecOptional,
+  Configuration,
+  createNewWalletKycAccount,
+  decodeCrock,
+  eddsaSign,
+  encodeCrock,
+  j2s,
+  Logger,
+  signAmlQuery,
+  TalerKycAml,
+  TalerProtocolTimestamp,
+  TalerSignaturePurpose,
+  TransactionIdStr,
+  TransactionMajorState,
+  TransactionMinorState,
+  WalletKycRequest,
+} from "@gnu-taler/taler-util";
+import { readResponseJsonOrThrow } from "@gnu-taler/taler-util/http";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import {
+  createKycTestkudosEnvironment,
+  postAmlDecision,
+  withdrawViaBankV3,
+} from "../harness/environments.js";
+import { GlobalTestState, harnessHttpLib, waitMs } from 
"../harness/harness.js";
+
+const logger = new Logger("test-kyc-two-forms.ts");
+
+export const AML_PROGRAM_NEXT_MEASURE_FORM: TalerKycAml.AmlProgramDefinition = 
{
+  name: "TWO_FORMS",
+  logic: (input, config) => {
+    const outcome: TalerKycAml.AmlOutcome = {
+      to_investigate: false,
+      properties: {},
+      events: [],
+      new_rules: {
+        expiration_time: { t_s: 1 },
+        rules: [],
+        successor_measure: "M2",
+        custom_measures: {},
+      },
+    };
+    logger.info("aml program TWO FORMS outcome", j2s(outcome));
+
+    return outcome;
+  },
+  requiredAttributes: [],
+  requiredInputs: [],
+  requiredContext: [],
+};
+
+function adjustExchangeConfig(config: Configuration) {
+  config.setString("exchange", "enable_kyc", "yes");
+
+  // config.setString("KYC-RULE-R1", "operation_type", "withdraw");
+  // config.setString("KYC-RULE-R1", "enabled", "yes");
+  // config.setString("KYC-RULE-R1", "exposed", "yes");
+  // config.setString("KYC-RULE-R1", "is_and_combinator", "no");
+  // config.setString("KYC-RULE-R1", "threshold", "TESTKUDOS:5");
+  // config.setString("KYC-RULE-R1", "timeframe", "1d");
+  // config.setString("KYC-RULE-R1", "next_measures", "M1");
+
+  config.setString("KYC-RULE-R1", "operation_type", "balance");
+  config.setString("KYC-RULE-R1", "enabled", "yes");
+  config.setString("KYC-RULE-R1", "exposed", "yes");
+  config.setString("KYC-RULE-R1", "is_and_combinator", "no");
+  config.setString("KYC-RULE-R1", "threshold", "TESTKUDOS:5");
+  config.setString("KYC-RULE-R1", "timeframe", "forever");
+  config.setString("KYC-RULE-R1", "next_measures", "M1");
+
+  config.setString("KYC-MEASURE-M1", "check_name", "C1");
+  config.setString("KYC-MEASURE-M1", "context", "{}");
+  config.setString("KYC-MEASURE-M1", "program", "P1");
+
+  config.setString("KYC-MEASURE-M2", "check_name", "C2");
+  config.setString("KYC-MEASURE-M2", "context", "{}");
+  config.setString("KYC-MEASURE-M2", "program", "P2");
+
+  config.setString("KYC-MEASURE-M3", "check_name", "C3");
+  config.setString("KYC-MEASURE-M3", "context", "{}");
+  config.setString("KYC-MEASURE-M3", "program", "NOP");
+
+  config.setString("KYC-MEASURE-M4", "check_name", "C4");
+  config.setString("KYC-MEASURE-M4", "context", "{}");
+  config.setString("KYC-MEASURE-M4", "program", "NOP");
+
+  config.setString("KYC-MEASURE-MF", "check_name", "SKIP");
+  config.setString("KYC-MEASURE-MF", "context", "{}");
+  config.setString("KYC-MEASURE-MF", "program", "NOP");
+
+  config.setString(
+    "AML-PROGRAM-P1",
+    "command",
+    "taler-harness aml-program run-program --name TWO_FORMS",
+  );
+  config.setString("AML-PROGRAM-P1", "enabled", "true");
+  config.setString("AML-PROGRAM-P1", "description", "remove all rules");
+  config.setString("AML-PROGRAM-P1", "description_i18n", "{}");
+  config.setString("AML-PROGRAM-P1", "fallback", "M1");
+
+  config.setString("AML-PROGRAM-P2", "command", "/bin/true");
+  config.setString("AML-PROGRAM-P2", "enabled", "true");
+  config.setString("AML-PROGRAM-P2", "description", "does nothing");
+  config.setString("AML-PROGRAM-P2", "description_i18n", "{}");
+  config.setString("AML-PROGRAM-P2", "fallback", "M1");
+
+  config.setString("AML-PROGRAM-NOP", "command", "/bin/true");
+  config.setString("AML-PROGRAM-NOP", "enabled", "true");
+  config.setString(
+    "AML-PROGRAM-NOP",
+    "description",
+    "does nothing (never used)",
+  );
+  config.setString("AML-PROGRAM-NOP", "description_i18n", "{}");
+  config.setString("AML-PROGRAM-NOP", "fallback", "MF");
+
+  config.setString("KYC-CHECK-C1", "type", "FORM");
+  config.setString("KYC-CHECK-C1", "form_name", "firstForm");
+  config.setString("KYC-CHECK-C1", "description", "starting check!");
+  config.setString("KYC-CHECK-C1", "description_i18n", "{}");
+  config.setString("KYC-CHECK-C1", "outputs", "NAME");
+  config.setString("KYC-CHECK-C1", "fallback", "MF");
+
+  config.setString("KYC-CHECK-C2", "type", "FORM");
+  config.setString("KYC-CHECK-C2", "form_name", "secondForm");
+  config.setString("KYC-CHECK-C2", "description", "final check!");
+  config.setString("KYC-CHECK-C2", "description_i18n", "{}");
+  config.setString("KYC-CHECK-C2", "outputs", "FINAL");
+  config.setString("KYC-CHECK-C2", "fallback", "MF");
+
+  config.setString("KYC-CHECK-C3", "type", "FORM");
+  config.setString("KYC-CHECK-C3", "form_name", "thirdForm");
+  config.setString(
+    "KYC-CHECK-C3",
+    "description",
+    "this is check c3 (never used)",
+  );
+  config.setString("KYC-CHECK-C3", "description_i18n", "{}");
+  config.setString("KYC-CHECK-C3", "fallback", "MF");
+
+  config.setString("KYC-CHECK-C4", "type", "FORM");
+  config.setString("KYC-CHECK-C4", "form_name", "fourthForm");
+  config.setString(
+    "KYC-CHECK-C4",
+    "description",
+    "this is check c4 (never used)",
+  );
+  config.setString("KYC-CHECK-C4", "description_i18n", "{}");
+  config.setString("KYC-CHECK-C4", "fallback", "MF");
+}
+
+/**
+ * Test setting a `new_measure` as the AML officer.
+ */
+export async function runKycTwoFormsTest(t: GlobalTestState) {
+  // Set up test environment
+
+  const { exchange, amlKeypair } = await createKycTestkudosEnvironment(t, {
+    adjustExchangeConfig,
+    onWalletNotification: () => {},
+  });
+
+  // Withdraw digital cash into the wallet.
+  let kycPaytoHash: string;
+  let accessToken: string;
+  let latestFormId: string;
+
+  // {
+  //   logger.info("step 1) Withdraw to trigger AML and 2) get access token")
+  //   const wres = await withdrawViaBankV3(t, {
+  //     amount: "TESTKUDOS:20",
+  //     bankClient,
+  //     exchange,
+  //     walletClient,
+  //   });
+
+  //   await walletClient.call(WalletApiOperation.TestingWaitTransactionState, 
{
+  //     transactionId: wres.transactionId as TransactionIdStr,
+  //     txState: {
+  //       major: TransactionMajorState.Pending,
+  //       minor: TransactionMinorState.KycRequired,
+  //     },
+  //   });
+
+  //   const txDetails = await walletClient.call(
+  //     WalletApiOperation.GetTransactionById,
+  //     {
+  //       transactionId: wres.transactionId,
+  //     },
+  //   );
+
+  //   accessToken = txDetails.kycAccessToken;
+  //   kycPaytoHash = txDetails.kycPaytoHash;
+  //   firstTransaction = wres.transactionId;
+  // }
+  const account = await createNewWalletKycAccount(new Uint8Array());
+  {
+    logger.info("step 1) Check balance to trigger AML");
+
+    const balance: AmountString = "TESTKUDOS:20";
+    const sigBlob = buildSigPS(TalerSignaturePurpose.WALLET_ACCOUNT_SETUP)
+      .put(amountToBuffer(balance))
+      .build();
+    const body: WalletKycRequest = {
+      balance,
+      reserve_pub: account.id,
+      reserve_sig: encodeCrock(eddsaSign(sigBlob, account.signingKey)),
+    };
+    const infoResp = await harnessHttpLib.fetch(
+      new URL(`kyc-wallet`, exchange.baseUrl).href,
+      {
+        method: "POST",
+        body,
+      },
+    );
+
+    t.assertDeepEqual(infoResp.status, 451);
+    const clientInfo = await readResponseJsonOrThrow(
+      infoResp,
+      codecOptional(codecForLegitimizationNeededResponse()),
+    );
+
+    t.assertTrue(clientInfo?.h_payto !== undefined);
+    kycPaytoHash = clientInfo?.h_payto;
+  }
+
+  {
+    logger.info("step 2) Get account access token");
+    const sigBlob = buildSigPS(TalerSignaturePurpose.KYC_AUTH).build();
+
+    const infoResp = await harnessHttpLib.fetch(
+      new URL(`kyc-check/${kycPaytoHash}`, exchange.baseUrl).href,
+      {
+        headers: {
+          "Account-Owner-Signature": encodeCrock(
+            eddsaSign(sigBlob, account.signingKey),
+          ),
+        },
+      },
+    );
+
+    t.assertDeepEqual(infoResp.status, 202);
+    const clientInfo = await readResponseJsonOrThrow(
+      infoResp,
+      codecOptional(codecForAccountKycStatus()),
+    );
+    t.assertTrue(clientInfo?.access_token !== undefined);
+    accessToken = clientInfo?.access_token;
+  }
+
+  // {
+  //   // step 2) Check KYC info
+  //   const infoResp = await harnessHttpLib.fetch(
+  //     new URL(`kyc-info/${accessToken}`, exchange.baseUrl).href,
+  //   );
+
+  //   const clientInfo = await readResponseJsonOrThrow(
+  //     infoResp,
+  //     codecOptional(codecForKycProcessClientInformation()),
+  //   );
+
+  //   console.log(j2s(clientInfo));
+  //   t.assertDeepEqual(infoResp.status, 200);
+  //   t.assertDeepEqual(clientInfo?.requirements.length, 1);
+  //   t.assertDeepEqual(clientInfo?.requirements[0].form, "firstForm");
+  // }
+
+  {
+    logger.info("step 3) Check KYC info, should be waiting for the first 
form");
+    const infoResp = await harnessHttpLib.fetch(
+      new URL(`kyc-info/${accessToken}?timeout_ms=1000`, 
exchange.baseUrl).href,
+    );
+    const clientInfo = await readResponseJsonOrThrow(
+      infoResp,
+      codecOptional(codecForKycProcessClientInformation()),
+    );
+
+    console.log(j2s(clientInfo));
+    t.assertDeepEqual(infoResp.status, 200);
+    t.assertDeepEqual(clientInfo?.requirements.length, 1);
+    t.assertDeepEqual(clientInfo?.requirements[0].form, "firstForm");
+    t.assertTrue(!!clientInfo?.requirements[0].id);
+    latestFormId = clientInfo?.requirements[0].id;
+  }
+
+  {
+    logger.info("step 4) Complete form");
+    const infoResp = await harnessHttpLib.fetch(
+      new URL(`kyc-upload/${latestFormId}`, exchange.baseUrl).href,
+      {
+        method: "POST",
+        headers: {
+          "Content-Type": "application/x-www-form-urlencoded",
+        },
+        body: "NAME=who",
+      },
+    );
+
+    t.assertDeepEqual(infoResp.status, 204);
+  }
+
+  {
+    logger.info(
+      "step 5) Check KYC info again, should see the second form but this time 
is too fast",
+    );
+
+    ///////////////////////////////////////////
+    // if the request is too early id result in garbage
+    // can't be ignored because browser see this
+    ///////////////////////////////////////////
+    {
+      const infoResp = await harnessHttpLib.fetch(
+        new URL(`kyc-info/${accessToken}?timeout_ms=1000`, exchange.baseUrl)
+          .href,
+      );
+      {
+        const clientInfo = await readResponseJsonOrThrow(
+          infoResp,
+          codecOptional(codecForKycProcessClientInformation()),
+        );
+
+        console.log(j2s(clientInfo));
+        // t.assertDeepEqual(infoResp.status, 200);
+        // t.assertDeepEqual(clientInfo?.requirements.length, 1);
+        // t.assertDeepEqual(clientInfo?.requirements[0].form, "secondForm");
+  
+      }
+    }
+  }
+
+  await waitMs(2000);
+  {
+    logger.info("step 6) Check KYC info again after some time, should see the 
second form");
+      //doing a second request shouldnt fail
+      const infoResp = await harnessHttpLib.fetch(
+        new URL(`kyc-info/${accessToken}?timeout_ms=1000`, exchange.baseUrl)
+          .href,
+      );
+
+      const clientInfo = await readResponseJsonOrThrow(
+        infoResp,
+        codecOptional(codecForKycProcessClientInformation()),
+      );
+
+      console.log(j2s(clientInfo));
+      t.assertDeepEqual(infoResp.status, 200);
+      t.assertDeepEqual(clientInfo?.requirements.length, 1);
+      t.assertDeepEqual(clientInfo?.requirements[0].form, "secondForm");
+  }
+
+  await waitMs(2000);
+
+  {
+    logger.info("step 6) Check KYC info again after some time, here the 
exchange fails");
+    const infoResp = await harnessHttpLib.fetch(
+      new URL(`kyc-info/${accessToken}?timeout_ms=1000`, 
exchange.baseUrl).href,
+    );
+
+    const clientInfo = await readResponseJsonOrThrow(
+      infoResp,
+      codecOptional(codecForKycProcessClientInformation()),
+    );
+
+    console.log(j2s(clientInfo));
+    t.assertDeepEqual(infoResp.status, 200);
+    t.assertDeepEqual(clientInfo?.requirements.length, 1);
+    t.assertDeepEqual(clientInfo?.requirements[0].form, "secondForm");
+  }
+}
+
+runKycTwoFormsTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/testrunner.ts 
b/packages/taler-harness/src/integrationtests/testrunner.ts
index 0816d2584..fefb35393 100644
--- a/packages/taler-harness/src/integrationtests/testrunner.ts
+++ b/packages/taler-harness/src/integrationtests/testrunner.ts
@@ -148,6 +148,7 @@ import { runWithdrawalHugeTest } from 
"./test-withdrawal-huge.js";
 import { runWithdrawalIdempotentTest } from "./test-withdrawal-idempotent.js";
 import { runWithdrawalManualTest } from "./test-withdrawal-manual.js";
 import { runWithdrawalPrepareTest } from "./test-withdrawal-prepare.js";
+import { runKycTwoFormsTest } from "./test-kyc-two-forms.js";
 
 /**
  * Test runner.
@@ -274,6 +275,7 @@ const allTests: TestMainFunction[] = [
   runKycBalanceWithdrawalTest,
   runKycNewMeasureTest,
   runKycSkipExpirationTest,
+  runKycTwoFormsTest,
   runKycDepositDepositTest,
   runKycMerchantDepositTest,
   runKycMerchantAggregateTest,
diff --git a/packages/taler-util/src/types-taler-exchange.ts 
b/packages/taler-util/src/types-taler-exchange.ts
index 346ee1ba1..6f50a5c9c 100644
--- a/packages/taler-util/src/types-taler-exchange.ts
+++ b/packages/taler-util/src/types-taler-exchange.ts
@@ -1800,7 +1800,7 @@ export interface KycProcessClientInformation {
 
   // List of available voluntary checks the client could pay for.
   // Since **vATTEST**.
-  voluntary_checks?: { [name: string]: KycCheckPublicInformation };
+  voluntary_measures?: KycRequirementInformation[];
 }
 
 declare const opaque_kycReq: unique symbol;
@@ -2648,8 +2648,8 @@ export const codecForKycProcessClientInformation =
       )
       .property("is_and_combinator", codecOptional(codecForBoolean()))
       .property(
-        "voluntary_checks",
-        codecOptional(codecForMap(codecForKycCheckPublicInformation())),
+        "voluntary_measures",
+        codecOptional(codecForList(codecForKycRequirementInformation())),
       )
       .build("TalerExchangeApi.KycProcessClientInformation");
 
diff --git a/packages/taler-util/src/types-taler-kyc-aml.ts 
b/packages/taler-util/src/types-taler-kyc-aml.ts
index 284a7d984..186f7daf2 100644
--- a/packages/taler-util/src/types-taler-kyc-aml.ts
+++ b/packages/taler-util/src/types-taler-kyc-aml.ts
@@ -20,6 +20,7 @@ import {
   codecForAccountProperties,
   codecForAny,
   codecForList,
+  codecOptional,
 } from "./index.js";
 import {
   AmountString,
@@ -95,15 +96,15 @@ export interface AmlProgramInput {
   // keys will match the HTML FORM field names and
   // the values will use the KycStructuredFormData
   // encoding.
-  attributes: any;
+  attributes?: any;
 
   // JSON array with the results of historic
   // AML decisions about the account.
-  aml_history: AmlHistoryEntry[];
+  aml_history?: AmlHistoryEntry[];
 
   // JSON array with the results of historic
   // KYC data about the account.
-  kyc_history: KycHistoryEntry[];
+  kyc_history?: KycHistoryEntry[];
 
   // Default KYC rules of the exchange (exposed and not exposed).
   //
@@ -340,8 +341,8 @@ export interface MeasureInformation {
 
 export const codecForAmlProgramInput = (): Codec<AmlProgramInput> =>
   buildCodecForObject<AmlProgramInput>()
-    .property("aml_history", codecForList(codecForAny()))
-    .property("kyc_history", codecForList(codecForAny()))
-    .property("attributes", codecForAccountProperties())
-    .property("context", codecForAny())
+    .property("aml_history", codecOptional( codecForList(codecForAny())))
+    .property("kyc_history", codecOptional(codecForList(codecForAny())))
+    .property("attributes", codecOptional(codecForAccountProperties()))
+    .property("context", codecOptional(codecForAny()))
     .build("AmlProgramInput");

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