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: allow peer-pull


From: gnunet
Subject: [taler-wallet-core] branch master updated: wallet-core: allow peer-pull with coins locked behind refresh
Date: Thu, 04 Apr 2024 20:48:29 +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 ab724bdbd wallet-core: allow peer-pull with coins locked behind refresh
ab724bdbd is described below

commit ab724bdbd2059484335211662b63a9ae415a270c
Author: Florian Dold <florian@dold.me>
AuthorDate: Thu Apr 4 20:48:19 2024 +0200

    wallet-core: allow peer-pull with coins locked behind refresh
---
 .../test-wallet-blocked-pay-peer-pull.ts           | 177 +++++++++++++++++++++
 .../src/integrationtests/testrunner.ts             |   2 +
 .../taler-wallet-core/src/pay-peer-pull-debit.ts   | 145 +++++++++++++----
 3 files changed, 293 insertions(+), 31 deletions(-)

diff --git 
a/packages/taler-harness/src/integrationtests/test-wallet-blocked-pay-peer-pull.ts
 
b/packages/taler-harness/src/integrationtests/test-wallet-blocked-pay-peer-pull.ts
new file mode 100644
index 000000000..36a6fea05
--- /dev/null
+++ 
b/packages/taler-harness/src/integrationtests/test-wallet-blocked-pay-peer-pull.ts
@@ -0,0 +1,177 @@
+/*
+ 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 {
+  AbsoluteTime,
+  AmountString,
+  Duration,
+  NotificationType,
+  TransactionMajorState,
+  TransactionMinorState,
+  TransactionType,
+  j2s,
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { CoinConfig } from "../harness/denomStructures.js";
+import { GlobalTestState } from "../harness/harness.js";
+import {
+  createSimpleTestkudosEnvironmentV2,
+  createWalletDaemonWithClient,
+  makeTestPaymentV2,
+  withdrawViaBankV2,
+} from "../harness/helpers.js";
+
+const coinCommon = {
+  cipher: "RSA" as const,
+  durationLegal: "3 years",
+  durationSpend: "2 years",
+  durationWithdraw: "7 days",
+  feeDeposit: "TESTKUDOS:0",
+  feeRefresh: "TESTKUDOS:0",
+  feeRefund: "TESTKUDOS:0",
+  feeWithdraw: "TESTKUDOS:0",
+  rsaKeySize: 1024,
+};
+
+/**
+ * Run test for a peer push payment with balance locked behind a pending 
refresh.
+ */
+export async function runWalletBlockedPayPeerPullTest(t: GlobalTestState) {
+  // Set up test environment
+
+  const coinConfigList: CoinConfig[] = [
+    {
+      ...coinCommon,
+      name: "n1",
+      value: "TESTKUDOS:1",
+    },
+    {
+      ...coinCommon,
+      name: "n5",
+      value: "TESTKUDOS:5",
+    },
+  ];
+
+  const { bank, exchange, merchant } = await 
createSimpleTestkudosEnvironmentV2(
+    t,
+    coinConfigList,
+  );
+
+  // Withdraw digital cash into the wallet.
+
+  const { walletClient: w1 } = await createWalletDaemonWithClient(t, {
+    name: "w1",
+    persistent: true,
+    config: {
+      testing: {
+        devModeActive: true,
+      },
+    },
+  });
+
+  const { walletClient: w2 } = await createWalletDaemonWithClient(t, {
+    name: "w2",
+    persistent: true,
+    config: {
+      testing: {
+        devModeActive: true,
+      },
+    },
+  });
+
+  await withdrawViaBankV2(t, {
+    walletClient: w1,
+    bank,
+    exchange,
+    amount: "TESTKUDOS:20",
+  });
+
+  await w1.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
+
+  // Prevent the wallet from doing refreshes by injecting a 5xx
+  // status for all refresh requests.
+  await w1.call(WalletApiOperation.ApplyDevExperiment, {
+    devExperimentUri: "taler://dev-experiment/start-block-refresh",
+  });
+
+  // Do a payment that causes a refresh.
+  await makeTestPaymentV2(t, {
+    merchant,
+    walletClient: w1,
+    order: {
+      summary: "test",
+      amount: "TESTKUDOS:2",
+    },
+  });
+
+  await w2.call(WalletApiOperation.AddExchange, {
+    exchangeBaseUrl: exchange.baseUrl,
+  });
+
+  const pullCreditReadyCond = w2.waitForNotificationCond((n) => {
+    return (
+      n.type === NotificationType.TransactionStateTransition &&
+      n.transactionId.startsWith("txn:peer-pull-credit:") &&
+      n.newTxState.major === TransactionMajorState.Pending &&
+      n.newTxState.minor === TransactionMinorState.Ready
+    );
+  });
+
+  const initResp = await w2.call(WalletApiOperation.InitiatePeerPullCredit, {
+    partialContractTerms: {
+      summary: "hi!",
+      amount: "TESTKUDOS:18" as AmountString,
+      purse_expiration: AbsoluteTime.toProtocolTimestamp(
+        AbsoluteTime.addDuration(
+          AbsoluteTime.now(),
+          Duration.fromSpec({ hours: 1 }),
+        ),
+      ),
+    },
+  });
+
+  await pullCreditReadyCond;
+
+  const initTx = await w2.call(WalletApiOperation.GetTransactionById, {
+    transactionId: initResp.transactionId,
+  });
+
+  t.assertDeepEqual(initTx.type, TransactionType.PeerPullCredit);
+  t.assertTrue(!!initTx.talerUri);
+
+  const checkResp = await w1.call(WalletApiOperation.PreparePeerPullDebit, {
+    talerUri: initTx.talerUri,
+  });
+
+  console.log(`check resp ${j2s(checkResp)}`);
+
+  const confirmResp = await w1.call(WalletApiOperation.ConfirmPeerPullDebit, {
+    transactionId: checkResp.transactionId,
+  });
+
+  console.log(`confirm resp ${j2s(confirmResp)}`);
+
+  await w1.call(WalletApiOperation.ApplyDevExperiment, {
+    devExperimentUri: "taler://dev-experiment/stop-block-refresh",
+  });
+
+  await w1.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
+}
+
+runWalletBlockedPayPeerPullTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/testrunner.ts 
b/packages/taler-harness/src/integrationtests/testrunner.ts
index acc9f5e29..9841cb37b 100644
--- a/packages/taler-harness/src/integrationtests/testrunner.ts
+++ b/packages/taler-harness/src/integrationtests/testrunner.ts
@@ -92,6 +92,7 @@ import { runWalletBalanceZeroTest } from 
"./test-wallet-balance-zero.js";
 import { runWalletBalanceTest } from "./test-wallet-balance.js";
 import { runWalletBlockedDepositTest } from "./test-wallet-blocked-deposit.js";
 import { runWalletBlockedPayMerchantTest } from 
"./test-wallet-blocked-pay-merchant.js";
+import { runWalletBlockedPayPeerPullTest } from 
"./test-wallet-blocked-pay-peer-pull.js";
 import { runWalletBlockedPayPeerPushTest } from 
"./test-wallet-blocked-pay-peer-push.js";
 import { runWalletCliTerminationTest } from "./test-wallet-cli-termination.js";
 import { runWalletConfigTest } from "./test-wallet-config.js";
@@ -218,6 +219,7 @@ const allTests: TestMainFunction[] = [
   runWalletBlockedDepositTest,
   runWalletBlockedPayMerchantTest,
   runWalletBlockedPayPeerPushTest,
+  runWalletBlockedPayPeerPullTest,
 ];
 
 export interface TestRunSpec {
diff --git a/packages/taler-wallet-core/src/pay-peer-pull-debit.ts 
b/packages/taler-wallet-core/src/pay-peer-pull-debit.ts
index 9bfa14ca2..705317eb6 100644
--- a/packages/taler-wallet-core/src/pay-peer-pull-debit.ts
+++ b/packages/taler-wallet-core/src/pay-peer-pull-debit.ts
@@ -37,6 +37,7 @@ import {
   PreparePeerPullDebitRequest,
   PreparePeerPullDebitResponse,
   RefreshReason,
+  SelectedProspectiveCoin,
   TalerError,
   TalerErrorCode,
   TalerPreciseTimestamp,
@@ -427,8 +428,88 @@ async function processPeerPullDebitPendingDeposit(
   const pursePub = peerPullInc.pursePub;
 
   const coinSel = peerPullInc.coinSel;
+
   if (!coinSel) {
-    throw Error("invalid state, no coins selected");
+    const instructedAmount = Amounts.parseOrThrow(peerPullInc.amount);
+
+    const coinSelRes = await selectPeerCoins(wex, {
+      instructedAmount,
+    });
+    if (logger.shouldLogTrace()) {
+      logger.trace(`selected p2p coins (pull): ${j2s(coinSelRes)}`);
+    }
+
+    let coins: SelectedProspectiveCoin[] | undefined = undefined;
+
+    switch (coinSelRes.type) {
+      case "failure":
+        throw TalerError.fromDetail(
+          TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE,
+          {
+            insufficientBalanceDetails: coinSelRes.insufficientBalanceDetails,
+          },
+        );
+      case "prospective":
+        throw Error("insufficient balance (locked behind refresh)");
+      case "success":
+        coins = coinSelRes.result.coins;
+        break;
+      default:
+        assertUnreachable(coinSelRes);
+    }
+
+    const peerPullDebitId = peerPullInc.peerPullDebitId;
+    const totalAmount = await getTotalPeerPaymentCost(wex, coins);
+
+    // FIXME: Missing notification here!
+
+    const transitionDone = await wex.db.runReadWriteTx(
+      [
+        "exchanges",
+        "coins",
+        "denominations",
+        "refreshGroups",
+        "refreshSessions",
+        "peerPullDebit",
+        "coinAvailability",
+      ],
+      async (tx) => {
+        const pi = await tx.peerPullDebit.get(peerPullDebitId);
+        if (!pi) {
+          return false;
+        }
+        if (pi.status !== PeerPullDebitRecordStatus.PendingDeposit) {
+          return false;
+        }
+        if (pi.coinSel) {
+          return false;
+        }
+        await spendCoins(wex, tx, {
+          // allocationId: `txn:peer-pull-debit:${req.peerPullDebitId}`,
+          allocationId: constructTransactionIdentifier({
+            tag: TransactionType.PeerPullDebit,
+            peerPullDebitId,
+          }),
+          coinPubs: coinSelRes.result.coins.map((x) => x.coinPub),
+          contributions: coinSelRes.result.coins.map((x) =>
+            Amounts.parseOrThrow(x.contribution),
+          ),
+          refreshReason: RefreshReason.PayPeerPull,
+        });
+        pi.coinSel = {
+          coinPubs: coinSelRes.result.coins.map((x) => x.coinPub),
+          contributions: coinSelRes.result.coins.map((x) => x.contribution),
+          totalCost: Amounts.stringify(totalAmount),
+        };
+        await tx.peerPullDebit.put(pi);
+        return true;
+      },
+    );
+    if (transitionDone) {
+      return TaskRunResult.progress();
+    } else {
+      return TaskRunResult.backoff();
+    }
   }
 
   const coins = await queryCoinInfosForSelection(wex, coinSel);
@@ -595,8 +676,6 @@ export async function confirmPeerPullDebit(
 
   const instructedAmount = Amounts.parseOrThrow(peerPullInc.amount);
 
-  // FIXME: Select coins once with pending coins, once without.
-
   const coinSelRes = await selectPeerCoins(wex, {
     instructedAmount,
   });
@@ -604,6 +683,8 @@ export async function confirmPeerPullDebit(
     logger.trace(`selected p2p coins (pull): ${j2s(coinSelRes)}`);
   }
 
+  let coins: SelectedProspectiveCoin[] | undefined = undefined;
+
   switch (coinSelRes.type) {
     case "failure":
       throw TalerError.fromDetail(
@@ -613,19 +694,18 @@ export async function confirmPeerPullDebit(
         },
       );
     case "prospective":
-      throw Error("insufficient balance (blocked on refresh)");
+      coins = coinSelRes.result.prospectiveCoins;
+      break;
     case "success":
+      coins = coinSelRes.result.coins;
       break;
     default:
       assertUnreachable(coinSelRes);
   }
 
-  const sel = coinSelRes.result;
+  const totalAmount = await getTotalPeerPaymentCost(wex, coins);
 
-  const totalAmount = await getTotalPeerPaymentCost(
-    wex,
-    coinSelRes.result.coins,
-  );
+  // FIXME: Missing notification here!
 
   await wex.db.runReadWriteTx(
     [
@@ -638,31 +718,33 @@ export async function confirmPeerPullDebit(
       "coinAvailability",
     ],
     async (tx) => {
-      await spendCoins(wex, tx, {
-        // allocationId: `txn:peer-pull-debit:${req.peerPullDebitId}`,
-        allocationId: constructTransactionIdentifier({
-          tag: TransactionType.PeerPullDebit,
-          peerPullDebitId,
-        }),
-        coinPubs: sel.coins.map((x) => x.coinPub),
-        contributions: sel.coins.map((x) =>
-          Amounts.parseOrThrow(x.contribution),
-        ),
-        refreshReason: RefreshReason.PayPeerPull,
-      });
-
       const pi = await tx.peerPullDebit.get(peerPullDebitId);
       if (!pi) {
         throw Error();
       }
-      if (pi.status === PeerPullDebitRecordStatus.DialogProposed) {
-        pi.status = PeerPullDebitRecordStatus.PendingDeposit;
+      if (pi.status !== PeerPullDebitRecordStatus.DialogProposed) {
+        return;
+      }
+      if (coinSelRes.type == "success") {
+        await spendCoins(wex, tx, {
+          // allocationId: `txn:peer-pull-debit:${req.peerPullDebitId}`,
+          allocationId: constructTransactionIdentifier({
+            tag: TransactionType.PeerPullDebit,
+            peerPullDebitId,
+          }),
+          coinPubs: coinSelRes.result.coins.map((x) => x.coinPub),
+          contributions: coinSelRes.result.coins.map((x) =>
+            Amounts.parseOrThrow(x.contribution),
+          ),
+          refreshReason: RefreshReason.PayPeerPull,
+        });
         pi.coinSel = {
-          coinPubs: sel.coins.map((x) => x.coinPub),
-          contributions: sel.coins.map((x) => x.contribution),
+          coinPubs: coinSelRes.result.coins.map((x) => x.coinPub),
+          contributions: coinSelRes.result.coins.map((x) => x.contribution),
           totalCost: Amounts.stringify(totalAmount),
         };
       }
+      pi.status = PeerPullDebitRecordStatus.PendingDeposit;
       await tx.peerPullDebit.put(pi);
     },
   );
@@ -788,6 +870,8 @@ export async function preparePeerPullDebit(
     logger.trace(`selected p2p coins (pull): ${j2s(coinSelRes)}`);
   }
 
+  let coins: SelectedProspectiveCoin[] | undefined = undefined;
+
   switch (coinSelRes.type) {
     case "failure":
       throw TalerError.fromDetail(
@@ -797,17 +881,16 @@ export async function preparePeerPullDebit(
         },
       );
     case "prospective":
-      throw Error("insufficient balance (waiting on refresh)");
+      coins = coinSelRes.result.prospectiveCoins;
+      break;
     case "success":
+      coins = coinSelRes.result.coins;
       break;
     default:
       assertUnreachable(coinSelRes);
   }
 
-  const totalAmount = await getTotalPeerPaymentCost(
-    wex,
-    coinSelRes.result.coins,
-  );
+  const totalAmount = await getTotalPeerPaymentCost(wex, coins);
 
   await wex.db.runReadWriteTx(
     ["peerPullDebit", "contractTerms"],

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