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


From: gnunet
Subject: [taler-wallet-core] branch master updated: wallet-core: allow peer-push with coins locked behind refresh
Date: Thu, 04 Apr 2024 15:43:14 +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 e7a966f75 wallet-core: allow peer-push with coins locked behind refresh
e7a966f75 is described below

commit e7a966f755e78bf9ec200f29e49706045d1e1a54
Author: Florian Dold <florian@dold.me>
AuthorDate: Thu Apr 4 15:43:12 2024 +0200

    wallet-core: allow peer-push with coins locked behind refresh
---
 .../test-wallet-blocked-pay-merchant.ts            |   2 +-
 ...ant.ts => test-wallet-blocked-pay-peer-push.ts} |  59 ++++---
 .../src/integrationtests/testrunner.ts             |   2 +
 packages/taler-wallet-core/src/coinSelection.ts    |   9 +-
 packages/taler-wallet-core/src/db.ts               |   2 +-
 packages/taler-wallet-core/src/pay-peer-common.ts  |  14 +-
 .../taler-wallet-core/src/pay-peer-pull-debit.ts   |   3 -
 .../taler-wallet-core/src/pay-peer-push-debit.ts   | 177 +++++++++++++++------
 8 files changed, 171 insertions(+), 97 deletions(-)

diff --git 
a/packages/taler-harness/src/integrationtests/test-wallet-blocked-pay-merchant.ts
 
b/packages/taler-harness/src/integrationtests/test-wallet-blocked-pay-merchant.ts
index a74f4fd1d..15167d133 100644
--- 
a/packages/taler-harness/src/integrationtests/test-wallet-blocked-pay-merchant.ts
+++ 
b/packages/taler-harness/src/integrationtests/test-wallet-blocked-pay-merchant.ts
@@ -45,7 +45,7 @@ const coinCommon = {
 };
 
 /**
- * Run test for refreshe after a payment.
+ * Run test for paying a merchant with balance locked behind a pending refresh.
  */
 export async function runWalletBlockedPayMerchantTest(t: GlobalTestState) {
   // Set up test environment
diff --git 
a/packages/taler-harness/src/integrationtests/test-wallet-blocked-pay-merchant.ts
 
b/packages/taler-harness/src/integrationtests/test-wallet-blocked-pay-peer-push.ts
similarity index 69%
copy from 
packages/taler-harness/src/integrationtests/test-wallet-blocked-pay-merchant.ts
copy to 
packages/taler-harness/src/integrationtests/test-wallet-blocked-pay-peer-push.ts
index a74f4fd1d..7427f2b07 100644
--- 
a/packages/taler-harness/src/integrationtests/test-wallet-blocked-pay-merchant.ts
+++ 
b/packages/taler-harness/src/integrationtests/test-wallet-blocked-pay-peer-push.ts
@@ -18,8 +18,12 @@
  * Imports.
  */
 import {
-  MerchantApiClient,
-  PreparePayResultType,
+  AbsoluteTime,
+  AmountString,
+  Duration,
+  NotificationType,
+  TransactionMajorState,
+  TransactionMinorState,
   j2s,
 } from "@gnu-taler/taler-util";
 import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
@@ -45,9 +49,9 @@ const coinCommon = {
 };
 
 /**
- * Run test for refreshe after a payment.
+ * Run test for a peer push payment with balance locked behind a pending 
refresh.
  */
-export async function runWalletBlockedPayMerchantTest(t: GlobalTestState) {
+export async function runWalletBlockedPayPeerPushTest(t: GlobalTestState) {
   // Set up test environment
 
   const coinConfigList: CoinConfig[] = [
@@ -105,38 +109,41 @@ export async function runWalletBlockedPayMerchantTest(t: 
GlobalTestState) {
     },
   });
 
-  const merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl());
-
-  const orderResp = await merchantClient.createOrder({
-    order: {
-      summary: "My Payment",
-      amount: "TESTKUDOS:18",
-    },
-  });
-
-  let orderStatus = await merchantClient.queryPrivateOrderStatus({
-    orderId: orderResp.order_id,
+  const checkResp = await w1.call(WalletApiOperation.CheckPeerPushDebit, {
+    amount: "TESTKUDOS:18" as AmountString,
   });
 
-  t.assertTrue(orderStatus.order_status === "unpaid");
+  console.log(`check resp ${j2s(checkResp)}`);
 
-  // Make wallet pay for the order
+  const readyCond = w1.waitForNotificationCond((n) => {
+    return (
+      n.type === NotificationType.TransactionStateTransition &&
+      n.transactionId.startsWith("txn:peer-push-debit:") &&
+      n.newTxState.major === TransactionMajorState.Pending &&
+      n.newTxState.minor === TransactionMinorState.Ready
+    );
+  });
 
-  const preparePayResult = await w1.call(WalletApiOperation.PreparePayForUri, {
-    talerPayUri: orderStatus.taler_pay_uri,
+  const confirmResp = await w1.call(WalletApiOperation.InitiatePeerPushDebit, {
+    partialContractTerms: {
+      summary: "hi!",
+      amount: "TESTKUDOS:18" as AmountString,
+      purse_expiration: AbsoluteTime.toProtocolTimestamp(
+        AbsoluteTime.addDuration(
+          AbsoluteTime.now(),
+          Duration.fromSpec({ hours: 1 }),
+        ),
+      ),
+    },
   });
 
-  console.log(`prepare pay result: ${j2s(preparePayResult)}`);
-
-  t.assertTrue(
-    preparePayResult.status === PreparePayResultType.PaymentPossible,
-  );
+  console.log(`confirm resp ${j2s(confirmResp)}`);
 
   await w1.call(WalletApiOperation.ApplyDevExperiment, {
     devExperimentUri: "taler://dev-experiment/stop-block-refresh",
   });
 
-  await w1.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
+  await readyCond;
 }
 
-runWalletBlockedPayMerchantTest.suites = ["wallet"];
+runWalletBlockedPayPeerPushTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/testrunner.ts 
b/packages/taler-harness/src/integrationtests/testrunner.ts
index 2934e36e3..acc9f5e29 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 { runWalletBlockedPayPeerPushTest } from 
"./test-wallet-blocked-pay-peer-push.js";
 import { runWalletCliTerminationTest } from "./test-wallet-cli-termination.js";
 import { runWalletConfigTest } from "./test-wallet-config.js";
 import { runWalletCryptoWorkerTest } from "./test-wallet-cryptoworker.js";
@@ -216,6 +217,7 @@ const allTests: TestMainFunction[] = [
   runWalletDenomExpireTest,
   runWalletBlockedDepositTest,
   runWalletBlockedPayMerchantTest,
+  runWalletBlockedPayPeerPushTest,
 ];
 
 export interface TestRunSpec {
diff --git a/packages/taler-wallet-core/src/coinSelection.ts 
b/packages/taler-wallet-core/src/coinSelection.ts
index bce51fd91..0027241c4 100644
--- a/packages/taler-wallet-core/src/coinSelection.ts
+++ b/packages/taler-wallet-core/src/coinSelection.ts
@@ -1001,13 +1001,6 @@ export interface PeerCoinSelectionRequest {
    * selection instead of selecting completely new coins.
    */
   repair?: PreviousPayCoins;
-
-  /**
-   * If set to true, the coin selection will also use coins that are not
-   * materially available yet, but that are expected to become available
-   * as the output of a refresh operation.
-   */
-  includePendingCoins: boolean;
 }
 
 export async function computeCoinSelMaxExpirationDate(
@@ -1191,7 +1184,7 @@ export async function selectPeerCoins(
           false,
         );
 
-        if (!avRes && req.includePendingCoins) {
+        if (!avRes) {
           // Try to see if we can do a prospective selection
           const prospectiveAvRes = await internalSelectPeerCoins(
             wex,
diff --git a/packages/taler-wallet-core/src/db.ts 
b/packages/taler-wallet-core/src/db.ts
index a675fa8dd..6db85d8f6 100644
--- a/packages/taler-wallet-core/src/db.ts
+++ b/packages/taler-wallet-core/src/db.ts
@@ -1892,7 +1892,7 @@ export interface PeerPushDebitRecord {
 
   totalCost: AmountString;
 
-  coinSel: DbPeerPushPaymentCoinSelection;
+  coinSel?: DbPeerPushPaymentCoinSelection;
 
   contractTermsHash: HashCodeString;
 
diff --git a/packages/taler-wallet-core/src/pay-peer-common.ts 
b/packages/taler-wallet-core/src/pay-peer-common.ts
index 599010c1d..6ad8ecb70 100644
--- a/packages/taler-wallet-core/src/pay-peer-common.ts
+++ b/packages/taler-wallet-core/src/pay-peer-common.ts
@@ -22,7 +22,7 @@ import {
   AmountString,
   Amounts,
   Codec,
-  SelectedCoin,
+  SelectedProspectiveCoin,
   TalerProtocolTimestamp,
   buildCodecForObject,
   checkDbInvariant,
@@ -74,21 +74,17 @@ export async function queryCoinInfosForSelection(
 
 export async function getTotalPeerPaymentCost(
   wex: WalletExecutionContext,
-  pcs: SelectedCoin[],
+  pcs: SelectedProspectiveCoin[],
 ): Promise<AmountJson> {
   const currency = Amounts.currencyOf(pcs[0].contribution);
   return wex.db.runReadOnlyTx(["coins", "denominations"], async (tx) => {
     const costs: AmountJson[] = [];
     for (let i = 0; i < pcs.length; i++) {
-      const coin = await tx.coins.get(pcs[i].coinPub);
-      if (!coin) {
-        throw Error("can't calculate payment cost, coin not found");
-      }
       const denomInfo = await getDenomInfo(
         wex,
         tx,
-        coin.exchangeBaseUrl,
-        coin.denomPubHash,
+        pcs[i].exchangeBaseUrl,
+        pcs[i].denomPubHash,
       );
       if (!denomInfo) {
         throw Error(
@@ -98,7 +94,7 @@ export async function getTotalPeerPaymentCost(
       const allDenoms = await getCandidateWithdrawalDenomsTx(
         wex,
         tx,
-        coin.exchangeBaseUrl,
+        pcs[i].exchangeBaseUrl,
         currency,
       );
       const amountLeft = Amounts.sub(
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 2cc241187..9bfa14ca2 100644
--- a/packages/taler-wallet-core/src/pay-peer-pull-debit.ts
+++ b/packages/taler-wallet-core/src/pay-peer-pull-debit.ts
@@ -373,7 +373,6 @@ async function handlePurseCreationConflict(
   const coinSelRes = await selectPeerCoins(ws, {
     instructedAmount,
     repair,
-    includePendingCoins: false,
   });
 
   switch (coinSelRes.type) {
@@ -600,7 +599,6 @@ export async function confirmPeerPullDebit(
 
   const coinSelRes = await selectPeerCoins(wex, {
     instructedAmount,
-    includePendingCoins: false,
   });
   if (logger.shouldLogTrace()) {
     logger.trace(`selected p2p coins (pull): ${j2s(coinSelRes)}`);
@@ -785,7 +783,6 @@ export async function preparePeerPullDebit(
 
   const coinSelRes = await selectPeerCoins(wex, {
     instructedAmount,
-    includePendingCoins: true,
   });
   if (logger.shouldLogTrace()) {
     logger.trace(`selected p2p coins (pull): ${j2s(coinSelRes)}`);
diff --git a/packages/taler-wallet-core/src/pay-peer-push-debit.ts 
b/packages/taler-wallet-core/src/pay-peer-push-debit.ts
index 51b865b99..b6771be89 100644
--- a/packages/taler-wallet-core/src/pay-peer-push-debit.ts
+++ b/packages/taler-wallet-core/src/pay-peer-push-debit.ts
@@ -26,6 +26,7 @@ import {
   Logger,
   NotificationType,
   RefreshReason,
+  SelectedProspectiveCoin,
   TalerError,
   TalerErrorCode,
   TalerPreciseTimestamp,
@@ -38,6 +39,7 @@ import {
   TransactionState,
   TransactionType,
   assertUnreachable,
+  checkDbInvariant,
   checkLogicInvariant,
   encodeCrock,
   getRandomBytes,
@@ -345,8 +347,8 @@ export async function checkPeerPushDebit(
   );
   const coinSelRes = await selectPeerCoins(wex, {
     instructedAmount,
-    includePendingCoins: true,
   });
+  let coins: SelectedProspectiveCoin[] | undefined = undefined;
   switch (coinSelRes.type) {
     case "failure":
       throw TalerError.fromDetail(
@@ -356,17 +358,16 @@ export async function checkPeerPushDebit(
         },
       );
     case "prospective":
-      throw Error("not supported");
+      coins = coinSelRes.result.prospectiveCoins;
+      break;
     case "success":
+      coins = coinSelRes.result.coins;
       break;
     default:
       assertUnreachable(coinSelRes);
   }
-  logger.trace(`selected peer coins (len=${coinSelRes.result.coins.length})`);
-  const totalAmount = await getTotalPeerPaymentCost(
-    wex,
-    coinSelRes.result.coins,
-  );
+  logger.trace(`selected peer coins (len=${coins.length})`);
+  const totalAmount = await getTotalPeerPaymentCost(wex, coins);
   logger.trace("computed total peer payment cost");
   return {
     exchangeBaseUrl: coinSelRes.result.exchangeBaseUrl,
@@ -401,6 +402,8 @@ async function handlePurseCreationConflict(
   const instructedAmount = Amounts.parseOrThrow(peerPushInitiation.amount);
   const sel = peerPushInitiation.coinSel;
 
+  checkDbInvariant(!!sel);
+
   const repair: PreviousPayCoins = [];
 
   for (let i = 0; i < sel.coinPubs.length; i++) {
@@ -415,7 +418,6 @@ async function handlePurseCreationConflict(
   const coinSelRes = await selectPeerCoins(wex, {
     instructedAmount,
     repair,
-    includePendingCoins: false,
   });
 
   switch (coinSelRes.type) {
@@ -479,6 +481,75 @@ async function processPeerPushDebitCreateReserve(
     );
   }
 
+  if (!peerPushInitiation.coinSel) {
+    const coinSelRes = await selectPeerCoins(wex, {
+      instructedAmount: Amounts.parseOrThrow(peerPushInitiation.amount),
+    });
+
+    switch (coinSelRes.type) {
+      case "failure":
+        throw TalerError.fromDetail(
+          TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE,
+          {
+            insufficientBalanceDetails: coinSelRes.insufficientBalanceDetails,
+          },
+        );
+      case "prospective":
+        throw Error("insufficient funds (blocked on refresh)");
+      case "success":
+        break;
+      default:
+        assertUnreachable(coinSelRes);
+    }
+    const transitionDone = await wex.db.runReadWriteTx(
+      [
+        "exchanges",
+        "contractTerms",
+        "coins",
+        "coinAvailability",
+        "denominations",
+        "refreshGroups",
+        "refreshSessions",
+        "peerPushDebit",
+      ],
+      async (tx) => {
+        const ppi = await tx.peerPushDebit.get(pursePub);
+        if (!ppi) {
+          return false;
+        }
+        if (ppi.coinSel) {
+          return false;
+        }
+
+        ppi.coinSel = {
+          coinPubs: coinSelRes.result.coins.map((x) => x.coinPub),
+          contributions: coinSelRes.result.coins.map((x) => x.contribution),
+        };
+        // FIXME: Instead of directly doing a spendCoin here,
+        // we might want to mark the coins as used and spend them
+        // after we've been able to create the purse.
+        await spendCoins(wex, tx, {
+          allocationId: constructTransactionIdentifier({
+            tag: TransactionType.PeerPushDebit,
+            pursePub,
+          }),
+          coinPubs: coinSelRes.result.coins.map((x) => x.coinPub),
+          contributions: coinSelRes.result.coins.map((x) =>
+            Amounts.parseOrThrow(x.contribution),
+          ),
+          refreshReason: RefreshReason.PayPeerPush,
+        });
+
+        await tx.peerPushDebit.put(ppi);
+        return true;
+      },
+    );
+    if (transitionDone) {
+      return TaskRunResult.progress();
+    }
+    return TaskRunResult.backoff();
+  }
+
   const purseSigResp = await wex.cryptoApi.signPurseCreation({
     hContractTerms,
     mergePub: peerPushInitiation.mergePub,
@@ -625,6 +696,10 @@ async function processPeerPushDebitAbortingDeletePurse(
       const oldTxState = computePeerPushDebitTransactionState(ppiRec);
       const coinPubs: CoinRefreshRequest[] = [];
 
+      if (!ppiRec.coinSel) {
+        return undefined;
+      }
+
       for (let i = 0; i < ppiRec.coinSel.coinPubs.length; i++) {
         coinPubs.push({
           amount: ppiRec.coinSel.contributions[i],
@@ -859,23 +934,26 @@ async function processPeerPushDebitReady(
         const oldTxState = computePeerPushDebitTransactionState(ppiRec);
         const coinPubs: CoinRefreshRequest[] = [];
 
-        for (let i = 0; i < ppiRec.coinSel.coinPubs.length; i++) {
-          coinPubs.push({
-            amount: ppiRec.coinSel.contributions[i],
-            coinPub: ppiRec.coinSel.coinPubs[i],
-          });
+        if (ppiRec.coinSel) {
+          for (let i = 0; i < ppiRec.coinSel.coinPubs.length; i++) {
+            coinPubs.push({
+              amount: ppiRec.coinSel.contributions[i],
+              coinPub: ppiRec.coinSel.coinPubs[i],
+            });
+          }
+
+          const refresh = await createRefreshGroup(
+            wex,
+            tx,
+            currency,
+            coinPubs,
+            RefreshReason.AbortPeerPushDebit,
+            transactionId,
+          );
+
+          ppiRec.abortRefreshGroupId = refresh.refreshGroupId;
         }
-
-        const refresh = await createRefreshGroup(
-          wex,
-          tx,
-          currency,
-          coinPubs,
-          RefreshReason.AbortPeerPushDebit,
-          transactionId,
-        );
         ppiRec.status = PeerPushDebitStatus.AbortingRefreshExpired;
-        ppiRec.abortRefreshGroupId = refresh.refreshGroupId;
         await tx.peerPushDebit.put(ppiRec);
         const newTxState = computePeerPushDebitTransactionState(ppiRec);
         return {
@@ -954,12 +1032,12 @@ export async function initiatePeerPushDebit(
 
   const contractKeyPair = await wex.cryptoApi.createEddsaKeypair({});
 
-  // FIXME: Check first if possible with pending coins, in that case defer 
coin selection
   const coinSelRes = await selectPeerCoins(wex, {
     instructedAmount,
-    includePendingCoins: false,
   });
 
+  let coins: SelectedProspectiveCoin[] | undefined = undefined;
+
   switch (coinSelRes.type) {
     case "failure":
       throw TalerError.fromDetail(
@@ -969,8 +1047,10 @@ export async function initiatePeerPushDebit(
         },
       );
     case "prospective":
-      throw Error("blocked on pending refresh");
+      coins = coinSelRes.result.prospectiveCoins;
+      break;
     case "success":
+      coins = coinSelRes.result.coins;
       break;
     default:
       assertUnreachable(coinSelRes);
@@ -981,10 +1061,7 @@ export async function initiatePeerPushDebit(
   logger.info(`selected p2p coins (push):`);
   logger.trace(`${j2s(coinSelRes)}`);
 
-  const totalAmount = await getTotalPeerPaymentCost(
-    wex,
-    coinSelRes.result.coins,
-  );
+  const totalAmount = await getTotalPeerPaymentCost(wex, coins);
 
   logger.info(`computed total peer payment cost`);
 
@@ -1008,21 +1085,6 @@ export async function initiatePeerPushDebit(
       "peerPushDebit",
     ],
     async (tx) => {
-      // FIXME: Instead of directly doing a spendCoin here,
-      // we might want to mark the coins as used and spend them
-      // after we've been able to create the purse.
-      await spendCoins(wex, tx, {
-        allocationId: constructTransactionIdentifier({
-          tag: TransactionType.PeerPushDebit,
-          pursePub: pursePair.pub,
-        }),
-        coinPubs: sel.coins.map((x) => x.coinPub),
-        contributions: sel.coins.map((x) =>
-          Amounts.parseOrThrow(x.contribution),
-        ),
-        refreshReason: RefreshReason.PayPeerPush,
-      });
-
       const ppi: PeerPushDebitRecord = {
         amount: Amounts.stringify(instructedAmount),
         contractPriv: contractKeyPair.priv,
@@ -1037,13 +1099,30 @@ export async function initiatePeerPushDebit(
         timestampCreated: timestampPreciseToDb(TalerPreciseTimestamp.now()),
         status: PeerPushDebitStatus.PendingCreatePurse,
         contractEncNonce,
-        coinSel: {
-          coinPubs: sel.coins.map((x) => x.coinPub),
-          contributions: sel.coins.map((x) => x.contribution),
-        },
         totalCost: Amounts.stringify(totalAmount),
       };
 
+      if (coinSelRes.type === "success") {
+        ppi.coinSel = {
+          coinPubs: coinSelRes.result.coins.map((x) => x.coinPub),
+          contributions: coinSelRes.result.coins.map((x) => x.contribution),
+        };
+        // FIXME: Instead of directly doing a spendCoin here,
+        // we might want to mark the coins as used and spend them
+        // after we've been able to create the purse.
+        await spendCoins(wex, tx, {
+          allocationId: constructTransactionIdentifier({
+            tag: TransactionType.PeerPushDebit,
+            pursePub: pursePair.pub,
+          }),
+          coinPubs: coinSelRes.result.coins.map((x) => x.coinPub),
+          contributions: coinSelRes.result.coins.map((x) =>
+            Amounts.parseOrThrow(x.contribution),
+          ),
+          refreshReason: RefreshReason.PayPeerPush,
+        });
+      }
+
       await tx.peerPushDebit.add(ppi);
 
       await tx.contractTerms.put({

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