aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2022-09-20 21:44:21 +0200
committerFlorian Dold <florian@dold.me>2022-09-20 23:17:42 +0200
commit16a5bb40834c01e50e84144bb644517e67a66187 (patch)
treef6e7167aca608316ad03cf1e0debdb65eabe02cc
parent52ec740c825d4e94fd59ef0a5cd8e8b73f4dfc06 (diff)
downloadwallet-core-16a5bb40834c01e50e84144bb644517e67a66187.tar.xz
wallet-core: make basic backup work again
-rw-r--r--packages/taler-util/src/backupTypes.ts291
-rw-r--r--packages/taler-util/src/transactionsTypes.ts17
-rw-r--r--packages/taler-wallet-cli/src/index.ts20
-rw-r--r--packages/taler-wallet-cli/src/integrationtests/test-wallet-backup-basic.ts31
-rw-r--r--packages/taler-wallet-cli/src/integrationtests/test-wallet-backup-doublespend.ts20
-rw-r--r--packages/taler-wallet-core/src/db-utils.ts27
-rw-r--r--packages/taler-wallet-core/src/db.ts50
-rw-r--r--packages/taler-wallet-core/src/operations/backup/export.ts93
-rw-r--r--packages/taler-wallet-core/src/operations/backup/import.ts89
-rw-r--r--packages/taler-wallet-core/src/operations/backup/index.ts10
-rw-r--r--packages/taler-wallet-core/src/operations/withdraw.ts7
-rw-r--r--packages/taler-wallet-core/src/util/query.ts65
-rw-r--r--packages/taler-wallet-core/src/wallet-api-types.ts10
-rw-r--r--packages/taler-wallet-core/src/wallet.ts16
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
@@ -132,6 +160,13 @@ export interface WalletBackupContentV1 {
exchange_details: BackupExchangeDetails[];
/**
+ * Withdrawal groups.
+ *
+ * Sorted by the withdrawal group ID.
+ */
+ withdrawal_groups: BackupWithdrawalGroup[];
+
+ /**
* Grouped refresh sessions.
*
* Sorted by the refresh group ID.
@@ -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.
@@ -1149,11 +1161,6 @@ export interface BackupExchangeDetails {
denominations: BackupDenomination[];
/**
- * Reserves at the exchange.
- */
- reserves: BackupReserve[];
-
- /**
* Last observed protocol version.
*/
protocol_version: string;
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
@@ -887,6 +887,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
@@ -50,6 +50,24 @@ 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
* for all previous versions must be written, which should be
@@ -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;
@@ -625,6 +615,46 @@ export class DbAccess<StoreMap> {
}
/**
+ * 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.
*
* The {@link namePicker} must be a function that selects a list of object
@@ -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;