aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2023-08-30 18:01:18 +0200
committerFlorian Dold <florian@dold.me>2023-08-30 18:01:18 +0200
commita713d90c3c564408309d92223d383ecc9225924f (patch)
tree18142a4ce3d98df6e8a4945dbca23c47715eee6a
parent0a4782a0da631aba31dc0ecef7427df2467cc3e6 (diff)
downloadwallet-core-a713d90c3c564408309d92223d383ecc9225924f.tar.xz
wallet-core: remove old sync code, add stored backups skeleton
-rw-r--r--packages/taler-util/src/backup-types.ts1283
-rw-r--r--packages/taler-util/src/wallet-types.ts18
-rw-r--r--packages/taler-wallet-core/src/db.ts49
-rw-r--r--packages/taler-wallet-core/src/operations/backup/export.ts586
-rw-r--r--packages/taler-wallet-core/src/operations/backup/import.ts874
-rw-r--r--packages/taler-wallet-core/src/operations/backup/index.ts188
-rw-r--r--packages/taler-wallet-core/src/operations/backup/state.ts92
-rw-r--r--packages/taler-wallet-core/src/wallet-api-types.ts42
-rw-r--r--packages/taler-wallet-core/src/wallet.ts32
-rw-r--r--packages/taler-wallet-webextension/src/wxBackend.ts2
10 files changed, 213 insertions, 2953 deletions
diff --git a/packages/taler-util/src/backup-types.ts b/packages/taler-util/src/backup-types.ts
index 0211ff740..2eba1e4ca 100644
--- a/packages/taler-util/src/backup-types.ts
+++ b/packages/taler-util/src/backup-types.ts
@@ -14,1289 +14,6 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-/**
- * Type declarations for the backup content format.
- *
- * Contains some redundancy with the other type declarations,
- * as the backup schema must remain very stable and should be self-contained.
- *
- * Future:
- * 1. Ghost spends (coin unexpectedly spent by a wallet with shared data)
- * 2. Ghost withdrawals (reserve unexpectedly emptied by another wallet with shared data)
- * 3. Track losses through re-denomination of payments/refreshes
- * 4. (Feature:) Payments to own bank account and P2P-payments need to be backed up
- * 5. Track last/next update time, so on restore we need to do less work
- * 6. Currency render preferences?
- *
- * Questions:
- * 1. What happens when two backups are merged that have
- * the same coin in different refresh groups?
- * => Both are added, one will eventually fail
- * 2. Should we make more information forgettable? I.e. is
- * the coin selection still relevant for a purchase after the coins
- * are legally expired?
- * => Yes, still needs to be implemented
- * 3. What about re-denominations / re-selection of payment coins?
- * Is it enough to store a clock value for the selection?
- * => Coin derivation should also consider denom pub hash
- *
- * General considerations / decisions:
- * 1. Information about previously occurring errors and
- * retries is never backed up.
- * 2. The ToS text of an exchange is never backed up.
- * 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>
- */
-
-/**
- * Imports.
- */
-import { DenominationPubKey, UnblindedSignature } from "./taler-types.js";
-import {
- TalerProtocolDuration,
- TalerProtocolTimestamp,
- TalerPreciseTimestamp,
-} 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.
- */
-type BackupAmountString = string;
-
-/**
- * A human-recognizable identifier here that is
- * reasonable unique and assigned the first time the wallet is
- * started/installed, such as:
- *
- * `${wallet-implementation} ${os} ${hostname} (${short-uid})`
- * => e.g. "GNU Taler Android iceking ABC123"
- */
-type DeviceIdString = string;
-
-/**
- * Contract terms JSON.
- */
-type RawContractTerms = any;
-
-/**
- * Unique identifier for an operation, used to either (a) reference
- * the operation in a tombstone (b) disambiguate conflicting writes.
- */
-type OperationUid = string;
-
-/**
- * Content of the backup.
- *
- * The contents of the wallet must be serialized in a deterministic
- * way across implementations, so that the normalized backup content
- * JSON is identical when the wallet's content is identical.
- */
-export interface WalletBackupContentV1 {
- /**
- * Magic constant to identify that this is a backup content JSON.
- */
- schema_id: typeof BACKUP_TAG;
-
- /**
- * Version of the schema.
- */
- schema_version: typeof BACKUP_VERSION_MAJOR;
-
- minor_version: number;
-
- /**
- * Root public key of the wallet. This field is present as
- * a sanity check if the backup content JSON is loaded from file.
- */
- wallet_root_pub: string;
-
- /**
- * Current device identifier that "owns" the backup.
- *
- * This identifier allows one wallet to notice when another
- * wallet is "alive" and connected to the same sync provider.
- */
- current_device_id: DeviceIdString;
-
- /**
- * Timestamp of the backup.
- *
- * This timestamp should only be advanced if the content
- * of the backup changes.
- */
- timestamp: TalerPreciseTimestamp;
-
- /**
- * Per-exchange data sorted by exchange master public key.
- *
- * Sorted by the exchange public key.
- */
- exchanges: BackupExchange[];
-
- exchange_details: BackupExchangeDetails[];
-
- /**
- * Withdrawal groups.
- *
- * Sorted by the withdrawal group ID.
- */
- withdrawal_groups: BackupWithdrawalGroup[];
-
- /**
- * Grouped refresh sessions.
- *
- * Sorted by the refresh group ID.
- */
- refresh_groups: BackupRefreshGroup[];
-
- /**
- * Tips.
- *
- * Sorted by the wallet tip ID.
- */
- tips: BackupTip[];
-
- /**
- * Accepted purchases.
- *
- * Sorted by the proposal ID.
- */
- purchases: BackupPurchase[];
-
- /**
- * All backup providers. Backup providers
- * in this list should be considered "active".
- *
- * Sorted by the provider base URL.
- */
- backup_providers: BackupBackupProvider[];
-
- /**
- * Recoup groups.
- */
- recoup_groups: BackupRecoupGroup[];
-
- /**
- * Trusted auditors, either for official (3 letter) or local (4-12 letter)
- * currencies.
- *
- * Auditors are sorted by their canonicalized base URL.
- */
- trusted_auditors: { [currency: string]: BackupTrustAuditor[] };
-
- /**
- * Trusted exchange. Only applicable for local currencies (4-12 letter currency code).
- *
- * Exchanges are sorted by their canonicalized base URL.
- */
- trusted_exchanges: { [currency: string]: BackupTrustExchange[] };
-
- /**
- * Interning table for forgettable values of contract terms.
- *
- * Used to reduce storage space, as many forgettable items (product image,
- * addresses, etc.) might be shared among many contract terms.
- */
- intern_table: { [hash: string]: any };
-
- /**
- * Permanent error reports.
- */
- error_reports: BackupErrorReport[];
-
- /**
- * Deletion tombstones. Lexically sorted.
- */
- 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?: TalerPreciseTimestamp;
-
- /**
- * Time when the reserve was confirmed by the bank.
- *
- * Set to undefined if not confirmed yet.
- */
- timestamp_bank_confirmed?: TalerPreciseTimestamp;
- }
- | {
- 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;
-
- /**
- * Detailed info based on the type of withdrawal group.
- */
- info: BackupWgInfo;
-
- secret_seed: string;
-
- reserve_priv: string;
-
- exchange_base_url: string;
-
- timestamp_created: TalerPreciseTimestamp;
-
- timestamp_finish?: TalerPreciseTimestamp;
-
- 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;
-
- effective_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>"
- */
-export type Tombstone = string;
-
-/**
- * Detailed error report.
- *
- * For auditor-relevant reports with attached cryptographic proof,
- * the error report also should contain the submission status to
- * the auditor(s).
- */
-interface BackupErrorReport {
- // FIXME: specify!
-}
-
-/**
- * Trust declaration for an auditor.
- *
- * The trust applies based on the public key of
- * the auditor, irrespective of what base URL the exchange
- * is referencing.
- */
-export interface BackupTrustAuditor {
- /**
- * Base URL of the auditor.
- */
- auditor_base_url: string;
-
- /**
- * Public key of the auditor.
- */
- auditor_pub: string;
-
- /**
- * UIDs for the operation of adding this auditor
- * as a trusted auditor.
- */
- uids: OperationUid;
-}
-
-/**
- * Trust declaration for an exchange.
- *
- * The trust only applies for the combination of base URL
- * and public key. If the master public key changes while the base
- * URL stays the same, the exchange has to be re-added by a wallet update
- * or by the user.
- */
-export interface BackupTrustExchange {
- /**
- * Canonicalized exchange base URL.
- */
- exchange_base_url: string;
-
- /**
- * Master public key of the exchange.
- */
- exchange_master_pub: string;
-
- /**
- * UIDs for the operation of adding this exchange
- * as trusted.
- */
- uids: OperationUid;
-}
-
-export class BackupBackupProviderTerms {
- /**
- * Last known supported protocol version.
- */
- supported_protocol_version: string;
-
- /**
- * Last known annual fee.
- */
- annual_fee: BackupAmountString;
-
- /**
- * Last known storage limit.
- */
- storage_limit_in_megabytes: number;
-}
-
-/**
- * Backup information about one backup storage provider.
- */
-export class BackupBackupProvider {
- /**
- * Canonicalized base URL of the provider.
- */
- base_url: string;
-
- /**
- * Last known terms. Might be unavailable in some situations, such
- * as directly after restoring form a backup recovery document.
- */
- terms?: BackupBackupProviderTerms;
-
- /**
- * Proposal IDs for payments to this provider.
- */
- pay_proposal_ids: string[];
-
- /**
- * UIDs for adding this backup provider.
- */
- uids: OperationUid[];
-}
-
-/**
- * Status of recoup operations that were grouped together.
- *
- * The remaining amount of the corresponding coins must be set to
- * zero when the recoup group is created/imported.
- */
-export interface BackupRecoupGroup {
- /**
- * Unique identifier for the recoup group record.
- */
- recoup_group_id: string;
-
- /**
- * Timestamp when the recoup was started.
- */
- timestamp_created: TalerPreciseTimestamp;
-
- timestamp_finish?: TalerPreciseTimestamp;
- finish_clock?: TalerProtocolTimestamp;
- // FIXME: Use some enum here!
- finish_is_failure?: boolean;
-
- /**
- * Information about each coin being recouped.
- */
- coins: {
- coin_pub: string;
- recoup_finished: boolean;
- }[];
-}
-
-/**
- * Types of coin sources.
- */
-export enum BackupCoinSourceType {
- Withdraw = "withdraw",
- Refresh = "refresh",
- Reward = "reward",
-}
-
-/**
- * Metadata about a coin obtained via withdrawing.
- */
-export interface BackupWithdrawCoinSource {
- type: BackupCoinSourceType.Withdraw;
-
- /**
- * Can be the empty string for orphaned coins.
- */
- withdrawal_group_id: string;
-
- /**
- * Index of the coin in the withdrawal session.
- */
- coin_index: number;
-
- /**
- * Reserve public key for the reserve we got this coin from.
- */
- reserve_pub: string;
-}
-
-/**
- * Metadata about a coin obtained from refreshing.
- *
- * FIXME: Currently does not link to the refreshGroupId because
- * the wallet DB doesn't do this. Not really necessary,
- * but would be more consistent.
- */
-export interface BackupRefreshCoinSource {
- type: BackupCoinSourceType.Refresh;
-
- /**
- * Public key of the coin that was refreshed into this coin.
- */
- old_coin_pub: string;
-
- refresh_group_id: string;
-}
-
-/**
- * Metadata about a coin obtained from a tip.
- */
-export interface BackupTipCoinSource {
- type: BackupCoinSourceType.Reward;
-
- /**
- * Wallet's identifier for the tip that this coin
- * originates from.
- */
- wallet_tip_id: string;
-
- /**
- * Index in the tip planchets of the tip.
- */
- coin_index: number;
-}
-
-/**
- * Metadata about a coin depending on the origin.
- */
-export type BackupCoinSource =
- | BackupWithdrawCoinSource
- | BackupRefreshCoinSource
- | BackupTipCoinSource;
-
-/**
- * Backup information about a coin.
- *
- * (Always part of a BackupExchange/BackupDenom)
- */
-export interface BackupCoin {
- /**
- * Where did the coin come from? Used for recouping coins.
- */
- coin_source: BackupCoinSource;
-
- /**
- * Private key to authorize operations on the coin.
- */
- coin_priv: string;
-
- /**
- * Unblinded signature by the exchange.
- */
- denom_sig: UnblindedSignature;
-
- /**
- * Information about where and how the coin was spent.
- */
- spend_allocation:
- | {
- id: string;
- amount: BackupAmountString;
- }
- | undefined;
-
- /**
- * Blinding key used when withdrawing the coin.
- * Potentionally used again during payback.
- */
- blinding_key: string;
-
- /**
- * Does the wallet think that the coin is still fresh?
- *
- * Note that even if a fresh coin is imported, it should still
- * be refreshed in most situations.
- */
- fresh: boolean;
-}
-
-/**
- * Status of a tip we got from a merchant.
- */
-export interface BackupTip {
- /**
- * Tip ID chosen by the wallet.
- */
- wallet_tip_id: string;
-
- /**
- * The merchant's identifier for this tip.
- */
- merchant_tip_id: string;
-
- /**
- * Secret seed used for the tipping planchets.
- */
- secret_seed: string;
-
- /**
- * Has the user accepted the tip? Only after the tip has been accepted coins
- * withdrawn from the tip may be used.
- */
- timestamp_accepted: TalerPreciseTimestamp | undefined;
-
- /**
- * When was the tip first scanned by the wallet?
- */
- timestamp_created: TalerPreciseTimestamp;
-
- timestamp_finished?: TalerPreciseTimestamp;
- finish_is_failure?: boolean;
-
- /**
- * The tipped amount.
- */
- tip_amount_raw: BackupAmountString;
-
- /**
- * Timestamp, the tip can't be picked up anymore after this deadline.
- */
- timestamp_expiration: TalerProtocolTimestamp;
-
- /**
- * The exchange that will sign our coins, chosen by the merchant.
- */
- exchange_base_url: string;
-
- /**
- * Base URL of the merchant that is giving us the tip.
- */
- merchant_base_url: string;
-
- /**
- * Selected denominations. Determines the effective tip amount.
- */
- selected_denoms: BackupDenomSel;
-
- /**
- * The url to be redirected after the tip is accepted.
- */
- next_url: string | undefined;
-
- /**
- * UID for the denomination selection.
- * Used to disambiguate when merging.
- */
- selected_denoms_uid: OperationUid;
-}
-
-/**
- * Reasons for why a coin is being refreshed.
- */
-export enum BackupRefreshReason {
- Manual = "manual",
- Pay = "pay",
- Refund = "refund",
- AbortPay = "abort-pay",
- Recoup = "recoup",
- BackupRestored = "backup-restored",
- Scheduled = "scheduled",
-}
-
-/**
- * Information about one refresh session, always part
- * of a refresh group.
- *
- * (Public key of the old coin is stored in the refresh group.)
- */
-export interface BackupRefreshSession {
- /**
- * Hashed denominations of the newly requested coins.
- */
- new_denoms: BackupDenomSel;
-
- /**
- * Seed used to derive the planchets and
- * transfer private keys for this refresh session.
- */
- session_secret_seed: string;
-
- /**
- * The no-reveal-index after we've done the melting.
- */
- noreveal_index?: number;
-}
-
-/**
- * Refresh session for one coin inside a refresh group.
- */
-export interface BackupRefreshOldCoin {
- /**
- * Public key of the old coin,
- */
- coin_pub: string;
-
- /**
- * Requested amount to refresh. Must be subtracted from the coin's remaining
- * amount as soon as the coin is added to the refresh group.
- */
- input_amount: BackupAmountString;
-
- /**
- * Estimated output (may change if it takes a long time to create the
- * actual session).
- */
- estimated_output_amount: BackupAmountString;
-
- /**
- * Did the refresh session finish (or was it unnecessary/impossible to create
- * one)
- */
- finished: boolean;
-
- /**
- * Refresh session (if created) or undefined it not created yet.
- */
- refresh_session: BackupRefreshSession | undefined;
-}
-
-/**
- * Information about one refresh group.
- *
- * May span more than one exchange, but typically doesn't
- */
-export interface BackupRefreshGroup {
- refresh_group_id: string;
-
- reason: BackupRefreshReason;
-
- /**
- * Details per old coin.
- */
- old_coins: BackupRefreshOldCoin[];
-
- timestamp_created: TalerPreciseTimestamp;
-
- timestamp_finish?: TalerPreciseTimestamp;
- finish_is_failure?: boolean;
-}
-
-export enum BackupRefundState {
- Failed = "failed",
- Applied = "applied",
- Pending = "pending",
-}
-
-/**
- * Common information about a refund.
- */
-export interface BackupRefundItemCommon {
- /**
- * Execution time as claimed by the merchant
- */
- execution_time: TalerProtocolTimestamp;
-
- /**
- * Time when the wallet became aware of the refund.
- */
- obtained_time: TalerProtocolTimestamp;
-
- /**
- * Amount refunded for the coin.
- */
- refund_amount: BackupAmountString;
-
- /**
- * Coin being refunded.
- */
- coin_pub: string;
-
- /**
- * The refund transaction ID for the refund.
- */
- rtransaction_id: number;
-
- /**
- * Upper bound on the refresh cost incurred by
- * applying this refund.
- *
- * Might be lower in practice when two refunds on the same
- * coin are refreshed in the same refresh operation.
- *
- * Used to display fees, and stored since it's expensive to recompute
- * accurately.
- */
- total_refresh_cost_bound: BackupAmountString;
-}
-
-/**
- * Failed refund, either because the merchant did
- * something wrong or it expired.
- */
-export interface BackupRefundFailedItem extends BackupRefundItemCommon {
- type: BackupRefundState.Failed;
-}
-
-export interface BackupRefundPendingItem extends BackupRefundItemCommon {
- type: BackupRefundState.Pending;
-}
-
-export interface BackupRefundAppliedItem extends BackupRefundItemCommon {
- type: BackupRefundState.Applied;
-}
-
-/**
- * State of one refund from the merchant, maintained by the wallet.
- */
-export type BackupRefundItem =
- | BackupRefundFailedItem
- | BackupRefundPendingItem
- | BackupRefundAppliedItem;
-
-/**
- * Data we store when the payment was accepted.
- */
-export interface BackupPayInfo {
- pay_coins: {
- /**
- * Public keys of the coins that were selected.
- */
- coin_pub: string;
-
- /**
- * Amount that each coin contributes.
- */
- contribution: BackupAmountString;
- }[];
-
- /**
- * Unique ID to disambiguate pay coin selection on merge.
- */
- pay_coins_uid: OperationUid;
-
- /**
- * Total cost initially shown to the user.
- *
- * This includes the amount taken by the merchant, fees (wire/deposit) contributed
- * by the customer, refreshing fees, fees for withdraw-after-refresh and "trimmings"
- * of coins that are too small to spend.
- *
- * Note that in rare situations, this cost might not be accurate (e.g.
- * when the payment or refresh gets re-denominated).
- * We might show adjustments to this later, but currently we don't do so.
- */
- total_pay_cost: BackupAmountString;
-}
-
-export interface BackupPurchase {
- /**
- * Proposal ID for this purchase. Uniquely identifies the
- * purchase and the proposal.
- */
- proposal_id: string;
-
- /**
- * Status of the proposal.
- */
- proposal_status: BackupProposalStatus;
-
- /**
- * Proposal that this one got "redirected" to as part of
- * the repurchase detection.
- */
- repurchase_proposal_id: string | undefined;
-
- /**
- * Session ID we got when downloading the contract.
- */
- download_session_id?: string;
-
- /**
- * Merchant-assigned order ID of the proposal.
- */
- order_id: string;
-
- /**
- * Base URL of the merchant that proposed the purchase.
- */
- merchant_base_url: string;
-
- /**
- * Claim token initially given by the merchant.
- */
- claim_token: string | undefined;
-
- /**
- * Contract terms we got from the merchant.
- */
- contract_terms_raw?: RawContractTerms;
-
- /**
- * Signature on the contract terms.
- *
- * FIXME: Better name needed.
- */
- merchant_sig?: string;
-
- /**
- * Private key for the nonce. Might eventually be used
- * to prove ownership of the contract.
- */
- nonce_priv: string;
-
- pay_info: BackupPayInfo | undefined;
-
- /**
- * Timestamp of the first time that sending a payment to the merchant
- * for this purchase was successful.
- */
- timestamp_first_successful_pay: TalerPreciseTimestamp | undefined;
-
- /**
- * Signature by the merchant confirming the payment.
- */
- merchant_pay_sig: string | undefined;
-
- /**
- * Text to be shown to the point-of-sale staff as a proof of payment.
- */
- pos_confirmation: string | undefined;
-
- timestamp_proposed: TalerPreciseTimestamp;
-
- /**
- * When was the purchase made?
- * Refers to the time that the user accepted.
- */
- timestamp_accepted: TalerPreciseTimestamp | undefined;
-
- /**
- * Pending refunds for the purchase. A refund is pending
- * when the merchant reports a transient error from the exchange.
- */
- refunds: BackupRefundItem[];
-
- /**
- * Continue querying the refund status until this deadline has expired.
- */
- auto_refund_deadline: TalerProtocolTimestamp | undefined;
-
- shared: boolean;
-}
-
-/**
- * Info about one denomination in the backup.
- *
- * Note that the wallet only backs up validated denominations.
- */
-export interface BackupDenomination {
- /**
- * Value of one coin of the denomination.
- */
- value: BackupAmountString;
-
- /**
- * The denomination public key.
- */
- denom_pub: DenominationPubKey;
-
- /**
- * Fee for withdrawing.
- */
- fee_withdraw: BackupAmountString;
-
- /**
- * Fee for depositing.
- */
- fee_deposit: BackupAmountString;
-
- /**
- * Fee for refreshing.
- */
- fee_refresh: BackupAmountString;
-
- /**
- * Fee for refunding.
- */
- fee_refund: BackupAmountString;
-
- /**
- * Validity start date of the denomination.
- */
- stamp_start: TalerProtocolTimestamp;
-
- /**
- * Date after which the currency can't be withdrawn anymore.
- */
- stamp_expire_withdraw: TalerProtocolTimestamp;
-
- /**
- * Date after the denomination officially doesn't exist anymore.
- */
- stamp_expire_legal: TalerProtocolTimestamp;
-
- /**
- * Data after which coins of this denomination can't be deposited anymore.
- */
- stamp_expire_deposit: TalerProtocolTimestamp;
-
- /**
- * Signature by the exchange's master key over the denomination
- * information.
- */
- master_sig: string;
-
- /**
- * Was this denomination still offered by the exchange the last time
- * we checked?
- * Only false when the exchange redacts a previously published denomination.
- */
- is_offered: boolean;
-
- /**
- * Did the exchange revoke the denomination?
- * When this field is set to true in the database, the same transaction
- * should also mark all affected coins as revoked.
- */
- is_revoked: boolean;
-
- /**
- * Coins of this denomination.
- */
- coins: BackupCoin[];
-
- /**
- * The list issue date of the exchange "/keys" response
- * that this denomination was last seen in.
- */
- list_issue_date: TalerProtocolTimestamp;
-}
-
-/**
- * Denomination selection.
- */
-export type BackupDenomSel = {
- denom_pub_hash: string;
- count: number;
-}[];
-
-/**
- * Wire fee for one wire payment target type as stored in the
- * wallet's database.
- *
- * (Flattened to a list to make the declaration simpler).
- */
-export interface BackupExchangeWireFee {
- wire_type: string;
-
- /**
- * Fee for wire transfers.
- */
- wire_fee: string;
-
- /**
- * Fees to close and refund a reserve.
- */
- closing_fee: string;
-
- /**
- * Start date of the fee.
- */
- start_stamp: TalerProtocolTimestamp;
-
- /**
- * End date of the fee.
- */
- end_stamp: TalerProtocolTimestamp;
-
- /**
- * Signature made by the exchange master key.
- */
- sig: string;
-}
-
-/**
- * Global fee as stored in the wallet's database.
- *
- */
-export interface BackupExchangeGlobalFees {
- startDate: TalerProtocolTimestamp;
- endDate: TalerProtocolTimestamp;
-
- historyFee: BackupAmountString;
- accountFee: BackupAmountString;
- purseFee: BackupAmountString;
-
- historyTimeout: TalerProtocolDuration;
- purseTimeout: TalerProtocolDuration;
-
- purseLimit: number;
-
- signature: string;
-}
-/**
- * Structure of one exchange signing key in the /keys response.
- */
-export class BackupExchangeSignKey {
- stamp_start: TalerProtocolTimestamp;
- stamp_expire: TalerProtocolTimestamp;
- stamp_end: TalerProtocolTimestamp;
- key: string;
- master_sig: string;
-}
-
-/**
- * Signature by the auditor that a particular denomination key is audited.
- */
-export class BackupAuditorDenomSig {
- /**
- * Denomination public key's hash.
- */
- denom_pub_h: string;
-
- /**
- * The signature.
- */
- auditor_sig: string;
-}
-
-/**
- * Auditor information as given by the exchange in /keys.
- */
-export class BackupExchangeAuditor {
- /**
- * Auditor's public key.
- */
- auditor_pub: string;
-
- /**
- * Base URL of the auditor.
- */
- auditor_url: string;
-
- /**
- * List of signatures for denominations by the auditor.
- */
- denomination_keys: BackupAuditorDenomSig[];
-}
-
-/**
- * Backup information for an exchange. Serves effectively
- * as a pointer to the exchange details identified by
- * the base URL, master public key and currency.
- */
-export interface BackupExchange {
- base_url: string;
-
- master_public_key: string;
-
- currency: string;
-
- /**
- * Time when the pointer to the exchange details
- * was last updated.
- *
- * Used to facilitate automatic merging.
- */
- update_clock: TalerPreciseTimestamp;
-}
-
-/**
- * Backup information about an exchange's details.
- *
- * Note that one base URL can have multiple exchange
- * details. The BackupExchange stores a pointer
- * to the current exchange details.
- */
-export interface BackupExchangeDetails {
- /**
- * Canonicalized base url of the exchange.
- */
- base_url: string;
-
- /**
- * Master public key of the exchange.
- */
- master_public_key: string;
-
- /**
- * Auditors (partially) auditing the exchange.
- */
- auditors: BackupExchangeAuditor[];
-
- /**
- * Currency that the exchange offers.
- */
- currency: string;
-
- /**
- * Denominations offered by the exchange.
- */
- denominations: BackupDenomination[];
-
- /**
- * Last observed protocol version.
- */
- protocol_version: string;
-
- /**
- * Closing delay of reserves.
- */
- reserve_closing_delay: TalerProtocolDuration;
-
- /**
- * Signing keys we got from the exchange, can also contain
- * older signing keys that are not returned by /keys anymore.
- */
- signing_keys: BackupExchangeSignKey[];
-
- wire_fees: BackupExchangeWireFee[];
-
- global_fees: BackupExchangeGlobalFees[];
-
- /**
- * Bank accounts offered by the exchange;
- */
- accounts: {
- payto_uri: string;
- master_sig: string;
- }[];
-
- /**
- * ETag for last terms of service download.
- */
- tos_accepted_etag: string | undefined;
-
- /**
- * Timestamp when the ToS has been accepted.
- */
- tos_accepted_timestamp: TalerPreciseTimestamp | undefined;
-}
-
-export enum BackupProposalStatus {
- /**
- * Proposed (and either downloaded or not,
- * depending on whether contract terms are present),
- * but the user needs to accept/reject it.
- */
- Proposed = "proposed",
- /**
- * Proposed, other wallet may also have
- * the purchase
- */
- Shared = "shared",
- /**
- * The user has rejected the proposal.
- */
- Refused = "refused",
- /**
- * Downloading or processing the proposal has failed permanently.
- *
- * FIXME: Should this be modeled as a "misbehavior report" instead?
- */
- PermanentlyFailed = "permanently-failed",
- /**
- * Downloaded proposal was detected as a re-purchase.
- */
- Repurchase = "repurchase",
-
- Paid = "paid",
-}
-
export interface BackupRecovery {
walletRootPriv: string;
providers: {
diff --git a/packages/taler-util/src/wallet-types.ts b/packages/taler-util/src/wallet-types.ts
index 01c1838d5..accab746f 100644
--- a/packages/taler-util/src/wallet-types.ts
+++ b/packages/taler-util/src/wallet-types.ts
@@ -2655,3 +2655,21 @@ export interface TransactionRecordFilter {
onlyState?: TransactionStateFilter;
onlyCurrency?: string;
}
+
+export interface StoredBackupList {
+ storedBackups: {
+ name: string;
+ }[];
+}
+
+export interface CreateStoredBackupResponse {
+ name: string;
+}
+
+export interface RecoverStoredBackupRequest {
+ name: string;
+}
+
+export interface DeleteStoredBackupRequest {
+ name: string;
+}
diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts
index e68385267..1255e8c71 100644
--- a/packages/taler-wallet-core/src/db.ts
+++ b/packages/taler-wallet-core/src/db.ts
@@ -2769,6 +2769,24 @@ export const walletMetadataStore = {
),
};
+export interface StoredBackupMeta {
+ name: string;
+}
+
+export interface StoredBackupData {
+ name: string;
+ data: any;
+}
+
+export const StoredBackupStores = {
+ backupMeta: describeStore(
+ "backupMeta",
+ describeContents<MetaConfigRecord>({ keyPath: "name" }),
+ {},
+ ),
+ backupData: describeStore("backupData", describeContents<any>({}), {}),
+};
+
export interface DbDumpRecord {
/**
* Key, serialized with structuredEncapsulated.
@@ -2831,6 +2849,7 @@ export async function exportSingleDb(
return new Promise((resolve, reject) => {
const tx = myDb.transaction(Array.from(myDb.objectStoreNames));
tx.addEventListener("complete", () => {
+ myDb.close();
resolve(singleDbDump);
});
// tslint:disable-next-line:prefer-for-of
@@ -3211,6 +3230,36 @@ function onMetaDbUpgradeNeeded(
);
}
+function onStoredBackupsDbUpgradeNeeded(
+ db: IDBDatabase,
+ oldVersion: number,
+ newVersion: number,
+ upgradeTransaction: IDBTransaction,
+) {
+ upgradeFromStoreMap(
+ StoredBackupStores,
+ db,
+ oldVersion,
+ newVersion,
+ upgradeTransaction,
+ );
+}
+
+export async function openStoredBackupsDatabase(
+ idbFactory: IDBFactory,
+): Promise<DbAccess<typeof StoredBackupStores>> {
+ const backupsDbHandle = await openDatabase(
+ idbFactory,
+ TALER_WALLET_META_DB_NAME,
+ 1,
+ () => {},
+ onStoredBackupsDbUpgradeNeeded,
+ );
+
+ const handle = new DbAccess(backupsDbHandle, StoredBackupStores);
+ return handle;
+}
+
/**
* Return a promise that resolves
* to the taler wallet db.
diff --git a/packages/taler-wallet-core/src/operations/backup/export.ts b/packages/taler-wallet-core/src/operations/backup/export.ts
deleted file mode 100644
index c9446a05f..000000000
--- a/packages/taler-wallet-core/src/operations/backup/export.ts
+++ /dev/null
@@ -1,586 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2020 Taler Systems SA
-
- 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
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- * Implementation of wallet backups (export/import/upload) and sync
- * server management.
- *
- * @author Florian Dold <dold@taler.net>
- */
-
-/**
- * Imports.
- */
-import {
- AbsoluteTime,
- Amounts,
- BackupBackupProvider,
- BackupBackupProviderTerms,
- BackupCoin,
- BackupCoinSource,
- BackupCoinSourceType,
- BackupDenomination,
- BackupExchange,
- BackupExchangeDetails,
- BackupExchangeSignKey,
- BackupExchangeWireFee,
- BackupOperationStatus,
- BackupPayInfo,
- BackupProposalStatus,
- BackupPurchase,
- BackupRecoupGroup,
- BackupRefreshGroup,
- BackupRefreshOldCoin,
- BackupRefreshSession,
- BackupRefundItem,
- BackupRefundState,
- BackupTip,
- BackupWgInfo,
- BackupWgType,
- BackupWithdrawalGroup,
- BACKUP_VERSION_MAJOR,
- BACKUP_VERSION_MINOR,
- canonicalizeBaseUrl,
- canonicalJson,
- CoinStatus,
- encodeCrock,
- getRandomBytes,
- hash,
- Logger,
- stringToBytes,
- WalletBackupContentV1,
- TalerPreciseTimestamp,
-} from "@gnu-taler/taler-util";
-import {
- CoinSourceType,
- ConfigRecordKey,
- DenominationRecord,
- PurchaseStatus,
- RefreshCoinStatus,
- WithdrawalGroupStatus,
- WithdrawalRecordType,
-} from "../../db.js";
-import { InternalWalletState } from "../../internal-wallet-state.js";
-import { assertUnreachable } from "../../util/assertUnreachable.js";
-import { checkDbInvariant } from "../../util/invariants.js";
-import { getWalletBackupState, provideBackupState } from "./state.js";
-
-const logger = new Logger("backup/export.ts");
-
-export async function exportBackup(
- ws: InternalWalletState,
-): Promise<WalletBackupContentV1> {
- await provideBackupState(ws);
- return ws.db
- .mktx((x) => [
- x.config,
- x.exchanges,
- x.exchangeDetails,
- x.exchangeSignKeys,
- x.coins,
- x.contractTerms,
- x.denominations,
- x.purchases,
- x.refreshGroups,
- x.backupProviders,
- x.rewards,
- x.recoupGroups,
- x.withdrawalGroups,
- ])
- .runReadWrite(async (tx) => {
- const bs = await getWalletBackupState(ws, tx);
-
- const backupExchangeDetails: BackupExchangeDetails[] = [];
- const backupExchanges: BackupExchange[] = [];
- const backupCoinsByDenom: { [dph: string]: BackupCoin[] } = {};
- const backupDenominationsByExchange: {
- [url: string]: BackupDenomination[];
- } = {};
- const backupPurchases: BackupPurchase[] = [];
- const backupRefreshGroups: BackupRefreshGroup[] = [];
- const backupBackupProviders: BackupBackupProvider[] = [];
- const backupTips: BackupTip[] = [];
- const backupRecoupGroups: BackupRecoupGroup[] = [];
- const backupWithdrawalGroups: BackupWithdrawalGroup[] = [];
-
- await tx.withdrawalGroups.iter().forEachAsync(async (wg) => {
- 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),
- info,
- timestamp_created: wg.timestampStart,
- timestamp_finish: wg.timestampFinish,
- withdrawal_group_id: wg.withdrawalGroupId,
- secret_seed: wg.secretSeed,
- exchange_base_url: wg.exchangeBaseUrl,
- instructed_amount: Amounts.stringify(wg.instructedAmount),
- effective_withdrawal_amount: Amounts.stringify(
- wg.effectiveWithdrawalAmount,
- ),
- reserve_priv: wg.reservePriv,
- restrict_age: wg.restrictAge,
- // FIXME: proper status conversion!
- operation_status:
- wg.status == WithdrawalGroupStatus.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,
- })),
- });
- });
-
- await tx.rewards.iter().forEach((tip) => {
- backupTips.push({
- exchange_base_url: tip.exchangeBaseUrl,
- merchant_base_url: tip.merchantBaseUrl,
- merchant_tip_id: tip.merchantRewardId,
- wallet_tip_id: tip.walletRewardId,
- next_url: tip.next_url,
- secret_seed: tip.secretSeed,
- selected_denoms: tip.denomsSel.selectedDenoms.map((x) => ({
- count: x.count,
- denom_pub_hash: x.denomPubHash,
- })),
- timestamp_finished: tip.pickedUpTimestamp,
- timestamp_accepted: tip.acceptedTimestamp,
- timestamp_created: tip.createdTimestamp,
- timestamp_expiration: tip.rewardExpiration,
- tip_amount_raw: Amounts.stringify(tip.rewardAmountRaw),
- selected_denoms_uid: tip.denomSelUid,
- });
- });
-
- await tx.recoupGroups.iter().forEach((recoupGroup) => {
- backupRecoupGroups.push({
- recoup_group_id: recoupGroup.recoupGroupId,
- timestamp_created: recoupGroup.timestampStarted,
- timestamp_finish: recoupGroup.timestampFinished,
- coins: recoupGroup.coinPubs.map((x, i) => ({
- coin_pub: x,
- recoup_finished: recoupGroup.recoupFinishedPerCoin[i],
- })),
- });
- });
-
- await tx.backupProviders.iter().forEach((bp) => {
- let terms: BackupBackupProviderTerms | undefined;
- if (bp.terms) {
- terms = {
- annual_fee: Amounts.stringify(bp.terms.annualFee),
- storage_limit_in_megabytes: bp.terms.storageLimitInMegabytes,
- supported_protocol_version: bp.terms.supportedProtocolVersion,
- };
- }
- backupBackupProviders.push({
- terms,
- base_url: canonicalizeBaseUrl(bp.baseUrl),
- pay_proposal_ids: bp.paymentProposalIds,
- uids: bp.uids,
- });
- });
-
- await tx.coins.iter().forEach((coin) => {
- let bcs: BackupCoinSource;
- switch (coin.coinSource.type) {
- case CoinSourceType.Refresh:
- bcs = {
- type: BackupCoinSourceType.Refresh,
- old_coin_pub: coin.coinSource.oldCoinPub,
- refresh_group_id: coin.coinSource.refreshGroupId,
- };
- break;
- case CoinSourceType.Reward:
- bcs = {
- type: BackupCoinSourceType.Reward,
- coin_index: coin.coinSource.coinIndex,
- wallet_tip_id: coin.coinSource.walletRewardId,
- };
- break;
- case CoinSourceType.Withdraw:
- bcs = {
- type: BackupCoinSourceType.Withdraw,
- coin_index: coin.coinSource.coinIndex,
- reserve_pub: coin.coinSource.reservePub,
- withdrawal_group_id: coin.coinSource.withdrawalGroupId,
- };
- break;
- }
-
- const coins = (backupCoinsByDenom[coin.denomPubHash] ??= []);
- coins.push({
- blinding_key: coin.blindingKey,
- coin_priv: coin.coinPriv,
- coin_source: bcs,
- fresh: coin.status === CoinStatus.Fresh,
- spend_allocation: coin.spendAllocation
- ? {
- amount: coin.spendAllocation.amount,
- id: coin.spendAllocation.id,
- }
- : undefined,
- denom_sig: coin.denomSig,
- });
- });
-
- await tx.denominations.iter().forEach((denom) => {
- const backupDenoms = (backupDenominationsByExchange[
- denom.exchangeBaseUrl
- ] ??= []);
- backupDenoms.push({
- coins: backupCoinsByDenom[denom.denomPubHash] ?? [],
- denom_pub: denom.denomPub,
- fee_deposit: Amounts.stringify(denom.fees.feeDeposit),
- fee_refresh: Amounts.stringify(denom.fees.feeRefresh),
- fee_refund: Amounts.stringify(denom.fees.feeRefund),
- fee_withdraw: Amounts.stringify(denom.fees.feeWithdraw),
- is_offered: denom.isOffered,
- is_revoked: denom.isRevoked,
- master_sig: denom.masterSig,
- stamp_expire_deposit: denom.stampExpireDeposit,
- stamp_expire_legal: denom.stampExpireLegal,
- stamp_expire_withdraw: denom.stampExpireWithdraw,
- stamp_start: denom.stampStart,
- value: Amounts.stringify(DenominationRecord.getValue(denom)),
- list_issue_date: denom.listIssueDate,
- });
- });
-
- await tx.exchanges.iter().forEachAsync(async (ex) => {
- const dp = ex.detailsPointer;
- if (!dp) {
- return;
- }
- backupExchanges.push({
- base_url: ex.baseUrl,
- currency: dp.currency,
- master_public_key: dp.masterPublicKey,
- update_clock: dp.updateClock,
- });
- });
-
- await tx.exchangeDetails.iter().forEachAsync(async (ex) => {
- // Only back up permanently added exchanges.
-
- const wi = ex.wireInfo;
- const wireFees: BackupExchangeWireFee[] = [];
-
- Object.keys(wi.feesForType).forEach((x) => {
- for (const f of wi.feesForType[x]) {
- wireFees.push({
- wire_type: x,
- closing_fee: Amounts.stringify(f.closingFee),
- end_stamp: f.endStamp,
- sig: f.sig,
- start_stamp: f.startStamp,
- wire_fee: Amounts.stringify(f.wireFee),
- });
- }
- });
- checkDbInvariant(ex.rowId != null);
- const exchangeSk =
- await tx.exchangeSignKeys.indexes.byExchangeDetailsRowId.getAll(
- ex.rowId,
- );
- let signingKeys: BackupExchangeSignKey[] = exchangeSk.map((x) => ({
- key: x.signkeyPub,
- master_sig: x.masterSig,
- stamp_end: x.stampEnd,
- stamp_expire: x.stampExpire,
- stamp_start: x.stampStart,
- }));
-
- backupExchangeDetails.push({
- base_url: ex.exchangeBaseUrl,
- reserve_closing_delay: ex.reserveClosingDelay,
- accounts: ex.wireInfo.accounts.map((x) => ({
- payto_uri: x.payto_uri,
- master_sig: x.master_sig,
- })),
- auditors: ex.auditors.map((x) => ({
- auditor_pub: x.auditor_pub,
- auditor_url: x.auditor_url,
- denomination_keys: x.denomination_keys,
- })),
- master_public_key: ex.masterPublicKey,
- currency: ex.currency,
- protocol_version: ex.protocolVersionRange,
- wire_fees: wireFees,
- signing_keys: signingKeys,
- global_fees: ex.globalFees.map((x) => ({
- accountFee: Amounts.stringify(x.accountFee),
- historyFee: Amounts.stringify(x.historyFee),
- purseFee: Amounts.stringify(x.purseFee),
- endDate: x.endDate,
- historyTimeout: x.historyTimeout,
- signature: x.signature,
- purseLimit: x.purseLimit,
- purseTimeout: x.purseTimeout,
- startDate: x.startDate,
- })),
- tos_accepted_etag: ex.tosAccepted?.etag,
- tos_accepted_timestamp: ex.tosAccepted?.timestamp,
- denominations:
- backupDenominationsByExchange[ex.exchangeBaseUrl] ?? [],
- });
- });
-
- const purchaseProposalIdSet = new Set<string>();
-
- await tx.purchases.iter().forEachAsync(async (purch) => {
- const refunds: BackupRefundItem[] = [];
- purchaseProposalIdSet.add(purch.proposalId);
- // for (const refundKey of Object.keys(purch.refunds)) {
- // const ri = purch.refunds[refundKey];
- // const common = {
- // coin_pub: ri.coinPub,
- // execution_time: ri.executionTime,
- // obtained_time: ri.obtainedTime,
- // refund_amount: Amounts.stringify(ri.refundAmount),
- // rtransaction_id: ri.rtransactionId,
- // total_refresh_cost_bound: Amounts.stringify(
- // ri.totalRefreshCostBound,
- // ),
- // };
- // switch (ri.type) {
- // case RefundState.Applied:
- // refunds.push({ type: BackupRefundState.Applied, ...common });
- // break;
- // case RefundState.Failed:
- // refunds.push({ type: BackupRefundState.Failed, ...common });
- // break;
- // case RefundState.Pending:
- // refunds.push({ type: BackupRefundState.Pending, ...common });
- // break;
- // }
- // }
-
- let propStatus: BackupProposalStatus;
- switch (purch.purchaseStatus) {
- case PurchaseStatus.Done:
- case PurchaseStatus.PendingQueryingAutoRefund:
- case PurchaseStatus.PendingQueryingRefund:
- propStatus = BackupProposalStatus.Paid;
- break;
- case PurchaseStatus.PendingPayingReplay:
- case PurchaseStatus.PendingDownloadingProposal:
- case PurchaseStatus.DialogProposed:
- case PurchaseStatus.PendingPaying:
- propStatus = BackupProposalStatus.Proposed;
- break;
- case PurchaseStatus.DialogShared:
- propStatus = BackupProposalStatus.Shared;
- break;
- case PurchaseStatus.FailedClaim:
- case PurchaseStatus.AbortedIncompletePayment:
- propStatus = BackupProposalStatus.PermanentlyFailed;
- break;
- case PurchaseStatus.AbortingWithRefund:
- case PurchaseStatus.AbortedProposalRefused:
- propStatus = BackupProposalStatus.Refused;
- break;
- case PurchaseStatus.RepurchaseDetected:
- propStatus = BackupProposalStatus.Repurchase;
- break;
- default: {
- const error = purch.purchaseStatus;
- throw Error(`purchase status ${error} is not handled`);
- }
- }
-
- const payInfo = purch.payInfo;
- let backupPayInfo: BackupPayInfo | undefined = undefined;
- if (payInfo) {
- backupPayInfo = {
- pay_coins: payInfo.payCoinSelection.coinPubs.map((x, i) => ({
- coin_pub: x,
- contribution: Amounts.stringify(
- payInfo.payCoinSelection.coinContributions[i],
- ),
- })),
- total_pay_cost: Amounts.stringify(payInfo.totalPayCost),
- pay_coins_uid: payInfo.payCoinSelectionUid,
- };
- }
-
- let contractTermsRaw = undefined;
- if (purch.download) {
- const contractTermsRecord = await tx.contractTerms.get(
- purch.download.contractTermsHash,
- );
- if (contractTermsRecord) {
- contractTermsRaw = contractTermsRecord.contractTermsRaw;
- }
- }
-
- backupPurchases.push({
- contract_terms_raw: contractTermsRaw,
- auto_refund_deadline: purch.autoRefundDeadline,
- merchant_pay_sig: purch.merchantPaySig,
- pos_confirmation: purch.posConfirmation,
- pay_info: backupPayInfo,
- proposal_id: purch.proposalId,
- refunds,
- timestamp_accepted: purch.timestampAccept,
- timestamp_first_successful_pay: purch.timestampFirstSuccessfulPay,
- nonce_priv: purch.noncePriv,
- merchant_sig: purch.download?.contractTermsMerchantSig,
- claim_token: purch.claimToken,
- merchant_base_url: purch.merchantBaseUrl,
- order_id: purch.orderId,
- proposal_status: propStatus,
- repurchase_proposal_id: purch.repurchaseProposalId,
- download_session_id: purch.downloadSessionId,
- timestamp_proposed: purch.timestamp,
- shared: purch.shared,
- });
- });
-
- await tx.refreshGroups.iter().forEach((rg) => {
- const oldCoins: BackupRefreshOldCoin[] = [];
-
- for (let i = 0; i < rg.oldCoinPubs.length; i++) {
- let refreshSession: BackupRefreshSession | undefined;
- const s = rg.refreshSessionPerCoin[i];
- if (s) {
- refreshSession = {
- new_denoms: s.newDenoms.map((x) => ({
- count: x.count,
- denom_pub_hash: x.denomPubHash,
- })),
- session_secret_seed: s.sessionSecretSeed,
- noreveal_index: s.norevealIndex,
- };
- }
- oldCoins.push({
- coin_pub: rg.oldCoinPubs[i],
- estimated_output_amount: Amounts.stringify(
- rg.estimatedOutputPerCoin[i],
- ),
- finished: rg.statusPerCoin[i] === RefreshCoinStatus.Finished,
- input_amount: Amounts.stringify(rg.inputPerCoin[i]),
- refresh_session: refreshSession,
- });
- }
-
- backupRefreshGroups.push({
- reason: rg.reason as any,
- refresh_group_id: rg.refreshGroupId,
- timestamp_created: rg.timestampCreated,
- timestamp_finish: rg.timestampFinished,
- old_coins: oldCoins,
- });
- });
-
- const ts = TalerPreciseTimestamp.now();
-
- if (!bs.lastBackupTimestamp) {
- bs.lastBackupTimestamp = ts;
- }
-
- const backupBlob: WalletBackupContentV1 = {
- schema_id: "gnu-taler-wallet-backup-content",
- schema_version: BACKUP_VERSION_MAJOR,
- minor_version: BACKUP_VERSION_MINOR,
- exchanges: backupExchanges,
- exchange_details: backupExchangeDetails,
- wallet_root_pub: bs.walletRootPub,
- backup_providers: backupBackupProviders,
- current_device_id: bs.deviceId,
- purchases: backupPurchases,
- recoup_groups: backupRecoupGroups,
- refresh_groups: backupRefreshGroups,
- tips: backupTips,
- timestamp: bs.lastBackupTimestamp,
- trusted_auditors: {},
- trusted_exchanges: {},
- intern_table: {},
- error_reports: [],
- tombstones: [],
- // FIXME!
- withdrawal_groups: backupWithdrawalGroups,
- };
-
- // If the backup changed, we change our nonce and timestamp.
-
- let h = encodeCrock(hash(stringToBytes(canonicalJson(backupBlob))));
- if (h !== bs.lastBackupPlainHash) {
- logger.trace(
- `plain backup hash changed (from ${bs.lastBackupPlainHash}to ${h})`,
- );
- bs.lastBackupTimestamp = ts;
- backupBlob.timestamp = ts;
- bs.lastBackupPlainHash = encodeCrock(
- hash(stringToBytes(canonicalJson(backupBlob))),
- );
- bs.lastBackupNonce = encodeCrock(getRandomBytes(32));
- logger.trace(
- `setting timestamp to ${AbsoluteTime.toIsoString(
- AbsoluteTime.fromPreciseTimestamp(ts),
- )} and nonce to ${bs.lastBackupNonce}`,
- );
- await tx.config.put({
- key: ConfigRecordKey.WalletBackupState,
- value: bs,
- });
- } else {
- logger.trace("backup hash did not change");
- }
-
- return backupBlob;
- });
-}
diff --git a/packages/taler-wallet-core/src/operations/backup/import.ts b/packages/taler-wallet-core/src/operations/backup/import.ts
deleted file mode 100644
index 836c65643..000000000
--- a/packages/taler-wallet-core/src/operations/backup/import.ts
+++ /dev/null
@@ -1,874 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2020 Taler Systems SA
-
- 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
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-import {
- AgeRestriction,
- AmountJson,
- Amounts,
- BackupCoin,
- BackupCoinSourceType,
- BackupDenomSel,
- BackupPayInfo,
- BackupProposalStatus,
- BackupRefreshReason,
- BackupRefundState,
- BackupWgType,
- codecForMerchantContractTerms,
- CoinStatus,
- DenomKeyType,
- DenomSelectionState,
- j2s,
- Logger,
- PayCoinSelection,
- RefreshReason,
- TalerProtocolTimestamp,
- TalerPreciseTimestamp,
- WalletBackupContentV1,
- WireInfo,
-} from "@gnu-taler/taler-util";
-import {
- CoinRecord,
- CoinSource,
- CoinSourceType,
- DenominationRecord,
- DenominationVerificationStatus,
- ProposalDownloadInfo,
- PurchaseStatus,
- PurchasePayInfo,
- RefreshCoinStatus,
- RefreshSessionRecord,
- WalletContractData,
- WalletStoresV1,
- WgInfo,
- WithdrawalGroupStatus,
- WithdrawalRecordType,
- RefreshOperationStatus,
- RewardRecordStatus,
-} from "../../db.js";
-import { InternalWalletState } from "../../internal-wallet-state.js";
-import { assertUnreachable } from "../../util/assertUnreachable.js";
-import { checkLogicInvariant } from "../../util/invariants.js";
-import { GetReadOnlyAccess, GetReadWriteAccess } from "../../util/query.js";
-import {
- constructTombstone,
- makeCoinAvailable,
- TombstoneTag,
-} from "../common.js";
-import { getExchangeDetails } from "../exchanges.js";
-import { extractContractData } from "../pay-merchant.js";
-import { provideBackupState } from "./state.js";
-
-const logger = new Logger("operations/backup/import.ts");
-
-function checkBackupInvariant(b: boolean, m?: string): asserts b {
- if (!b) {
- if (m) {
- throw Error(`BUG: backup invariant failed (${m})`);
- } else {
- throw Error("BUG: backup invariant failed");
- }
- }
-}
-
-/**
- * Re-compute information about the coin selection for a payment.
- */
-async function recoverPayCoinSelection(
- tx: GetReadWriteAccess<{
- exchanges: typeof WalletStoresV1.exchanges;
- exchangeDetails: typeof WalletStoresV1.exchangeDetails;
- coins: typeof WalletStoresV1.coins;
- denominations: typeof WalletStoresV1.denominations;
- }>,
- contractData: WalletContractData,
- payInfo: BackupPayInfo,
-): Promise<PayCoinSelection> {
- const coinPubs: string[] = payInfo.pay_coins.map((x) => x.coin_pub);
- const coinContributions: AmountJson[] = payInfo.pay_coins.map((x) =>
- Amounts.parseOrThrow(x.contribution),
- );
-
- const coveredExchanges: Set<string> = new Set();
-
- let totalWireFee: AmountJson = Amounts.zeroOfAmount(contractData.amount);
- let totalDepositFees: AmountJson = Amounts.zeroOfAmount(contractData.amount);
-
- for (const coinPub of coinPubs) {
- const coinRecord = await tx.coins.get(coinPub);
- checkBackupInvariant(!!coinRecord);
- const denom = await tx.denominations.get([
- coinRecord.exchangeBaseUrl,
- coinRecord.denomPubHash,
- ]);
- checkBackupInvariant(!!denom);
- totalDepositFees = Amounts.add(
- totalDepositFees,
- denom.fees.feeDeposit,
- ).amount;
-
- if (!coveredExchanges.has(coinRecord.exchangeBaseUrl)) {
- const exchangeDetails = await getExchangeDetails(
- tx,
- coinRecord.exchangeBaseUrl,
- );
- checkBackupInvariant(!!exchangeDetails);
- let wireFee: AmountJson | undefined;
- const feesForType = exchangeDetails.wireInfo.feesForType;
- checkBackupInvariant(!!feesForType);
- for (const fee of feesForType[contractData.wireMethod] || []) {
- if (
- fee.startStamp <= contractData.timestamp &&
- fee.endStamp >= contractData.timestamp
- ) {
- wireFee = Amounts.parseOrThrow(fee.wireFee);
- break;
- }
- }
- if (wireFee) {
- totalWireFee = Amounts.add(totalWireFee, wireFee).amount;
- }
- coveredExchanges.add(coinRecord.exchangeBaseUrl);
- }
- }
-
- let customerWireFee: AmountJson;
-
- const amortizedWireFee = Amounts.divide(
- totalWireFee,
- contractData.wireFeeAmortization,
- );
- if (Amounts.cmp(contractData.maxWireFee, amortizedWireFee) < 0) {
- customerWireFee = amortizedWireFee;
- } else {
- customerWireFee = Amounts.zeroOfAmount(contractData.amount);
- }
-
- const customerDepositFees = Amounts.sub(
- totalDepositFees,
- contractData.maxDepositFee,
- ).amount;
-
- return {
- coinPubs,
- coinContributions: coinContributions.map((x) => Amounts.stringify(x)),
- paymentAmount: Amounts.stringify(contractData.amount),
- customerWireFees: Amounts.stringify(customerWireFee),
- customerDepositFees: Amounts.stringify(customerDepositFees),
- };
-}
-
-async function getDenomSelStateFromBackup(
- tx: GetReadOnlyAccess<{ denominations: typeof WalletStoresV1.denominations }>,
- currency: string,
- exchangeBaseUrl: string,
- sel: BackupDenomSel,
-): Promise<DenomSelectionState> {
- const selectedDenoms: {
- denomPubHash: string;
- count: number;
- }[] = [];
- let totalCoinValue = Amounts.zeroOfCurrency(currency);
- let totalWithdrawCost = Amounts.zeroOfCurrency(currency);
- for (const s of sel) {
- const d = await tx.denominations.get([exchangeBaseUrl, s.denom_pub_hash]);
- checkBackupInvariant(!!d);
- totalCoinValue = Amounts.add(
- totalCoinValue,
- DenominationRecord.getValue(d),
- ).amount;
- totalWithdrawCost = Amounts.add(
- totalWithdrawCost,
- DenominationRecord.getValue(d),
- d.fees.feeWithdraw,
- ).amount;
- }
- return {
- selectedDenoms,
- totalCoinValue: Amounts.stringify(totalCoinValue),
- totalWithdrawCost: Amounts.stringify(totalWithdrawCost),
- };
-}
-
-export interface CompletedCoin {
- coinPub: string;
- coinEvHash: string;
-}
-
-/**
- * Precomputed cryptographic material for a backup import.
- *
- * We separate this data from the backup blob as we want the backup
- * blob to be small, and we can't compute it during the database transaction,
- * as the async crypto worker communication would auto-close the database transaction.
- */
-export interface BackupCryptoPrecomputedData {
- rsaDenomPubToHash: Record<string, string>;
- coinPrivToCompletedCoin: Record<string, CompletedCoin>;
- proposalNoncePrivToPub: { [priv: string]: string };
- proposalIdToContractTermsHash: { [proposalId: string]: string };
- reservePrivToPub: Record<string, string>;
-}
-
-export async function importCoin(
- ws: InternalWalletState,
- tx: GetReadWriteAccess<{
- coins: typeof WalletStoresV1.coins;
- coinAvailability: typeof WalletStoresV1.coinAvailability;
- denominations: typeof WalletStoresV1.denominations;
- }>,
- cryptoComp: BackupCryptoPrecomputedData,
- args: {
- backupCoin: BackupCoin;
- exchangeBaseUrl: string;
- denomPubHash: string;
- },
-): Promise<void> {
- const { backupCoin, exchangeBaseUrl, denomPubHash } = args;
- const compCoin = cryptoComp.coinPrivToCompletedCoin[backupCoin.coin_priv];
- checkLogicInvariant(!!compCoin);
- const existingCoin = await tx.coins.get(compCoin.coinPub);
- if (!existingCoin) {
- let coinSource: CoinSource;
- switch (backupCoin.coin_source.type) {
- case BackupCoinSourceType.Refresh:
- coinSource = {
- type: CoinSourceType.Refresh,
- oldCoinPub: backupCoin.coin_source.old_coin_pub,
- refreshGroupId: backupCoin.coin_source.refresh_group_id,
- };
- break;
- case BackupCoinSourceType.Reward:
- coinSource = {
- type: CoinSourceType.Reward,
- coinIndex: backupCoin.coin_source.coin_index,
- walletRewardId: backupCoin.coin_source.wallet_tip_id,
- };
- break;
- case BackupCoinSourceType.Withdraw:
- coinSource = {
- type: CoinSourceType.Withdraw,
- coinIndex: backupCoin.coin_source.coin_index,
- reservePub: backupCoin.coin_source.reserve_pub,
- withdrawalGroupId: backupCoin.coin_source.withdrawal_group_id,
- };
- break;
- }
- const coinRecord: CoinRecord = {
- blindingKey: backupCoin.blinding_key,
- coinEvHash: compCoin.coinEvHash,
- coinPriv: backupCoin.coin_priv,
- denomSig: backupCoin.denom_sig,
- coinPub: compCoin.coinPub,
- exchangeBaseUrl,
- denomPubHash,
- status: backupCoin.fresh ? CoinStatus.Fresh : CoinStatus.Dormant,
- coinSource,
- // FIXME!
- maxAge: AgeRestriction.AGE_UNRESTRICTED,
- // FIXME!
- ageCommitmentProof: undefined,
- // FIXME!
- spendAllocation: undefined,
- };
- if (coinRecord.status === CoinStatus.Fresh) {
- await makeCoinAvailable(ws, tx, coinRecord);
- } else {
- await tx.coins.put(coinRecord);
- }
- }
-}
-
-export async function importBackup(
- ws: InternalWalletState,
- backupBlobArg: any,
- cryptoComp: BackupCryptoPrecomputedData,
-): Promise<void> {
- await provideBackupState(ws);
-
- logger.info(`importing backup ${j2s(backupBlobArg)}`);
-
- return ws.db
- .mktx((x) => [
- x.config,
- x.exchangeDetails,
- x.exchanges,
- x.coins,
- x.coinAvailability,
- x.denominations,
- x.purchases,
- x.refreshGroups,
- x.backupProviders,
- x.rewards,
- x.recoupGroups,
- x.withdrawalGroups,
- x.tombstones,
- x.depositGroups,
- ])
- .runReadWrite(async (tx) => {
- // FIXME: validate schema!
- const backupBlob = backupBlobArg as WalletBackupContentV1;
-
- // FIXME: validate version
-
- for (const tombstone of backupBlob.tombstones) {
- await tx.tombstones.put({
- id: tombstone,
- });
- }
-
- const tombstoneSet = new Set(
- (await tx.tombstones.iter().toArray()).map((x) => x.id),
- );
-
- // FIXME: Validate that the "details pointer" is correct
-
- for (const backupExchange of backupBlob.exchanges) {
- const existingExchange = await tx.exchanges.get(
- backupExchange.base_url,
- );
- if (existingExchange) {
- continue;
- }
- // await tx.exchanges.put({
- // baseUrl: backupExchange.base_url,
- // detailsPointer: {
- // currency: backupExchange.currency,
- // masterPublicKey: backupExchange.master_public_key,
- // updateClock: backupExchange.update_clock,
- // },
- // lastUpdate: undefined,
- // nextUpdate: TalerPreciseTimestamp.now(),
- // nextRefreshCheck: TalerPreciseTimestamp.now(),
- // lastKeysEtag: undefined,
- // lastWireEtag: undefined,
- // });
- }
-
- for (const backupExchangeDetails of backupBlob.exchange_details) {
- const existingExchangeDetails =
- await tx.exchangeDetails.indexes.byPointer.get([
- backupExchangeDetails.base_url,
- backupExchangeDetails.currency,
- backupExchangeDetails.master_public_key,
- ]);
-
- if (!existingExchangeDetails) {
- const wireInfo: WireInfo = {
- accounts: backupExchangeDetails.accounts.map((x) => ({
- master_sig: x.master_sig,
- payto_uri: x.payto_uri,
- })),
- feesForType: {},
- };
- for (const fee of backupExchangeDetails.wire_fees) {
- const w = (wireInfo.feesForType[fee.wire_type] ??= []);
- w.push({
- closingFee: Amounts.stringify(fee.closing_fee),
- endStamp: fee.end_stamp,
- sig: fee.sig,
- startStamp: fee.start_stamp,
- wireFee: Amounts.stringify(fee.wire_fee),
- });
- }
- let tosAccepted = undefined;
- if (
- backupExchangeDetails.tos_accepted_etag &&
- backupExchangeDetails.tos_accepted_timestamp
- ) {
- tosAccepted = {
- etag: backupExchangeDetails.tos_accepted_etag,
- timestamp: backupExchangeDetails.tos_accepted_timestamp,
- };
- }
- await tx.exchangeDetails.put({
- exchangeBaseUrl: backupExchangeDetails.base_url,
- wireInfo,
- currency: backupExchangeDetails.currency,
- auditors: backupExchangeDetails.auditors.map((x) => ({
- auditor_pub: x.auditor_pub,
- auditor_url: x.auditor_url,
- denomination_keys: x.denomination_keys,
- })),
- masterPublicKey: backupExchangeDetails.master_public_key,
- protocolVersionRange: backupExchangeDetails.protocol_version,
- reserveClosingDelay: backupExchangeDetails.reserve_closing_delay,
- tosCurrentEtag: backupExchangeDetails.tos_accepted_etag || "",
- tosAccepted,
- globalFees: backupExchangeDetails.global_fees.map((x) => ({
- accountFee: Amounts.stringify(x.accountFee),
- historyFee: Amounts.stringify(x.historyFee),
- purseFee: Amounts.stringify(x.purseFee),
- endDate: x.endDate,
- historyTimeout: x.historyTimeout,
- signature: x.signature,
- purseLimit: x.purseLimit,
- purseTimeout: x.purseTimeout,
- startDate: x.startDate,
- })),
- });
- }
-
- for (const backupDenomination of backupExchangeDetails.denominations) {
- if (backupDenomination.denom_pub.cipher !== DenomKeyType.Rsa) {
- throw Error("unsupported cipher");
- }
- const denomPubHash =
- cryptoComp.rsaDenomPubToHash[
- backupDenomination.denom_pub.rsa_public_key
- ];
- checkLogicInvariant(!!denomPubHash);
- const existingDenom = await tx.denominations.get([
- backupExchangeDetails.base_url,
- denomPubHash,
- ]);
- if (!existingDenom) {
- const value = Amounts.parseOrThrow(backupDenomination.value);
-
- await tx.denominations.put({
- denomPub: backupDenomination.denom_pub,
- denomPubHash: denomPubHash,
- exchangeBaseUrl: backupExchangeDetails.base_url,
- exchangeMasterPub: backupExchangeDetails.master_public_key,
- fees: {
- feeDeposit: Amounts.stringify(backupDenomination.fee_deposit),
- feeRefresh: Amounts.stringify(backupDenomination.fee_refresh),
- feeRefund: Amounts.stringify(backupDenomination.fee_refund),
- feeWithdraw: Amounts.stringify(backupDenomination.fee_withdraw),
- },
- isOffered: backupDenomination.is_offered,
- isRevoked: backupDenomination.is_revoked,
- masterSig: backupDenomination.master_sig,
- stampExpireDeposit: backupDenomination.stamp_expire_deposit,
- stampExpireLegal: backupDenomination.stamp_expire_legal,
- stampExpireWithdraw: backupDenomination.stamp_expire_withdraw,
- stampStart: backupDenomination.stamp_start,
- verificationStatus: DenominationVerificationStatus.VerifiedGood,
- currency: value.currency,
- amountFrac: value.fraction,
- amountVal: value.value,
- listIssueDate: backupDenomination.list_issue_date,
- });
- }
- for (const backupCoin of backupDenomination.coins) {
- await importCoin(ws, tx, cryptoComp, {
- backupCoin,
- denomPubHash,
- exchangeBaseUrl: backupExchangeDetails.base_url,
- });
- }
- }
- }
-
- for (const backupWg of backupBlob.withdrawal_groups) {
- const reservePub = cryptoComp.reservePrivToPub[backupWg.reserve_priv];
- checkLogicInvariant(!!reservePub);
- const ts = constructTombstone({
- tag: 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);
- }
- const instructedAmount = Amounts.parseOrThrow(
- backupWg.instructed_amount,
- );
- await tx.withdrawalGroups.put({
- withdrawalGroupId: backupWg.withdrawal_group_id,
- exchangeBaseUrl: backupWg.exchange_base_url,
- instructedAmount: Amounts.stringify(instructedAmount),
- secretSeed: backupWg.secret_seed,
- denomsSel: await getDenomSelStateFromBackup(
- tx,
- instructedAmount.currency,
- backupWg.exchange_base_url,
- backupWg.selected_denoms,
- ),
- denomSelUid: backupWg.selected_denoms_uid,
- rawWithdrawalAmount: Amounts.stringify(
- backupWg.raw_withdrawal_amount,
- ),
- effectiveWithdrawalAmount: Amounts.stringify(
- backupWg.effective_withdrawal_amount,
- ),
- reservePriv: backupWg.reserve_priv,
- reservePub,
- status: backupWg.timestamp_finish
- ? WithdrawalGroupStatus.Finished
- : WithdrawalGroupStatus.PendingQueryingStatus, // FIXME!
- timestampStart: backupWg.timestamp_created,
- wgInfo,
- restrictAge: backupWg.restrict_age,
- senderWire: undefined, // FIXME!
- timestampFinish: backupWg.timestamp_finish,
- });
- }
-
- for (const backupPurchase of backupBlob.purchases) {
- const ts = constructTombstone({
- tag: TombstoneTag.DeletePayment,
- proposalId: backupPurchase.proposal_id,
- });
- if (tombstoneSet.has(ts)) {
- continue;
- }
- const existingPurchase = await tx.purchases.get(
- backupPurchase.proposal_id,
- );
- let proposalStatus: PurchaseStatus;
- switch (backupPurchase.proposal_status) {
- case BackupProposalStatus.Paid:
- proposalStatus = PurchaseStatus.Done;
- break;
- case BackupProposalStatus.Shared:
- proposalStatus = PurchaseStatus.DialogShared;
- break;
- case BackupProposalStatus.Proposed:
- proposalStatus = PurchaseStatus.DialogProposed;
- break;
- case BackupProposalStatus.PermanentlyFailed:
- proposalStatus = PurchaseStatus.AbortedIncompletePayment;
- break;
- case BackupProposalStatus.Refused:
- proposalStatus = PurchaseStatus.AbortedProposalRefused;
- break;
- case BackupProposalStatus.Repurchase:
- proposalStatus = PurchaseStatus.RepurchaseDetected;
- break;
- default: {
- const error: never = backupPurchase.proposal_status;
- throw Error(`backup status ${error} is not handled`);
- }
- }
- if (!existingPurchase) {
- //const refunds: { [refundKey: string]: WalletRefundItem } = {};
- // for (const backupRefund of backupPurchase.refunds) {
- // const key = `${backupRefund.coin_pub}-${backupRefund.rtransaction_id}`;
- // const coin = await tx.coins.get(backupRefund.coin_pub);
- // checkBackupInvariant(!!coin);
- // const denom = await tx.denominations.get([
- // coin.exchangeBaseUrl,
- // coin.denomPubHash,
- // ]);
- // checkBackupInvariant(!!denom);
- // const common = {
- // coinPub: backupRefund.coin_pub,
- // executionTime: backupRefund.execution_time,
- // obtainedTime: backupRefund.obtained_time,
- // refundAmount: Amounts.stringify(backupRefund.refund_amount),
- // refundFee: Amounts.stringify(denom.fees.feeRefund),
- // rtransactionId: backupRefund.rtransaction_id,
- // totalRefreshCostBound: Amounts.stringify(
- // backupRefund.total_refresh_cost_bound,
- // ),
- // };
- // switch (backupRefund.type) {
- // case BackupRefundState.Applied:
- // refunds[key] = {
- // type: RefundState.Applied,
- // ...common,
- // };
- // break;
- // case BackupRefundState.Failed:
- // refunds[key] = {
- // type: RefundState.Failed,
- // ...common,
- // };
- // break;
- // case BackupRefundState.Pending:
- // refunds[key] = {
- // type: RefundState.Pending,
- // ...common,
- // };
- // break;
- // }
- // }
- const parsedContractTerms = codecForMerchantContractTerms().decode(
- backupPurchase.contract_terms_raw,
- );
- const amount = Amounts.parseOrThrow(parsedContractTerms.amount);
- const contractTermsHash =
- cryptoComp.proposalIdToContractTermsHash[
- backupPurchase.proposal_id
- ];
- let maxWireFee: AmountJson;
- if (parsedContractTerms.max_wire_fee) {
- maxWireFee = Amounts.parseOrThrow(parsedContractTerms.max_wire_fee);
- } else {
- maxWireFee = Amounts.zeroOfCurrency(amount.currency);
- }
- const download: ProposalDownloadInfo = {
- contractTermsHash,
- contractTermsMerchantSig: backupPurchase.merchant_sig!,
- currency: amount.currency,
- fulfillmentUrl: backupPurchase.contract_terms_raw.fulfillment_url,
- };
-
- const contractData = extractContractData(
- backupPurchase.contract_terms_raw,
- contractTermsHash,
- download.contractTermsMerchantSig,
- );
-
- let payInfo: PurchasePayInfo | undefined = undefined;
- if (backupPurchase.pay_info) {
- payInfo = {
- payCoinSelection: await recoverPayCoinSelection(
- tx,
- contractData,
- backupPurchase.pay_info,
- ),
- payCoinSelectionUid: backupPurchase.pay_info.pay_coins_uid,
- totalPayCost: Amounts.stringify(
- backupPurchase.pay_info.total_pay_cost,
- ),
- };
- }
-
- await tx.purchases.put({
- proposalId: backupPurchase.proposal_id,
- noncePriv: backupPurchase.nonce_priv,
- noncePub:
- cryptoComp.proposalNoncePrivToPub[backupPurchase.nonce_priv],
- autoRefundDeadline: TalerProtocolTimestamp.never(),
- timestampAccept: backupPurchase.timestamp_accepted,
- timestampFirstSuccessfulPay:
- backupPurchase.timestamp_first_successful_pay,
- timestampLastRefundStatus: undefined,
- merchantPaySig: backupPurchase.merchant_pay_sig,
- posConfirmation: backupPurchase.pos_confirmation,
- lastSessionId: undefined,
- download,
- //refunds,
- claimToken: backupPurchase.claim_token,
- downloadSessionId: backupPurchase.download_session_id,
- merchantBaseUrl: backupPurchase.merchant_base_url,
- orderId: backupPurchase.order_id,
- payInfo,
- refundAmountAwaiting: undefined,
- repurchaseProposalId: backupPurchase.repurchase_proposal_id,
- purchaseStatus: proposalStatus,
- timestamp: backupPurchase.timestamp_proposed,
- shared: backupPurchase.shared,
- });
- }
- }
-
- for (const backupRefreshGroup of backupBlob.refresh_groups) {
- const ts = constructTombstone({
- tag: TombstoneTag.DeleteRefreshGroup,
- refreshGroupId: backupRefreshGroup.refresh_group_id,
- });
- if (tombstoneSet.has(ts)) {
- continue;
- }
- const existingRg = await tx.refreshGroups.get(
- backupRefreshGroup.refresh_group_id,
- );
- if (!existingRg) {
- let reason: RefreshReason;
- switch (backupRefreshGroup.reason) {
- case BackupRefreshReason.AbortPay:
- reason = RefreshReason.AbortPay;
- break;
- case BackupRefreshReason.BackupRestored:
- reason = RefreshReason.BackupRestored;
- break;
- case BackupRefreshReason.Manual:
- reason = RefreshReason.Manual;
- break;
- case BackupRefreshReason.Pay:
- reason = RefreshReason.PayMerchant;
- break;
- case BackupRefreshReason.Recoup:
- reason = RefreshReason.Recoup;
- break;
- case BackupRefreshReason.Refund:
- reason = RefreshReason.Refund;
- break;
- case BackupRefreshReason.Scheduled:
- reason = RefreshReason.Scheduled;
- break;
- }
- const refreshSessionPerCoin: (RefreshSessionRecord | undefined)[] =
- [];
- for (const oldCoin of backupRefreshGroup.old_coins) {
- const c = await tx.coins.get(oldCoin.coin_pub);
- checkBackupInvariant(!!c);
- const d = await tx.denominations.get([
- c.exchangeBaseUrl,
- c.denomPubHash,
- ]);
- checkBackupInvariant(!!d);
-
- if (oldCoin.refresh_session) {
- const denomSel = await getDenomSelStateFromBackup(
- tx,
- d.currency,
- c.exchangeBaseUrl,
- oldCoin.refresh_session.new_denoms,
- );
- refreshSessionPerCoin.push({
- sessionSecretSeed: oldCoin.refresh_session.session_secret_seed,
- norevealIndex: oldCoin.refresh_session.noreveal_index,
- newDenoms: oldCoin.refresh_session.new_denoms.map((x) => ({
- count: x.count,
- denomPubHash: x.denom_pub_hash,
- })),
- amountRefreshOutput: Amounts.stringify(denomSel.totalCoinValue),
- });
- } else {
- refreshSessionPerCoin.push(undefined);
- }
- }
- await tx.refreshGroups.put({
- timestampFinished: backupRefreshGroup.timestamp_finish,
- timestampCreated: backupRefreshGroup.timestamp_created,
- refreshGroupId: backupRefreshGroup.refresh_group_id,
- currency: Amounts.currencyOf(
- backupRefreshGroup.old_coins[0].input_amount,
- ),
- reason,
- lastErrorPerCoin: {},
- oldCoinPubs: backupRefreshGroup.old_coins.map((x) => x.coin_pub),
- statusPerCoin: backupRefreshGroup.old_coins.map((x) =>
- x.finished
- ? RefreshCoinStatus.Finished
- : RefreshCoinStatus.Pending,
- ),
- operationStatus: backupRefreshGroup.timestamp_finish
- ? RefreshOperationStatus.Finished
- : RefreshOperationStatus.Pending,
- inputPerCoin: backupRefreshGroup.old_coins.map(
- (x) => x.input_amount,
- ),
- estimatedOutputPerCoin: backupRefreshGroup.old_coins.map(
- (x) => x.estimated_output_amount,
- ),
- refreshSessionPerCoin,
- });
- }
- }
-
- for (const backupTip of backupBlob.tips) {
- const ts = constructTombstone({
- tag: TombstoneTag.DeleteReward,
- walletTipId: backupTip.wallet_tip_id,
- });
- if (tombstoneSet.has(ts)) {
- continue;
- }
- const existingTip = await tx.rewards.get(backupTip.wallet_tip_id);
- if (!existingTip) {
- const tipAmountRaw = Amounts.parseOrThrow(backupTip.tip_amount_raw);
- const denomsSel = await getDenomSelStateFromBackup(
- tx,
- tipAmountRaw.currency,
- backupTip.exchange_base_url,
- backupTip.selected_denoms,
- );
- await tx.rewards.put({
- acceptedTimestamp: backupTip.timestamp_accepted,
- createdTimestamp: backupTip.timestamp_created,
- denomsSel,
- next_url: backupTip.next_url,
- exchangeBaseUrl: backupTip.exchange_base_url,
- merchantBaseUrl: backupTip.exchange_base_url,
- merchantRewardId: backupTip.merchant_tip_id,
- pickedUpTimestamp: backupTip.timestamp_finished,
- secretSeed: backupTip.secret_seed,
- rewardAmountEffective: Amounts.stringify(denomsSel.totalCoinValue),
- rewardAmountRaw: Amounts.stringify(tipAmountRaw),
- rewardExpiration: backupTip.timestamp_expiration,
- walletRewardId: backupTip.wallet_tip_id,
- denomSelUid: backupTip.selected_denoms_uid,
- status: RewardRecordStatus.Done, // FIXME!
- });
- }
- }
-
- // We now process tombstones.
- // The import code above should already prevent
- // importing things that are tombstoned,
- // but we do tombstone processing last just to be sure.
-
- for (const tombstone of tombstoneSet) {
- const [type, ...rest] = tombstone.split(":");
- if (type === TombstoneTag.DeleteDepositGroup) {
- await tx.depositGroups.delete(rest[0]);
- } else if (type === TombstoneTag.DeletePayment) {
- await tx.purchases.delete(rest[0]);
- } else if (type === TombstoneTag.DeleteRefreshGroup) {
- await tx.refreshGroups.delete(rest[0]);
- } else if (type === TombstoneTag.DeleteRefund) {
- // Nothing required, will just prevent display
- // in the transactions list
- } else if (type === TombstoneTag.DeleteReward) {
- await tx.rewards.delete(rest[0]);
- } else if (type === TombstoneTag.DeleteWithdrawalGroup) {
- await tx.withdrawalGroups.delete(rest[0]);
- } else {
- logger.warn(`unable to process tombstone of type '${type}'`);
- }
- }
- });
-}
diff --git a/packages/taler-wallet-core/src/operations/backup/index.ts b/packages/taler-wallet-core/src/operations/backup/index.ts
index e35765165..a5e8dbd42 100644
--- a/packages/taler-wallet-core/src/operations/backup/index.ts
+++ b/packages/taler-wallet-core/src/operations/backup/index.ts
@@ -43,7 +43,6 @@ import {
TalerErrorDetail,
TalerPreciseTimestamp,
URL,
- WalletBackupContentV1,
buildCodecForObject,
buildCodecForUnion,
bytesToString,
@@ -99,9 +98,8 @@ import {
TaskIdentifiers,
} from "../common.js";
import { checkPaymentByProposalId, preparePayForUri } from "../pay-merchant.js";
-import { exportBackup } from "./export.js";
-import { BackupCryptoPrecomputedData, importBackup } from "./import.js";
-import { getWalletBackupState, provideBackupState } from "./state.js";
+import { WalletStoresV1 } from "../../db.js";
+import { GetReadOnlyAccess } from "../../util/query.js";
const logger = new Logger("operations/backup.ts");
@@ -131,7 +129,7 @@ const magic = "TLRWBK01";
*/
export async function encryptBackup(
config: WalletBackupConfState,
- blob: WalletBackupContentV1,
+ blob: any,
): Promise<Uint8Array> {
const chunks: Uint8Array[] = [];
chunks.push(stringToBytes(magic));
@@ -150,64 +148,6 @@ export async function encryptBackup(
return concatArrays(chunks);
}
-/**
- * Compute cryptographic values for a backup blob.
- *
- * FIXME: Take data that we already know from the DB.
- * FIXME: Move computations into crypto worker.
- */
-async function computeBackupCryptoData(
- cryptoApi: TalerCryptoInterface,
- backupContent: WalletBackupContentV1,
-): Promise<BackupCryptoPrecomputedData> {
- const cryptoData: BackupCryptoPrecomputedData = {
- coinPrivToCompletedCoin: {},
- rsaDenomPubToHash: {},
- proposalIdToContractTermsHash: {},
- proposalNoncePrivToPub: {},
- reservePrivToPub: {},
- };
- for (const backupExchangeDetails of backupContent.exchange_details) {
- for (const backupDenom of backupExchangeDetails.denominations) {
- if (backupDenom.denom_pub.cipher !== DenomKeyType.Rsa) {
- throw Error("unsupported cipher");
- }
- for (const backupCoin of backupDenom.coins) {
- const coinPub = encodeCrock(
- eddsaGetPublic(decodeCrock(backupCoin.coin_priv)),
- );
- const blindedCoin = rsaBlind(
- hash(decodeCrock(backupCoin.coin_priv)),
- decodeCrock(backupCoin.blinding_key),
- decodeCrock(backupDenom.denom_pub.rsa_public_key),
- );
- cryptoData.coinPrivToCompletedCoin[backupCoin.coin_priv] = {
- coinEvHash: encodeCrock(hash(blindedCoin)),
- coinPub,
- };
- }
- cryptoData.rsaDenomPubToHash[backupDenom.denom_pub.rsa_public_key] =
- encodeCrock(hashDenomPub(backupDenom.denom_pub));
- }
- }
- for (const backupWg of backupContent.withdrawal_groups) {
- cryptoData.reservePrivToPub[backupWg.reserve_priv] = encodeCrock(
- eddsaGetPublic(decodeCrock(backupWg.reserve_priv)),
- );
- }
- for (const purch of backupContent.purchases) {
- if (!purch.contract_terms_raw) continue;
- const { h: contractTermsHash } = await cryptoApi.hashString({
- str: canonicalJson(purch.contract_terms_raw),
- });
- const noncePub = encodeCrock(eddsaGetPublic(decodeCrock(purch.nonce_priv)));
- cryptoData.proposalNoncePrivToPub[purch.nonce_priv] = noncePub;
- cryptoData.proposalIdToContractTermsHash[purch.proposal_id] =
- contractTermsHash;
- }
- return cryptoData;
-}
-
function deriveAccountKeyPair(
bc: WalletBackupConfState,
providerUrl: string,
@@ -262,7 +202,9 @@ async function runBackupCycleForProvider(
return TaskRunResult.finished();
}
- const backupJson = await exportBackup(ws);
+ //const backupJson = await exportBackup(ws);
+ // FIXME: re-implement backup
+ const backupJson = {};
const backupConfig = await provideBackupState(ws);
const encBackup = await encryptBackup(backupConfig, backupJson);
const currentBackupHash = hash(encBackup);
@@ -441,9 +383,9 @@ async function runBackupCycleForProvider(
logger.info("conflicting backup found");
const backupEnc = new Uint8Array(await resp.bytes());
const backupConfig = await provideBackupState(ws);
- const blob = await decryptBackup(backupConfig, backupEnc);
- const cryptoData = await computeBackupCryptoData(ws.cryptoApi, blob);
- await importBackup(ws, blob, cryptoData);
+ // const blob = await decryptBackup(backupConfig, backupEnc);
+ // FIXME: Re-implement backup import with merging
+ // await importBackup(ws, blob, cryptoData);
await ws.db
.mktx((x) => [x.backupProviders, x.operationRetries])
.runReadWrite(async (tx) => {
@@ -789,18 +731,6 @@ export interface BackupInfo {
providers: ProviderInfo[];
}
-export async function importBackupPlain(
- ws: InternalWalletState,
- blob: any,
-): Promise<void> {
- // FIXME: parse
- const backup: WalletBackupContentV1 = blob;
-
- const cryptoData = await computeBackupCryptoData(ws.cryptoApi, backup);
-
- await importBackup(ws, blob, cryptoData);
-}
-
export enum ProviderPaymentType {
Unpaid = "unpaid",
Pending = "pending",
@@ -1036,23 +966,10 @@ export async function loadBackupRecovery(
}
}
-export async function exportBackupEncrypted(
- ws: InternalWalletState,
-): Promise<Uint8Array> {
- await provideBackupState(ws);
- const blob = await exportBackup(ws);
- const bs = await ws.db
- .mktx((x) => [x.config])
- .runReadOnly(async (tx) => {
- return await getWalletBackupState(ws, tx);
- });
- return encryptBackup(bs, blob);
-}
-
export async function decryptBackup(
backupConfig: WalletBackupConfState,
data: Uint8Array,
-): Promise<WalletBackupContentV1> {
+): Promise<any> {
const rMagic = bytesToString(data.slice(0, 8));
if (rMagic != magic) {
throw Error("invalid backup file (magic tag mismatch)");
@@ -1068,12 +985,85 @@ export async function decryptBackup(
return JSON.parse(bytesToString(gunzipSync(dataCompressed)));
}
-export async function importBackupEncrypted(
+export async function provideBackupState(
ws: InternalWalletState,
- data: Uint8Array,
+): Promise<WalletBackupConfState> {
+ const bs: ConfigRecord | undefined = await ws.db
+ .mktx((stores) => [stores.config])
+ .runReadOnly(async (tx) => {
+ return await tx.config.get(ConfigRecordKey.WalletBackupState);
+ });
+ if (bs) {
+ checkDbInvariant(bs.key === ConfigRecordKey.WalletBackupState);
+ return bs.value;
+ }
+ // We need to generate the key outside of the transaction
+ // due to how IndexedDB works.
+ const k = await ws.cryptoApi.createEddsaKeypair({});
+ const d = getRandomBytes(5);
+ // FIXME: device ID should be configured when wallet is initialized
+ // and be based on hostname
+ const deviceId = `wallet-core-${encodeCrock(d)}`;
+ return await ws.db
+ .mktx((x) => [x.config])
+ .runReadWrite(async (tx) => {
+ let backupStateEntry: ConfigRecord | undefined = await tx.config.get(
+ ConfigRecordKey.WalletBackupState,
+ );
+ if (!backupStateEntry) {
+ backupStateEntry = {
+ key: ConfigRecordKey.WalletBackupState,
+ value: {
+ deviceId,
+ walletRootPub: k.pub,
+ walletRootPriv: k.priv,
+ lastBackupPlainHash: undefined,
+ },
+ };
+ await tx.config.put(backupStateEntry);
+ }
+ checkDbInvariant(
+ backupStateEntry.key === ConfigRecordKey.WalletBackupState,
+ );
+ return backupStateEntry.value;
+ });
+}
+
+export async function getWalletBackupState(
+ ws: InternalWalletState,
+ tx: GetReadOnlyAccess<{ config: typeof WalletStoresV1.config }>,
+): Promise<WalletBackupConfState> {
+ const bs = await tx.config.get(ConfigRecordKey.WalletBackupState);
+ checkDbInvariant(!!bs, "wallet backup state should be in DB");
+ checkDbInvariant(bs.key === ConfigRecordKey.WalletBackupState);
+ return bs.value;
+}
+
+export async function setWalletDeviceId(
+ ws: InternalWalletState,
+ deviceId: string,
): Promise<void> {
- const backupConfig = await provideBackupState(ws);
- const blob = await decryptBackup(backupConfig, data);
- const cryptoData = await computeBackupCryptoData(ws.cryptoApi, blob);
- await importBackup(ws, blob, cryptoData);
+ await provideBackupState(ws);
+ await ws.db
+ .mktx((x) => [x.config])
+ .runReadWrite(async (tx) => {
+ let backupStateEntry: ConfigRecord | undefined = await tx.config.get(
+ ConfigRecordKey.WalletBackupState,
+ );
+ if (
+ !backupStateEntry ||
+ backupStateEntry.key !== ConfigRecordKey.WalletBackupState
+ ) {
+ return;
+ }
+ backupStateEntry.value.deviceId = deviceId;
+ await tx.config.put(backupStateEntry);
+ });
+}
+
+export async function getWalletDeviceId(
+ ws: InternalWalletState,
+): Promise<string> {
+ const bs = await provideBackupState(ws);
+ return bs.deviceId;
}
diff --git a/packages/taler-wallet-core/src/operations/backup/state.ts b/packages/taler-wallet-core/src/operations/backup/state.ts
index fa632f44c..d02ead783 100644
--- a/packages/taler-wallet-core/src/operations/backup/state.ts
+++ b/packages/taler-wallet-core/src/operations/backup/state.ts
@@ -14,96 +14,4 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { encodeCrock, getRandomBytes } from "@gnu-taler/taler-util";
-import {
- ConfigRecord,
- ConfigRecordKey,
- WalletBackupConfState,
- WalletStoresV1,
-} from "../../db.js";
-import { checkDbInvariant } from "../../util/invariants.js";
-import { GetReadOnlyAccess } from "../../util/query.js";
-import { InternalWalletState } from "../../internal-wallet-state.js";
-export async function provideBackupState(
- ws: InternalWalletState,
-): Promise<WalletBackupConfState> {
- const bs: ConfigRecord | undefined = await ws.db
- .mktx((stores) => [stores.config])
- .runReadOnly(async (tx) => {
- return await tx.config.get(ConfigRecordKey.WalletBackupState);
- });
- if (bs) {
- checkDbInvariant(bs.key === ConfigRecordKey.WalletBackupState);
- return bs.value;
- }
- // We need to generate the key outside of the transaction
- // due to how IndexedDB works.
- const k = await ws.cryptoApi.createEddsaKeypair({});
- const d = getRandomBytes(5);
- // FIXME: device ID should be configured when wallet is initialized
- // and be based on hostname
- const deviceId = `wallet-core-${encodeCrock(d)}`;
- return await ws.db
- .mktx((x) => [x.config])
- .runReadWrite(async (tx) => {
- let backupStateEntry: ConfigRecord | undefined = await tx.config.get(
- ConfigRecordKey.WalletBackupState,
- );
- if (!backupStateEntry) {
- backupStateEntry = {
- key: ConfigRecordKey.WalletBackupState,
- value: {
- deviceId,
- walletRootPub: k.pub,
- walletRootPriv: k.priv,
- lastBackupPlainHash: undefined,
- },
- };
- await tx.config.put(backupStateEntry);
- }
- checkDbInvariant(
- backupStateEntry.key === ConfigRecordKey.WalletBackupState,
- );
- return backupStateEntry.value;
- });
-}
-
-export async function getWalletBackupState(
- ws: InternalWalletState,
- tx: GetReadOnlyAccess<{ config: typeof WalletStoresV1.config }>,
-): Promise<WalletBackupConfState> {
- const bs = await tx.config.get(ConfigRecordKey.WalletBackupState);
- checkDbInvariant(!!bs, "wallet backup state should be in DB");
- checkDbInvariant(bs.key === ConfigRecordKey.WalletBackupState);
- return bs.value;
-}
-
-export async function setWalletDeviceId(
- ws: InternalWalletState,
- deviceId: string,
-): Promise<void> {
- await provideBackupState(ws);
- await ws.db
- .mktx((x) => [x.config])
- .runReadWrite(async (tx) => {
- let backupStateEntry: ConfigRecord | undefined = await tx.config.get(
- ConfigRecordKey.WalletBackupState,
- );
- if (
- !backupStateEntry ||
- backupStateEntry.key !== ConfigRecordKey.WalletBackupState
- ) {
- return;
- }
- backupStateEntry.value.deviceId = deviceId;
- await tx.config.put(backupStateEntry);
- });
-}
-
-export async function getWalletDeviceId(
- ws: InternalWalletState,
-): Promise<string> {
- const bs = await provideBackupState(ws);
- return bs.deviceId;
-}
diff --git a/packages/taler-wallet-core/src/wallet-api-types.ts b/packages/taler-wallet-core/src/wallet-api-types.ts
index 06ccdf6f3..4d9d40c43 100644
--- a/packages/taler-wallet-core/src/wallet-api-types.ts
+++ b/packages/taler-wallet-core/src/wallet-api-types.ts
@@ -106,7 +106,6 @@ import {
UserAttentionsResponse,
ValidateIbanRequest,
ValidateIbanResponse,
- WalletBackupContentV1,
WalletCoreVersion,
WalletCurrencyInfo,
WithdrawFakebankRequest,
@@ -116,6 +115,10 @@ import {
SharePaymentResult,
GetCurrencyInfoRequest,
GetCurrencyInfoResponse,
+ StoredBackupList,
+ CreateStoredBackupResponse,
+ RecoverStoredBackupRequest,
+ DeleteStoredBackupRequest,
} from "@gnu-taler/taler-util";
import { AuditorTrustRecord, WalletContractData } from "./db.js";
import {
@@ -195,7 +198,6 @@ export enum WalletApiOperation {
GenerateDepositGroupTxId = "generateDepositGroupTxId",
CreateDepositGroup = "createDepositGroup",
SetWalletDeviceId = "setWalletDeviceId",
- ExportBackupPlain = "exportBackupPlain",
WithdrawFakebank = "withdrawFakebank",
ImportDb = "importDb",
ExportDb = "exportDb",
@@ -214,6 +216,10 @@ export enum WalletApiOperation {
TestingWaitTransactionsFinal = "testingWaitTransactionsFinal",
TestingWaitRefreshesFinal = "testingWaitRefreshesFinal",
GetScopedCurrencyInfo = "getScopedCurrencyInfo",
+ ListStoredBackups = "listStoredBackups",
+ CreateStoredBackup = "createStoredBackup",
+ DeleteStoredBackup = "deleteStoredBackup",
+ RecoverStoredBackup = "recoverStoredBackup",
}
// group: Initialization
@@ -713,13 +719,28 @@ export type SetWalletDeviceIdOp = {
response: EmptyObject;
};
-/**
- * Export a backup JSON, mostly useful for testing.
- */
-export type ExportBackupPlainOp = {
- op: WalletApiOperation.ExportBackupPlain;
+export type ListStoredBackupsOp = {
+ op: WalletApiOperation.ListStoredBackups;
+ request: EmptyObject;
+ response: StoredBackupList;
+};
+
+export type CreateStoredBackupsOp = {
+ op: WalletApiOperation.CreateStoredBackup;
request: EmptyObject;
- response: WalletBackupContentV1;
+ response: CreateStoredBackupResponse;
+};
+
+export type RecoverStoredBackupsOp = {
+ op: WalletApiOperation.RecoverStoredBackup;
+ request: RecoverStoredBackupRequest;
+ response: EmptyObject;
+};
+
+export type DeleteStoredBackupOp = {
+ op: WalletApiOperation.DeleteStoredBackup;
+ request: DeleteStoredBackupRequest;
+ response: EmptyObject;
};
// group: Peer Payments
@@ -1062,7 +1083,6 @@ export type WalletOperations = {
[WalletApiOperation.GenerateDepositGroupTxId]: GenerateDepositGroupTxIdOp;
[WalletApiOperation.CreateDepositGroup]: CreateDepositGroupOp;
[WalletApiOperation.SetWalletDeviceId]: SetWalletDeviceIdOp;
- [WalletApiOperation.ExportBackupPlain]: ExportBackupPlainOp;
[WalletApiOperation.ExportBackupRecovery]: ExportBackupRecoveryOp;
[WalletApiOperation.ImportBackupRecovery]: ImportBackupRecoveryOp;
[WalletApiOperation.RunBackupCycle]: RunBackupCycleOp;
@@ -1092,6 +1112,10 @@ export type WalletOperations = {
[WalletApiOperation.TestingWaitTransactionsFinal]: TestingWaitTransactionsFinal;
[WalletApiOperation.TestingWaitRefreshesFinal]: TestingWaitRefreshesFinal;
[WalletApiOperation.GetScopedCurrencyInfo]: GetScopedCurrencyInfoOp;
+ [WalletApiOperation.CreateStoredBackup]: CreateStoredBackupsOp;
+ [WalletApiOperation.ListStoredBackups]: ListStoredBackupsOp;
+ [WalletApiOperation.DeleteStoredBackup]: DeleteStoredBackupOp;
+ [WalletApiOperation.RecoverStoredBackup]: RecoverStoredBackupsOp;
};
export type WalletCoreRequestType<
diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts
index 9f754ed69..283539a08 100644
--- a/packages/taler-wallet-core/src/wallet.ts
+++ b/packages/taler-wallet-core/src/wallet.ts
@@ -120,6 +120,7 @@ import {
codecForSharePaymentRequest,
GetCurrencyInfoResponse,
codecForGetCurrencyInfoRequest,
+ CreateStoredBackupResponse,
} from "@gnu-taler/taler-util";
import {
HttpRequestLibrary,
@@ -139,6 +140,7 @@ import {
clearDatabase,
exportDb,
importDb,
+ openStoredBackupsDatabase,
openTalerDatabase,
} from "./db.js";
import { DevExperimentHttpLib, applyDevExperiment } from "./dev-experiments.js";
@@ -158,7 +160,6 @@ import {
getUserAttentionsUnreadCount,
markAttentionRequestAsRead,
} from "./operations/attention.js";
-import { exportBackup } from "./operations/backup/export.js";
import {
addBackupProvider,
codecForAddBackupProviderRequest,
@@ -166,13 +167,12 @@ import {
codecForRunBackupCycle,
getBackupInfo,
getBackupRecovery,
- importBackupPlain,
loadBackupRecovery,
processBackupForProvider,
removeBackupProvider,
runBackupCycle,
+ setWalletDeviceId,
} from "./operations/backup/index.js";
-import { setWalletDeviceId } from "./operations/backup/state.js";
import { getBalanceDetail, getBalances } from "./operations/balance.js";
import {
TaskIdentifiers,
@@ -1025,6 +1025,17 @@ export async function getClientFromWalletState(
return client;
}
+async function createStoredBackup(
+ ws: InternalWalletState,
+): Promise<CreateStoredBackupResponse> {
+ const backup = await exportDb(ws.idb);
+ const backupsDb = await openStoredBackupsDatabase(ws.idb);
+ const name = `backup-${new Date().getTime()}`;
+ backupsDb.mktxAll().runReadWrite(async (tx) => {});
+
+ throw Error("not implemented");
+}
+
/**
* Implementation of the "wallet-core" API.
*/
@@ -1041,6 +1052,14 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>(
// FIXME: Can we make this more type-safe by using the request/response type
// definitions we already have?
switch (operation) {
+ case WalletApiOperation.CreateStoredBackup:
+ return createStoredBackup(ws);
+ case WalletApiOperation.DeleteStoredBackup:
+ return {};
+ case WalletApiOperation.ListStoredBackups:
+ return {};
+ case WalletApiOperation.RecoverStoredBackup:
+ return {};
case WalletApiOperation.InitWallet: {
logger.trace("initializing wallet");
ws.initCalled = true;
@@ -1382,9 +1401,6 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>(
const req = codecForAcceptTipRequest().decode(payload);
return await acceptTip(ws, req.walletRewardId);
}
- case WalletApiOperation.ExportBackupPlain: {
- return exportBackup(ws);
- }
case WalletApiOperation.AddBackupProvider: {
const req = codecForAddBackupProviderRequest().decode(payload);
return await addBackupProvider(ws, req);
@@ -1535,9 +1551,7 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>(
await clearDatabase(ws.db.idbHandle());
return {};
case WalletApiOperation.Recycle: {
- const backup = await exportBackup(ws);
- await clearDatabase(ws.db.idbHandle());
- await importBackupPlain(ws, backup);
+ throw Error("not implemented");
return {};
}
case WalletApiOperation.ExportDb: {
diff --git a/packages/taler-wallet-webextension/src/wxBackend.ts b/packages/taler-wallet-webextension/src/wxBackend.ts
index f071d78df..b7484164d 100644
--- a/packages/taler-wallet-webextension/src/wxBackend.ts
+++ b/packages/taler-wallet-webextension/src/wxBackend.ts
@@ -139,7 +139,7 @@ async function runGarbageCollector(): Promise<void> {
if (!dbBeforeGc) {
throw Error("no current db before running gc");
}
- const dump = await exportDb(dbBeforeGc.idbHandle());
+ const dump = await exportDb(indexedDB as any);
await deleteTalerDatabase(indexedDB as any);
logger.info("cleaned");