gnunet-svn
[Top][All Lists]
Advanced

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

[taler-wallet-core] branch master updated (9d044058 -> a66b636d)


From: gnunet
Subject: [taler-wallet-core] branch master updated (9d044058 -> a66b636d)
Date: Wed, 14 Sep 2022 21:27:07 +0200

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

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

    from 9d044058 fixing properly adding query params
     new c021876b wallet-core: cache fresh coin count in DB
     new a66b636d wallet-core: restructure denomination record for easier 
querying

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-util/src/walletTypes.ts             |   1 +
 .../src/crypto/cryptoImplementation.ts             |  15 +-
 packages/taler-wallet-core/src/db.ts               | 113 +++++---
 packages/taler-wallet-core/src/dbless.ts           |  28 +-
 .../src/operations/backup/export.ts                |  11 +-
 .../src/operations/backup/import.ts                |  46 ++--
 .../taler-wallet-core/src/operations/deposits.ts   |  26 +-
 .../taler-wallet-core/src/operations/exchanges.ts  |  15 +-
 packages/taler-wallet-core/src/operations/pay.ts   | 100 ++-----
 .../src/operations/peer-to-peer.ts                 |  46 ++--
 .../taler-wallet-core/src/operations/refresh.ts    |  29 ++-
 .../taler-wallet-core/src/operations/refund.ts     |  23 +-
 packages/taler-wallet-core/src/operations/tip.ts   |   8 +-
 .../src/operations/withdraw.test.ts                | 288 ++++++++++-----------
 .../taler-wallet-core/src/operations/withdraw.ts   |  37 ++-
 packages/taler-wallet-core/src/util/query.ts       |   5 +-
 packages/taler-wallet-core/src/wallet.ts           | 172 +++++++++++-
 17 files changed, 587 insertions(+), 376 deletions(-)

diff --git a/packages/taler-util/src/walletTypes.ts 
b/packages/taler-util/src/walletTypes.ts
index 437ac296..701049c2 100644
--- a/packages/taler-util/src/walletTypes.ts
+++ b/packages/taler-util/src/walletTypes.ts
@@ -529,6 +529,7 @@ export interface PlanchetCreationRequest {
 export enum RefreshReason {
   Manual = "manual",
   PayMerchant = "pay-merchant",
+  PayDeposit = "pay-deposit",
   PayPeerPush = "pay-peer-push",
   PayPeerPull = "pay-peer-pull",
   Refund = "refund",
diff --git a/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts 
b/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts
index 4ec24a98..9eaf1d91 100644
--- a/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts
+++ b/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts
@@ -892,17 +892,22 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
     req: DenominationValidationRequest,
   ): Promise<ValidationResult> {
     const { masterPub, denom } = req;
+    const value: AmountJson = {
+      currency: denom.currency,
+      fraction: denom.amountFrac,
+      value: denom.amountVal,
+    };
     const p = 
buildSigPS(TalerSignaturePurpose.MASTER_DENOMINATION_KEY_VALIDITY)
       .put(decodeCrock(masterPub))
       .put(timestampRoundedToBuffer(denom.stampStart))
       .put(timestampRoundedToBuffer(denom.stampExpireWithdraw))
       .put(timestampRoundedToBuffer(denom.stampExpireDeposit))
       .put(timestampRoundedToBuffer(denom.stampExpireLegal))
-      .put(amountToBuffer(denom.value))
-      .put(amountToBuffer(denom.feeWithdraw))
-      .put(amountToBuffer(denom.feeDeposit))
-      .put(amountToBuffer(denom.feeRefresh))
-      .put(amountToBuffer(denom.feeRefund))
+      .put(amountToBuffer(value))
+      .put(amountToBuffer(denom.fees.feeWithdraw))
+      .put(amountToBuffer(denom.fees.feeDeposit))
+      .put(amountToBuffer(denom.fees.feeRefresh))
+      .put(amountToBuffer(denom.fees.feeRefund))
       .put(decodeCrock(denom.denomPubHash))
       .build();
     const sig = decodeCrock(denom.masterSig);
diff --git a/packages/taler-wallet-core/src/db.ts 
b/packages/taler-wallet-core/src/db.ts
index da566ff2..2c4d5582 100644
--- a/packages/taler-wallet-core/src/db.ts
+++ b/packages/taler-wallet-core/src/db.ts
@@ -29,7 +29,6 @@ import {
   CoinDepositPermission,
   ContractTerms,
   DenominationPubKey,
-  Duration,
   ExchangeSignKeyJson,
   InternationalizedString,
   MerchantInfo,
@@ -48,6 +47,7 @@ import {
 } from "@gnu-taler/taler-util";
 import { RetryInfo } from "./util/retries.js";
 import { Event, IDBDatabase } from "@gnu-taler/idb-bridge";
+import { DenomInfo } from "./internal-wallet-state.js";
 
 /**
  * Name of the Taler database.  This is effectively the major
@@ -213,26 +213,7 @@ export enum DenominationVerificationStatus {
   VerifiedBad = "verified-bad",
 }
 
-/**
- * Denomination record as stored in the wallet's database.
- */
-export interface DenominationRecord {
-  /**
-   * Value of one coin of the denomination.
-   */
-  value: AmountJson;
-
-  /**
-   * The denomination public key.
-   */
-  denomPub: DenominationPubKey;
-
-  /**
-   * Hash of the denomination public key.
-   * Stored in the database for faster lookups.
-   */
-  denomPubHash: string;
-
+export interface DenomFees {
   /**
    * Fee for withdrawing.
    */
@@ -252,6 +233,30 @@ export interface DenominationRecord {
    * Fee for refunding.
    */
   feeRefund: AmountJson;
+}
+
+/**
+ * Denomination record as stored in the wallet's database.
+ */
+export interface DenominationRecord {
+  currency: string;
+
+  amountVal: number;
+
+  amountFrac: number;
+
+  /**
+   * The denomination public key.
+   */
+  denomPub: DenominationPubKey;
+
+  /**
+   * Hash of the denomination public key.
+   * Stored in the database for faster lookups.
+   */
+  denomPubHash: string;
+
+  fees: DenomFees;
 
   /**
    * Validity start date of the denomination.
@@ -314,6 +319,37 @@ export interface DenominationRecord {
    * that includes this denomination.
    */
   listIssueDate: TalerProtocolTimestamp;
+
+  /**
+   * Number of fresh coins of this denomination that are available.
+   */
+  freshCoinCount?: number;
+}
+
+export namespace DenominationRecord {
+  export function getValue(d: DenominationRecord): AmountJson {
+    return {
+      currency: d.currency,
+      fraction: d.amountFrac,
+      value: d.amountVal,
+    };
+  }
+
+  export function toDenomInfo(d: DenominationRecord): DenomInfo {
+    return {
+      denomPub: d.denomPub,
+      denomPubHash: d.denomPubHash,
+      feeDeposit: d.fees.feeDeposit,
+      feeRefresh: d.fees.feeRefresh,
+      feeRefund: d.fees.feeRefund,
+      feeWithdraw: d.fees.feeWithdraw,
+      stampExpireDeposit: d.stampExpireDeposit,
+      stampExpireLegal: d.stampExpireLegal,
+      stampExpireWithdraw: d.stampExpireWithdraw,
+      stampStart: d.stampStart,
+      value: DenominationRecord.getValue(d),
+    };
+  }
 }
 
 /**
@@ -520,6 +556,13 @@ export enum CoinStatus {
    * Withdrawn and never shown to anybody.
    */
   Fresh = "fresh",
+
+  /**
+   * Fresh, but currently marked as "suspended", thus won't be used
+   * for spending.  Used for testing.
+   */
+  FreshSuspended = "fresh-suspended",
+
   /**
    * A coin that has been spent and refreshed.
    */
@@ -605,11 +648,6 @@ export interface CoinRecord {
    */
   exchangeBaseUrl: string;
 
-  /**
-   * The coin is currently suspended, and will not be used for payments.
-   */
-  suspended: boolean;
-
   /**
    * Blinding key used when withdrawing the coin.
    * Potentionally used again during payback.
@@ -2024,7 +2062,10 @@ export interface DatabaseDump {
   version: string;
 }
 
-async function recoverFromDump(db: IDBDatabase, dump: DatabaseDump): 
Promise<void> {
+async function recoverFromDump(
+  db: IDBDatabase,
+  dump: DatabaseDump,
+): Promise<void> {
   return new Promise((resolve, reject) => {
     const tx = db.transaction(Array.from(db.objectStoreNames), "readwrite");
     tx.addEventListener("complete", () => {
@@ -2041,13 +2082,13 @@ async function recoverFromDump(db: IDBDatabase, dump: 
DatabaseDump): Promise<voi
       });
     }
     tx.commit();
-  })
+  });
 }
 
 export async function importDb(db: IDBDatabase, object: any): Promise<void> {
   if ("name" in object && "stores" in object && "version" in object) {
     // looks like a database dump
-    const dump = object as DatabaseDump
+    const dump = object as DatabaseDump;
     return recoverFromDump(db, dump);
   }
 
@@ -2057,10 +2098,12 @@ export async function importDb(db: IDBDatabase, object: 
any): Promise<void> {
 
     if (TALER_META_DB_NAME in someDatabase) {
       //looks like a taler database
-      const currentMainDbValue = 
someDatabase[TALER_META_DB_NAME].objectStores.metaConfig.records[0].value.value
+      const currentMainDbValue =
+        someDatabase[TALER_META_DB_NAME].objectStores.metaConfig.records[0]
+          .value.value;
 
       if (currentMainDbValue !== TALER_DB_NAME) {
-        console.log("not the current database version")
+        console.log("not the current database version");
       }
 
       const talerDb = someDatabase[currentMainDbValue];
@@ -2071,17 +2114,17 @@ export async function importDb(db: IDBDatabase, object: 
any): Promise<void> {
         name: talerDb.schema.databaseName,
         version: talerDb.schema.databaseVersion,
         stores: {},
-      }
+      };
 
       for (let i = 0; i < objectStoreNames.length; i++) {
         const name = objectStoreNames[i];
         const storeDump = {} as { [s: string]: any };
         dump.stores[name] = storeDump;
         talerDb.objectStores[name].records.map((r: any) => {
-          const pkey = r.primaryKey
-          const key = typeof pkey === "string" ? pkey : pkey.join(",")
+          const pkey = r.primaryKey;
+          const key = typeof pkey === "string" ? pkey : pkey.join(",");
           storeDump[key] = r.value;
-        })
+        });
       }
 
       return recoverFromDump(db, dump);
diff --git a/packages/taler-wallet-core/src/dbless.ts 
b/packages/taler-wallet-core/src/dbless.ts
index 3a775c3f..652ba8f5 100644
--- a/packages/taler-wallet-core/src/dbless.ts
+++ b/packages/taler-wallet-core/src/dbless.ts
@@ -48,6 +48,7 @@ import {
   UnblindedSignature,
   BankWithdrawDetails,
   parseWithdrawUri,
+  AmountJson,
 } from "@gnu-taler/taler-util";
 import { TalerCryptoInterface } from "./crypto/cryptoImplementation.js";
 import { DenominationRecord } from "./db.js";
@@ -158,11 +159,15 @@ export async function withdrawCoin(args: {
   const planchet = await cryptoApi.createPlanchet({
     coinIndex: 0,
     denomPub: denom.denomPub,
-    feeWithdraw: denom.feeWithdraw,
+    feeWithdraw: denom.fees.feeWithdraw,
     reservePriv: reserveKeyPair.reservePriv,
     reservePub: reserveKeyPair.reservePub,
     secretSeed: encodeCrock(getRandomBytes(32)),
-    value: denom.value,
+    value: {
+      currency: denom.currency,
+      fraction: denom.amountFrac,
+      value: denom.amountVal,
+    },
   });
 
   const reqBody: ExchangeWithdrawRequest = {
@@ -192,8 +197,8 @@ export async function withdrawCoin(args: {
     denomSig: ubSig,
     denomPub: denom.denomPub,
     denomPubHash: denom.denomPubHash,
-    feeDeposit: Amounts.stringify(denom.feeDeposit),
-    feeRefresh: Amounts.stringify(denom.feeRefresh),
+    feeDeposit: Amounts.stringify(denom.fees.feeDeposit),
+    feeRefresh: Amounts.stringify(denom.fees.feeRefresh),
     exchangeBaseUrl: args.exchangeBaseUrl,
   };
 }
@@ -203,7 +208,12 @@ export function findDenomOrThrow(
   amount: AmountString,
 ): DenominationRecord {
   for (const d of exchangeInfo.keys.currentDenominations) {
-    if (Amounts.cmp(d.value, amount) === 0 && isWithdrawableDenom(d)) {
+    const value: AmountJson = {
+      currency: d.currency,
+      fraction: d.amountFrac,
+      value: d.amountVal,
+    };
+    if (Amounts.cmp(value, amount) === 0 && isWithdrawableDenom(d)) {
       return d;
     }
   }
@@ -281,8 +291,12 @@ export async function refreshCoin(req: {
       count: 1,
       denomPub: x.denomPub,
       denomPubHash: x.denomPubHash,
-      feeWithdraw: x.feeWithdraw,
-      value: x.value,
+      feeWithdraw: x.fees.feeWithdraw,
+      value: {
+        currency: x.currency,
+        fraction: x.amountFrac,
+        value: x.amountVal,
+      },
     })),
   });
 
diff --git a/packages/taler-wallet-core/src/operations/backup/export.ts 
b/packages/taler-wallet-core/src/operations/backup/export.ts
index fb1fbf90..35d5e6ef 100644
--- a/packages/taler-wallet-core/src/operations/backup/export.ts
+++ b/packages/taler-wallet-core/src/operations/backup/export.ts
@@ -62,6 +62,7 @@ import {
   AbortStatus,
   CoinSourceType,
   CoinStatus,
+  DenominationRecord,
   ProposalStatus,
   RefreshCoinStatus,
   RefundState,
@@ -221,10 +222,10 @@ export async function exportBackup(
         backupDenoms.push({
           coins: backupCoinsByDenom[denom.denomPubHash] ?? [],
           denom_pub: denom.denomPub,
-          fee_deposit: Amounts.stringify(denom.feeDeposit),
-          fee_refresh: Amounts.stringify(denom.feeRefresh),
-          fee_refund: Amounts.stringify(denom.feeRefund),
-          fee_withdraw: Amounts.stringify(denom.feeWithdraw),
+          fee_deposit: Amounts.stringify(denom.fees.feeDeposit),
+          fee_refresh: Amounts.stringify(denom.fees.feeRefresh),
+          fee_refund: Amounts.stringify(denom.fees.feeRefund),
+          fee_withdraw: Amounts.stringify(denom.fees.feeWithdraw),
           is_offered: denom.isOffered,
           is_revoked: denom.isRevoked,
           master_sig: denom.masterSig,
@@ -232,7 +233,7 @@ export async function exportBackup(
           stamp_expire_legal: denom.stampExpireLegal,
           stamp_expire_withdraw: denom.stampExpireWithdraw,
           stamp_start: denom.stampStart,
-          value: Amounts.stringify(denom.value),
+          value: Amounts.stringify(DenominationRecord.getValue(denom)),
           list_issue_date: denom.listIssueDate,
         });
       });
diff --git a/packages/taler-wallet-core/src/operations/backup/import.ts 
b/packages/taler-wallet-core/src/operations/backup/import.ts
index 8f5d019d..53dc50f3 100644
--- a/packages/taler-wallet-core/src/operations/backup/import.ts
+++ b/packages/taler-wallet-core/src/operations/backup/import.ts
@@ -38,6 +38,7 @@ import {
   CoinSource,
   CoinSourceType,
   CoinStatus,
+  DenominationRecord,
   DenominationVerificationStatus,
   DenomSelectionState,
   OperationStatus,
@@ -108,7 +109,10 @@ async function recoverPayCoinSelection(
       coinRecord.denomPubHash,
     ]);
     checkBackupInvariant(!!denom);
-    totalDepositFees = Amounts.add(totalDepositFees, denom.feeDeposit).amount;
+    totalDepositFees = Amounts.add(
+      totalDepositFees,
+      denom.fees.feeDeposit,
+    ).amount;
 
     if (!coveredExchanges.has(coinRecord.exchangeBaseUrl)) {
       const exchangeDetails = await getExchangeDetails(
@@ -175,16 +179,19 @@ async function getDenomSelStateFromBackup(
     denomPubHash: string;
     count: number;
   }[] = [];
-  let totalCoinValue = Amounts.getZero(d0.value.currency);
-  let totalWithdrawCost = Amounts.getZero(d0.value.currency);
+  let totalCoinValue = Amounts.getZero(d0.currency);
+  let totalWithdrawCost = Amounts.getZero(d0.currency);
   for (const s of sel) {
     const d = await tx.denominations.get([exchangeBaseUrl, s.denom_pub_hash]);
     checkBackupInvariant(!!d);
-    totalCoinValue = Amounts.add(totalCoinValue, d.value).amount;
+    totalCoinValue = Amounts.add(
+      totalCoinValue,
+      DenominationRecord.getValue(d),
+    ).amount;
     totalWithdrawCost = Amounts.add(
       totalWithdrawCost,
-      d.value,
-      d.feeWithdraw,
+      DenominationRecord.getValue(d),
+      d.fees.feeWithdraw,
     ).amount;
   }
   return {
@@ -352,17 +359,25 @@ export async function importBackup(
               `importing backup denomination: ${j2s(backupDenomination)}`,
             );
 
+            const value = Amounts.parseOrThrow(backupDenomination.value);
+
             await tx.denominations.put({
               denomPub: backupDenomination.denom_pub,
               denomPubHash: denomPubHash,
               exchangeBaseUrl: backupExchangeDetails.base_url,
               exchangeMasterPub: backupExchangeDetails.master_public_key,
-              feeDeposit: Amounts.parseOrThrow(backupDenomination.fee_deposit),
-              feeRefresh: Amounts.parseOrThrow(backupDenomination.fee_refresh),
-              feeRefund: Amounts.parseOrThrow(backupDenomination.fee_refund),
-              feeWithdraw: Amounts.parseOrThrow(
-                backupDenomination.fee_withdraw,
-              ),
+              fees: {
+                feeDeposit: Amounts.parseOrThrow(
+                  backupDenomination.fee_deposit,
+                ),
+                feeRefresh: Amounts.parseOrThrow(
+                  backupDenomination.fee_refresh,
+                ),
+                feeRefund: Amounts.parseOrThrow(backupDenomination.fee_refund),
+                feeWithdraw: Amounts.parseOrThrow(
+                  backupDenomination.fee_withdraw,
+                ),
+              },
               isOffered: backupDenomination.is_offered,
               isRevoked: backupDenomination.is_revoked,
               masterSig: backupDenomination.master_sig,
@@ -371,7 +386,9 @@ export async function importBackup(
               stampExpireWithdraw: backupDenomination.stamp_expire_withdraw,
               stampStart: backupDenomination.stamp_start,
               verificationStatus: DenominationVerificationStatus.VerifiedGood,
-              value: Amounts.parseOrThrow(backupDenomination.value),
+              currency: value.currency,
+              amountFrac: value.fraction,
+              amountVal: value.value,
               listIssueDate: backupDenomination.list_issue_date,
             });
           }
@@ -413,7 +430,6 @@ export async function importBackup(
                 currentAmount: Amounts.parseOrThrow(backupCoin.current_amount),
                 denomSig: backupCoin.denom_sig,
                 coinPub: compCoin.coinPub,
-                suspended: false,
                 exchangeBaseUrl: backupExchangeDetails.base_url,
                 denomPubHash,
                 status: backupCoin.fresh
@@ -649,7 +665,7 @@ export async function importBackup(
               executionTime: backupRefund.execution_time,
               obtainedTime: backupRefund.obtained_time,
               refundAmount: Amounts.parseOrThrow(backupRefund.refund_amount),
-              refundFee: denom.feeRefund,
+              refundFee: denom.fees.feeRefund,
               rtransactionId: backupRefund.rtransaction_id,
               totalRefreshCostBound: Amounts.parseOrThrow(
                 backupRefund.total_refresh_cost_bound,
diff --git a/packages/taler-wallet-core/src/operations/deposits.ts 
b/packages/taler-wallet-core/src/operations/deposits.ts
index 5838be76..612de824 100644
--- a/packages/taler-wallet-core/src/operations/deposits.ts
+++ b/packages/taler-wallet-core/src/operations/deposits.ts
@@ -33,31 +33,28 @@ import {
   getRandomBytes,
   hashWire,
   Logger,
-  NotificationType,
   parsePaytoUri,
   PayCoinSelection,
   PrepareDepositRequest,
   PrepareDepositResponse,
-  TalerErrorDetail,
+  RefreshReason,
   TalerProtocolTimestamp,
   TrackDepositGroupRequest,
   TrackDepositGroupResponse,
   URL,
 } from "@gnu-taler/taler-util";
 import {
+  DenominationRecord,
   DepositGroupRecord,
-  OperationAttemptErrorResult,
   OperationAttemptResult,
   OperationStatus,
 } from "../db.js";
 import { InternalWalletState } from "../internal-wallet-state.js";
 import { selectPayCoins } from "../util/coinSelection.js";
 import { readSuccessResponseJsonOrThrow } from "../util/http.js";
-import { RetryInfo } from "../util/retries.js";
-import { guardOperationException } from "./common.js";
+import { spendCoins } from "../wallet.js";
 import { getExchangeDetails } from "./exchanges.js";
 import {
-  applyCoinSpend,
   CoinSelectionRequest,
   extractContractData,
   generateDepositPermissions,
@@ -525,12 +522,12 @@ export async function createDepositGroup(
       x.refreshGroups,
     ])
     .runReadWrite(async (tx) => {
-      await applyCoinSpend(
-        ws,
-        tx,
-        payCoinSel,
-        `deposit-group:${depositGroup.depositGroupId}`,
-      );
+      await spendCoins(ws, tx, {
+        allocationId: `deposit-group:${depositGroup.depositGroupId}`,
+        coinPubs: payCoinSel.coinPubs,
+        contributions: payCoinSel.coinContributions,
+        refreshReason: RefreshReason.PayDeposit,
+      });
       await tx.depositGroups.put(depositGroup);
     });
 
@@ -640,7 +637,10 @@ export async function getTotalFeesForDepositAmount(
         const allDenoms = await tx.denominations.indexes.byExchangeBaseUrl
           .iter(coin.exchangeBaseUrl)
           .filter((x) =>
-            Amounts.isSameCurrency(x.value, pcs.coinContributions[i]),
+            Amounts.isSameCurrency(
+              DenominationRecord.getValue(x),
+              pcs.coinContributions[i],
+            ),
           );
         const amountLeft = Amounts.sub(
           denom.value,
diff --git a/packages/taler-wallet-core/src/operations/exchanges.ts 
b/packages/taler-wallet-core/src/operations/exchanges.ts
index 50497844..ca85ff46 100644
--- a/packages/taler-wallet-core/src/operations/exchanges.ts
+++ b/packages/taler-wallet-core/src/operations/exchanges.ts
@@ -81,15 +81,18 @@ function denominationRecordFromKeys(
   let denomPub: DenominationPubKey;
   denomPub = denomIn.denom_pub;
   const denomPubHash = encodeCrock(hashDenomPub(denomPub));
+  const value = Amounts.parseOrThrow(denomIn.value);
   const d: DenominationRecord = {
     denomPub,
     denomPubHash,
     exchangeBaseUrl,
     exchangeMasterPub,
-    feeDeposit: Amounts.parseOrThrow(denomIn.fee_deposit),
-    feeRefresh: Amounts.parseOrThrow(denomIn.fee_refresh),
-    feeRefund: Amounts.parseOrThrow(denomIn.fee_refund),
-    feeWithdraw: Amounts.parseOrThrow(denomIn.fee_withdraw),
+    fees: {
+      feeDeposit: Amounts.parseOrThrow(denomIn.fee_deposit),
+      feeRefresh: Amounts.parseOrThrow(denomIn.fee_refresh),
+      feeRefund: Amounts.parseOrThrow(denomIn.fee_refund),
+      feeWithdraw: Amounts.parseOrThrow(denomIn.fee_withdraw),
+    },
     isOffered: true,
     isRevoked: false,
     masterSig: denomIn.master_sig,
@@ -98,7 +101,9 @@ function denominationRecordFromKeys(
     stampExpireWithdraw: denomIn.stamp_expire_withdraw,
     stampStart: denomIn.stamp_start,
     verificationStatus: DenominationVerificationStatus.Unverified,
-    value: Amounts.parseOrThrow(denomIn.value),
+    amountFrac: value.fraction,
+    amountVal: value.value,
+    currency: value.currency,
     listIssueDate,
   };
   return d;
diff --git a/packages/taler-wallet-core/src/operations/pay.ts 
b/packages/taler-wallet-core/src/operations/pay.ts
index 322e9048..5a0d3cee 100644
--- a/packages/taler-wallet-core/src/operations/pay.ts
+++ b/packages/taler-wallet-core/src/operations/pay.ts
@@ -100,6 +100,7 @@ import {
 } from "../util/http.js";
 import { GetReadWriteAccess } from "../util/query.js";
 import { RetryInfo, RetryTags, scheduleRetry } from "../util/retries.js";
+import { spendCoins } from "../wallet.js";
 import { getExchangeDetails } from "./exchanges.js";
 import { createRefreshGroup, getTotalRefreshCost } from "./refresh.js";
 
@@ -140,13 +141,20 @@ export async function getTotalPaymentCost(
         const allDenoms = await tx.denominations.indexes.byExchangeBaseUrl
           .iter(coin.exchangeBaseUrl)
           .filter((x) =>
-            Amounts.isSameCurrency(x.value, pcs.coinContributions[i]),
+            Amounts.isSameCurrency(
+              DenominationRecord.getValue(x),
+              pcs.coinContributions[i],
+            ),
           );
         const amountLeft = Amounts.sub(
-          denom.value,
+          DenominationRecord.getValue(denom),
           pcs.coinContributions[i],
         ).amount;
-        const refreshCost = getTotalRefreshCost(allDenoms, denom, amountLeft);
+        const refreshCost = getTotalRefreshCost(
+          allDenoms,
+          DenominationRecord.toDenomInfo(denom),
+          amountLeft,
+        );
         costs.push(pcs.coinContributions[i]);
         costs.push(refreshCost);
       }
@@ -156,9 +164,6 @@ export async function getTotalPaymentCost(
 }
 
 function isSpendableCoin(coin: CoinRecord, denom: DenominationRecord): boolean 
{
-  if (coin.suspended) {
-    return false;
-  }
   if (denom.isRevoked) {
     return false;
   }
@@ -305,7 +310,7 @@ export async function getCandidatePayCoins(
           if (!denom) {
             throw Error("db inconsistent");
           }
-          if (denom.value.currency !== currency) {
+          if (denom.currency !== currency) {
             logger.warn(
               `same pubkey for different currencies at exchange 
${exchange.baseUrl}`,
             );
@@ -316,10 +321,10 @@ export async function getCandidatePayCoins(
           }
           candidateCoins.push({
             availableAmount: coin.currentAmount,
-            value: denom.value,
+            value: DenominationRecord.getValue(denom),
             coinPub: coin.coinPub,
             denomPub: denom.denomPub,
-            feeDeposit: denom.feeDeposit,
+            feeDeposit: denom.fees.feeDeposit,
             exchangeBaseUrl: denom.exchangeBaseUrl,
             ageCommitmentProof: coin.ageCommitmentProof,
           });
@@ -347,65 +352,6 @@ export async function getCandidatePayCoins(
   };
 }
 
-/**
- * Apply a coin selection to the database.  Marks coins as spent
- * and creates a refresh session for the remaining amount.
- *
- * FIXME:  This does not deal well with conflicting spends!
- * When two payments are made in parallel, the same coin can be selected
- * for two payments.
- * However, this is a situation that can also happen via sync.
- */
-export async function applyCoinSpend(
-  ws: InternalWalletState,
-  tx: GetReadWriteAccess<{
-    coins: typeof WalletStoresV1.coins;
-    refreshGroups: typeof WalletStoresV1.refreshGroups;
-    denominations: typeof WalletStoresV1.denominations;
-  }>,
-  coinSelection: PayCoinSelection,
-  allocationId: string,
-): Promise<void> {
-  logger.info(`applying coin spend ${j2s(coinSelection)}`);
-  for (let i = 0; i < coinSelection.coinPubs.length; i++) {
-    const coin = await tx.coins.get(coinSelection.coinPubs[i]);
-    if (!coin) {
-      throw Error("coin allocated for payment doesn't exist anymore");
-    }
-    const contrib = coinSelection.coinContributions[i];
-    if (coin.status !== CoinStatus.Fresh) {
-      const alloc = coin.allocation;
-      if (!alloc) {
-        continue;
-      }
-      if (alloc.id !== allocationId) {
-        // FIXME: assign error code
-        throw Error("conflicting coin allocation (id)");
-      }
-      if (0 !== Amounts.cmp(alloc.amount, contrib)) {
-        // FIXME: assign error code
-        throw Error("conflicting coin allocation (contrib)");
-      }
-      continue;
-    }
-    coin.status = CoinStatus.Dormant;
-    coin.allocation = {
-      id: allocationId,
-      amount: Amounts.stringify(contrib),
-    };
-    const remaining = Amounts.sub(coin.currentAmount, contrib);
-    if (remaining.saturated) {
-      throw Error("not enough remaining balance on coin for payment");
-    }
-    coin.currentAmount = remaining.amount;
-    await tx.coins.put(coin);
-  }
-  const refreshCoinPubs = coinSelection.coinPubs.map((x) => ({
-    coinPub: x,
-  }));
-  await createRefreshGroup(ws, tx, refreshCoinPubs, RefreshReason.PayMerchant);
-}
-
 /**
  * Record all information that is necessary to
  * pay for a proposal in the wallet's database.
@@ -468,7 +414,12 @@ async function recordConfirmPay(
         await tx.proposals.put(p);
       }
       await tx.purchases.put(t);
-      await applyCoinSpend(ws, tx, coinSelection, `proposal:${t.proposalId}`);
+      await spendCoins(ws, tx, {
+        allocationId: `proposal:${t.proposalId}`,
+        coinPubs: coinSelection.coinPubs,
+        contributions: coinSelection.coinContributions,
+        refreshReason: RefreshReason.PayMerchant,
+      });
     });
 
   ws.notify({
@@ -1005,7 +956,7 @@ async function handleInsufficientFunds(
           coinPub,
           contribution: contrib,
           exchangeBaseUrl: coin.exchangeBaseUrl,
-          feeDeposit: denom.feeDeposit,
+          feeDeposit: denom.fees.feeDeposit,
         });
       }
     });
@@ -1038,7 +989,12 @@ async function handleInsufficientFunds(
       p.payCoinSelectionUid = encodeCrock(getRandomBytes(32));
       p.coinDepositPermissions = undefined;
       await tx.purchases.put(p);
-      await applyCoinSpend(ws, tx, res, `proposal:${p.proposalId}`);
+      await spendCoins(ws, tx, {
+        allocationId: `proposal:${p.proposalId}`,
+        coinPubs: p.payCoinSelection.coinPubs,
+        contributions: p.payCoinSelection.coinContributions,
+        refreshReason: RefreshReason.PayMerchant,
+      });
     });
 }
 
@@ -1320,7 +1276,7 @@ export async function generateDepositPermissions(
       denomKeyType: denom.denomPub.cipher,
       denomSig: coin.denomSig,
       exchangeBaseUrl: coin.exchangeBaseUrl,
-      feeDeposit: denom.feeDeposit,
+      feeDeposit: denom.fees.feeDeposit,
       merchantPub: contractData.merchantPub,
       refundDeadline: contractData.refundDeadline,
       spendAmount: payCoinSel.coinContributions[i],
diff --git a/packages/taler-wallet-core/src/operations/peer-to-peer.ts 
b/packages/taler-wallet-core/src/operations/peer-to-peer.ts
index 59dad3d5..449a91c6 100644
--- a/packages/taler-wallet-core/src/operations/peer-to-peer.ts
+++ b/packages/taler-wallet-core/src/operations/peer-to-peer.ts
@@ -75,6 +75,8 @@ import { internalCreateWithdrawalGroup } from "./withdraw.js";
 import { GetReadOnlyAccess } from "../util/query.js";
 import { createRefreshGroup } from "./refresh.js";
 import { updateExchangeFromUrl } from "./exchanges.js";
+import { spendCoins } from "../wallet.js";
+import { RetryTags } from "../util/retries.js";
 
 const logger = new Logger("operations/peer-to-peer.ts");
 
@@ -256,18 +258,14 @@ export async function initiatePeerToPeerPush(
         return undefined;
       }
 
-      const pubs: CoinPublicKey[] = [];
-      for (const c of sel.coins) {
-        const coin = await tx.coins.get(c.coinPub);
-        checkDbInvariant(!!coin);
-        coin.currentAmount = Amounts.sub(
-          coin.currentAmount,
-          Amounts.parseOrThrow(c.contribution),
-        ).amount;
-        coin.status = CoinStatus.Dormant;
-        pubs.push({ coinPub: coin.coinPub });
-        await tx.coins.put(coin);
-      }
+      await spendCoins(ws, tx, {
+        allocationId: `peer-push:${pursePair.pub}`,
+        coinPubs: sel.coins.map((x) => x.coinPub),
+        contributions: sel.coins.map((x) =>
+          Amounts.parseOrThrow(x.contribution),
+        ),
+        refreshReason: RefreshReason.PayPeerPush,
+      });
 
       await tx.peerPushPaymentInitiations.add({
         amount: Amounts.stringify(instructedAmount),
@@ -284,8 +282,6 @@ export async function initiatePeerToPeerPush(
         timestampCreated: TalerProtocolTimestamp.now(),
       });
 
-      await createRefreshGroup(ws, tx, pubs, RefreshReason.PayPeerPush);
-
       return sel;
     });
   logger.info(`selected p2p coins (push): ${j2s(coinSelRes)}`);
@@ -588,20 +584,14 @@ export async function acceptPeerPullPayment(
         return undefined;
       }
 
-      const pubs: CoinPublicKey[] = [];
-      for (const c of sel.coins) {
-        const coin = await tx.coins.get(c.coinPub);
-        checkDbInvariant(!!coin);
-        coin.currentAmount = Amounts.sub(
-          coin.currentAmount,
-          Amounts.parseOrThrow(c.contribution),
-        ).amount;
-        coin.status = CoinStatus.Dormant;
-        pubs.push({ coinPub: coin.coinPub });
-        await tx.coins.put(coin);
-      }
-
-      await createRefreshGroup(ws, tx, pubs, RefreshReason.PayPeerPull);
+      await spendCoins(ws, tx, {
+        allocationId: `peer-pull:${req.peerPullPaymentIncomingId}`,
+        coinPubs: sel.coins.map((x) => x.coinPub),
+        contributions: sel.coins.map((x) =>
+          Amounts.parseOrThrow(x.contribution),
+        ),
+        refreshReason: RefreshReason.PayPeerPull,
+      });
 
       const pi = await tx.peerPullPaymentIncoming.get(
         req.peerPullPaymentIncomingId,
diff --git a/packages/taler-wallet-core/src/operations/refresh.ts 
b/packages/taler-wallet-core/src/operations/refresh.ts
index 719093bd..2d9ad2c0 100644
--- a/packages/taler-wallet-core/src/operations/refresh.ts
+++ b/packages/taler-wallet-core/src/operations/refresh.ts
@@ -77,6 +77,7 @@ import {
 import { checkDbInvariant } from "../util/invariants.js";
 import { GetReadWriteAccess } from "../util/query.js";
 import { RetryInfo, runOperationHandlerForResult } from "../util/retries.js";
+import { makeCoinAvailable } from "../wallet.js";
 import { guardOperationException } from "./common.js";
 import { updateExchangeFromUrl } from "./exchanges.js";
 import {
@@ -111,7 +112,11 @@ export function getTotalRefreshCost(
   const resultingAmount = Amounts.add(
     Amounts.getZero(withdrawAmount.currency),
     ...withdrawDenoms.selectedDenoms.map(
-      (d) => Amounts.mult(denomMap[d.denomPubHash].value, d.count).amount,
+      (d) =>
+        Amounts.mult(
+          DenominationRecord.getValue(denomMap[d.denomPubHash]),
+          d.count,
+        ).amount,
     ),
   ).amount;
   const totalCost = Amounts.sub(amountLeft, resultingAmount).amount;
@@ -670,7 +675,6 @@ async function refreshReveal(
           type: CoinSourceType.Refresh,
           oldCoinPub: refreshGroup.oldCoinPubs[coinIndex],
         },
-        suspended: false,
         coinEvHash: pc.coinEvHash,
         ageCommitmentProof: pc.ageCommitmentProof,
       };
@@ -680,7 +684,7 @@ async function refreshReveal(
   }
 
   await ws.db
-    .mktx((x) => [x.coins, x.refreshGroups])
+    .mktx((x) => [x.coins, x.denominations, x.refreshGroups])
     .runReadWrite(async (tx) => {
       const rg = await tx.refreshGroups.get(refreshGroupId);
       if (!rg) {
@@ -694,7 +698,7 @@ async function refreshReveal(
       rg.statusPerCoin[coinIndex] = RefreshCoinStatus.Finished;
       updateGroupStatus(rg);
       for (const coin of coins) {
-        await tx.coins.put(coin);
+        await makeCoinAvailable(ws, tx, coin);
       }
       await tx.refreshGroups.put(rg);
     });
@@ -865,10 +869,22 @@ export async function createRefreshGroup(
       !!denom,
       "denomination for existing coin must be in database",
     );
+    if (coin.status !== CoinStatus.Dormant) {
+      coin.status = CoinStatus.Dormant;
+      const denom = await tx.denominations.get([
+        coin.exchangeBaseUrl,
+        coin.denomPubHash,
+      ]);
+      checkDbInvariant(!!denom);
+      checkDbInvariant(
+        denom.freshCoinCount != null && denom.freshCoinCount > 0,
+      );
+      denom.freshCoinCount--;
+      await tx.denominations.put(denom);
+    }
     const refreshAmount = coin.currentAmount;
     inputPerCoin.push(refreshAmount);
     coin.currentAmount = Amounts.getZero(refreshAmount.currency);
-    coin.status = CoinStatus.Dormant;
     await tx.coins.put(coin);
     const denoms = await getDenoms(coin.exchangeBaseUrl);
     const cost = getTotalRefreshCost(denoms, denom, refreshAmount);
@@ -965,9 +981,6 @@ export async function autoRefresh(
         if (coin.status !== CoinStatus.Fresh) {
           continue;
         }
-        if (coin.suspended) {
-          continue;
-        }
         const denom = await tx.denominations.get([
           exchangeBaseUrl,
           coin.denomPubHash,
diff --git a/packages/taler-wallet-core/src/operations/refund.ts 
b/packages/taler-wallet-core/src/operations/refund.ts
index 5ee0680d..f028dfbf 100644
--- a/packages/taler-wallet-core/src/operations/refund.ts
+++ b/packages/taler-wallet-core/src/operations/refund.ts
@@ -51,6 +51,7 @@ import {
 import {
   AbortStatus,
   CoinStatus,
+  DenominationRecord,
   OperationAttemptResult,
   PurchaseRecord,
   RefundReason,
@@ -148,7 +149,7 @@ async function applySuccessfulRefund(
   }
   refreshCoinsMap[coin.coinPub] = { coinPub: coin.coinPub };
   const refundAmount = Amounts.parseOrThrow(r.refund_amount);
-  const refundFee = denom.feeRefund;
+  const refundFee = denom.fees.feeRefund;
   coin.status = CoinStatus.Dormant;
   coin.currentAmount = Amounts.add(coin.currentAmount, refundAmount).amount;
   coin.currentAmount = Amounts.sub(coin.currentAmount, refundFee).amount;
@@ -162,12 +163,12 @@ async function applySuccessfulRefund(
   const amountLeft = Amounts.sub(
     Amounts.add(coin.currentAmount, Amounts.parseOrThrow(r.refund_amount))
       .amount,
-    denom.feeRefund,
+    denom.fees.feeRefund,
   ).amount;
 
   const totalRefreshCostBound = getTotalRefreshCost(
     allDenoms,
-    denom,
+    DenominationRecord.toDenomInfo(denom),
     amountLeft,
   );
 
@@ -176,7 +177,7 @@ async function applySuccessfulRefund(
     obtainedTime: AbsoluteTime.toTimestamp(AbsoluteTime.now()),
     executionTime: r.execution_time,
     refundAmount: Amounts.parseOrThrow(r.refund_amount),
-    refundFee: denom.feeRefund,
+    refundFee: denom.fees.feeRefund,
     totalRefreshCostBound,
     coinPub: r.coin_pub,
     rtransactionId: r.rtransaction_id,
@@ -214,12 +215,12 @@ async function storePendingRefund(
   const amountLeft = Amounts.sub(
     Amounts.add(coin.currentAmount, Amounts.parseOrThrow(r.refund_amount))
       .amount,
-    denom.feeRefund,
+    denom.fees.feeRefund,
   ).amount;
 
   const totalRefreshCostBound = getTotalRefreshCost(
     allDenoms,
-    denom,
+    DenominationRecord.toDenomInfo(denom),
     amountLeft,
   );
 
@@ -228,7 +229,7 @@ async function storePendingRefund(
     obtainedTime: AbsoluteTime.toTimestamp(AbsoluteTime.now()),
     executionTime: r.execution_time,
     refundAmount: Amounts.parseOrThrow(r.refund_amount),
-    refundFee: denom.feeRefund,
+    refundFee: denom.fees.feeRefund,
     totalRefreshCostBound,
     coinPub: r.coin_pub,
     rtransactionId: r.rtransaction_id,
@@ -267,12 +268,12 @@ async function storeFailedRefund(
   const amountLeft = Amounts.sub(
     Amounts.add(coin.currentAmount, Amounts.parseOrThrow(r.refund_amount))
       .amount,
-    denom.feeRefund,
+    denom.fees.feeRefund,
   ).amount;
 
   const totalRefreshCostBound = getTotalRefreshCost(
     allDenoms,
-    denom,
+    DenominationRecord.toDenomInfo(denom),
     amountLeft,
   );
 
@@ -281,7 +282,7 @@ async function storeFailedRefund(
     obtainedTime: TalerProtocolTimestamp.now(),
     executionTime: r.execution_time,
     refundAmount: Amounts.parseOrThrow(r.refund_amount),
-    refundFee: denom.feeRefund,
+    refundFee: denom.fees.feeRefund,
     totalRefreshCostBound,
     coinPub: r.coin_pub,
     rtransactionId: r.rtransaction_id,
@@ -314,7 +315,7 @@ async function storeFailedRefund(
         coin.currentAmount = Amounts.add(coin.currentAmount, contrib).amount;
         coin.currentAmount = Amounts.sub(
           coin.currentAmount,
-          denom.feeRefund,
+          denom.fees.feeRefund,
         ).amount;
       }
       refreshCoinsMap[coin.coinPub] = { coinPub: coin.coinPub };
diff --git a/packages/taler-wallet-core/src/operations/tip.ts 
b/packages/taler-wallet-core/src/operations/tip.ts
index 04da2b98..c8f327a5 100644
--- a/packages/taler-wallet-core/src/operations/tip.ts
+++ b/packages/taler-wallet-core/src/operations/tip.ts
@@ -51,6 +51,7 @@ import {
   readSuccessResponseJsonOrThrow,
 } from "../util/http.js";
 import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js";
+import { makeCoinAvailable } from "../wallet.js";
 import { updateExchangeFromUrl } from "./exchanges.js";
 import {
   getCandidateWithdrawalDenoms,
@@ -305,18 +306,17 @@ export async function processTip(
         coinIndex: i,
         walletTipId: walletTipId,
       },
-      currentAmount: denom.value,
+      currentAmount: DenominationRecord.getValue(denom),
       denomPubHash: denom.denomPubHash,
       denomSig: { cipher: DenomKeyType.Rsa, rsa_signature: denomSigRsa.sig },
       exchangeBaseUrl: tipRecord.exchangeBaseUrl,
       status: CoinStatus.Fresh,
-      suspended: false,
       coinEvHash: planchet.coinEvHash,
     });
   }
 
   await ws.db
-    .mktx((x) => [x.coins, x.tips, x.withdrawalGroups])
+    .mktx((x) => [x.coins, x.denominations, x.tips])
     .runReadWrite(async (tx) => {
       const tr = await tx.tips.get(walletTipId);
       if (!tr) {
@@ -328,7 +328,7 @@ export async function processTip(
       tr.pickedUpTimestamp = TalerProtocolTimestamp.now();
       await tx.tips.put(tr);
       for (const cr of newCoinRecords) {
-        await tx.coins.put(cr);
+        await makeCoinAvailable(ws, tx, cr);
       }
     });
 
diff --git a/packages/taler-wallet-core/src/operations/withdraw.test.ts 
b/packages/taler-wallet-core/src/operations/withdraw.test.ts
index 9f914671..70b4f73c 100644
--- a/packages/taler-wallet-core/src/operations/withdraw.test.ts
+++ b/packages/taler-wallet-core/src/operations/withdraw.test.ts
@@ -38,25 +38,27 @@ test("withdrawal selection bug repro", (t) => {
         
"Q21FQSSG4FXNT96Z14CHXM8N1RZAG9GPHAV8PRWS0PZAAVWH7PBW6R97M2CH19KKP65NNSWXY7B6S53PT3CBM342E357ZXDDJ8RDVW8",
       exchangeBaseUrl: "https://exchange.demo.taler.net/";,
       exchangeMasterPub: "",
-      feeDeposit: {
-        currency: "KUDOS",
-        fraction: 1000000,
-        value: 0,
-      },
-      feeRefresh: {
-        currency: "KUDOS",
-        fraction: 1000000,
-        value: 0,
-      },
-      feeRefund: {
-        currency: "KUDOS",
-        fraction: 1000000,
-        value: 0,
-      },
-      feeWithdraw: {
-        currency: "KUDOS",
-        fraction: 1000000,
-        value: 0,
+      fees: {
+        feeDeposit: {
+          currency: "KUDOS",
+          fraction: 1000000,
+          value: 0,
+        },
+        feeRefresh: {
+          currency: "KUDOS",
+          fraction: 1000000,
+          value: 0,
+        },
+        feeRefund: {
+          currency: "KUDOS",
+          fraction: 1000000,
+          value: 0,
+        },
+        feeWithdraw: {
+          currency: "KUDOS",
+          fraction: 1000000,
+          value: 0,
+        },
       },
       isOffered: true,
       isRevoked: false,
@@ -75,11 +77,9 @@ test("withdrawal selection bug repro", (t) => {
         t_s: 1585229388,
       },
       verificationStatus: DenominationVerificationStatus.Unverified,
-      value: {
-        currency: "KUDOS",
-        fraction: 0,
-        value: 1000,
-      },
+      currency: "KUDOS",
+      amountFrac: 0,
+      amountVal: 1000,
       listIssueDate: { t_s: 0 },
     },
     {
@@ -94,25 +94,27 @@ test("withdrawal selection bug repro", (t) => {
         
"447WA23SCBATMABHA0793F92MYTBYVPYMMQHCPKMKVY5P7RZRFMQ6VRW0Y8HRA7177GTBT0TBT08R21DZD129AJ995H9G09XBFE55G8",
       exchangeBaseUrl: "https://exchange.demo.taler.net/";,
       exchangeMasterPub: "",
-      feeDeposit: {
-        currency: "KUDOS",
-        fraction: 1000000,
-        value: 0,
-      },
-      feeRefresh: {
-        currency: "KUDOS",
-        fraction: 1000000,
-        value: 0,
-      },
-      feeRefund: {
-        currency: "KUDOS",
-        fraction: 1000000,
-        value: 0,
-      },
-      feeWithdraw: {
-        currency: "KUDOS",
-        fraction: 1000000,
-        value: 0,
+      fees: {
+        feeDeposit: {
+          currency: "KUDOS",
+          fraction: 1000000,
+          value: 0,
+        },
+        feeRefresh: {
+          currency: "KUDOS",
+          fraction: 1000000,
+          value: 0,
+        },
+        feeRefund: {
+          currency: "KUDOS",
+          fraction: 1000000,
+          value: 0,
+        },
+        feeWithdraw: {
+          currency: "KUDOS",
+          fraction: 1000000,
+          value: 0,
+        },
       },
       isOffered: true,
       isRevoked: false,
@@ -131,11 +133,9 @@ test("withdrawal selection bug repro", (t) => {
         t_s: 1585229388,
       },
       verificationStatus: DenominationVerificationStatus.Unverified,
-      value: {
-        currency: "KUDOS",
-        fraction: 0,
-        value: 10,
-      },
+      amountFrac: 0,
+      amountVal: 10,
+      currency: "KUDOS",
       listIssueDate: { t_s: 0 },
     },
     {
@@ -149,25 +149,27 @@ test("withdrawal selection bug repro", (t) => {
         
"JS61DTKAFM0BX8Q4XV3ZSKB921SM8QK745Z2AFXTKFMBHHFNBD8TQ5ETJHFNDGBGX22FFN2A2ERNYG1SGSDQWNQHQQ2B14DBVJYJG8R",
       exchangeBaseUrl: "https://exchange.demo.taler.net/";,
       exchangeMasterPub: "",
-      feeDeposit: {
-        currency: "KUDOS",
-        fraction: 1000000,
-        value: 0,
-      },
-      feeRefresh: {
-        currency: "KUDOS",
-        fraction: 1000000,
-        value: 0,
-      },
-      feeRefund: {
-        currency: "KUDOS",
-        fraction: 1000000,
-        value: 0,
-      },
-      feeWithdraw: {
-        currency: "KUDOS",
-        fraction: 1000000,
-        value: 0,
+      fees: {
+        feeDeposit: {
+          currency: "KUDOS",
+          fraction: 1000000,
+          value: 0,
+        },
+        feeRefresh: {
+          currency: "KUDOS",
+          fraction: 1000000,
+          value: 0,
+        },
+        feeRefund: {
+          currency: "KUDOS",
+          fraction: 1000000,
+          value: 0,
+        },
+        feeWithdraw: {
+          currency: "KUDOS",
+          fraction: 1000000,
+          value: 0,
+        },
       },
       isOffered: true,
       isRevoked: false,
@@ -186,11 +188,9 @@ test("withdrawal selection bug repro", (t) => {
         t_s: 1585229388,
       },
       verificationStatus: DenominationVerificationStatus.Unverified,
-      value: {
-        currency: "KUDOS",
-        fraction: 0,
-        value: 5,
-      },
+      amountFrac: 0,
+      amountVal: 5,
+      currency: "KUDOS",
       listIssueDate: { t_s: 0 },
     },
     {
@@ -205,25 +205,27 @@ test("withdrawal selection bug repro", (t) => {
         
"8T51NEY81VMPQ180EQ5WR0YH7GMNNT90W55Q0514KZM18AZT71FHJGJHQXGK0WTA7ACN1X2SD0S53XPBQ1A9KH960R48VCVVM6E3TH8",
       exchangeBaseUrl: "https://exchange.demo.taler.net/";,
       exchangeMasterPub: "",
-      feeDeposit: {
-        currency: "KUDOS",
-        fraction: 1000000,
-        value: 0,
-      },
-      feeRefresh: {
-        currency: "KUDOS",
-        fraction: 1000000,
-        value: 0,
-      },
-      feeRefund: {
-        currency: "KUDOS",
-        fraction: 1000000,
-        value: 0,
-      },
-      feeWithdraw: {
-        currency: "KUDOS",
-        fraction: 1000000,
-        value: 0,
+      fees: {
+        feeDeposit: {
+          currency: "KUDOS",
+          fraction: 1000000,
+          value: 0,
+        },
+        feeRefresh: {
+          currency: "KUDOS",
+          fraction: 1000000,
+          value: 0,
+        },
+        feeRefund: {
+          currency: "KUDOS",
+          fraction: 1000000,
+          value: 0,
+        },
+        feeWithdraw: {
+          currency: "KUDOS",
+          fraction: 1000000,
+          value: 0,
+        },
       },
       isOffered: true,
       isRevoked: false,
@@ -242,11 +244,9 @@ test("withdrawal selection bug repro", (t) => {
         t_s: 1585229388,
       },
       verificationStatus: DenominationVerificationStatus.Unverified,
-      value: {
-        currency: "KUDOS",
-        fraction: 0,
-        value: 1,
-      },
+      amountFrac: 0,
+      amountVal: 1,
+      currency: "KUDOS",
       listIssueDate: { t_s: 0 },
     },
     {
@@ -260,25 +260,27 @@ test("withdrawal selection bug repro", (t) => {
         
"A41HW0Q2H9PCNMEWW0C0N45QAYVXZ8SBVRRAHE4W6X24SV1TH38ANTWDT80JXEBW9Z8PVPGT9GFV2EYZWJ5JW5W1N34NFNKHQSZ1PFR",
       exchangeBaseUrl: "https://exchange.demo.taler.net/";,
       exchangeMasterPub: "",
-      feeDeposit: {
-        currency: "KUDOS",
-        fraction: 1000000,
-        value: 0,
-      },
-      feeRefresh: {
-        currency: "KUDOS",
-        fraction: 1000000,
-        value: 0,
-      },
-      feeRefund: {
-        currency: "KUDOS",
-        fraction: 1000000,
-        value: 0,
-      },
-      feeWithdraw: {
-        currency: "KUDOS",
-        fraction: 1000000,
-        value: 0,
+      fees: {
+        feeDeposit: {
+          currency: "KUDOS",
+          fraction: 1000000,
+          value: 0,
+        },
+        feeRefresh: {
+          currency: "KUDOS",
+          fraction: 1000000,
+          value: 0,
+        },
+        feeRefund: {
+          currency: "KUDOS",
+          fraction: 1000000,
+          value: 0,
+        },
+        feeWithdraw: {
+          currency: "KUDOS",
+          fraction: 1000000,
+          value: 0,
+        },
       },
       isOffered: true,
       isRevoked: false,
@@ -297,11 +299,9 @@ test("withdrawal selection bug repro", (t) => {
         t_s: 1585229388,
       },
       verificationStatus: DenominationVerificationStatus.Unverified,
-      value: {
-        currency: "KUDOS",
-        fraction: 10000000,
-        value: 0,
-      },
+      amountFrac: 10000000,
+      amountVal: 0,
+      currency: "KUDOS",
       listIssueDate: { t_s: 0 },
     },
     {
@@ -315,25 +315,27 @@ test("withdrawal selection bug repro", (t) => {
         
"F5NGBX33DTV4595XZZVK0S2MA1VMXFEJQERE5EBP5DS4QQ9EFRANN7YHWC1TKSHT2K6CQWDBRES8D3DWR0KZF5RET40B4AZXZ0RW1ZG",
       exchangeBaseUrl: "https://exchange.demo.taler.net/";,
       exchangeMasterPub: "",
-      feeDeposit: {
-        currency: "KUDOS",
-        fraction: 1000000,
-        value: 0,
-      },
-      feeRefresh: {
-        currency: "KUDOS",
-        fraction: 1000000,
-        value: 0,
-      },
-      feeRefund: {
-        currency: "KUDOS",
-        fraction: 1000000,
-        value: 0,
-      },
-      feeWithdraw: {
-        currency: "KUDOS",
-        fraction: 1000000,
-        value: 0,
+      fees: {
+        feeDeposit: {
+          currency: "KUDOS",
+          fraction: 1000000,
+          value: 0,
+        },
+        feeRefresh: {
+          currency: "KUDOS",
+          fraction: 1000000,
+          value: 0,
+        },
+        feeRefund: {
+          currency: "KUDOS",
+          fraction: 1000000,
+          value: 0,
+        },
+        feeWithdraw: {
+          currency: "KUDOS",
+          fraction: 1000000,
+          value: 0,
+        },
       },
       isOffered: true,
       isRevoked: false,
@@ -352,11 +354,9 @@ test("withdrawal selection bug repro", (t) => {
         t_s: 1585229388,
       },
       verificationStatus: DenominationVerificationStatus.Unverified,
-      value: {
-        currency: "KUDOS",
-        fraction: 0,
-        value: 2,
-      },
+      amountFrac: 0,
+      amountVal: 2,
+      currency: "KUDOS",
       listIssueDate: { t_s: 0 },
     },
   ];
diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts 
b/packages/taler-wallet-core/src/operations/withdraw.ts
index 1b838377..47252a7e 100644
--- a/packages/taler-wallet-core/src/operations/withdraw.ts
+++ b/packages/taler-wallet-core/src/operations/withdraw.ts
@@ -93,11 +93,11 @@ import {
 } from "../util/http.js";
 import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js";
 import { DbAccess, GetReadOnlyAccess } from "../util/query.js";
-import { RetryInfo } from "../util/retries.js";
 import {
   WALLET_BANK_INTEGRATION_PROTOCOL_VERSION,
   WALLET_EXCHANGE_PROTOCOL_VERSION,
 } from "../versions.js";
+import { makeCoinAvailable } from "../wallet.js";
 import {
   getExchangeDetails,
   getExchangePaytoUri,
@@ -243,11 +243,19 @@ export function selectWithdrawalDenominations(
   let totalWithdrawCost = Amounts.getZero(amountAvailable.currency);
 
   denoms = denoms.filter(isWithdrawableDenom);
-  denoms.sort((d1, d2) => Amounts.cmp(d2.value, d1.value));
+  denoms.sort((d1, d2) =>
+    Amounts.cmp(
+      DenominationRecord.getValue(d2),
+      DenominationRecord.getValue(d1),
+    ),
+  );
 
   for (const d of denoms) {
     let count = 0;
-    const cost = Amounts.add(d.value, d.feeWithdraw).amount;
+    const cost = Amounts.add(
+      DenominationRecord.getValue(d),
+      d.fees.feeWithdraw,
+    ).amount;
     for (;;) {
       if (Amounts.cmp(remaining, cost) < 0) {
         break;
@@ -258,7 +266,7 @@ export function selectWithdrawalDenominations(
     if (count > 0) {
       totalCoinValue = Amounts.add(
         totalCoinValue,
-        Amounts.mult(d.value, count).amount,
+        Amounts.mult(DenominationRecord.getValue(d), count).amount,
       ).amount;
       totalWithdrawCost = Amounts.add(
         totalWithdrawCost,
@@ -306,22 +314,30 @@ export function selectForcedWithdrawalDenominations(
   let totalWithdrawCost = Amounts.getZero(amountAvailable.currency);
 
   denoms = denoms.filter(isWithdrawableDenom);
-  denoms.sort((d1, d2) => Amounts.cmp(d2.value, d1.value));
+  denoms.sort((d1, d2) =>
+    Amounts.cmp(
+      DenominationRecord.getValue(d2),
+      DenominationRecord.getValue(d1),
+    ),
+  );
 
   for (const fds of forcedDenomSel.denoms) {
     const count = fds.count;
     const denom = denoms.find((x) => {
-      return Amounts.cmp(x.value, fds.value) == 0;
+      return Amounts.cmp(DenominationRecord.getValue(x), fds.value) == 0;
     });
     if (!denom) {
       throw Error(
         `unable to find denom for forced selection (value ${fds.value})`,
       );
     }
-    const cost = Amounts.add(denom.value, denom.feeWithdraw).amount;
+    const cost = Amounts.add(
+      DenominationRecord.getValue(denom),
+      denom.fees.feeWithdraw,
+    ).amount;
     totalCoinValue = Amounts.add(
       totalCoinValue,
-      Amounts.mult(denom.value, count).amount,
+      Amounts.mult(DenominationRecord.getValue(denom), count).amount,
     ).amount;
     totalWithdrawCost = Amounts.add(
       totalWithdrawCost,
@@ -805,7 +821,6 @@ async function processPlanchetVerifyAndStoreCoin(
       reservePub: planchet.reservePub,
       withdrawalGroupId: withdrawalGroup.withdrawalGroupId,
     },
-    suspended: false,
     ageCommitmentProof: planchet.ageCommitmentProof,
   };
 
@@ -815,7 +830,7 @@ async function processPlanchetVerifyAndStoreCoin(
   // withdrawal succeeded.  If so, mark the withdrawal
   // group as finished.
   const firstSuccess = await ws.db
-    .mktx((x) => [x.coins, x.withdrawalGroups, x.planchets])
+    .mktx((x) => [x.coins, x.denominations, x.withdrawalGroups, x.planchets])
     .runReadWrite(async (tx) => {
       const p = await tx.planchets.get(planchetCoinPub);
       if (!p || p.withdrawalDone) {
@@ -823,7 +838,7 @@ async function processPlanchetVerifyAndStoreCoin(
       }
       p.withdrawalDone = true;
       await tx.planchets.put(p);
-      await tx.coins.add(coin);
+      await makeCoinAvailable(ws, tx, coin);
       return true;
     });
 
diff --git a/packages/taler-wallet-core/src/util/query.ts 
b/packages/taler-wallet-core/src/util/query.ts
index 02595925..17b71365 100644
--- a/packages/taler-wallet-core/src/util/query.ts
+++ b/packages/taler-wallet-core/src/util/query.ts
@@ -445,14 +445,15 @@ function runTx<Arg, Res>(
       if (!gotFunResult) {
         const msg =
           "BUG: transaction closed before transaction function returned";
-        console.error(msg);
+        logger.error(msg);
+        logger.error(`${stack.stack}`);
         reject(Error(msg));
       }
       resolve(funResult);
     };
     tx.onerror = () => {
       logger.error("error in transaction");
-      logger.error(`${stack}`);
+      logger.error(`${stack.stack}`);
     };
     tx.onabort = () => {
       let msg: string;
diff --git a/packages/taler-wallet-core/src/wallet.ts 
b/packages/taler-wallet-core/src/wallet.ts
index 0e777225..02ed8a61 100644
--- a/packages/taler-wallet-core/src/wallet.ts
+++ b/packages/taler-wallet-core/src/wallet.ts
@@ -99,7 +99,10 @@ import {
 } from "./crypto/workers/cryptoDispatcher.js";
 import {
   AuditorTrustRecord,
+  CoinRecord,
   CoinSourceType,
+  CoinStatus,
+  DenominationRecord,
   exportDb,
   importDb,
   OperationAttemptResult,
@@ -216,6 +219,7 @@ import {
   HttpRequestLibrary,
   readSuccessResponseJsonOrThrow,
 } from "./util/http.js";
+import { checkDbInvariant } from "./util/invariants.js";
 import {
   AsyncCondition,
   OpenedPromise,
@@ -728,14 +732,29 @@ async function getExchangeDetailedInfo(
         return;
       }
 
-      const denominations = await tx.denominations.indexes.byExchangeBaseUrl
-        .iter(ex.baseUrl)
-        .toArray();
+      const denominationRecords =
+        await tx.denominations.indexes.byExchangeBaseUrl
+          .iter(ex.baseUrl)
+          .toArray();
 
-      if (!denominations) {
+      if (!denominationRecords) {
         return;
       }
 
+      const denominations: DenomInfo[] = denominationRecords.map((x) => ({
+        denomPub: x.denomPub,
+        denomPubHash: x.denomPubHash,
+        feeDeposit: x.fees.feeDeposit,
+        feeRefresh: x.fees.feeRefresh,
+        feeRefund: x.fees.feeRefund,
+        feeWithdraw: x.fees.feeWithdraw,
+        stampExpireDeposit: x.stampExpireDeposit,
+        stampExpireLegal: x.stampExpireLegal,
+        stampExpireWithdraw: x.stampExpireWithdraw,
+        stampStart: x.stampStart,
+        value: DenominationRecord.getValue(x),
+      }));
+
       return {
         info: {
           exchangeBaseUrl: ex.baseUrl,
@@ -750,7 +769,7 @@ async function getExchangeDetailedInfo(
           auditors: exchangeDetails.auditors,
           wireInfo: exchangeDetails.wireInfo,
         },
-        denominations: denominations,
+        denominations,
       };
     });
 
@@ -787,21 +806,135 @@ async function getExchangeDetailedInfo(
   };
 }
 
+export async function makeCoinAvailable(
+  ws: InternalWalletState,
+  tx: GetReadWriteAccess<{
+    coins: typeof WalletStoresV1.coins;
+    denominations: typeof WalletStoresV1.denominations;
+  }>,
+  coinRecord: CoinRecord,
+): Promise<void> {
+  const denom = await tx.denominations.get([
+    coinRecord.exchangeBaseUrl,
+    coinRecord.denomPubHash,
+  ]);
+  checkDbInvariant(!!denom);
+  if (!denom.freshCoinCount) {
+    denom.freshCoinCount = 0;
+  }
+  denom.freshCoinCount++;
+  await tx.coins.put(coinRecord);
+  await tx.denominations.put(denom);
+}
+
+export interface CoinsSpendInfo {
+  coinPubs: string[];
+  contributions: AmountJson[];
+  refreshReason: RefreshReason;
+  /**
+   * Identifier for what the coin has been spent for.
+   */
+  allocationId: string;
+}
+
+export async function spendCoins(
+  ws: InternalWalletState,
+  tx: GetReadWriteAccess<{
+    coins: typeof WalletStoresV1.coins;
+    refreshGroups: typeof WalletStoresV1.refreshGroups;
+    denominations: typeof WalletStoresV1.denominations;
+  }>,
+  csi: CoinsSpendInfo,
+): Promise<void> {
+  for (let i = 0; i < csi.coinPubs.length; i++) {
+    const coin = await tx.coins.get(csi.coinPubs[i]);
+    if (!coin) {
+      throw Error("coin allocated for payment doesn't exist anymore");
+    }
+    const denom = await tx.denominations.get([
+      coin.exchangeBaseUrl,
+      coin.denomPubHash,
+    ]);
+    checkDbInvariant(!!denom);
+    const contrib = csi.contributions[i];
+    if (coin.status !== CoinStatus.Fresh) {
+      const alloc = coin.allocation;
+      if (!alloc) {
+        continue;
+      }
+      if (alloc.id !== csi.allocationId) {
+        // FIXME: assign error code
+        throw Error("conflicting coin allocation (id)");
+      }
+      if (0 !== Amounts.cmp(alloc.amount, contrib)) {
+        // FIXME: assign error code
+        throw Error("conflicting coin allocation (contrib)");
+      }
+      continue;
+    }
+    coin.status = CoinStatus.Dormant;
+    coin.allocation = {
+      id: csi.allocationId,
+      amount: Amounts.stringify(contrib),
+    };
+    const remaining = Amounts.sub(coin.currentAmount, contrib);
+    if (remaining.saturated) {
+      throw Error("not enough remaining balance on coin for payment");
+    }
+    coin.currentAmount = remaining.amount;
+    checkDbInvariant(!!denom);
+    if (denom.freshCoinCount == null || denom.freshCoinCount === 0) {
+      throw Error(`invalid coin count ${denom.freshCoinCount} in DB`);
+    }
+    denom.freshCoinCount--;
+    await tx.coins.put(coin);
+    await tx.denominations.put(denom);
+  }
+  const refreshCoinPubs = csi.coinPubs.map((x) => ({
+    coinPub: x,
+  }));
+  await createRefreshGroup(ws, tx, refreshCoinPubs, RefreshReason.PayMerchant);
+}
+
 async function setCoinSuspended(
   ws: InternalWalletState,
   coinPub: string,
   suspended: boolean,
 ): Promise<void> {
   await ws.db
-    .mktx((x) => [x.coins])
+    .mktx((x) => [x.coins, x.denominations])
     .runReadWrite(async (tx) => {
       const c = await tx.coins.get(coinPub);
       if (!c) {
         logger.warn(`coin ${coinPub} not found, won't suspend`);
         return;
       }
-      c.suspended = suspended;
+      const denom = await tx.denominations.get([
+        c.exchangeBaseUrl,
+        c.denomPubHash,
+      ]);
+      checkDbInvariant(!!denom);
+      if (suspended) {
+        if (c.status !== CoinStatus.Fresh) {
+          return;
+        }
+        if (denom.freshCoinCount == null || denom.freshCoinCount === 0) {
+          throw Error(`invalid coin count ${denom.freshCoinCount} in DB`);
+        }
+        denom.freshCoinCount--;
+        c.status = CoinStatus.FreshSuspended;
+      } else {
+        if (c.status == CoinStatus.Dormant) {
+          return;
+        }
+        if (denom.freshCoinCount == null) {
+          denom.freshCoinCount = 0;
+        }
+        denom.freshCoinCount++;
+        c.status = CoinStatus.Fresh;
+      }
       await tx.coins.put(c);
+      await tx.denominations.put(denom);
     });
 }
 
@@ -852,12 +985,16 @@ async function dumpCoins(ws: InternalWalletState): 
Promise<CoinDumpJson> {
           coin_pub: c.coinPub,
           denom_pub: denomInfo.denomPub,
           denom_pub_hash: c.denomPubHash,
-          denom_value: Amounts.stringify(denom.value),
+          denom_value: Amounts.stringify({
+            value: denom.amountVal,
+            currency: denom.currency,
+            fraction: denom.amountFrac,
+          }),
           exchange_base_url: c.exchangeBaseUrl,
           refresh_parent_coin_pub: refreshParentCoinPub,
           remaining_value: Amounts.stringify(c.currentAmount),
           withdrawal_reserve_pub: withdrawalReservePub,
-          coin_suspended: c.suspended,
+          coin_suspended: c.status === CoinStatus.FreshSuspended,
           ageCommitmentProof: c.ageCommitmentProof,
         });
       }
@@ -1449,9 +1586,22 @@ class InternalWalletStateImpl implements 
InternalWalletState {
     }
     const d = await tx.denominations.get([exchangeBaseUrl, denomPubHash]);
     if (d) {
-      this.denomCache[key] = d;
+      const denomInfo = {
+        denomPub: d.denomPub,
+        denomPubHash: d.denomPubHash,
+        feeDeposit: d.fees.feeDeposit,
+        feeRefresh: d.fees.feeRefresh,
+        feeRefund: d.fees.feeRefund,
+        feeWithdraw: d.fees.feeWithdraw,
+        stampExpireDeposit: d.stampExpireDeposit,
+        stampExpireLegal: d.stampExpireLegal,
+        stampExpireWithdraw: d.stampExpireWithdraw,
+        stampStart: d.stampStart,
+        value: DenominationRecord.getValue(d),
+      };
+      return denomInfo;
     }
-    return d;
+    return undefined;
   }
 
   notify(n: WalletNotification): void {

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