gnunet-svn
[Top][All Lists]
Advanced

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

[taler-wallet-core] branch master updated: wallet-core: make basic backu


From: gnunet
Subject: [taler-wallet-core] branch master updated: wallet-core: make basic backup work again
Date: Tue, 20 Sep 2022 23:18:00 +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 16a5bb408 wallet-core: make basic backup work again
16a5bb408 is described below

commit 16a5bb40834c01e50e84144bb644517e67a66187
Author: Florian Dold <florian@dold.me>
AuthorDate: Tue Sep 20 21:44:21 2022 +0200

    wallet-core: make basic backup work again
---
 packages/taler-util/src/backupTypes.ts             | 291 +++++++++++----------
 packages/taler-util/src/transactionsTypes.ts       |  17 +-
 packages/taler-wallet-cli/src/index.ts             |  20 ++
 .../integrationtests/test-wallet-backup-basic.ts   |  31 ++-
 .../test-wallet-backup-doublespend.ts              |  20 +-
 packages/taler-wallet-core/src/db-utils.ts         |  27 ++
 packages/taler-wallet-core/src/db.ts               |  50 +++-
 .../src/operations/backup/export.ts                |  93 +++++--
 .../src/operations/backup/import.ts                |  89 +++++++
 .../src/operations/backup/index.ts                 |  10 +-
 .../taler-wallet-core/src/operations/withdraw.ts   |   7 +-
 packages/taler-wallet-core/src/util/query.ts       |  65 +++--
 packages/taler-wallet-core/src/wallet-api-types.ts |  10 +
 packages/taler-wallet-core/src/wallet.ts           |  16 +-
 14 files changed, 533 insertions(+), 213 deletions(-)

diff --git a/packages/taler-util/src/backupTypes.ts 
b/packages/taler-util/src/backupTypes.ts
index b31a83831..90f95ce9d 100644
--- a/packages/taler-util/src/backupTypes.ts
+++ b/packages/taler-util/src/backupTypes.ts
@@ -47,6 +47,15 @@
  * 3. Derived information is never backed up (hashed values, public keys
  *    when we know the private key).
  *
+ * Problems:
+ *
+ * Withdrawal group fork/merging loses money:
+ * - Before the withdrawal happens, wallet forks into two backups.
+ * - Both wallets need to re-denominate the withdrawal (unlikely but possible).
+ * - Because the backup doesn't store planchets where a withdrawal was 
attempted,
+ *   after merging some money will be list.
+ * - Fix: backup withdrawal objects also store planchets where withdrawal has 
been attempted
+ *
  * @author Florian Dold <dold@taler.net>
  */
 
@@ -56,6 +65,23 @@
 import { DenominationPubKey, UnblindedSignature } from "./talerTypes.js";
 import { TalerProtocolDuration, TalerProtocolTimestamp } from "./time.js";
 
+export const BACKUP_TAG = "gnu-taler-wallet-backup-content" as const;
+/**
+ * Major version.  Each increment means a backwards-incompatible change.
+ * Typically this means that a custom converter needs to be written.
+ */
+export const BACKUP_VERSION_MAJOR = 1 as const;
+
+/**
+ * Minor version.  Each increment means that information is added to the backup
+ * in a backwards-compatible way.
+ *
+ * Wallets can always import a smaller minor version than their own backup 
code version.
+ * When importing a bigger version, data loss is possible and the user should 
be urged to
+ * upgrade their wallet first.
+ */
+export const BACKUP_VERSION_MINOR = 1 as const;
+
 /**
  * Type alias for strings that are to be treated like amounts.
  */
@@ -93,12 +119,14 @@ export interface WalletBackupContentV1 {
   /**
    * Magic constant to identify that this is a backup content JSON.
    */
-  schema_id: "gnu-taler-wallet-backup-content";
+  schema_id: typeof BACKUP_TAG;
 
   /**
    * Version of the schema.
    */
-  schema_version: 1;
+  schema_version: typeof BACKUP_VERSION_MAJOR;
+
+  minor_version: number;
 
   /**
    * Root public key of the wallet.  This field is present as
@@ -131,6 +159,13 @@ export interface WalletBackupContentV1 {
 
   exchange_details: BackupExchangeDetails[];
 
+  /**
+   * Withdrawal groups.
+   *
+   * Sorted by the withdrawal group ID.
+   */
+  withdrawal_groups: BackupWithdrawalGroup[];
+
   /**
    * Grouped refresh sessions.
    *
@@ -208,6 +243,118 @@ export interface WalletBackupContentV1 {
   tombstones: Tombstone[];
 }
 
+export enum BackupOperationStatus {
+  Cancelled = "cancelled",
+  Finished = "finished",
+  Pending = "pending",
+}
+
+export enum BackupWgType {
+  BankManual = "bank-manual",
+  BankIntegrated = "bank-integrated",
+  PeerPullCredit = "peer-pull-credit",
+  PeerPushCredit = "peer-push-credit",
+  Recoup = "recoup",
+}
+
+export type BackupWgInfo =
+  | {
+      type: BackupWgType.BankManual;
+    }
+  | {
+      type: BackupWgType.BankIntegrated;
+      taler_withdraw_uri: string;
+
+      /**
+       * URL that the user can be redirected to, and allows
+       * them to confirm (or abort) the bank-integrated withdrawal.
+       */
+      confirm_url?: string;
+
+      /**
+       * Exchange payto URI that the bank will use to fund the reserve.
+       */
+      exchange_payto_uri: string;
+
+      /**
+       * Time when the information about this reserve was posted to the bank.
+       *
+       * Only applies if bankWithdrawStatusUrl is defined.
+       *
+       * Set to undefined if that hasn't happened yet.
+       */
+      timestamp_reserve_info_posted?: TalerProtocolTimestamp;
+
+      /**
+       * Time when the reserve was confirmed by the bank.
+       *
+       * Set to undefined if not confirmed yet.
+       */
+      timestamp_bank_confirmed?: TalerProtocolTimestamp;
+    }
+  | {
+      type: BackupWgType.PeerPullCredit;
+      contract_terms: any;
+      contract_priv: string;
+    }
+  | {
+      type: BackupWgType.PeerPushCredit;
+      contract_terms: any;
+    }
+  | {
+      type: BackupWgType.Recoup;
+    };
+
+/**
+ * FIXME: Open questions:
+ * - Do we have to store the denomination selection?  Why?
+ *   (If deterministic, amount shouldn't change. Not storing it is simpler.)
+ */
+export interface BackupWithdrawalGroup {
+  withdrawal_group_id: string;
+
+  /**
+   * Detailled info based on the type of withdrawal group.
+   */
+  info: BackupWgInfo;
+
+  secret_seed: string;
+
+  reserve_priv: string;
+
+  exchange_base_url: string;
+
+  timestamp_created: TalerProtocolTimestamp;
+
+  timestamp_finish?: TalerProtocolTimestamp;
+
+  operation_status: BackupOperationStatus;
+
+  instructed_amount: BackupAmountString;
+
+  /**
+   * Amount including fees (i.e. the amount subtracted from the
+   * reserve to withdraw all coins in this withdrawal session).
+   *
+   * Note that this *includes* the amount remaining in the reserve
+   * that is too small to be withdrawn, and thus can't be derived
+   * from selectedDenoms.
+   */
+  raw_withdrawal_amount: BackupAmountString;
+
+  /**
+   * Restrict withdrawals from this reserve to this age.
+   */
+  restrict_age?: number;
+
+  /**
+   * Multiset of denominations selected for withdrawal.
+   */
+  selected_denoms: BackupDenomSel;
+
+  selected_denoms_uid: OperationUid;
+}
+
 /**
  * Tombstone in the format "<type>:<key>"
  */
@@ -619,46 +766,6 @@ export interface BackupRefreshGroup {
   finish_is_failure?: boolean;
 }
 
-/**
- * Backup information for a withdrawal group.
- *
- * Always part of a BackupReserve.
- */
-export interface BackupWithdrawalGroup {
-  withdrawal_group_id: string;
-
-  /**
-   * Secret seed to derive the planchets.
-   */
-  secret_seed: string;
-
-  /**
-   * When was the withdrawal operation started started?
-   * Timestamp in milliseconds.
-   */
-  timestamp_created: TalerProtocolTimestamp;
-
-  timestamp_finish?: TalerProtocolTimestamp;
-  finish_is_failure?: boolean;
-
-  /**
-   * Amount including fees (i.e. the amount subtracted from the
-   * reserve to withdraw all coins in this withdrawal session).
-   *
-   * Note that this *includes* the amount remaining in the reserve
-   * that is too small to be withdrawn, and thus can't be derived
-   * from selectedDenoms.
-   */
-  raw_withdrawal_amount: BackupAmountString;
-
-  /**
-   * Multiset of denominations selected for withdrawal.
-   */
-  selected_denoms: BackupDenomSel;
-
-  selected_denoms_id: OperationUid;
-}
-
 export enum BackupRefundState {
   Failed = "failed",
   Applied = "applied",
@@ -914,101 +1021,6 @@ export type BackupDenomSel = {
   count: number;
 }[];
 
-export interface BackupReserve {
-  /**
-   * The reserve private key.
-   */
-  reserve_priv: string;
-
-  /**
-   * Time when the reserve was created.
-   */
-  timestamp_created: TalerProtocolTimestamp;
-
-  /**
-   * Timestamp of the last observed activity.
-   *
-   * Used to compute when to give up querying the exchange.
-   */
-  timestamp_last_activity: TalerProtocolTimestamp;
-
-  /**
-   * Timestamp of when the reserve closed.
-   *
-   * Note that the last activity can be after the closing time
-   * due to recouping.
-   */
-  timestamp_closed?: TalerProtocolTimestamp;
-
-  /**
-   * Wire information (as payto URI) for the bank account that
-   * transferred funds for this reserve.
-   */
-  sender_wire?: string;
-
-  /**
-   * Amount that was sent by the user to fund the reserve.
-   */
-  instructed_amount: BackupAmountString;
-
-  /**
-   * Extra state for when this is a withdrawal involving
-   * a Taler-integrated bank.
-   */
-  bank_info?: {
-    /**
-     * Status URL that the wallet will use to query the status
-     * of the Taler withdrawal operation on the bank's side.
-     */
-    status_url: string;
-
-    /**
-     * URL that the user should be instructed to navigate to
-     * in order to confirm the transfer (or show instructions/help
-     * on how to do that at a PoS terminal).
-     */
-    confirm_url?: string;
-
-    /**
-     * Exchange payto URI that the bank will use to fund the reserve.
-     */
-    exchange_payto_uri: string;
-
-    /**
-     * Time when the information about this reserve was posted to the bank.
-     */
-    timestamp_reserve_info_posted: TalerProtocolTimestamp | undefined;
-
-    /**
-     * Time when the reserve was confirmed by the bank.
-     *
-     * Set to undefined if not confirmed yet.
-     */
-    timestamp_bank_confirmed: TalerProtocolTimestamp | undefined;
-  };
-
-  /**
-   * Pre-allocated withdrawal group ID that will be
-   * used for the first withdrawal.
-   *
-   * (Already created so it can be referenced in the transactions list
-   * before it really exists, as there'll be an entry for the withdrawal
-   * even before the withdrawal group really has been created).
-   */
-  initial_withdrawal_group_id: string;
-
-  /**
-   * Denominations selected for the initial withdrawal.
-   * Stored here to show costs before withdrawal has begun.
-   */
-  initial_selected_denoms: BackupDenomSel;
-
-  /**
-   * Groups of withdrawal operations for this reserve.  Typically just one.
-   */
-  withdrawal_groups: BackupWithdrawalGroup[];
-}
-
 /**
  * Wire fee for one wire payment target type as stored in the
  * wallet's database.
@@ -1148,11 +1160,6 @@ export interface BackupExchangeDetails {
    */
   denominations: BackupDenomination[];
 
-  /**
-   * Reserves at the exchange.
-   */
-  reserves: BackupReserve[];
-
   /**
    * Last observed protocol version.
    */
diff --git a/packages/taler-util/src/transactionsTypes.ts 
b/packages/taler-util/src/transactionsTypes.ts
index e5b0695f8..3dc4a93d7 100644
--- a/packages/taler-util/src/transactionsTypes.ts
+++ b/packages/taler-util/src/transactionsTypes.ts
@@ -87,10 +87,14 @@ export interface TransactionCommon {
    */
   frozen: boolean;
 
-  // Raw amount of the transaction (exclusive of fees or other extra costs)
+  /**
+   * Raw amount of the transaction (exclusive of fees or other extra costs).
+   */
   amountRaw: AmountString;
 
-  // Amount added or removed from the wallet's balance (including all fees and 
other costs)
+  /**
+   * Amount added or removed from the wallet's balance (including all fees and 
other costs).
+   */
   amountEffective: AmountString;
 
   error?: TalerErrorDetail;
@@ -509,10 +513,11 @@ export interface TransactionByIdRequest {
   transactionId: string;
 }
 
-export const codecForTransactionByIdRequest = (): 
Codec<TransactionByIdRequest> =>
-  buildCodecForObject<TransactionByIdRequest>()
-    .property("transactionId", codecForString())
-    .build("TransactionByIdRequest");
+export const codecForTransactionByIdRequest =
+  (): Codec<TransactionByIdRequest> =>
+    buildCodecForObject<TransactionByIdRequest>()
+      .property("transactionId", codecForString())
+      .build("TransactionByIdRequest");
 
 export const codecForTransactionsRequest = (): Codec<TransactionsRequest> =>
   buildCodecForObject<TransactionsRequest>()
diff --git a/packages/taler-wallet-cli/src/index.ts 
b/packages/taler-wallet-cli/src/index.ts
index 31e0b0f65..8fd0de642 100644
--- a/packages/taler-wallet-cli/src/index.ts
+++ b/packages/taler-wallet-cli/src/index.ts
@@ -886,6 +886,26 @@ currenciesCli
     });
   });
 
+advancedCli
+  .subcommand("clearDatabase", "clear-database", {
+    help: "Clear the database, irrevocable deleting all data in the wallet.",
+  })
+  .action(async (args) => {
+    await withWallet(args, async (wallet) => {
+      await wallet.client.call(WalletApiOperation.ClearDb, {});
+    });
+  });
+
+advancedCli
+  .subcommand("recycle", "recycle", {
+    help: "Export, clear and re-import the database via the backup mechamism.",
+  })
+  .action(async (args) => {
+    await withWallet(args, async (wallet) => {
+      await wallet.client.call(WalletApiOperation.Recycle, {});
+    });
+  });
+
 advancedCli
   .subcommand("payPrepare", "pay-prepare", {
     help: "Claim an order but don't pay yet.",
diff --git 
a/packages/taler-wallet-cli/src/integrationtests/test-wallet-backup-basic.ts 
b/packages/taler-wallet-cli/src/integrationtests/test-wallet-backup-basic.ts
index 23e01e5e1..c82d1e650 100644
--- a/packages/taler-wallet-cli/src/integrationtests/test-wallet-backup-basic.ts
+++ b/packages/taler-wallet-cli/src/integrationtests/test-wallet-backup-basic.ts
@@ -17,9 +17,13 @@
 /**
  * Imports.
  */
+import { j2s } from "@gnu-taler/taler-util";
 import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
 import { GlobalTestState, WalletCli } from "../harness/harness.js";
-import { createSimpleTestkudosEnvironment, withdrawViaBank } from 
"../harness/helpers.js";
+import {
+  createSimpleTestkudosEnvironment,
+  withdrawViaBank,
+} from "../harness/helpers.js";
 import { SyncService } from "../harness/sync";
 
 /**
@@ -28,13 +32,8 @@ import { SyncService } from "../harness/sync";
 export async function runWalletBackupBasicTest(t: GlobalTestState) {
   // Set up test environment
 
-  const {
-    commonDb,
-    merchant,
-    wallet,
-    bank,
-    exchange,
-  } = await createSimpleTestkudosEnvironment(t);
+  const { commonDb, merchant, wallet, bank, exchange } =
+    await createSimpleTestkudosEnvironment(t);
 
   const sync = await SyncService.create(t, {
     currency: "TESTKUDOS",
@@ -106,6 +105,9 @@ export async function runWalletBackupBasicTest(t: 
GlobalTestState) {
     {},
   );
 
+  const txs = await wallet.client.call(WalletApiOperation.GetTransactions, {});
+  console.log(`backed up transactions ${j2s(txs)}`);
+
   const wallet2 = new WalletCli(t, "wallet2");
 
   // Check that the second wallet is a fresh wallet.
@@ -129,6 +131,11 @@ export async function runWalletBackupBasicTest(t: 
GlobalTestState) {
 
   // Now do some basic checks that the restored wallet is still functional
   {
+    const txs = await wallet2.client.call(
+      WalletApiOperation.GetTransactions,
+      {},
+    );
+    console.log(`restored transactions ${j2s(txs)}`);
     const bal1 = await wallet2.client.call(WalletApiOperation.GetBalances, {});
 
     t.assertAmountEquals(bal1.balances[0].available, "TESTKUDOS:14.1");
@@ -140,8 +147,16 @@ export async function runWalletBackupBasicTest(t: 
GlobalTestState) {
       amount: "TESTKUDOS:10",
     });
 
+    await exchange.runWirewatchOnce();
+
     await wallet2.runUntilDone();
 
+    const txs2 = await wallet2.client.call(
+      WalletApiOperation.GetTransactions,
+      {},
+    );
+    console.log(`tx after withdraw after restore ${j2s(txs2)}`);
+
     const bal2 = await wallet2.client.call(WalletApiOperation.GetBalances, {});
 
     t.assertAmountEquals(bal2.balances[0].available, "TESTKUDOS:23.82");
diff --git 
a/packages/taler-wallet-cli/src/integrationtests/test-wallet-backup-doublespend.ts
 
b/packages/taler-wallet-cli/src/integrationtests/test-wallet-backup-doublespend.ts
index 8c20dcc2b..ec1d6417b 100644
--- 
a/packages/taler-wallet-cli/src/integrationtests/test-wallet-backup-doublespend.ts
+++ 
b/packages/taler-wallet-cli/src/integrationtests/test-wallet-backup-doublespend.ts
@@ -19,7 +19,11 @@
  */
 import { PreparePayResultType } from "@gnu-taler/taler-util";
 import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
-import { GlobalTestState, WalletCli, MerchantPrivateApi } from 
"../harness/harness.js";
+import {
+  GlobalTestState,
+  WalletCli,
+  MerchantPrivateApi,
+} from "../harness/harness.js";
 import {
   createSimpleTestkudosEnvironment,
   makeTestPayment,
@@ -33,13 +37,8 @@ import { SyncService } from "../harness/sync";
 export async function runWalletBackupDoublespendTest(t: GlobalTestState) {
   // Set up test environment
 
-  const {
-    commonDb,
-    merchant,
-    wallet,
-    bank,
-    exchange,
-  } = await createSimpleTestkudosEnvironment(t);
+  const { commonDb, merchant, wallet, bank, exchange } =
+    await createSimpleTestkudosEnvironment(t);
 
   const sync = await SyncService.create(t, {
     currency: "TESTKUDOS",
@@ -139,8 +138,9 @@ export async function runWalletBackupDoublespendTest(t: 
GlobalTestState) {
       },
     );
 
-    t.assertTrue(
-      preparePayResult.status === PreparePayResultType.PaymentPossible,
+    t.assertDeepEqual(
+      preparePayResult.status,
+      PreparePayResultType.PaymentPossible,
     );
 
     const res = await wallet2.client.call(WalletApiOperation.ConfirmPay, {
diff --git a/packages/taler-wallet-core/src/db-utils.ts 
b/packages/taler-wallet-core/src/db-utils.ts
index de54719c9..b32b3d585 100644
--- a/packages/taler-wallet-core/src/db-utils.ts
+++ b/packages/taler-wallet-core/src/db-utils.ts
@@ -72,6 +72,33 @@ function upgradeFromStoreMap(
   throw Error("upgrade not supported");
 }
 
+function promiseFromTransaction(transaction: IDBTransaction): Promise<void> {
+  return new Promise<void>((resolve, reject) => {
+    transaction.oncomplete = () => {
+      resolve();
+    };
+    transaction.onerror = () => {
+      reject();
+    };
+  });
+}
+
+/**
+ * Purge all data in the given database.
+ */
+export function clearDatabase(db: IDBDatabase): Promise<void> {
+  // db.objectStoreNames is a DOMStringList, so we need to convert
+  let stores: string[] = [];
+  for (let i = 0; i < db.objectStoreNames.length; i++) {
+    stores.push(db.objectStoreNames[i]);
+  }
+  const tx = db.transaction(stores, "readwrite");
+  for (const store of stores) {
+    tx.objectStore(store).clear();
+  }
+  return promiseFromTransaction(tx);
+}
+
 function onTalerDbUpgradeNeeded(
   db: IDBDatabase,
   oldVersion: number,
diff --git a/packages/taler-wallet-core/src/db.ts 
b/packages/taler-wallet-core/src/db.ts
index 1275b0cf2..078060297 100644
--- a/packages/taler-wallet-core/src/db.ts
+++ b/packages/taler-wallet-core/src/db.ts
@@ -1,6 +1,6 @@
 /*
  This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2021-2022 Taler Systems S.A.
 
  GNU Taler is free software; you can redistribute it and/or modify it under the
  terms of the GNU General Public License as published by the Free Software
@@ -49,6 +49,24 @@ import {
 import { RetryInfo, RetryTags } from "./util/retries.js";
 import { Event, IDBDatabase } from "@gnu-taler/idb-bridge";
 
+/**
+ * This file contains the database schema of the Taler wallet together
+ * with some helper functions.
+ *
+ * Some design considerations:
+ * - By convention, each object store must have a corresponding "<Name>Record"
+ *   interface defined for it.
+ * - For records that represent operations, there should be exactly
+ *   one top-level enum field that indicates the status of the operation.
+ *   This field should be present even if redundant, because the field
+ *   will have an index.
+ * - Amounts are stored as strings, except when they are needed for
+ *   indexing.
+ * - Optional fields should be avoided, use "T | undefined" instead.
+ *
+ * @author Florian Dold <dold@taler.net>
+ */
+
 /**
  * Name of the Taler database.  This is effectively the major
  * version of the DB schema. Whenever it changes, custom import logic
@@ -76,6 +94,9 @@ export const CURRENT_DB_CONFIG_KEY = "currentMainDbName";
  */
 export const WALLET_DB_MINOR_VERSION = 1;
 
+/**
+ * Status of a withdrawal.
+ */
 export enum ReserveRecordStatus {
   /**
    * Reserve must be registered with the bank.
@@ -293,7 +314,7 @@ export interface DenominationRecord {
    * Was this denomination still offered by the exchange the last time
    * we checked?
    * Only false when the exchange redacts a previously published denomination.
-   * 
+   *
    * FIXME: Consider rolling this and isRevoked into some bitfield?
    */
   isOffered: boolean;
@@ -520,6 +541,9 @@ export interface PlanchetRecord {
    */
   coinIdx: number;
 
+  /**
+   * FIXME: make this an enum!
+   */
   withdrawalDone: boolean;
 
   lastError: TalerErrorDetail | undefined;
@@ -639,6 +663,9 @@ export interface CoinRecord {
 
   /**
    * Amount that's left on the coin.
+   *
+   * FIXME: This is pretty redundant with "allocation" and "status".
+   * Do we really need this?
    */
   currentAmount: AmountJson;
 
@@ -716,6 +743,9 @@ export interface ProposalDownload {
    */
   contractTermsRaw: any;
 
+  /**
+   * Extracted / parsed data from the contract terms.
+   */
   contractData: WalletContractData;
 }
 
@@ -780,6 +810,9 @@ export interface TipRecord {
    */
   tipAmountRaw: AmountJson;
 
+  /**
+   * Effect on the balance (including fees etc).
+   */
   tipAmountEffective: AmountJson;
 
   /**
@@ -800,6 +833,9 @@ export interface TipRecord {
   /**
    * Denomination selection made by the wallet for picking up
    * this tip.
+   *
+   * FIXME: Put this into some DenomSelectionCacheRecord instead of
+   * storing it here!
    */
   denomsSel: DenomSelectionState;
 
@@ -889,6 +925,8 @@ export interface RefreshGroupRecord {
 
   /**
    * No coins are pending, but at least one is frozen.
+   *
+   * FIXME: What does this mean?
    */
   frozen?: boolean;
 }
@@ -1319,11 +1357,15 @@ export interface WithdrawalGroupRecord {
   /**
    * Operation status of the withdrawal group.
    * Used for indexing in the database.
+   *
+   * FIXME: Redundant with reserveStatus
    */
   operationStatus: OperationStatus;
 
   /**
    * Current status of the reserve.
+   *
+   * FIXME: Wrong name!
    */
   reserveStatus: ReserveRecordStatus;
 
@@ -1756,6 +1798,10 @@ export interface CoinAvailabilityRecord {
   freshCoinCount: number;
 }
 
+/**
+ * Schema definition for the IndexedDB
+ * wallet database.
+ */
 export const WalletStoresV1 = {
   coinAvailability: describeStore(
     "coinAvailability",
diff --git a/packages/taler-wallet-core/src/operations/backup/export.ts 
b/packages/taler-wallet-core/src/operations/backup/export.ts
index 35d5e6ef7..b39e6dc27 100644
--- a/packages/taler-wallet-core/src/operations/backup/export.ts
+++ b/packages/taler-wallet-core/src/operations/backup/export.ts
@@ -25,6 +25,7 @@
  * Imports.
  */
 import {
+  AbsoluteTime,
   Amounts,
   BackupBackupProvider,
   BackupBackupProviderTerms,
@@ -35,6 +36,7 @@ import {
   BackupExchange,
   BackupExchangeDetails,
   BackupExchangeWireFee,
+  BackupOperationStatus,
   BackupProposal,
   BackupProposalStatus,
   BackupPurchase,
@@ -44,30 +46,35 @@ import {
   BackupRefreshSession,
   BackupRefundItem,
   BackupRefundState,
-  BackupReserve,
   BackupTip,
+  BackupWgInfo,
+  BackupWgType,
   BackupWithdrawalGroup,
+  BACKUP_VERSION_MAJOR,
+  BACKUP_VERSION_MINOR,
   canonicalizeBaseUrl,
   canonicalJson,
-  Logger,
-  WalletBackupContentV1,
-  hash,
   encodeCrock,
   getRandomBytes,
+  hash,
+  Logger,
   stringToBytes,
-  AbsoluteTime,
+  WalletBackupContentV1,
 } from "@gnu-taler/taler-util";
-import { InternalWalletState } from "../../internal-wallet-state.js";
 import {
   AbortStatus,
   CoinSourceType,
   CoinStatus,
   DenominationRecord,
+  OperationStatus,
   ProposalStatus,
   RefreshCoinStatus,
   RefundState,
   WALLET_BACKUP_STATE_KEY,
+  WithdrawalRecordType,
 } from "../../db.js";
+import { InternalWalletState } from "../../internal-wallet-state.js";
+import { assertUnreachable } from "../../util/assertUnreachable.js";
 import { getWalletBackupState, provideBackupState } from "./state.js";
 
 const logger = new Logger("backup/export.ts");
@@ -100,31 +107,75 @@ export async function exportBackup(
       const backupDenominationsByExchange: {
         [url: string]: BackupDenomination[];
       } = {};
-      const backupReservesByExchange: { [url: string]: BackupReserve[] } = {};
       const backupPurchases: BackupPurchase[] = [];
       const backupProposals: BackupProposal[] = [];
       const backupRefreshGroups: BackupRefreshGroup[] = [];
       const backupBackupProviders: BackupBackupProvider[] = [];
       const backupTips: BackupTip[] = [];
       const backupRecoupGroups: BackupRecoupGroup[] = [];
-      const withdrawalGroupsByReserve: {
-        [reservePub: string]: BackupWithdrawalGroup[];
-      } = {};
+      const backupWithdrawalGroups: BackupWithdrawalGroup[] = [];
 
       await tx.withdrawalGroups.iter().forEachAsync(async (wg) => {
-        const withdrawalGroups = (withdrawalGroupsByReserve[wg.reservePub] ??=
-          []);
-        withdrawalGroups.push({
+        let info: BackupWgInfo;
+        switch (wg.wgInfo.withdrawalType) {
+          case WithdrawalRecordType.BankIntegrated:
+            info = {
+              type: BackupWgType.BankIntegrated,
+              exchange_payto_uri: wg.wgInfo.bankInfo.exchangePaytoUri,
+              taler_withdraw_uri: wg.wgInfo.bankInfo.talerWithdrawUri,
+              confirm_url: wg.wgInfo.bankInfo.confirmUrl,
+              timestamp_bank_confirmed:
+                wg.wgInfo.bankInfo.timestampBankConfirmed,
+              timestamp_reserve_info_posted:
+                wg.wgInfo.bankInfo.timestampReserveInfoPosted,
+            };
+            break;
+          case WithdrawalRecordType.BankManual:
+            info = {
+              type: BackupWgType.BankManual,
+            };
+            break;
+          case WithdrawalRecordType.PeerPullCredit:
+            info = {
+              type: BackupWgType.PeerPullCredit,
+              contract_priv: wg.wgInfo.contractPriv,
+              contract_terms: wg.wgInfo.contractTerms,
+            };
+            break;
+          case WithdrawalRecordType.PeerPushCredit:
+            info = {
+              type: BackupWgType.PeerPushCredit,
+              contract_terms: wg.wgInfo.contractTerms,
+            };
+            break;
+          case WithdrawalRecordType.Recoup:
+            info = {
+              type: BackupWgType.Recoup,
+            };
+            break;
+          default:
+            assertUnreachable(wg.wgInfo);
+        }
+        backupWithdrawalGroups.push({
           raw_withdrawal_amount: Amounts.stringify(wg.rawWithdrawalAmount),
-          selected_denoms: wg.denomsSel.selectedDenoms.map((x) => ({
-            count: x.count,
-            denom_pub_hash: x.denomPubHash,
-          })),
+          info,
           timestamp_created: wg.timestampStart,
           timestamp_finish: wg.timestampFinish,
           withdrawal_group_id: wg.withdrawalGroupId,
           secret_seed: wg.secretSeed,
-          selected_denoms_id: wg.denomSelUid,
+          exchange_base_url: wg.exchangeBaseUrl,
+          instructed_amount: Amounts.stringify(wg.instructedAmount),
+          reserve_priv: wg.reservePriv,
+          restrict_age: wg.restrictAge,
+          operation_status:
+            wg.operationStatus == OperationStatus.Finished
+              ? BackupOperationStatus.Finished
+              : BackupOperationStatus.Pending,
+          selected_denoms_uid: wg.denomSelUid,
+          selected_denoms: wg.denomsSel.selectedDenoms.map((x) => ({
+            count: x.count,
+            denom_pub_hash: x.denomPubHash,
+          })),
         });
       });
 
@@ -299,7 +350,6 @@ export async function exportBackup(
           tos_accepted_timestamp: ex.termsOfServiceAcceptedTimestamp,
           denominations:
             backupDenominationsByExchange[ex.exchangeBaseUrl] ?? [],
-          reserves: backupReservesByExchange[ex.exchangeBaseUrl] ?? [],
         });
       });
 
@@ -439,7 +489,8 @@ export async function exportBackup(
 
       const backupBlob: WalletBackupContentV1 = {
         schema_id: "gnu-taler-wallet-backup-content",
-        schema_version: 1,
+        schema_version: BACKUP_VERSION_MAJOR,
+        minor_version: BACKUP_VERSION_MINOR,
         exchanges: backupExchanges,
         exchange_details: backupExchangeDetails,
         wallet_root_pub: bs.walletRootPub,
@@ -456,6 +507,8 @@ export async function exportBackup(
         intern_table: {},
         error_reports: [],
         tombstones: [],
+        // FIXME!
+        withdrawal_groups: backupWithdrawalGroups,
       };
 
       // If the backup changed, we change our nonce and timestamp.
diff --git a/packages/taler-wallet-core/src/operations/backup/import.ts 
b/packages/taler-wallet-core/src/operations/backup/import.ts
index be09952cd..507a6cf10 100644
--- a/packages/taler-wallet-core/src/operations/backup/import.ts
+++ b/packages/taler-wallet-core/src/operations/backup/import.ts
@@ -24,6 +24,7 @@ import {
   BackupPurchase,
   BackupRefreshReason,
   BackupRefundState,
+  BackupWgType,
   codecForContractTerms,
   DenomKeyType,
   j2s,
@@ -53,8 +54,11 @@ import {
   WalletContractData,
   WalletRefundItem,
   WalletStoresV1,
+  WgInfo,
+  WithdrawalRecordType,
 } from "../../db.js";
 import { InternalWalletState } from "../../internal-wallet-state.js";
+import { assertUnreachable } from "../../util/assertUnreachable.js";
 import {
   checkDbInvariant,
   checkLogicInvariant,
@@ -444,6 +448,91 @@ export async function importBackup(
           }
         }
 
+        for (const backupWg of backupBlob.withdrawal_groups) {
+          const reservePub = 
cryptoComp.reservePrivToPub[backupWg.reserve_priv];
+          checkLogicInvariant(!!reservePub);
+          const ts = makeEventId(TombstoneTag.DeleteReserve, reservePub);
+          if (tombstoneSet.has(ts)) {
+            continue;
+          }
+          const existingWg = await tx.withdrawalGroups.get(
+            backupWg.withdrawal_group_id,
+          );
+          if (existingWg) {
+            continue;
+          }
+          let wgInfo: WgInfo;
+          switch (backupWg.info.type) {
+            case BackupWgType.BankIntegrated:
+              wgInfo = {
+                withdrawalType: WithdrawalRecordType.BankIntegrated,
+                bankInfo: {
+                  exchangePaytoUri: backupWg.info.exchange_payto_uri,
+                  talerWithdrawUri: backupWg.info.taler_withdraw_uri,
+                  confirmUrl: backupWg.info.confirm_url,
+                  timestampBankConfirmed:
+                    backupWg.info.timestamp_bank_confirmed,
+                  timestampReserveInfoPosted:
+                    backupWg.info.timestamp_reserve_info_posted,
+                },
+              };
+              break;
+            case BackupWgType.BankManual:
+              wgInfo = {
+                withdrawalType: WithdrawalRecordType.BankManual,
+              };
+              break;
+            case BackupWgType.PeerPullCredit:
+              wgInfo = {
+                withdrawalType: WithdrawalRecordType.PeerPullCredit,
+                contractTerms: backupWg.info.contract_terms,
+                contractPriv: backupWg.info.contract_priv,
+              };
+              break;
+            case BackupWgType.PeerPushCredit:
+              wgInfo = {
+                withdrawalType: WithdrawalRecordType.PeerPushCredit,
+                contractTerms: backupWg.info.contract_terms,
+              };
+              break;
+            case BackupWgType.Recoup:
+              wgInfo = {
+                withdrawalType: WithdrawalRecordType.Recoup,
+              };
+              break;
+            default:
+              assertUnreachable(backupWg.info);
+          }
+          await tx.withdrawalGroups.put({
+            withdrawalGroupId: backupWg.withdrawal_group_id,
+            exchangeBaseUrl: backupWg.exchange_base_url,
+            instructedAmount: Amounts.parseOrThrow(backupWg.instructed_amount),
+            secretSeed: backupWg.secret_seed,
+            operationStatus: backupWg.timestamp_finish
+              ? OperationStatus.Finished
+              : OperationStatus.Pending,
+            denomsSel: await getDenomSelStateFromBackup(
+              tx,
+              backupWg.exchange_base_url,
+              backupWg.selected_denoms,
+            ),
+            denomSelUid: backupWg.selected_denoms_uid,
+            rawWithdrawalAmount: Amounts.parseOrThrow(
+              backupWg.raw_withdrawal_amount,
+            ),
+            reservePriv: backupWg.reserve_priv,
+            reservePub,
+            reserveStatus: backupWg.timestamp_finish
+              ? ReserveRecordStatus.Dormant
+              : ReserveRecordStatus.QueryingStatus, // FIXME!
+            timestampStart: backupWg.timestamp_created,
+            wgInfo,
+            restrictAge: backupWg.restrict_age,
+            senderWire: undefined, // FIXME!
+            timestampFinish: backupWg.timestamp_finish,
+          });
+        }
+
         // FIXME: import reserves with new schema
 
         // for (const backupReserve of backupExchangeDetails.reserves) {
diff --git a/packages/taler-wallet-core/src/operations/backup/index.ts 
b/packages/taler-wallet-core/src/operations/backup/index.ts
index c7c93e909..b69c0b7b7 100644
--- a/packages/taler-wallet-core/src/operations/backup/index.ts
+++ b/packages/taler-wallet-core/src/operations/backup/index.ts
@@ -187,11 +187,11 @@ async function computeBackupCryptoData(
       cryptoData.rsaDenomPubToHash[backupDenom.denom_pub.rsa_public_key] =
         encodeCrock(hashDenomPub(backupDenom.denom_pub));
     }
-    for (const backupReserve of backupExchangeDetails.reserves) {
-      cryptoData.reservePrivToPub[backupReserve.reserve_priv] = encodeCrock(
-        eddsaGetPublic(decodeCrock(backupReserve.reserve_priv)),
-      );
-    }
+  }
+  for (const backupWg of backupContent.withdrawal_groups) {
+    cryptoData.reservePrivToPub[backupWg.reserve_priv] = encodeCrock(
+      eddsaGetPublic(decodeCrock(backupWg.reserve_priv)),
+    );
   }
   for (const prop of backupContent.proposals) {
     const { h: contractTermsHash } = await cryptoApi.hashString({
diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts 
b/packages/taler-wallet-core/src/operations/withdraw.ts
index de9721f3d..7dd874f49 100644
--- a/packages/taler-wallet-core/src/operations/withdraw.ts
+++ b/packages/taler-wallet-core/src/operations/withdraw.ts
@@ -96,12 +96,13 @@ import { DbAccess, GetReadOnlyAccess } from 
"../util/query.js";
 import {
   OperationAttemptResult,
   OperationAttemptResultType,
+  RetryTags,
 } from "../util/retries.js";
 import {
   WALLET_BANK_INTEGRATION_PROTOCOL_VERSION,
   WALLET_EXCHANGE_PROTOCOL_VERSION,
 } from "../versions.js";
-import { makeCoinAvailable } from "../wallet.js";
+import { makeCoinAvailable, storeOperationPending } from "../wallet.js";
 import {
   getExchangeDetails,
   getExchangePaytoUri,
@@ -1099,6 +1100,7 @@ export async function processWithdrawalGroup(
   );
 
   if (withdrawalGroup.denomsSel.selectedDenoms.length === 0) {
+    logger.warn("Finishing empty withdrawal group (no denoms)");
     await ws.db
       .mktx((x) => [x.withdrawalGroups])
       .runReadWrite(async (tx) => {
@@ -1107,6 +1109,7 @@ export async function processWithdrawalGroup(
           return;
         }
         wg.operationStatus = OperationStatus.Finished;
+        wg.timestampFinish = TalerProtocolTimestamp.now();
         await tx.withdrawalGroups.put(wg);
       });
     return {
@@ -1185,7 +1188,7 @@ export async function processWithdrawalGroup(
             errorsPerCoin[x.coinIdx] = x.lastError;
           }
         });
-      logger.trace(`now withdrawn ${numFinished} of ${numTotalCoins} coins`);
+      logger.info(`now withdrawn ${numFinished} of ${numTotalCoins} coins`);
       if (wg.timestampFinish === undefined && numFinished === numTotalCoins) {
         finishedForFirstTime = true;
         wg.timestampFinish = TalerProtocolTimestamp.now();
diff --git a/packages/taler-wallet-core/src/util/query.ts 
b/packages/taler-wallet-core/src/util/query.ts
index 8b8c30f35..d1aae6fd6 100644
--- a/packages/taler-wallet-core/src/util/query.ts
+++ b/packages/taler-wallet-core/src/util/query.ts
@@ -409,10 +409,12 @@ export type GetReadWriteAccess<BoundStores> = {
 
 type ReadOnlyTransactionFunction<BoundStores, T> = (
   t: GetReadOnlyAccess<BoundStores>,
+  rawTx: IDBTransaction,
 ) => Promise<T>;
 
 type ReadWriteTransactionFunction<BoundStores, T> = (
   t: GetReadWriteAccess<BoundStores>,
+  rawTx: IDBTransaction,
 ) => Promise<T>;
 
 export interface TransactionContext<BoundStores> {
@@ -420,22 +422,10 @@ export interface TransactionContext<BoundStores> {
   runReadOnly<T>(f: ReadOnlyTransactionFunction<BoundStores, T>): Promise<T>;
 }
 
-type CheckDescriptor<T> = T extends StoreWithIndexes<
-  infer SN,
-  infer SD,
-  infer IM
->
-  ? StoreWithIndexes<SN, SD, IM>
-  : unknown;
-
-type GetPickerType<F, SM> = F extends (x: SM) => infer Out
-  ? { [P in keyof Out]: CheckDescriptor<Out[P]> }
-  : unknown;
-
 function runTx<Arg, Res>(
   tx: IDBTransaction,
   arg: Arg,
-  f: (t: Arg) => Promise<Res>,
+  f: (t: Arg, t2: IDBTransaction) => Promise<Res>,
 ): Promise<Res> {
   const stack = Error("Failed transaction was started here.");
   return new Promise((resolve, reject) => {
@@ -474,7 +464,7 @@ function runTx<Arg, Res>(
       logger.error(msg);
       reject(new TransactionAbortedError(msg));
     };
-    const resP = Promise.resolve().then(() => f(arg));
+    const resP = Promise.resolve().then(() => f(arg, tx));
     resP
       .then((result) => {
         gotFunResult = true;
@@ -624,6 +614,46 @@ export class DbAccess<StoreMap> {
     return this.db;
   }
 
+  /**
+   * Run a transaction with all object stores.
+   */
+  mktxAll(): TransactionContext<StoreMap> {
+    const storeNames: string[] = [];
+    const accessibleStores: { [x: string]: StoreWithIndexes<any, any, any> } =
+      {};
+
+    for (let i = 0; i < this.db.objectStoreNames.length; i++) {
+      const sn = this.db.objectStoreNames[i];
+      const swi = (this.stores as any)[sn] as StoreWithIndexes<any, any, any>;
+      if (!swi) {
+        throw Error(`store metadata not available (${sn})`);
+      }
+      storeNames.push(sn);
+      accessibleStores[sn] = swi;
+    }
+
+    const runReadOnly = <T>(
+      txf: ReadOnlyTransactionFunction<StoreMap, T>,
+    ): Promise<T> => {
+      const tx = this.db.transaction(storeNames, "readonly");
+      const readContext = makeReadContext(tx, accessibleStores);
+      return runTx(tx, readContext, txf);
+    };
+
+    const runReadWrite = <T>(
+      txf: ReadWriteTransactionFunction<StoreMap, T>,
+    ): Promise<T> => {
+      const tx = this.db.transaction(storeNames, "readwrite");
+      const writeContext = makeWriteContext(tx, accessibleStores);
+      return runTx(tx, writeContext, txf);
+    };
+
+    return {
+      runReadOnly,
+      runReadWrite,
+    };
+  }
+
   /**
    * Run a transaction with selected object stores.
    *
@@ -638,13 +668,14 @@ export class DbAccess<StoreMap> {
       [X in StoreNamesOf<StoreList>]: StoreList[number] & { storeName: X };
     },
   >(namePicker: (x: StoreMap) => StoreList): TransactionContext<BoundStores> {
+    const storeNames: string[] = [];
+    const accessibleStores: { [x: string]: StoreWithIndexes<any, any, any> } =
+      {};
+
     const storePick = namePicker(this.stores) as any;
     if (typeof storePick !== "object" || storePick === null) {
       throw Error();
     }
-    const storeNames: string[] = [];
-    const accessibleStores: { [x: string]: StoreWithIndexes<any, any, any> } =
-      {};
     for (const swiPicked of storePick) {
       const swi = swiPicked as StoreWithIndexes<any, any, any>;
       if (swi.mark !== storeWithIndexesSymbol) {
diff --git a/packages/taler-wallet-core/src/wallet-api-types.ts 
b/packages/taler-wallet-core/src/wallet-api-types.ts
index 665be80fb..f2c76731b 100644
--- a/packages/taler-wallet-core/src/wallet-api-types.ts
+++ b/packages/taler-wallet-core/src/wallet-api-types.ts
@@ -134,6 +134,8 @@ export enum WalletApiOperation {
   InitiatePeerPullPayment = "initiatePeerPullPayment",
   CheckPeerPullPayment = "checkPeerPullPayment",
   AcceptPeerPullPayment = "acceptPeerPullPayment",
+  ClearDb = "clearDb",
+  Recycle = "recycle",
 }
 
 export type WalletOperations = {
@@ -317,6 +319,14 @@ export type WalletOperations = {
     request: AcceptPeerPullPaymentRequest;
     response: {};
   };
+  [WalletApiOperation.ClearDb]: {
+    request: {};
+    response: {};
+  };
+  [WalletApiOperation.Recycle]: {
+    request: {};
+    response: {};
+  };
 };
 
 export type RequestType<
diff --git a/packages/taler-wallet-core/src/wallet.ts 
b/packages/taler-wallet-core/src/wallet.ts
index 1b74f2025..2e362da6e 100644
--- a/packages/taler-wallet-core/src/wallet.ts
+++ b/packages/taler-wallet-core/src/wallet.ts
@@ -99,6 +99,7 @@ import {
   CryptoDispatcher,
   CryptoWorkerFactory,
 } from "./crypto/workers/cryptoDispatcher.js";
+import { clearDatabase } from "./db-utils.js";
 import {
   AuditorTrustRecord,
   CoinRecord,
@@ -114,7 +115,6 @@ import {
   makeErrorDetail,
   TalerError,
 } from "./errors.js";
-import { createDenominationTimeline } from "./index.browser.js";
 import {
   ExchangeOperations,
   InternalWalletState,
@@ -131,6 +131,7 @@ import {
   codecForRunBackupCycle,
   getBackupInfo,
   getBackupRecovery,
+  importBackupPlain,
   loadBackupRecovery,
   processBackupForProvider,
   removeBackupProvider,
@@ -215,6 +216,7 @@ import {
 } from "./pending-types.js";
 import { assertUnreachable } from "./util/assertUnreachable.js";
 import { AsyncOpMemoMap, AsyncOpMemoSingle } from "./util/asyncMemo.js";
+import { createDenominationTimeline } from "./util/denominations.js";
 import {
   HttpRequestLibrary,
   readSuccessResponseJsonOrThrow,
@@ -1060,8 +1062,11 @@ async function dispatchRequestInternal(
       `wallet must be initialized before running operation ${operation}`,
     );
   }
+  // FIXME: Can we make this more type-safe by using the request/response type
+  // definitions we already have?
   switch (operation) {
     case "initWallet": {
+      logger.info("initializing wallet");
       ws.initCalled = true;
       if (typeof payload === "object" && (payload as any).skipDefaults) {
         logger.info("skipping defaults");
@@ -1371,6 +1376,15 @@ async function dispatchRequestInternal(
       logger.info(`started fakebank withdrawal: ${j2s(fbResp)}`);
       return {};
     }
+    case "clearDb":
+      await clearDatabase(ws.db.idbHandle());
+      return {};
+    case "recycle": {
+      const backup = await exportBackup(ws);
+      await clearDatabase(ws.db.idbHandle());
+      await importBackupPlain(ws, backup);
+      return {};
+    }
     case "exportDb": {
       const dbDump = await exportDb(ws.db.idbHandle());
       return dbDump;

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