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: make changes to


From: gnunet
Subject: [taler-wallet-core] branch master updated: wallet-core: make changes to available amount atomic
Date: Mon, 26 Jun 2023 19:27:51 +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 a84413648 wallet-core: make changes to available amount atomic
a84413648 is described below

commit a844136489611525726c117cb28086b854bee5c0
Author: Florian Dold <florian@dold.me>
AuthorDate: Mon Jun 26 19:27:34 2023 +0200

    wallet-core: make changes to available amount atomic
    
    W.r.t. transactions
---
 packages/taler-wallet-core/src/db.ts               | 37 ++++++++++-
 .../taler-wallet-core/src/operations/balance.ts    | 60 +++++++++++++----
 .../taler-wallet-core/src/operations/common.ts     | 39 +++++++++++
 .../taler-wallet-core/src/operations/refresh.ts    | 76 +++++++++++++++-------
 packages/taler-wallet-core/src/operations/tip.ts   |  4 +-
 .../taler-wallet-core/src/operations/withdraw.ts   | 21 ++++--
 .../taler-wallet-core/src/util/coinSelection.ts    |  2 -
 7 files changed, 187 insertions(+), 52 deletions(-)

diff --git a/packages/taler-wallet-core/src/db.ts 
b/packages/taler-wallet-core/src/db.ts
index ab2e95c23..89bb8b053 100644
--- a/packages/taler-wallet-core/src/db.ts
+++ b/packages/taler-wallet-core/src/db.ts
@@ -119,7 +119,7 @@ export const CURRENT_DB_CONFIG_KEY = "currentMainDbName";
  * backwards-compatible way or object stores and indices
  * are added.
  */
-export const WALLET_DB_MINOR_VERSION = 8;
+export const WALLET_DB_MINOR_VERSION = 9;
 
 /**
  * Ranges for operation status fields.
@@ -723,6 +723,14 @@ export interface CoinRecord {
    */
   coinSource: CoinSource;
 
+  /**
+   * Source transaction ID of the coin.
+   *
+   * Used to make the coin visible after the transaction
+   * has entered a final state.
+   */
+  sourceTransactionId?: string;
+
   /**
    * Public key of the coin.
    */
@@ -768,6 +776,14 @@ export interface CoinRecord {
    */
   status: CoinStatus;
 
+  /**
+   * Non-zero for visible.
+   *
+   * A coin is visible when it is fresh and the
+   * source transaction is in a final state.
+   */
+  visible?: number;
+
   /**
    * Information about what the coin has been allocated for.
    *
@@ -894,7 +910,7 @@ export enum RefreshCoinStatus {
    * The refresh for this coin has been frozen, because of a permanent error.
    * More info in lastErrorPerCoin.
    */
-  Frozen = OperationStatusRange.DORMANT_START + 1,
+  Failed = OperationStatusRange.DORMANT_START + 1,
 }
 
 export enum OperationStatus {
@@ -1748,7 +1764,6 @@ export interface DepositKycInfo {
   exchangeBaseUrl: string;
 }
 
-
 /**
  * Record for a deposits that the wallet observed
  * as a result of double spending, but which is not
@@ -2132,6 +2147,15 @@ export interface CoinAvailabilityRecord {
    * Number of fresh coins of this denomination that are available.
    */
   freshCoinCount: number;
+
+  /**
+   * Number of fresh coins that are available
+   * and visible, i.e. the source transaction is in
+   * a final state.
+   *
+   * (Optional for backwards compatibility, defaults to 0.)
+   */
+  visibleCoinCount?: number;
 }
 
 export interface ContractTermsRecord {
@@ -2318,6 +2342,13 @@ export const WalletStoresV1 = {
         ["exchangeBaseUrl", "denomPubHash", "maxAge", "status"],
       ),
       byCoinEvHash: describeIndex("byCoinEvHash", "coinEvHash"),
+      bySourceTransactionId: describeIndex(
+        "bySourceTransactionId",
+        "sourceTransactionId",
+        {
+          versionAdded: 9,
+        },
+      ),
     },
   ),
   reserves: describeStore(
diff --git a/packages/taler-wallet-core/src/operations/balance.ts 
b/packages/taler-wallet-core/src/operations/balance.ts
index 3ab6649d7..af88181c0 100644
--- a/packages/taler-wallet-core/src/operations/balance.ts
+++ b/packages/taler-wallet-core/src/operations/balance.ts
@@ -20,14 +20,17 @@
  * There are multiple definition of the wallet's balance.
  * We use the following terminology:
  *
- * - "available": Balance that the wallet believes will certainly be available
- *   for spending, modulo any failures of the exchange or double spending 
issues.
- *   This includes available coins *not* allocated to any
- *   spending/refresh/... operation. Pending withdrawals are *not* counted
- *   towards this balance, because they are not certain to succeed.
- *   Pending refreshes *are* counted towards this balance.
- *   This balance type is nice to show to the user, because it does not
- *   temporarily decrease after payment when we are waiting for refreshes
+ * - "available": Balance that is available
+ *   for spending from transactions in their final state and
+ *   expected to be available from pending refreshes.
+ *
+ * - "pending-incoming": Expected (positive!) delta
+ *   to the available balance that we expect to have
+ *   after pending operations reach the "done" state.
+ *
+ * - "pending-outgoing": Amount that is currently allocated
+ *   to be spent, but the spend operation could still be aborted
+ *   and part of the pending-outgoing amount could be recovered.
  *
  * - "material": Balance that the wallet believes it could spend *right now*,
  *   without waiting for any operations to complete.
@@ -61,11 +64,13 @@ import {
   AllowedExchangeInfo,
   RefreshGroupRecord,
   WalletStoresV1,
+  WithdrawalGroupStatus,
 } from "../db.js";
 import { InternalWalletState } from "../internal-wallet-state.js";
 import { checkLogicInvariant } from "../util/invariants.js";
 import { GetReadOnlyAccess } from "../util/query.js";
 import { getExchangeDetails } from "./exchanges.js";
+import { assertUnreachable } from "../util/assertUnreachable.js";
 
 /**
  * Logger.
@@ -133,7 +138,8 @@ export async function getBalancesInsideTransaction(
 
   await tx.coinAvailability.iter().forEach((ca) => {
     const b = initBalance(ca.currency);
-    for (let i = 0; i < ca.freshCoinCount; i++) {
+    const count = ca.visibleCoinCount ?? 0;
+    for (let i = 0; i < count; i++) {
       b.available = Amounts.add(b.available, {
         currency: ca.currency,
         fraction: ca.amountFrac,
@@ -150,14 +156,40 @@ export async function getBalancesInsideTransaction(
     ).amount;
   });
 
-  await tx.withdrawalGroups.iter().forEach((wds) => {
-    if (wds.timestampFinish) {
-      return;
+  // FIXME: Use indexing to filter out final transactions.
+  await tx.withdrawalGroups.iter().forEach((wgRecord) => {
+    switch (wgRecord.status) {
+      case WithdrawalGroupStatus.AbortedBank:
+      case WithdrawalGroupStatus.AbortedExchange:
+      case WithdrawalGroupStatus.FailedAbortingBank:
+      case WithdrawalGroupStatus.FailedBankAborted:
+      case WithdrawalGroupStatus.Finished:
+        // Does not count as pendingIncoming
+        return;
+      case WithdrawalGroupStatus.PendingReady:
+      case WithdrawalGroupStatus.AbortingBank:
+      case WithdrawalGroupStatus.PendingAml:
+      case WithdrawalGroupStatus.PendingKyc:
+      case WithdrawalGroupStatus.PendingQueryingStatus:
+      case WithdrawalGroupStatus.SuspendedWaitConfirmBank:
+      case WithdrawalGroupStatus.SuspendedReady:
+      case WithdrawalGroupStatus.SuspendedRegisteringBank:
+      case WithdrawalGroupStatus.SuspendedKyc:
+      case WithdrawalGroupStatus.SuspendedAbortingBank:
+      case WithdrawalGroupStatus.SuspendedAml:
+      case WithdrawalGroupStatus.PendingRegisteringBank:
+      case WithdrawalGroupStatus.PendingWaitConfirmBank:
+      case WithdrawalGroupStatus.SuspendedQueryingStatus:
+        break;
+      default:
+        assertUnreachable(wgRecord.status);
     }
-    const b = initBalance(Amounts.currencyOf(wds.denomsSel.totalWithdrawCost));
+    const b = initBalance(
+      Amounts.currencyOf(wgRecord.denomsSel.totalWithdrawCost),
+    );
     b.pendingIncoming = Amounts.add(
       b.pendingIncoming,
-      wds.denomsSel.totalCoinValue,
+      wgRecord.denomsSel.totalCoinValue,
     ).amount;
   });
 
diff --git a/packages/taler-wallet-core/src/operations/common.ts 
b/packages/taler-wallet-core/src/operations/common.ts
index 37f1a2f22..52e4c4b53 100644
--- a/packages/taler-wallet-core/src/operations/common.ts
+++ b/packages/taler-wallet-core/src/operations/common.ts
@@ -81,6 +81,38 @@ export interface CoinsSpendInfo {
   allocationId: TransactionIdStr;
 }
 
+export async function makeCoinsVisible(
+  ws: InternalWalletState,
+  tx: GetReadWriteAccess<{
+    coins: typeof WalletStoresV1.coins;
+    coinAvailability: typeof WalletStoresV1.coinAvailability;
+  }>,
+  transactionId: string,
+): Promise<void> {
+  const coins = await tx.coins.indexes.bySourceTransactionId.getAll(
+    transactionId,
+  );
+  for (const coinRecord of coins) {
+    if (!coinRecord.visible) {
+      coinRecord.visible = 1;
+      await tx.coins.put(coinRecord);
+      const ageRestriction = coinRecord.maxAge;
+      const car = await tx.coinAvailability.get([
+        coinRecord.exchangeBaseUrl,
+        coinRecord.denomPubHash,
+        ageRestriction,
+      ]);
+      if (!car) {
+        logger.error("missing coin availability record");
+        continue;
+      }
+      const visCount = car.visibleCoinCount ?? 0;
+      car.visibleCoinCount = visCount + 1;
+      await tx.coinAvailability.put(car);
+    }
+  }
+}
+
 export async function makeCoinAvailable(
   ws: InternalWalletState,
   tx: GetReadWriteAccess<{
@@ -195,6 +227,13 @@ export async function spendCoins(
       );
     }
     coinAvailability.freshCoinCount--;
+    if (coin.visible) {
+      if (!coinAvailability.visibleCoinCount) {
+        logger.error("coin availability inconsistent");
+      } else {
+        coinAvailability.visibleCoinCount--;
+      }
+    }
     await tx.coins.put(coin);
     await tx.coinAvailability.put(coinAvailability);
   }
diff --git a/packages/taler-wallet-core/src/operations/refresh.ts 
b/packages/taler-wallet-core/src/operations/refresh.ts
index 9c9ad8bbd..b91872735 100644
--- a/packages/taler-wallet-core/src/operations/refresh.ts
+++ b/packages/taler-wallet-core/src/operations/refresh.ts
@@ -46,16 +46,20 @@ import {
   NotificationType,
   RefreshGroupId,
   RefreshReason,
+  TalerError,
   TalerErrorCode,
   TalerErrorDetail,
   TalerPreciseTimestamp,
-  TalerProtocolTimestamp,
   TransactionAction,
   TransactionMajorState,
   TransactionState,
   TransactionType,
   URL,
 } from "@gnu-taler/taler-util";
+import {
+  readSuccessResponseJsonOrThrow,
+  readUnexpectedResponseDetails,
+} from "@gnu-taler/taler-util/http";
 import { TalerCryptoInterface } from "../crypto/cryptoImplementation.js";
 import {
   DerivedRefreshSession,
@@ -72,25 +76,23 @@ import {
   RefreshReasonDetails,
   WalletStoresV1,
 } from "../db.js";
-import { TalerError } from "@gnu-taler/taler-util";
+import { isWithdrawableDenom, PendingTaskType } from "../index.js";
 import {
   EXCHANGE_COINS_LOCK,
   InternalWalletState,
 } from "../internal-wallet-state.js";
 import { assertUnreachable } from "../util/assertUnreachable.js";
-import {
-  readSuccessResponseJsonOrThrow,
-  readUnexpectedResponseDetails,
-} from "@gnu-taler/taler-util/http";
+import { selectWithdrawalDenominations } from "../util/coinSelection.js";
 import { checkDbInvariant } from "../util/invariants.js";
 import { GetReadOnlyAccess, GetReadWriteAccess } from "../util/query.js";
-import { constructTaskIdentifier, makeCoinAvailable, OperationAttemptResult, 
OperationAttemptResultType } from "./common.js";
-import { updateExchangeFromUrl } from "./exchanges.js";
-import { selectWithdrawalDenominations } from "../util/coinSelection.js";
 import {
-  isWithdrawableDenom,
-  PendingTaskType,
-} from "../index.js";
+  constructTaskIdentifier,
+  makeCoinAvailable,
+  makeCoinsVisible,
+  OperationAttemptResult,
+  OperationAttemptResultType,
+} from "./common.js";
+import { updateExchangeFromUrl } from "./exchanges.js";
 import {
   constructTransactionIdentifier,
   notifyTransition,
@@ -144,24 +146,26 @@ export function getTotalRefreshCost(
   return totalCost;
 }
 
-function updateGroupStatus(rg: RefreshGroupRecord): void {
-  const allDone = fnutil.all(
+function updateGroupStatus(rg: RefreshGroupRecord): { final: boolean } {
+  const allFinal = fnutil.all(
     rg.statusPerCoin,
-    (x) => x === RefreshCoinStatus.Finished || x === RefreshCoinStatus.Frozen,
+    (x) => x === RefreshCoinStatus.Finished || x === RefreshCoinStatus.Failed,
   );
-  const anyFrozen = fnutil.any(
+  const anyFailed = fnutil.any(
     rg.statusPerCoin,
-    (x) => x === RefreshCoinStatus.Frozen,
+    (x) => x === RefreshCoinStatus.Failed,
   );
-  if (allDone) {
-    if (anyFrozen) {
+  if (allFinal) {
+    if (anyFailed) {
       rg.timestampFinished = TalerPreciseTimestamp.now();
       rg.operationStatus = RefreshOperationStatus.Failed;
     } else {
       rg.timestampFinished = TalerPreciseTimestamp.now();
       rg.operationStatus = RefreshOperationStatus.Finished;
     }
+    return { final: true };
   }
+  return { final: false };
 }
 
 /**
@@ -248,22 +252,30 @@ async function refreshCreateSession(
     ws.config.testing.denomselAllowLate,
   );
 
+  const transactionId = constructTransactionIdentifier({
+    tag: TransactionType.Refresh,
+    refreshGroupId,
+  });
+
   if (newCoinDenoms.selectedDenoms.length === 0) {
     logger.trace(
       `not refreshing, available amount ${amountToPretty(
         availableAmount,
       )} too small`,
     );
+    // FIXME: State transition notification missing.
     await ws.db
-      .mktx((x) => [x.coins, x.refreshGroups])
+      .mktx((x) => [x.coins, x.coinAvailability, x.refreshGroups])
       .runReadWrite(async (tx) => {
         const rg = await tx.refreshGroups.get(refreshGroupId);
         if (!rg) {
           return;
         }
         rg.statusPerCoin[coinIndex] = RefreshCoinStatus.Finished;
-        updateGroupStatus(rg);
-
+        const updateRes = updateGroupStatus(rg);
+        if (updateRes.final) {
+          await makeCoinsVisible(ws, tx, transactionId);
+        }
         await tx.refreshGroups.put(rg);
       });
     return;
@@ -418,10 +430,15 @@ async function refreshMelt(
     });
   });
 
+  const transactionId = constructTransactionIdentifier({
+    tag: TransactionType.Refresh,
+    refreshGroupId,
+  });
+
   if (resp.status === HttpStatusCode.NotFound) {
     const errDetails = await readUnexpectedResponseDetails(resp);
     await ws.db
-      .mktx((x) => [x.refreshGroups])
+      .mktx((x) => [x.refreshGroups, x.coins, x.coinAvailability])
       .runReadWrite(async (tx) => {
         const rg = await tx.refreshGroups.get(refreshGroupId);
         if (!rg) {
@@ -433,9 +450,12 @@ async function refreshMelt(
         if (rg.statusPerCoin[coinIndex] !== RefreshCoinStatus.Pending) {
           return;
         }
-        rg.statusPerCoin[coinIndex] = RefreshCoinStatus.Frozen;
+        rg.statusPerCoin[coinIndex] = RefreshCoinStatus.Failed;
         rg.lastErrorPerCoin[coinIndex] = errDetails;
-        updateGroupStatus(rg);
+        const updateRes = updateGroupStatus(rg);
+        if (updateRes.final) {
+          await makeCoinsVisible(ws, tx, transactionId);
+        }
         await tx.refreshGroups.put(rg);
       });
     return;
@@ -672,6 +692,11 @@ async function refreshReveal(
 
   const coins: CoinRecord[] = [];
 
+  const transactionId = constructTransactionIdentifier({
+    tag: TransactionType.Refresh,
+    refreshGroupId,
+  });
+
   for (let i = 0; i < refreshSession.newDenoms.length; i++) {
     const ncd = newCoinDenoms[i];
     for (let j = 0; j < refreshSession.newDenoms[i].count; j++) {
@@ -701,6 +726,7 @@ async function refreshReveal(
           refreshGroupId,
           oldCoinPub: refreshGroup.oldCoinPubs[coinIndex],
         },
+        sourceTransactionId: transactionId,
         coinEvHash: pc.coinEvHash,
         maxAge: pc.maxAge,
         ageCommitmentProof: pc.ageCommitmentProof,
diff --git a/packages/taler-wallet-core/src/operations/tip.ts 
b/packages/taler-wallet-core/src/operations/tip.ts
index b43fd2e8a..1b40e36f1 100644
--- a/packages/taler-wallet-core/src/operations/tip.ts
+++ b/packages/taler-wallet-core/src/operations/tip.ts
@@ -57,7 +57,7 @@ import {
   readSuccessResponseJsonOrThrow,
 } from "@gnu-taler/taler-util/http";
 import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js";
-import { constructTaskIdentifier, makeCoinAvailable, OperationAttemptResult, 
OperationAttemptResultType } from "./common.js";
+import { constructTaskIdentifier, makeCoinAvailable, makeCoinsVisible, 
OperationAttemptResult, OperationAttemptResultType } from "./common.js";
 import { updateExchangeFromUrl } from "./exchanges.js";
 import {
   getCandidateWithdrawalDenoms,
@@ -387,6 +387,7 @@ export async function processTip(
         coinIndex: i,
         walletTipId: walletTipId,
       },
+      sourceTransactionId: transactionId,
       denomPubHash: denom.denomPubHash,
       denomSig: { cipher: DenomKeyType.Rsa, rsa_signature: denomSigRsa.sig },
       exchangeBaseUrl: tipRecord.exchangeBaseUrl,
@@ -416,6 +417,7 @@ export async function processTip(
       for (const cr of newCoinRecords) {
         await makeCoinAvailable(ws, tx, cr);
       }
+      await makeCoinsVisible(ws, tx, transactionId);
       return { oldTxState, newTxState };
     });
   notifyTransition(ws, transactionId, transitionInfo);
diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts 
b/packages/taler-wallet-core/src/operations/withdraw.ts
index 8eb7f6457..e3897f84e 100644
--- a/packages/taler-wallet-core/src/operations/withdraw.ts
+++ b/packages/taler-wallet-core/src/operations/withdraw.ts
@@ -97,6 +97,7 @@ import {
   TaskIdentifiers,
   constructTaskIdentifier,
   makeCoinAvailable,
+  makeCoinsVisible,
   makeExchangeListItem,
   runLongpollAsync,
 } from "../operations/common.js";
@@ -1029,6 +1030,11 @@ async function processPlanchetVerifyAndStoreCoin(
     return;
   }
 
+  const transactionId = constructTransactionIdentifier({
+    tag: TransactionType.Withdrawal,
+    withdrawalGroupId: wgContext.wgRecord.withdrawalGroupId,
+  });
+
   const { planchet, denomInfo } = d;
 
   const planchetDenomPub = denomInfo.denomPub;
@@ -1099,6 +1105,7 @@ async function processPlanchetVerifyAndStoreCoin(
       reservePub: withdrawalGroup.reservePub,
       withdrawalGroupId: withdrawalGroup.withdrawalGroupId,
     },
+    sourceTransactionId: transactionId,
     maxAge: withdrawalGroup.restrictAge ?? AgeRestriction.AGE_UNRESTRICTED,
     ageCommitmentProof: planchet.ageCommitmentProof,
     spendAllocation: undefined,
@@ -1111,7 +1118,7 @@ async function processPlanchetVerifyAndStoreCoin(
   // Check if this is the first time that the whole
   // withdrawal succeeded.  If so, mark the withdrawal
   // group as finished.
-  const firstSuccess = await ws.db
+  const success = await ws.db
     .mktx((x) => [
       x.coins,
       x.denominations,
@@ -1130,7 +1137,9 @@ async function processPlanchetVerifyAndStoreCoin(
       return true;
     });
 
-  ws.notify({ type: NotificationType.BalanceChange });
+  if (success) {
+    ws.notify({ type: NotificationType.BalanceChange });
+  }
 }
 
 /**
@@ -1495,10 +1504,7 @@ async function processWithdrawalGroupPendingReady(
         };
       });
     notifyTransition(ws, transactionId, transitionInfo);
-    return {
-      type: OperationAttemptResultType.Finished,
-      result: undefined,
-    };
+    return OperationAttemptResult.finishedEmpty();
   }
 
   const numTotalCoins = withdrawalGroup.denomsSel.selectedDenoms
@@ -1563,7 +1569,7 @@ async function processWithdrawalGroupPendingReady(
   const maxReportedErrors = 5;
 
   const res = await ws.db
-    .mktx((x) => [x.coins, x.withdrawalGroups, x.planchets])
+    .mktx((x) => [x.coins, x.coinAvailability, x.withdrawalGroups, 
x.planchets])
     .runReadWrite(async (tx) => {
       const wg = await tx.withdrawalGroups.get(withdrawalGroupId);
       if (!wg) {
@@ -1588,6 +1594,7 @@ async function processWithdrawalGroupPendingReady(
       if (wg.timestampFinish === undefined && numFinished === numTotalCoins) {
         wg.timestampFinish = TalerPreciseTimestamp.now();
         wg.status = WithdrawalGroupStatus.Finished;
+        await makeCoinsVisible(ws, tx, transactionId);
       }
 
       const newTxState = computeWithdrawalTransactionStatus(wg);
diff --git a/packages/taler-wallet-core/src/util/coinSelection.ts 
b/packages/taler-wallet-core/src/util/coinSelection.ts
index 26dc0dedc..d3c6ffc67 100644
--- a/packages/taler-wallet-core/src/util/coinSelection.ts
+++ b/packages/taler-wallet-core/src/util/coinSelection.ts
@@ -31,7 +31,6 @@ import {
   AmountJson,
   AmountResponse,
   Amounts,
-  AmountString,
   CoinStatus,
   ConvertAmountRequest,
   DenominationInfo,
@@ -42,7 +41,6 @@ import {
   ForcedDenomSel,
   GetAmountRequest,
   GetPlanForOperationRequest,
-  GetPlanForOperationResponse,
   j2s,
   Logger,
   parsePaytoUri,

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