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: implement denom-


From: gnunet
Subject: [taler-wallet-core] branch master updated: wallet-core: implement denom-loss transaction
Date: Sun, 31 Mar 2024 16:50:39 +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 f45340eb1 wallet-core: implement denom-loss transaction
f45340eb1 is described below

commit f45340eb11435f47a3a561c724cd356e5b4ba885
Author: Florian Dold <florian@dold.me>
AuthorDate: Sun Mar 31 16:50:34 2024 +0200

    wallet-core: implement denom-loss transaction
---
 .../src/integrationtests/test-denom-unoffered.ts   |   9 +-
 packages/taler-util/src/notifications.ts           |   1 -
 packages/taler-util/src/transactions-types.ts      |  20 +-
 packages/taler-wallet-core/src/db.ts               |  38 ++-
 packages/taler-wallet-core/src/dev-experiments.ts  |  24 ++
 packages/taler-wallet-core/src/exchanges.ts        | 279 +++++++++++++++++----
 packages/taler-wallet-core/src/recoup.ts           |  36 +--
 packages/taler-wallet-core/src/shepherd.ts         |  18 +-
 packages/taler-wallet-core/src/transactions.ts     |  98 +++++++-
 packages/taler-wallet-core/src/versions.ts         |   6 +-
 packages/taler-wallet-core/src/wallet.ts           |   4 +-
 11 files changed, 442 insertions(+), 91 deletions(-)

diff --git 
a/packages/taler-harness/src/integrationtests/test-denom-unoffered.ts 
b/packages/taler-harness/src/integrationtests/test-denom-unoffered.ts
index 79269d533..677739627 100644
--- a/packages/taler-harness/src/integrationtests/test-denom-unoffered.ts
+++ b/packages/taler-harness/src/integrationtests/test-denom-unoffered.ts
@@ -21,6 +21,7 @@ import {
   MerchantApiClient,
   PreparePayResultType,
   TalerErrorCode,
+  TransactionType,
 } from "@gnu-taler/taler-util";
 import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
 import { GlobalTestState } from "../harness/harness.js";
@@ -93,7 +94,7 @@ export async function runDenomUnofferedTest(t: 
GlobalTestState) {
   );
 
   const confirmResp = await walletClient.call(WalletApiOperation.ConfirmPay, {
-    proposalId: preparePayResult.proposalId,
+    transactionId: preparePayResult.transactionId,
   });
 
   const tx = await walletClient.call(WalletApiOperation.GetTransactionById, {
@@ -147,8 +148,12 @@ export async function runDenomUnofferedTest(t: 
GlobalTestState) {
 
   await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
 
-  const txs = await walletClient.call(WalletApiOperation.GetTransactions, {});
+  const txs = await walletClient.call(WalletApiOperation.GetTransactions, {
+    sort: "stable-ascending",
+  });
   console.log(JSON.stringify(txs, undefined, 2));
+
+  t.assertDeepEqual(txs.transactions[2].type, TransactionType.DenomLoss);
 }
 
 runDenomUnofferedTest.suites = ["wallet"];
diff --git a/packages/taler-util/src/notifications.ts 
b/packages/taler-util/src/notifications.ts
index f439b4a6f..43ca6271e 100644
--- a/packages/taler-util/src/notifications.ts
+++ b/packages/taler-util/src/notifications.ts
@@ -22,7 +22,6 @@
 /**
  * Imports.
  */
-import { CancellationToken } from "./CancellationToken.js";
 import { AbsoluteTime } from "./time.js";
 import { TransactionState } from "./transactions-types.js";
 import { ExchangeEntryState, TalerErrorDetail } from "./wallet-types.js";
diff --git a/packages/taler-util/src/transactions-types.ts 
b/packages/taler-util/src/transactions-types.ts
index 8c4c2c7ed..bddc03c25 100644
--- a/packages/taler-util/src/transactions-types.ts
+++ b/packages/taler-util/src/transactions-types.ts
@@ -222,7 +222,8 @@ export type Transaction =
   | TransactionPeerPushCredit
   | TransactionPeerPushDebit
   | TransactionInternalWithdrawal
-  | TransactionRecoup;
+  | TransactionRecoup
+  | TransactionDenomLoss;
 
 export enum TransactionType {
   Withdrawal = "withdrawal",
@@ -237,6 +238,7 @@ export enum TransactionType {
   PeerPullDebit = "peer-pull-debit",
   PeerPullCredit = "peer-pull-credit",
   Recoup = "recoup",
+  DenomLoss = "denom-loss",
 }
 
 export enum WithdrawalType {
@@ -298,6 +300,22 @@ interface WithdrawalDetailsForTalerBankIntegrationApi {
   exchangeCreditAccountDetails?: WithdrawalExchangeAccountDetails[];
 }
 
+export enum DenomLossEventType {
+  DenomExpired = "denom-expired",
+  DenomVanished = "denom-vanished",
+  DenomUnoffered = "denom-unoffered",
+}
+
+/**
+ * A transaction to indicate financial loss due to denominations
+ * that became unusable for deposits.
+ */
+export interface TransactionDenomLoss extends TransactionCommon {
+  type: TransactionType.DenomLoss;
+  lossEventType: DenomLossEventType;
+  exchangeBaseUrl: string;
+}
+
 /**
  * A withdrawal transaction (either bank-integrated or manual).
  */
diff --git a/packages/taler-wallet-core/src/db.ts 
b/packages/taler-wallet-core/src/db.ts
index 98390805b..4ead3cf5c 100644
--- a/packages/taler-wallet-core/src/db.ts
+++ b/packages/taler-wallet-core/src/db.ts
@@ -39,6 +39,7 @@ import {
   CoinPublicKeyString,
   CoinRefreshRequest,
   CoinStatus,
+  DenomLossEventType,
   DenomSelectionState,
   DenominationInfo,
   DenominationPubKey,
@@ -149,7 +150,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;
 
 declare const symDbProtocolTimestamp: unique symbol;
 
@@ -2355,11 +2356,46 @@ export interface TransactionRecord {
   currency: string;
 }
 
+export enum DenomLossStatus {
+  /**
+   * Done indicates that the loss happened.
+   */
+  Done = 0x0500_0000,
+
+  /**
+   * Aborted in the sense that the loss was reversed.
+   */
+  Aborted = 0x0503_0001,
+}
+
+export interface DenomLossEventRecord {
+  denomLossEventId: string;
+  currency: string;
+  denomPubHashes: string[];
+  status: DenomLossStatus;
+  timestampCreated: DbPreciseTimestamp;
+  amount: string;
+  eventType: DenomLossEventType;
+  exchangeBaseUrl: string;
+}
+
 /**
  * Schema definition for the IndexedDB
  * wallet database.
  */
 export const WalletStoresV1 = {
+  denomLossEvents: describeStoreV2({
+    recordCodec: passthroughCodec<DenomLossEventRecord>(),
+    storeName: "denomLossEvents",
+    keyPath: "denomLossEventId",
+    versionAdded: 9,
+    indexes: {
+      byCurrency: describeIndex("byCurrency", "currency", {
+        versionAdded: 9,
+      }),
+      byStatus: describeIndex("byStatus", "status"),
+    },
+  }),
   transactions: describeStoreV2({
     recordCodec: passthroughCodec<TransactionRecord>(),
     storeName: "transactions",
diff --git a/packages/taler-wallet-core/src/dev-experiments.ts 
b/packages/taler-wallet-core/src/dev-experiments.ts
index c94571ff8..130c042b6 100644
--- a/packages/taler-wallet-core/src/dev-experiments.ts
+++ b/packages/taler-wallet-core/src/dev-experiments.ts
@@ -26,6 +26,7 @@
  */
 
 import {
+  DenomLossEventType,
   Logger,
   RefreshReason,
   TalerPreciseTimestamp,
@@ -39,6 +40,8 @@ import {
   HttpResponse,
 } from "@gnu-taler/taler-util/http";
 import {
+  DenomLossEventRecord,
+  DenomLossStatus,
   RefreshGroupRecord,
   RefreshOperationStatus,
   timestampPreciseToDb,
@@ -88,6 +91,27 @@ export async function applyDevExperiment(
     return;
   }
 
+  if (parsedUri.devExperimentId == "insert-denom-loss") {
+    await wex.db.runReadWriteTx(["denomLossEvents"], async (tx) => {
+      const eventId = encodeCrock(getRandomBytes(32));
+      const newRg: DenomLossEventRecord = {
+        amount: "TESTKUDOS:42",
+        currency: "TESTKUDOS",
+        exchangeBaseUrl: "https://exchange.devexperiment.taler.net/";,
+        denomLossEventId: eventId,
+        denomPubHashes: [
+          encodeCrock(getRandomBytes(64)),
+          encodeCrock(getRandomBytes(64)),
+        ],
+        eventType: DenomLossEventType.DenomExpired,
+        status: DenomLossStatus.Done,
+        timestampCreated: timestampPreciseToDb(TalerPreciseTimestamp.now()),
+      };
+      await tx.denomLossEvents.put(newRg);
+    });
+    return;
+  }
+
   throw Error(`dev-experiment id not understood ${parsedUri.devExperimentId}`);
 }
 
diff --git a/packages/taler-wallet-core/src/exchanges.ts 
b/packages/taler-wallet-core/src/exchanges.ts
index 8b4bca2aa..a57215ee4 100644
--- a/packages/taler-wallet-core/src/exchanges.ts
+++ b/packages/taler-wallet-core/src/exchanges.ts
@@ -26,6 +26,7 @@
 import {
   AbsoluteTime,
   AgeRestriction,
+  Amount,
   Amounts,
   AsyncFlag,
   CancellationToken,
@@ -33,6 +34,7 @@ import {
   CoinStatus,
   DeleteExchangeRequest,
   DenomKeyType,
+  DenomLossEventType,
   DenomOperationMap,
   DenominationInfo,
   DenominationPubKey,
@@ -65,6 +67,10 @@ import {
   TalerPreciseTimestamp,
   TalerProtocolDuration,
   TalerProtocolTimestamp,
+  TransactionIdStr,
+  TransactionMajorState,
+  TransactionState,
+  TransactionType,
   URL,
   WalletNotification,
   WireFee,
@@ -77,6 +83,7 @@ import {
   codecForExchangeKeysJson,
   durationMul,
   encodeCrock,
+  getRandomBytes,
   hashDenomPub,
   j2s,
   makeErrorDetail,
@@ -90,9 +97,11 @@ import {
 } from "@gnu-taler/taler-util/http";
 import {
   PendingTaskType,
+  TaskIdStr,
   TaskIdentifiers,
   TaskRunResult,
   TaskRunResultType,
+  TransactionContext,
   constructTaskIdentifier,
   getAutoRefreshExecuteThreshold,
   getExchangeEntryStatusFromRecord,
@@ -101,6 +110,8 @@ import {
   getExchangeUpdateStatusFromRecord,
 } from "./common.js";
 import {
+  DenomLossEventRecord,
+  DenomLossStatus,
   DenominationRecord,
   DenominationVerificationStatus,
   ExchangeDetailsRecord,
@@ -126,6 +137,7 @@ import {
 import { DbReadOnlyTransaction } from "./query.js";
 import { createRecoupGroup } from "./recoup.js";
 import { createRefreshGroup } from "./refresh.js";
+import { constructTransactionIdentifier } from "./transactions.js";
 import { WALLET_EXCHANGE_PROTOCOL_VERSION } from "./versions.js";
 import { InternalWalletState, WalletExecutionContext } from "./wallet.js";
 
@@ -1390,8 +1402,6 @@ export async function updateExchangeFromUrlHandler(
     ["text/plain"],
   );
 
-  let recoupGroupId: string | undefined;
-
   logger.trace("updating exchange info in database");
 
   let detailsPointerChanged = false;
@@ -1406,11 +1416,11 @@ export async function updateExchangeFromUrlHandler(
       break;
     }
   }
-
-  const now = AbsoluteTime.now();
   let noFees = checkNoFees(keysInfo);
   let peerPaymentsDisabled = checkPeerPaymentsDisabled(keysInfo);
 
+  let denomLossResult: boolean = false;
+
   const updated = await wex.db.runReadWriteTx(
     [
       "exchanges",
@@ -1420,6 +1430,8 @@ export async function updateExchangeFromUrlHandler(
       "coins",
       "refreshGroups",
       "recoupGroups",
+      "coinAvailability",
+      "denomLossEvents",
     ],
     async (tx) => {
       const r = await tx.exchanges.get(exchangeBaseUrl);
@@ -1508,6 +1520,7 @@ export async function updateExchangeFromUrlHandler(
       const currentDenomSet = new Set<string>(
         keysInfo.currentDenominations.map((x) => x.denomPubHash),
       );
+
       for (const currentDenom of keysInfo.currentDenominations) {
         const oldDenom = oldDenomByDph.get(currentDenom.denomPubHash);
         if (oldDenom) {
@@ -1552,44 +1565,14 @@ export async function updateExchangeFromUrlHandler(
 
       logger.trace("done updating denominations in database");
 
-      // Handle recoup
-      const recoupDenomList = keysInfo.recoup;
-      const newlyRevokedCoinPubs: string[] = [];
-      logger.trace("recoup list from exchange", recoupDenomList);
-      for (const recoupInfo of recoupDenomList) {
-        const oldDenom = await tx.denominations.get([
-          r.baseUrl,
-          recoupInfo.h_denom_pub,
-        ]);
-        if (!oldDenom) {
-          // We never even knew about the revoked denomination, all good.
-          continue;
-        }
-        if (oldDenom.isRevoked) {
-          // We already marked the denomination as revoked,
-          // this implies we revoked all coins
-          logger.trace("denom already revoked");
-          continue;
-        }
-        logger.info("revoking denom", recoupInfo.h_denom_pub);
-        oldDenom.isRevoked = true;
-        await tx.denominations.put(oldDenom);
-        const affectedCoins = await tx.coins.indexes.byDenomPubHash
-          .iter(recoupInfo.h_denom_pub)
-          .toArray();
-        for (const ac of affectedCoins) {
-          newlyRevokedCoinPubs.push(ac.coinPub);
-        }
-      }
-      if (newlyRevokedCoinPubs.length != 0) {
-        logger.info("recouping coins", newlyRevokedCoinPubs);
-        recoupGroupId = await createRecoupGroup(
-          wex,
-          tx,
-          exchangeBaseUrl,
-          newlyRevokedCoinPubs,
-        );
-      }
+      denomLossResult = await handleDenomLoss(
+        wex,
+        tx,
+        newDetails.currency,
+        exchangeBaseUrl,
+      );
+
+      await handleRecoup(wex, tx, exchangeBaseUrl, keysInfo.recoup);
 
       const newExchangeState = getExchangeState(r);
 
@@ -1602,20 +1585,17 @@ export async function updateExchangeFromUrlHandler(
     },
   );
 
-  if (recoupGroupId) {
-    const recoupTaskId = constructTaskIdentifier({
-      tag: PendingTaskType.Recoup,
-      recoupGroupId,
-    });
-    // Asynchronously start recoup.  This doesn't need to finish
-    // for the exchange update to be considered finished.
-    wex.taskScheduler.startShepherdTask(recoupTaskId);
-  }
-
   if (!updated) {
     throw Error("something went wrong with updating the exchange");
   }
 
+  if (denomLossResult) {
+    wex.ws.notify({
+      type: NotificationType.BalanceChange,
+      hintTransactionId: "denom-loss:*",
+    });
+  }
+
   logger.trace("done updating exchange info in database");
 
   logger.trace(`doing auto-refresh check for '${exchangeBaseUrl}'`);
@@ -1709,6 +1689,201 @@ export async function updateExchangeFromUrlHandler(
   return TaskRunResult.progress();
 }
 
+async function handleDenomLoss(
+  wex: WalletExecutionContext,
+  tx: WalletDbReadWriteTransaction<
+    ["coinAvailability", "denominations", "denomLossEvents"]
+  >,
+  currency: string,
+  exchangeBaseUrl: string,
+): Promise<boolean> {
+  const coinAvailabilityRecs =
+    await 
tx.coinAvailability.indexes.byExchangeBaseUrl.getAll(exchangeBaseUrl);
+  const denomsVanished: string[] = [];
+  const denomsUnoffered: string[] = [];
+  const denomsExpired: string[] = [];
+  let amountVanished = Amount.zeroOfCurrency(currency);
+  let amountExpired = Amount.zeroOfCurrency(currency);
+  let amountUnoffered = Amount.zeroOfCurrency(currency);
+
+  for (const coinAv of coinAvailabilityRecs) {
+    if (coinAv.freshCoinCount <= 0) {
+      continue;
+    }
+    const n = coinAv.freshCoinCount;
+    const denom = await tx.denominations.get(coinAv.denomPubHash);
+    if (!denom) {
+      // Remove availability
+      coinAv.freshCoinCount = 0;
+      coinAv.visibleCoinCount = 0;
+      await tx.coinAvailability.put(coinAv);
+      denomsVanished.push(coinAv.denomPubHash);
+      const total = Amount.from(coinAv.value).mult(n);
+      amountVanished = amountVanished.add(total);
+      continue;
+    }
+    if (!denom.isOffered) {
+      // Remove availability
+      coinAv.freshCoinCount = 0;
+      coinAv.visibleCoinCount = 0;
+      await tx.coinAvailability.put(coinAv);
+      denomsUnoffered.push(coinAv.denomPubHash);
+      const total = Amount.from(coinAv.value).mult(n);
+      amountUnoffered = amountUnoffered.add(total);
+      continue;
+    }
+    const timestampExpireDeposit = timestampAbsoluteFromDb(
+      denom.stampExpireDeposit,
+    );
+    if (AbsoluteTime.isExpired(timestampExpireDeposit)) {
+      // Remove availability
+      coinAv.freshCoinCount = 0;
+      coinAv.visibleCoinCount = 0;
+      await tx.coinAvailability.put(coinAv);
+      denomsExpired.push(coinAv.denomPubHash);
+      const total = Amount.from(coinAv.value).mult(n);
+      amountExpired = amountExpired.add(total);
+      continue;
+    }
+  }
+  let hadLoss = false;
+
+  if (denomsVanished.length > 0) {
+    hadLoss = true;
+    await tx.denomLossEvents.add({
+      denomLossEventId: encodeCrock(getRandomBytes(32)),
+      amount: amountVanished.toString(),
+      currency,
+      exchangeBaseUrl,
+      denomPubHashes: denomsVanished,
+      eventType: DenomLossEventType.DenomVanished,
+      status: DenomLossStatus.Done,
+      timestampCreated: timestampPreciseToDb(TalerPreciseTimestamp.now()),
+    });
+  }
+
+  if (denomsUnoffered.length > 0) {
+    hadLoss = true;
+    await tx.denomLossEvents.add({
+      denomLossEventId: encodeCrock(getRandomBytes(32)),
+      amount: amountUnoffered.toString(),
+      currency,
+      exchangeBaseUrl,
+      denomPubHashes: denomsUnoffered,
+      eventType: DenomLossEventType.DenomUnoffered,
+      status: DenomLossStatus.Done,
+      timestampCreated: timestampPreciseToDb(TalerPreciseTimestamp.now()),
+    });
+  }
+
+  if (denomsExpired.length > 0) {
+    hadLoss = true;
+    await tx.denomLossEvents.add({
+      denomLossEventId: encodeCrock(getRandomBytes(32)),
+      amount: amountExpired.toString(),
+      currency,
+      exchangeBaseUrl,
+      denomPubHashes: denomsUnoffered,
+      eventType: DenomLossEventType.DenomExpired,
+      status: DenomLossStatus.Done,
+      timestampCreated: timestampPreciseToDb(TalerPreciseTimestamp.now()),
+    });
+  }
+
+  return hadLoss;
+}
+
+export function computeDenomLossTransactionStatus(
+  rec: DenomLossEventRecord,
+): TransactionState {
+  switch (rec.status) {
+    case DenomLossStatus.Aborted:
+      return {
+        major: TransactionMajorState.Aborted,
+      };
+    case DenomLossStatus.Done:
+      return {
+        major: TransactionMajorState.Done,
+      };
+  }
+}
+
+export class DenomLossTransactionContext implements TransactionContext {
+  get taskId(): TaskIdStr | undefined {
+    return undefined;
+  }
+  transactionId: TransactionIdStr;
+
+  abortTransaction(): Promise<void> {
+    throw new Error("Method not implemented.");
+  }
+  suspendTransaction(): Promise<void> {
+    throw new Error("Method not implemented.");
+  }
+  resumeTransaction(): Promise<void> {
+    throw new Error("Method not implemented.");
+  }
+  failTransaction(): Promise<void> {
+    throw new Error("Method not implemented.");
+  }
+  deleteTransaction(): Promise<void> {
+    throw new Error("Method not implemented.");
+  }
+
+  constructor(
+    wex: WalletExecutionContext,
+    public denomLossEventId: string,
+  ) {
+    this.transactionId = constructTransactionIdentifier({
+      tag: TransactionType.DenomLoss,
+      denomLossEventId,
+    });
+  }
+}
+
+async function handleRecoup(
+  wex: WalletExecutionContext,
+  tx: WalletDbReadWriteTransaction<
+    ["denominations", "coins", "recoupGroups", "refreshGroups"]
+  >,
+  exchangeBaseUrl: string,
+  recoup: Recoup[],
+): Promise<void> {
+  // Handle recoup
+  const recoupDenomList = recoup;
+  const newlyRevokedCoinPubs: string[] = [];
+  logger.trace("recoup list from exchange", recoupDenomList);
+  for (const recoupInfo of recoupDenomList) {
+    const oldDenom = await tx.denominations.get([
+      exchangeBaseUrl,
+      recoupInfo.h_denom_pub,
+    ]);
+    if (!oldDenom) {
+      // We never even knew about the revoked denomination, all good.
+      continue;
+    }
+    if (oldDenom.isRevoked) {
+      // We already marked the denomination as revoked,
+      // this implies we revoked all coins
+      logger.trace("denom already revoked");
+      continue;
+    }
+    logger.info("revoking denom", recoupInfo.h_denom_pub);
+    oldDenom.isRevoked = true;
+    await tx.denominations.put(oldDenom);
+    const affectedCoins = await tx.coins.indexes.byDenomPubHash.getAll(
+      recoupInfo.h_denom_pub,
+    );
+    for (const ac of affectedCoins) {
+      newlyRevokedCoinPubs.push(ac.coinPub);
+    }
+  }
+  if (newlyRevokedCoinPubs.length != 0) {
+    logger.info("recouping coins", newlyRevokedCoinPubs);
+    await createRecoupGroup(wex, tx, exchangeBaseUrl, newlyRevokedCoinPubs);
+  }
+}
+
 function getAutoRefreshExecuteThresholdForDenom(
   d: DenominationRecord,
 ): AbsoluteTime {
diff --git a/packages/taler-wallet-core/src/recoup.ts 
b/packages/taler-wallet-core/src/recoup.ts
index 8d5d3dd1f..b8b2cf808 100644
--- a/packages/taler-wallet-core/src/recoup.ts
+++ b/packages/taler-wallet-core/src/recoup.ts
@@ -26,7 +26,6 @@
  */
 import {
   Amounts,
-  CancellationToken,
   CoinStatus,
   Logger,
   RefreshReason,
@@ -63,11 +62,7 @@ import {
 } from "./db.js";
 import { createRefreshGroup } from "./refresh.js";
 import { constructTransactionIdentifier } from "./transactions.js";
-import {
-  WalletExecutionContext,
-  getDenomInfo,
-  type InternalWalletState,
-} from "./wallet.js";
+import { WalletExecutionContext, getDenomInfo } from "./wallet.js";
 import { internalCreateWithdrawalGroup } from "./withdraw.js";
 
 export const logger = new Logger("operations/recoup.ts");
@@ -237,15 +232,18 @@ export async function recoupWithdrawCoin(
   cs: WithdrawCoinSource,
 ): Promise<void> {
   const reservePub = cs.reservePub;
-  const denomInfo = await wex.db.runReadOnlyTx(["denominations"], async (tx) 
=> {
-    const denomInfo = await getDenomInfo(
-      wex,
-      tx,
-      coin.exchangeBaseUrl,
-      coin.denomPubHash,
-    );
-    return denomInfo;
-  });
+  const denomInfo = await wex.db.runReadOnlyTx(
+    ["denominations"],
+    async (tx) => {
+      const denomInfo = await getDenomInfo(
+        wex,
+        tx,
+        coin.exchangeBaseUrl,
+        coin.denomPubHash,
+      );
+      return denomInfo;
+    },
+  );
   if (!denomInfo) {
     // FIXME:  We should at least emit some pending operation / warning for 
this?
     return;
@@ -420,7 +418,7 @@ export async function processRecoupGroup(
   return TaskRunResult.finished();
 }
 
-export class RewardTransactionContext implements TransactionContext {
+export class RecoupTransactionContext implements TransactionContext {
   abortTransaction(): Promise<void> {
     throw new Error("Method not implemented.");
   }
@@ -440,7 +438,7 @@ export class RewardTransactionContext implements 
TransactionContext {
   public taskId: TaskIdStr;
 
   constructor(
-    public ws: InternalWalletState,
+    public wex: WalletExecutionContext,
     private recoupGroupId: string,
   ) {
     this.transactionId = constructTransactionIdentifier({
@@ -487,6 +485,10 @@ export async function createRecoupGroup(
 
   await tx.recoupGroups.put(recoupGroup);
 
+  const ctx = new RecoupTransactionContext(wex, recoupGroupId);
+
+  wex.taskScheduler.startShepherdTask(ctx.taskId);
+
   return recoupGroupId;
 }
 
diff --git a/packages/taler-wallet-core/src/shepherd.ts 
b/packages/taler-wallet-core/src/shepherd.ts
index f04bcd2c2..4ca472e7b 100644
--- a/packages/taler-wallet-core/src/shepherd.ts
+++ b/packages/taler-wallet-core/src/shepherd.ts
@@ -61,7 +61,10 @@ import {
   computeDepositTransactionStatus,
   processDepositGroup,
 } from "./deposits.js";
-import { updateExchangeFromUrlHandler } from "./exchanges.js";
+import {
+  computeDenomLossTransactionStatus,
+  updateExchangeFromUrlHandler,
+} from "./exchanges.js";
 import {
   computePayMerchantTransactionState,
   computeRefundTransactionState,
@@ -636,6 +639,7 @@ async function getTransactionState(
       "peerPushCredit",
       "rewards",
       "refreshGroups",
+      "denomLossEvents",
     ]
   >,
   transactionId: string,
@@ -674,12 +678,13 @@ async function getTransactionState(
       }
       return computeRefundTransactionState(rec);
     }
-    case TransactionType.PeerPullCredit:
+    case TransactionType.PeerPullCredit: {
       const rec = await tx.peerPullCredit.get(parsedTxId.pursePub);
       if (!rec) {
         return undefined;
       }
       return computePeerPullCreditTransactionState(rec);
+    }
     case TransactionType.PeerPullDebit: {
       const rec = await tx.peerPullDebit.get(parsedTxId.peerPullDebitId);
       if (!rec) {
@@ -717,6 +722,13 @@ async function getTransactionState(
     }
     case TransactionType.Recoup:
       throw Error("not yet supported");
+    case TransactionType.DenomLoss: {
+      const rec = await tx.denomLossEvents.get(parsedTxId.denomLossEventId);
+      if (!rec) {
+        return undefined;
+      }
+      return computeDenomLossTransactionStatus(rec);
+    }
     default:
       assertUnreachable(parsedTxId);
   }
@@ -864,6 +876,8 @@ export function listTaskForTransactionId(transactionId: 
string): TaskIdStr[] {
           withdrawalGroupId: tid.withdrawalGroupId,
         }),
       ];
+    case TransactionType.DenomLoss:
+      return [];
     default:
       assertUnreachable(tid);
   }
diff --git a/packages/taler-wallet-core/src/transactions.ts 
b/packages/taler-wallet-core/src/transactions.ts
index 7ece43dc5..e404c0354 100644
--- a/packages/taler-wallet-core/src/transactions.ts
+++ b/packages/taler-wallet-core/src/transactions.ts
@@ -23,7 +23,6 @@ import {
   Amounts,
   assertUnreachable,
   checkDbInvariant,
-  checkLogicInvariant,
   DepositTransactionTrackingState,
   j2s,
   Logger,
@@ -38,6 +37,7 @@ import {
   TalerErrorCode,
   TalerPreciseTimestamp,
   Transaction,
+  TransactionAction,
   TransactionByIdRequest,
   TransactionIdStr,
   TransactionMajorState,
@@ -59,6 +59,7 @@ import {
   TransactionContext,
 } from "./common.js";
 import {
+  DenomLossEventRecord,
   DepositElementStatus,
   DepositGroupRecord,
   OPERATION_STATUS_ACTIVE_FIRST,
@@ -76,7 +77,6 @@ import {
   RefreshGroupRecord,
   RefreshOperationStatus,
   RefundGroupRecord,
-  RewardRecord,
   timestampPreciseFromDb,
   timestampProtocolFromDb,
   WalletDbReadOnlyTransaction,
@@ -90,6 +90,8 @@ import {
   DepositTransactionContext,
 } from "./deposits.js";
 import {
+  computeDenomLossTransactionStatus,
+  DenomLossTransactionContext,
   ExchangeWireDetails,
   getExchangeWireDetailsInTx,
 } from "./exchanges.js";
@@ -127,11 +129,7 @@ import {
   computeRefreshTransactionState,
   RefreshTransactionContext,
 } from "./refresh.js";
-import {
-  computeRewardTransactionStatus,
-  computeTipTransactionActions,
-  RewardTransactionContext,
-} from "./reward.js";
+import { RewardTransactionContext } from "./reward.js";
 import type { WalletExecutionContext } from "./wallet.js";
 import {
   augmentPaytoUrisForWithdrawal,
@@ -212,6 +210,7 @@ const txOrder: { [t in TransactionType]: number } = {
   [TransactionType.Refresh]: 10,
   [TransactionType.Recoup]: 11,
   [TransactionType.InternalWithdrawal]: 12,
+  [TransactionType.DenomLoss]: 13,
 };
 
 export async function getTransactionById(
@@ -268,6 +267,19 @@ export async function getTransactionById(
       );
     }
 
+    case TransactionType.DenomLoss: {
+      const rec = await wex.db.runReadOnlyTx(
+        ["denomLossEvents"],
+        async (tx) => {
+          return tx.denomLossEvents.get(parsedTx.denomLossEventId);
+        },
+      );
+      if (!rec) {
+        throw Error("denom loss record not found");
+      }
+      return buildTransactionForDenomLoss(rec);
+    }
+
     case TransactionType.Recoup:
       throw new Error("not yet supported");
 
@@ -859,6 +871,24 @@ function buildTransactionForRefresh(
   };
 }
 
+function buildTransactionForDenomLoss(rec: DenomLossEventRecord): Transaction {
+  const txState = computeDenomLossTransactionStatus(rec);
+  return {
+    type: TransactionType.DenomLoss,
+    txState,
+    txActions: [TransactionAction.Delete],
+    amountRaw: Amounts.stringify(rec.amount),
+    amountEffective: Amounts.stringify(rec.amount),
+    timestamp: timestampPreciseFromDb(rec.timestampCreated),
+    transactionId: constructTransactionIdentifier({
+      tag: TransactionType.DenomLoss,
+      denomLossEventId: rec.denomLossEventId,
+    }),
+    lossEventType: rec.eventType,
+    exchangeBaseUrl: rec.exchangeBaseUrl,
+  };
+}
+
 function buildTransactionForDeposit(
   dg: DepositGroupRecord,
   ort?: OperationRetryRecord,
@@ -1079,6 +1109,7 @@ export async function getTransactions(
       "withdrawalGroups",
       "refreshGroups",
       "refundGroups",
+      "denomLossEvents",
     ],
     async (tx) => {
       await iterRecordsForPeerPushDebit(tx, filter, async (pi) => {
@@ -1325,6 +1356,21 @@ export async function getTransactions(
         }
       });
 
+      await iterRecordsForDenomLoss(tx, filter, async (rec) => {
+        const amount = Amounts.parseOrThrow(rec.amount);
+        const exchangesInTx = [rec.exchangeBaseUrl];
+        if (
+          shouldSkipCurrency(
+            transactionsRequest,
+            amount.currency,
+            exchangesInTx,
+          )
+        ) {
+          return;
+        }
+        transactions.push(buildTransactionForDenomLoss(rec));
+      });
+
       await iterRecordsForDeposit(tx, filter, async (dg) => {
         const amount = Amounts.parseOrThrow(dg.amount);
         const exchangesInTx = dg.infoPerExchange
@@ -1476,7 +1522,8 @@ export type ParsedTransactionIdentifier =
   | { tag: TransactionType.Reward; walletRewardId: string }
   | { tag: TransactionType.Withdrawal; withdrawalGroupId: string }
   | { tag: TransactionType.InternalWithdrawal; withdrawalGroupId: string }
-  | { tag: TransactionType.Recoup; recoupGroupId: string };
+  | { tag: TransactionType.Recoup; recoupGroupId: string }
+  | { tag: TransactionType.DenomLoss; denomLossEventId: string };
 
 export function constructTransactionIdentifier(
   pTxId: ParsedTransactionIdentifier,
@@ -1506,6 +1553,8 @@ export function constructTransactionIdentifier(
       return `txn:${pTxId.tag}:${pTxId.withdrawalGroupId}` as TransactionIdStr;
     case TransactionType.Recoup:
       return `txn:${pTxId.tag}:${pTxId.recoupGroupId}` as TransactionIdStr;
+    case TransactionType.DenomLoss:
+      return `txn:${pTxId.tag}:${pTxId.denomLossEventId}` as TransactionIdStr;
     default:
       assertUnreachable(pTxId);
   }
@@ -1565,6 +1614,11 @@ export function parseTransactionIdentifier(
         tag: TransactionType.Withdrawal,
         withdrawalGroupId: rest[0],
       };
+    case TransactionType.DenomLoss:
+      return {
+        tag: TransactionType.DenomLoss,
+        denomLossEventId: rest[0],
+      };
     default:
       return undefined;
   }
@@ -1636,6 +1690,9 @@ function maybeTaskFromTransaction(
         tag: PendingTaskType.Recoup,
         recoupGroupId: parsedTx.recoupGroupId,
       });
+    case TransactionType.DenomLoss:
+      // Nothing to do for denom loss
+      return undefined;
     default:
       assertUnreachable(parsedTx);
   }
@@ -1687,8 +1744,10 @@ async function getContextForTransaction(
     case TransactionType.Reward:
       return new RewardTransactionContext(wex, tx.walletRewardId);
     case TransactionType.Recoup:
+      //return new RecoupTransactionContext(ws, tx.recoupGroupId);
       throw new Error("not yet supported");
-    //return new RecoupTransactionContext(ws, tx.recoupGroupId);
+    case TransactionType.DenomLoss:
+      return new DenomLossTransactionContext(wex, tx.denomLossEventId);
     default:
       assertUnreachable(tx);
   }
@@ -1847,6 +1906,27 @@ async function iterRecordsForDeposit(
   }
 }
 
+async function iterRecordsForDenomLoss(
+  tx: WalletDbReadOnlyTransaction<["denomLossEvents"]>,
+  filter: TransactionRecordFilter,
+  f: (r: DenomLossEventRecord) => Promise<void>,
+): Promise<void> {
+  let dgs: DenomLossEventRecord[];
+  if (filter.onlyState === "nonfinal") {
+    const keyRange = GlobalIDB.KeyRange.bound(
+      OPERATION_STATUS_ACTIVE_FIRST,
+      OPERATION_STATUS_ACTIVE_LAST,
+    );
+    dgs = await tx.denomLossEvents.indexes.byStatus.getAll(keyRange);
+  } else {
+    dgs = await tx.denomLossEvents.indexes.byStatus.getAll();
+  }
+
+  for (const dg of dgs) {
+    await f(dg);
+  }
+}
+
 async function iterRecordsForRefund(
   tx: WalletDbReadOnlyTransaction<["refundGroups"]>,
   filter: TransactionRecordFilter,
diff --git a/packages/taler-wallet-core/src/versions.ts 
b/packages/taler-wallet-core/src/versions.ts
index bf8a9f7c8..ad58a66ec 100644
--- a/packages/taler-wallet-core/src/versions.ts
+++ b/packages/taler-wallet-core/src/versions.ts
@@ -50,11 +50,9 @@ export const WALLET_COREBANK_API_PROTOCOL_VERSION = "2:0:0";
 export const WALLET_BANK_CONVERSION_API_PROTOCOL_VERSION = "2:0:0";
 
 /**
- * Semver of the wallet-core API implementation.
- * Will be replaced with the value from package.json in a
- * post-compilation step (inside lib/).
+ * Libtool version of the wallet-core API.
  */
-export const WALLET_CORE_API_IMPLEMENTATION_VERSION = "3:0:2";
+export const WALLET_CORE_API_PROTOCOL_VERSION = "4:0:0";
 
 /**
  * Libtool rules:
diff --git a/packages/taler-wallet-core/src/wallet.ts 
b/packages/taler-wallet-core/src/wallet.ts
index c203f6648..7cc5ab93b 100644
--- a/packages/taler-wallet-core/src/wallet.ts
+++ b/packages/taler-wallet-core/src/wallet.ts
@@ -274,7 +274,7 @@ import {
   WALLET_BANK_CONVERSION_API_PROTOCOL_VERSION,
   WALLET_BANK_INTEGRATION_PROTOCOL_VERSION,
   WALLET_COREBANK_API_PROTOCOL_VERSION,
-  WALLET_CORE_API_IMPLEMENTATION_VERSION,
+  WALLET_CORE_API_PROTOCOL_VERSION,
   WALLET_EXCHANGE_PROTOCOL_VERSION,
   WALLET_MERCHANT_PROTOCOL_VERSION,
 } from "./versions.js";
@@ -1448,7 +1448,7 @@ export function getVersion(wex: WalletExecutionContext): 
WalletCoreVersion {
     implementationSemver: walletCoreBuildInfo.implementationSemver,
     implementationGitHash: walletCoreBuildInfo.implementationGitHash,
     hash: undefined,
-    version: WALLET_CORE_API_IMPLEMENTATION_VERSION,
+    version: WALLET_CORE_API_PROTOCOL_VERSION,
     exchange: WALLET_EXCHANGE_PROTOCOL_VERSION,
     merchant: WALLET_MERCHANT_PROTOCOL_VERSION,
     bankConversionApiRange: WALLET_BANK_CONVERSION_API_PROTOCOL_VERSION,

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