diff options
31 files changed, 696 insertions, 3345 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/taleruri.test.ts b/packages/taler-util/src/taleruri.test.ts index 0dcf92252..19b94e191 100644 --- a/packages/taler-util/src/taleruri.test.ts +++ b/packages/taler-util/src/taleruri.test.ts @@ -159,8 +159,8 @@ test("taler refund uri parsing with instance", (t) => {    t.is(r1.merchantBaseUrl, "https://merchant.example.com/instances/myinst/");  }); -test("taler tip pickup uri", (t) => { -  const url1 = "taler://tip/merchant.example.com/tipid"; +test("taler reward pickup uri", (t) => { +  const url1 = "taler://reward/merchant.example.com/tipid";    const r1 = parseRewardUri(url1);    if (!r1) {      t.fail(); @@ -169,26 +169,26 @@ test("taler tip pickup uri", (t) => {    t.is(r1.merchantBaseUrl, "https://merchant.example.com/");  }); -test("taler tip pickup uri with instance", (t) => { -  const url1 = "taler://tip/merchant.example.com/instances/tipm/tipid"; +test("taler reward pickup uri with instance", (t) => { +  const url1 = "taler://reward/merchant.example.com/instances/tipm/tipid";    const r1 = parseRewardUri(url1);    if (!r1) {      t.fail();      return;    }    t.is(r1.merchantBaseUrl, "https://merchant.example.com/instances/tipm/"); -  t.is(r1.merchantTipId, "tipid"); +  t.is(r1.merchantRewardId, "tipid");  }); -test("taler tip pickup uri with instance and prefix", (t) => { -  const url1 = "taler://tip/merchant.example.com/my/pfx/tipm/tipid"; +test("taler reward pickup uri with instance and prefix", (t) => { +  const url1 = "taler://reward/merchant.example.com/my/pfx/tipm/tipid";    const r1 = parseRewardUri(url1);    if (!r1) {      t.fail();      return;    }    t.is(r1.merchantBaseUrl, "https://merchant.example.com/my/pfx/tipm/"); -  t.is(r1.merchantTipId, "tipid"); +  t.is(r1.merchantRewardId, "tipid");  });  test("taler peer to peer push URI", (t) => { diff --git a/packages/taler-util/src/taleruri.ts b/packages/taler-util/src/taleruri.ts index fff1ca833..310986eaf 100644 --- a/packages/taler-util/src/taleruri.ts +++ b/packages/taler-util/src/taleruri.ts @@ -63,7 +63,7 @@ export interface RefundUriResult {  export interface RewardUriResult {    type: TalerUriAction.Reward;    merchantBaseUrl: string; -  merchantTipId: string; +  merchantRewardId: string;  }  export interface ExchangeUri { @@ -408,7 +408,7 @@ export function parseRewardUri(s: string): RewardUriResult | undefined {      return undefined;    }    const host = parts[0].toLowerCase(); -  const tipId = parts[parts.length - 1]; +  const rewardId = parts[parts.length - 1];    const pathSegments = parts.slice(1, parts.length - 1);    const hostAndSegments = [host, ...pathSegments].join("/");    const merchantBaseUrl = canonicalizeBaseUrl( @@ -418,7 +418,7 @@ export function parseRewardUri(s: string): RewardUriResult | undefined {    return {      type: TalerUriAction.Reward,      merchantBaseUrl, -    merchantTipId: tipId, +    merchantRewardId: rewardId,    };  } @@ -701,10 +701,10 @@ export function stringifyRefundUri({  }  export function stringifyRewardUri({    merchantBaseUrl, -  merchantTipId, +  merchantRewardId,  }: Omit<RewardUriResult, "type">): string {    const { proto, path } = getUrlInfo(merchantBaseUrl); -  return `${proto}://reward/${path}${merchantTipId}`; +  return `${proto}://reward/${path}${merchantRewardId}`;  }  export function stringifyExchangeUri({ diff --git a/packages/taler-util/src/time.ts b/packages/taler-util/src/time.ts index 75b3c7e94..55cda08a5 100644 --- a/packages/taler-util/src/time.ts +++ b/packages/taler-util/src/time.ts @@ -312,6 +312,14 @@ export namespace Duration {  }  export namespace AbsoluteTime { +  export function getStampMsNow(): number { +    return new Date().getTime(); +  } + +  export function getStampMsNever(): number { +    return Number.MAX_SAFE_INTEGER; +  } +    export function now(): AbsoluteTime {      return {        t_ms: new Date().getTime() + timeshift, @@ -398,6 +406,13 @@ export namespace AbsoluteTime {      };    } +  export function fromStampMs(stampMs: number): AbsoluteTime { +    return { +      t_ms: stampMs, +      [opaque_AbsoluteTime]: true, +    }; +  } +    export function fromPreciseTimestamp(t: TalerPreciseTimestamp): AbsoluteTime {      if (t.t_s === "never") {        return { t_ms: "never", [opaque_AbsoluteTime]: true }; @@ -409,6 +424,13 @@ export namespace AbsoluteTime {      };    } +  export function toStampMs(at: AbsoluteTime): number { +    if (at.t_ms === "never") { +      return Number.MAX_SAFE_INTEGER; +    } +    return at.t_ms; +  } +    export function toPreciseTimestamp(at: AbsoluteTime): TalerPreciseTimestamp {      if (at.t_ms == "never") {        return { diff --git a/packages/taler-util/src/wallet-types.ts b/packages/taler-util/src/wallet-types.ts index 04fb43ec6..accab746f 100644 --- a/packages/taler-util/src/wallet-types.ts +++ b/packages/taler-util/src/wallet-types.ts @@ -1262,17 +1262,25 @@ export interface ExchangeFullDetails {  }  export enum ExchangeTosStatus { -  New = "new", +  Pending = "pending", +  Proposed = "proposed",    Accepted = "accepted", -  Changed = "changed", -  NotFound = "not-found", -  Unknown = "unknown",  }  export enum ExchangeEntryStatus { -  Unknown = "unknown", -  Outdated = "outdated", -  Ok = "ok", +  Preset = "preset", +  Ephemeral = "ephemeral", +  Used = "used", +} + +export enum ExchangeUpdateStatus { +  Initial = "initial", +  InitialUpdate = "initial(update)", +  Suspended = "suspended", +  Failed = "failed", +  OutdatedUpdate = "outdated(update)", +  Ready = "ready", +  ReadyUpdate = "ready(update)",  }  export interface OperationErrorInfo { @@ -1285,13 +1293,9 @@ export interface ExchangeListItem {    currency: string | undefined;    paytoUris: string[];    tosStatus: ExchangeTosStatus; -  exchangeStatus: ExchangeEntryStatus; +  exchangeEntryStatus: ExchangeEntryStatus; +  exchangeUpdateStatus: ExchangeUpdateStatus;    ageRestrictionOptions: number[]; -  /** -   * Permanently added to the wallet, as opposed to just -   * temporarily queried. -   */ -  permanent: boolean;    /**     * Information about the last error that occurred when trying @@ -1370,8 +1374,8 @@ export const codecForExchangeListItem = (): Codec<ExchangeListItem> =>      .property("exchangeBaseUrl", codecForString())      .property("paytoUris", codecForList(codecForString()))      .property("tosStatus", codecForAny()) -    .property("exchangeStatus", codecForAny()) -    .property("permanent", codecForBoolean()) +    .property("exchangeEntryStatus", codecForAny()) +    .property("exchangeUpdateStatus", codecForAny())      .property("ageRestrictionOptions", codecForList(codecForNumber()))      .build("ExchangeListItem"); @@ -2651,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-cli/src/index.ts b/packages/taler-wallet-cli/src/index.ts index 9d840e5bb..36e7f7768 100644 --- a/packages/taler-wallet-cli/src/index.ts +++ b/packages/taler-wallet-cli/src/index.ts @@ -257,8 +257,7 @@ async function createLocalWallet(      },      cryptoWorkerType: walletCliArgs.wallet.cryptoWorker as any,      config: { -      features: { -      }, +      features: {},        testing: {          devModeActive: checkEnvFlag("TALER_WALLET_DEV_MODE"),          denomselAllowLate: checkEnvFlag( @@ -651,9 +650,12 @@ walletCli            });            break;          case TalerUriAction.Reward: { -          const res = await wallet.client.call(WalletApiOperation.PrepareReward, { -            talerRewardUri: uri, -          }); +          const res = await wallet.client.call( +            WalletApiOperation.PrepareReward, +            { +              talerRewardUri: uri, +            }, +          );            console.log("tip status", res);            await wallet.client.call(WalletApiOperation.AcceptReward, {              walletRewardId: res.walletRewardId, @@ -874,95 +876,32 @@ const backupCli = walletCli.subcommand("backupArgs", "backup", {    help: "Subcommands for backups",  }); -backupCli -  .subcommand("setDeviceId", "set-device-id") -  .requiredArgument("deviceId", clk.STRING, { -    help: "new device ID", -  }) -  .action(async (args) => { -    await withWallet(args, async (wallet) => { -      await wallet.client.call(WalletApiOperation.SetWalletDeviceId, { -        walletDeviceId: args.setDeviceId.deviceId, -      }); -    }); -  }); - -backupCli.subcommand("exportPlain", "export-plain").action(async (args) => { +backupCli.subcommand("exportDb", "export-db").action(async (args) => {    await withWallet(args, async (wallet) => { -    const backup = await wallet.client.call( -      WalletApiOperation.ExportBackupPlain, -      {}, -    ); +    const backup = await wallet.client.call(WalletApiOperation.ExportDb, {});      console.log(JSON.stringify(backup, undefined, 2));    });  }); -backupCli.subcommand("recoverySave", "save-recovery").action(async (args) => { +backupCli.subcommand("storeBackup", "store-backup").action(async (args) => {    await withWallet(args, async (wallet) => { -    const recoveryJson = await wallet.client.call( -      WalletApiOperation.ExportBackupRecovery, +    const resp = await wallet.client.call( +      WalletApiOperation.CreateStoredBackup,        {},      ); -    console.log(JSON.stringify(recoveryJson, undefined, 2)); +    console.log(JSON.stringify(resp, undefined, 2));    });  }); -backupCli.subcommand("run", "run").action(async (args) => { +backupCli.subcommand("importDb", "import-db").action(async (args) => {    await withWallet(args, async (wallet) => { -    await wallet.client.call(WalletApiOperation.RunBackupCycle, {}); -  }); -}); - -backupCli.subcommand("status", "status").action(async (args) => { -  await withWallet(args, async (wallet) => { -    const status = await wallet.client.call( -      WalletApiOperation.GetBackupInfo, -      {}, -    ); -    console.log(JSON.stringify(status, undefined, 2)); -  }); -}); - -backupCli -  .subcommand("recoveryLoad", "load-recovery") -  .maybeOption("strategy", ["--strategy"], clk.STRING, { -    help: "Strategy for resolving a conflict with the existing wallet key ('theirs' or 'ours')", -  }) -  .action(async (args) => { -    await withWallet(args, async (wallet) => { -      const data = JSON.parse(await read(process.stdin)); -      let strategy: RecoveryMergeStrategy | undefined; -      const stratStr = args.recoveryLoad.strategy; -      if (stratStr) { -        if (stratStr === "theirs") { -          strategy = RecoveryMergeStrategy.Theirs; -        } else if (stratStr === "ours") { -          strategy = RecoveryMergeStrategy.Theirs; -        } else { -          throw Error("invalid recovery strategy"); -        } -      } -      await wallet.client.call(WalletApiOperation.ImportBackupRecovery, { -        recovery: data, -        strategy, -      }); -    }); -  }); - -backupCli -  .subcommand("addProvider", "add-provider") -  .requiredArgument("url", clk.STRING) -  .maybeArgument("name", clk.STRING) -  .flag("activate", ["--activate"]) -  .action(async (args) => { -    await withWallet(args, async (wallet) => { -      await wallet.client.call(WalletApiOperation.AddBackupProvider, { -        backupProviderBaseUrl: args.addProvider.url, -        activate: args.addProvider.activate, -        name: args.addProvider.name || args.addProvider.url, -      }); +    const dumpRaw = await read(process.stdin); +    const dump = JSON.parse(dumpRaw); +    await wallet.client.call(WalletApiOperation.ImportDb, { +      dump,      });    }); +});  const depositCli = walletCli.subcommand("depositArgs", "deposit", {    help: "Subcommands for depositing money to payto:// accounts", @@ -1681,6 +1620,3 @@ async function read(stream: NodeJS.ReadStream) {  export function main() {    walletCli.run();  } -function classifyTalerUri(uri: string) { -  throw new Error("Function not implemented."); -} diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts index 0fad66d92..b52a503bc 100644 --- a/packages/taler-wallet-core/src/db.ts +++ b/packages/taler-wallet-core/src/db.ts @@ -23,6 +23,7 @@ import {    IDBFactory,    IDBObjectStore,    IDBTransaction, +  structuredEncapsulate,  } from "@gnu-taler/idb-bridge";  import {    AgeCommitmentProof, @@ -103,7 +104,7 @@ import { RetryInfo, TaskIdentifiers } from "./operations/common.js";   * for all previous versions must be written, which should be   * avoided.   */ -export const TALER_DB_NAME = "taler-wallet-main-v9"; +export const TALER_WALLET_MAIN_DB_NAME = "taler-wallet-main-v9";  /**   * Name of the metadata database.  This database is used @@ -111,7 +112,12 @@ export const TALER_DB_NAME = "taler-wallet-main-v9";   *   * (Minor migrations are handled via upgrade transactions.)   */ -export const TALER_META_DB_NAME = "taler-wallet-meta"; +export const TALER_WALLET_META_DB_NAME = "taler-wallet-meta"; + +/** + * Stored backups, mainly created when manually importing a backup. + */ +export const TALER_WALLET_STORED_BACKUPS_DB_NAME = "taler-wallet-stored-backups";  export const CURRENT_DB_CONFIG_KEY = "currentMainDbName"; @@ -566,10 +572,31 @@ export interface ExchangeDetailsPointer {    updateClock: TalerPreciseTimestamp;  } +export enum ExchangeEntryDbRecordStatus { +  Preset = 1, +  Ephemeral = 2, +  Used = 3, +} + +export enum ExchangeEntryDbUpdateStatus { +  Initial = 1, +  InitialUpdate = 2, +  Suspended = 3, +  Failed = 4, +  OutdatedUpdate = 5, +  Ready = 6, +  ReadyUpdate = 7, +} + +/** + * Timestamp stored as a IEEE 754 double, in milliseconds. + */ +export type DbIndexableTimestampMs = number; +  /**   * Exchange record as stored in the wallet's database.   */ -export interface ExchangeRecord { +export interface ExchangeEntryRecord {    /**     * Base url of the exchange.     */ @@ -594,13 +621,12 @@ export interface ExchangeRecord {     */    detailsPointer: ExchangeDetailsPointer | undefined; -  /** -   * Is this a permanent or temporary exchange record? -   */ -  permanent: boolean; +  entryStatus: ExchangeEntryDbRecordStatus; + +  updateStatus: ExchangeEntryDbUpdateStatus;    /** -   * Last time when the exchange was updated (both /keys and /wire). +   * Last time when the exchange /keys info was updated.     */    lastUpdate: TalerPreciseTimestamp | undefined; @@ -608,20 +634,21 @@ export interface ExchangeRecord {     * Next scheduled update for the exchange.     *     * (This field must always be present, so we can index on the timestamp.) +   * +   * FIXME: To index on the timestamp, this needs to be a number of +   * binary timestamp!     */ -  nextUpdate: TalerPreciseTimestamp; +  nextUpdateStampMs: DbIndexableTimestampMs;    lastKeysEtag: string | undefined; -  lastWireEtag: string | undefined; -    /**     * Next time that we should check if coins need to be refreshed.     *     * Updated whenever the exchange's denominations are updated or when     * the refresh check has been done.     */ -  nextRefreshCheck: TalerPreciseTimestamp; +  nextRefreshCheckStampMs: DbIndexableTimestampMs;    /**     * Public key of the reserve that we're currently using for @@ -2431,7 +2458,7 @@ export const WalletStoresV1 = {    ),    exchanges: describeStore(      "exchanges", -    describeContents<ExchangeRecord>({ +    describeContents<ExchangeEntryRecord>({        keyPath: "baseUrl",      }),      {}, @@ -2725,11 +2752,10 @@ export type WalletDbReadOnlyTransaction<    Stores extends StoreNames<typeof WalletStoresV1> & string,  > = DbReadOnlyTransaction<typeof WalletStoresV1, Stores>; -export type WalletReadWriteTransaction< +export type WalletDbReadWriteTransaction<    Stores extends StoreNames<typeof WalletStoresV1> & string,  > = DbReadWriteTransaction<typeof WalletStoresV1, Stores>; -  /**   * An applied migration.   */ @@ -2760,45 +2786,144 @@ export const walletMetadataStore = {    ),  }; -export function exportDb(db: IDBDatabase): Promise<any> { -  const dump = { -    name: db.name, -    stores: {} as { [s: string]: any }, -    version: db.version, +export interface StoredBackupMeta { +  name: string; +} + +export const StoredBackupStores = { +  backupMeta: describeStore( +    "backupMeta", +    describeContents<StoredBackupMeta>({ keyPath: "name" }), +    {}, +  ), +  backupData: describeStore("backupData", describeContents<any>({}), {}), +}; + +export interface DbDumpRecord { +  /** +   * Key, serialized with structuredEncapsulated. +   * +   * Only present for out-of-line keys (i.e. no key path). +   */ +  key?: any; +  /** +   * Value, serialized with structuredEncapsulated. +   */ +  value: any; +} + +export interface DbIndexDump { +  keyPath: string | string[]; +  multiEntry: boolean; +  unique: boolean; +} + +export interface DbStoreDump { +  keyPath?: string | string[]; +  autoIncrement: boolean; +  indexes: { [indexName: string]: DbIndexDump }; +  records: DbDumpRecord[]; +} + +export interface DbDumpDatabase { +  version: number; +  stores: { [storeName: string]: DbStoreDump }; +} + +export interface DbDump { +  databases: { +    [name: string]: DbDumpDatabase; +  }; +} + +export async function exportSingleDb( +  idb: IDBFactory, +  dbName: string, +): Promise<DbDumpDatabase> { +  const myDb = await openDatabase( +    idb, +    dbName, +    undefined, +    () => { +      // May not happen, since we're not requesting a specific version +      throw Error("unexpected version change"); +    }, +    () => { +      logger.info("unexpected onupgradeneeded"); +    }, +  ); + +  const singleDbDump: DbDumpDatabase = { +    version: myDb.version, +    stores: {},    };    return new Promise((resolve, reject) => { -    const tx = db.transaction(Array.from(db.objectStoreNames)); +    const tx = myDb.transaction(Array.from(myDb.objectStoreNames));      tx.addEventListener("complete", () => { -      resolve(dump); +      myDb.close(); +      resolve(singleDbDump);      });      // tslint:disable-next-line:prefer-for-of -    for (let i = 0; i < db.objectStoreNames.length; i++) { -      const name = db.objectStoreNames[i]; -      const storeDump = {} as { [s: string]: any }; -      dump.stores[name] = storeDump; -      tx.objectStore(name) -        .openCursor() -        .addEventListener("success", (e: Event) => { -          const cursor = (e.target as any).result; -          if (cursor) { -            storeDump[cursor.key] = cursor.value; -            cursor.continue(); +    for (let i = 0; i < myDb.objectStoreNames.length; i++) { +      const name = myDb.objectStoreNames[i]; +      const store = tx.objectStore(name); +      const storeDump: DbStoreDump = { +        autoIncrement: store.autoIncrement, +        keyPath: store.keyPath, +        indexes: {}, +        records: [], +      }; +      const indexNames = store.indexNames; +      for (let j = 0; j < indexNames.length; j++) { +        const idxName = indexNames[j]; +        const index = store.index(idxName); +        storeDump.indexes[idxName] = { +          keyPath: index.keyPath, +          multiEntry: index.multiEntry, +          unique: index.unique, +        }; +      } +      singleDbDump.stores[name] = storeDump; +      store.openCursor().addEventListener("success", (e: Event) => { +        const cursor = (e.target as any).result; +        if (cursor) { +          const rec: DbDumpRecord = { +            value: structuredEncapsulate(cursor.value), +          }; +          // Only store key if necessary, i.e. when +          // the key is not stored as part of the object via +          // a key path. +          if (store.keyPath == null) { +            rec.key = structuredEncapsulate(cursor.key);            } -        }); +          cursor.continue(); +        } +      });      }    });  } -export interface DatabaseDump { -  name: string; -  stores: { [s: string]: any }; -  version: string; +export async function exportDb(idb: IDBFactory): Promise<DbDump> { +  const dbDump: DbDump = { +    databases: {}, +  }; + +  dbDump.databases[TALER_WALLET_META_DB_NAME] = await exportSingleDb( +    idb, +    TALER_WALLET_META_DB_NAME, +  ); +  dbDump.databases[TALER_WALLET_MAIN_DB_NAME] = await exportSingleDb( +    idb, +    TALER_WALLET_MAIN_DB_NAME, +  ); + +  return dbDump;  }  async function recoverFromDump(    db: IDBDatabase, -  dump: DatabaseDump, +  dbDump: DbDumpDatabase,  ): Promise<void> {    return new Promise((resolve, reject) => {      const tx = db.transaction(Array.from(db.objectStoreNames), "readwrite"); @@ -2807,67 +2932,33 @@ async function recoverFromDump(      });      for (let i = 0; i < db.objectStoreNames.length; i++) {        const name = db.objectStoreNames[i]; -      const storeDump = dump.stores[name]; +      const storeDump = dbDump.stores[name];        if (!storeDump) continue; -      Object.keys(storeDump).forEach(async (key) => { -        const value = storeDump[key]; -        if (!value) return; -        tx.objectStore(name).put(value); -      }); +      for (let rec of storeDump.records) { +        tx.objectStore(name).put(rec.value, rec.key); +      }      }      tx.commit();    });  } -export async function importDb(db: IDBDatabase, object: any): Promise<void> { -  if ("name" in object && "stores" in object && "version" in object) { -    // looks like a database dump -    const dump = object as DatabaseDump; -    return recoverFromDump(db, dump); -  } - -  if ("databases" in object && "$types" in object) { -    // looks like a IDBDatabase -    const someDatabase = object.databases; - -    if (TALER_META_DB_NAME in someDatabase) { -      //looks like a taler database -      const currentMainDbValue = -        someDatabase[TALER_META_DB_NAME].objectStores.metaConfig.records[0] -          .value.value; - -      if (currentMainDbValue !== TALER_DB_NAME) { -        console.log("not the current database version"); -      } - -      const talerDb = someDatabase[currentMainDbValue]; - -      const objectStoreNames = Object.keys(talerDb.objectStores); - -      const dump: DatabaseDump = { -        name: talerDb.schema.databaseName, -        version: talerDb.schema.databaseVersion, -        stores: {}, -      }; - -      for (let i = 0; i < objectStoreNames.length; i++) { -        const name = objectStoreNames[i]; -        const storeDump = {} as { [s: string]: any }; -        dump.stores[name] = storeDump; -        talerDb.objectStores[name].records.map((r: any) => { -          const pkey = r.primaryKey; -          const key = -            typeof pkey === "string" || typeof pkey === "number" -              ? pkey -              : pkey.join(","); -          storeDump[key] = r.value; -        }); -      } +function checkDbDump(x: any): x is DbDump { +  return "databases" in x; +} -      return recoverFromDump(db, dump); +export async function importDb(db: IDBDatabase, dumpJson: any): Promise<void> { +  const d = dumpJson; +  if (checkDbDump(d)) { +    const walletDb = d.databases[TALER_WALLET_MAIN_DB_NAME]; +    if (!walletDb) { +      throw Error( +        `unable to import, main wallet database (${TALER_WALLET_MAIN_DB_NAME}) not found`, +      );      } +    await recoverFromDump(db, walletDb); +  } else { +    throw Error("unable to import, doesn't look like a valid DB dump");    } -  throw Error("could not import database");  }  export interface FixupDescription { @@ -3151,6 +3242,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_STORED_BACKUPS_DB_NAME, +    1, +    () => {}, +    onStoredBackupsDbUpgradeNeeded, +  ); + +  const handle = new DbAccess(backupsDbHandle, StoredBackupStores); +  return handle; +} +  /**   * Return a promise that resolves   * to the taler wallet db. @@ -3164,7 +3285,7 @@ export async function openTalerDatabase(  ): Promise<DbAccess<typeof WalletStoresV1>> {    const metaDbHandle = await openDatabase(      idbFactory, -    TALER_META_DB_NAME, +    TALER_WALLET_META_DB_NAME,      1,      () => {},      onMetaDbUpgradeNeeded, @@ -3177,17 +3298,17 @@ export async function openTalerDatabase(      .runReadWrite(async (tx) => {        const dbVersionRecord = await tx.metaConfig.get(CURRENT_DB_CONFIG_KEY);        if (!dbVersionRecord) { -        currentMainVersion = TALER_DB_NAME; +        currentMainVersion = TALER_WALLET_MAIN_DB_NAME;          await tx.metaConfig.put({            key: CURRENT_DB_CONFIG_KEY, -          value: TALER_DB_NAME, +          value: TALER_WALLET_MAIN_DB_NAME,          });        } else {          currentMainVersion = dbVersionRecord.value;        }      }); -  if (currentMainVersion !== TALER_DB_NAME) { +  if (currentMainVersion !== TALER_WALLET_MAIN_DB_NAME) {      switch (currentMainVersion) {        case "taler-wallet-main-v2":        case "taler-wallet-main-v3": @@ -3203,7 +3324,7 @@ export async function openTalerDatabase(            .runReadWrite(async (tx) => {              await tx.metaConfig.put({                key: CURRENT_DB_CONFIG_KEY, -              value: TALER_DB_NAME, +              value: TALER_WALLET_MAIN_DB_NAME,              });            });          break; @@ -3216,7 +3337,7 @@ export async function openTalerDatabase(    const mainDbHandle = await openDatabase(      idbFactory, -    TALER_DB_NAME, +    TALER_WALLET_MAIN_DB_NAME,      WALLET_DB_MINOR_VERSION,      onVersionChange,      onTalerDbUpgradeNeeded, @@ -3233,7 +3354,7 @@ export async function deleteTalerDatabase(    idbFactory: IDBFactory,  ): Promise<void> {    return new Promise((resolve, reject) => { -    const req = idbFactory.deleteDatabase(TALER_DB_NAME); +    const req = idbFactory.deleteDatabase(TALER_WALLET_MAIN_DB_NAME);      req.onerror = () => reject(req.error);      req.onsuccess = () => resolve();    }); diff --git a/packages/taler-wallet-core/src/host-impl.node.ts b/packages/taler-wallet-core/src/host-impl.node.ts index 6a4f21d79..0b6539306 100644 --- a/packages/taler-wallet-core/src/host-impl.node.ts +++ b/packages/taler-wallet-core/src/host-impl.node.ts @@ -139,13 +139,6 @@ export async function createNativeWalletHost2(      });    } -  const myVersionChange = (): Promise<void> => { -    logger.error("version change requested, should not happen"); -    throw Error( -      "BUG: wallet DB version change event can't happen with memory IDB", -    ); -  }; -    let dbResp: MakeDbResult;    if (args.persistentStoragePath &&args.persistentStoragePath.endsWith(".json")) { @@ -160,8 +153,6 @@ export async function createNativeWalletHost2(    shimIndexedDB(dbResp.idbFactory); -  const myDb = await openTalerDatabase(myIdbFactory, myVersionChange); -    let workerFactory;    const cryptoWorkerType = args.cryptoWorkerType ?? "node-worker-thread";    if (cryptoWorkerType === "sync") { @@ -189,7 +180,7 @@ export async function createNativeWalletHost2(    const timer = new SetTimeoutTimerAPI();    const w = await Wallet.create( -    myDb, +    myIdbFactory,      myHttpLib,      timer,      workerFactory, diff --git a/packages/taler-wallet-core/src/host-impl.qtart.ts b/packages/taler-wallet-core/src/host-impl.qtart.ts index 720f5affb..81dbe0acd 100644 --- a/packages/taler-wallet-core/src/host-impl.qtart.ts +++ b/packages/taler-wallet-core/src/host-impl.qtart.ts @@ -110,7 +110,7 @@ async function makeSqliteDb(        return {          ...myBackend.accessStats,          primitiveStatements: numStmt, -      } +      };      },      idbFactory: myBridgeIdbFactory,    }; @@ -167,12 +167,15 @@ export async function createNativeWalletHost2(    let dbResp: MakeDbResult; -  if (args.persistentStoragePath && args.persistentStoragePath.endsWith(".json")) { +  if ( +    args.persistentStoragePath && +    args.persistentStoragePath.endsWith(".json") +  ) {      logger.info("using JSON file DB backend (slow!)");      dbResp = await makeFileDb(args);    } else {      logger.info("using sqlite3 DB backend (experimental!)"); -    dbResp = await makeSqliteDb(args) +    dbResp = await makeSqliteDb(args);    }    const myIdbFactory: IDBFactory = dbResp.idbFactory as any as IDBFactory; @@ -189,22 +192,13 @@ export async function createNativeWalletHost2(      });    } -  const myVersionChange = (): Promise<void> => { -    logger.error("version change requested, should not happen"); -    throw Error( -      "BUG: wallet DB version change event can't happen with memory IDB", -    ); -  }; - -  const myDb = await openTalerDatabase(myIdbFactory, myVersionChange); -    let workerFactory;    workerFactory = new SynchronousCryptoWorkerFactoryPlain();    const timer = new SetTimeoutTimerAPI();    const w = await Wallet.create( -    myDb, +    myIdbFactory,      myHttpLib,      timer,      workerFactory, diff --git a/packages/taler-wallet-core/src/internal-wallet-state.ts b/packages/taler-wallet-core/src/internal-wallet-state.ts index 742af89a8..a189c9cb3 100644 --- a/packages/taler-wallet-core/src/internal-wallet-state.ts +++ b/packages/taler-wallet-core/src/internal-wallet-state.ts @@ -42,7 +42,7 @@ import { HttpRequestLibrary } from "@gnu-taler/taler-util/http";  import { TalerCryptoInterface } from "./crypto/cryptoImplementation.js";  import {    ExchangeDetailsRecord, -  ExchangeRecord, +  ExchangeEntryRecord,    RefreshReasonDetails,    WalletStoresV1,  } from "./db.js"; @@ -54,6 +54,7 @@ import {  } from "./util/query.js";  import { TimerGroup } from "./util/timer.js";  import { WalletConfig } from "./wallet-api-types.js"; +import { IDBFactory } from "@gnu-taler/idb-bridge";  export const EXCHANGE_COINS_LOCK = "exchange-coins-lock";  export const EXCHANGE_RESERVES_LOCK = "exchange-reserves-lock"; @@ -108,7 +109,7 @@ export interface ExchangeOperations {    ): Promise<ExchangeDetailsRecord | undefined>;    getExchangeTrust(      ws: InternalWalletState, -    exchangeInfo: ExchangeRecord, +    exchangeInfo: ExchangeEntryRecord,    ): Promise<TrustInfo>;    updateExchangeFromUrl(      ws: InternalWalletState, @@ -118,7 +119,7 @@ export interface ExchangeOperations {        cancellationToken?: CancellationToken;      },    ): Promise<{ -    exchange: ExchangeRecord; +    exchange: ExchangeEntryRecord;      exchangeDetails: ExchangeDetailsRecord;    }>;  } @@ -203,6 +204,9 @@ export interface InternalWalletState {      denomPubHash: string,    ): Promise<DenominationInfo | undefined>; +  ensureWalletDbOpen(): Promise<void>; + +  idb: IDBFactory;    db: DbAccess<typeof WalletStoresV1>;    http: HttpRequestLibrary; 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 a53b624e8..000000000 --- a/packages/taler-wallet-core/src/operations/backup/import.ts +++ /dev/null @@ -1,875 +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, -          }, -          permanent: true, -          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/operations/common.ts b/packages/taler-wallet-core/src/operations/common.ts index 7a8b78b53..e96beb5b2 100644 --- a/packages/taler-wallet-core/src/operations/common.ts +++ b/packages/taler-wallet-core/src/operations/common.ts @@ -30,6 +30,7 @@ import {    ExchangeEntryStatus,    ExchangeListItem,    ExchangeTosStatus, +  ExchangeUpdateStatus,    getErrorDetailFromException,    j2s,    Logger, @@ -47,7 +48,7 @@ import {    WalletStoresV1,    CoinRecord,    ExchangeDetailsRecord, -  ExchangeRecord, +  ExchangeEntryRecord,    BackupProviderRecord,    DepositGroupRecord,    PeerPullPaymentIncomingRecord, @@ -59,6 +60,8 @@ import {    RefreshGroupRecord,    RewardRecord,    WithdrawalGroupRecord, +  ExchangeEntryDbUpdateStatus, +  ExchangeEntryDbRecordStatus,  } from "../db.js";  import { makeErrorDetail, TalerError } from "@gnu-taler/taler-util";  import { InternalWalletState } from "../internal-wallet-state.js"; @@ -529,16 +532,16 @@ export function getExchangeTosStatus(    exchangeDetails: ExchangeDetailsRecord,  ): ExchangeTosStatus {    if (!exchangeDetails.tosAccepted) { -    return ExchangeTosStatus.New; +    return ExchangeTosStatus.Proposed;    }    if (exchangeDetails.tosAccepted?.etag == exchangeDetails.tosCurrentEtag) {      return ExchangeTosStatus.Accepted;    } -  return ExchangeTosStatus.Changed; +  return ExchangeTosStatus.Proposed;  }  export function makeExchangeListItem( -  r: ExchangeRecord, +  r: ExchangeEntryRecord,    exchangeDetails: ExchangeDetailsRecord | undefined,    lastError: TalerErrorDetail | undefined,  ): ExchangeListItem { @@ -547,30 +550,57 @@ export function makeExchangeListItem(          error: lastError,        }      : undefined; -  if (!exchangeDetails) { -    return { -      exchangeBaseUrl: r.baseUrl, -      currency: undefined, -      tosStatus: ExchangeTosStatus.Unknown, -      paytoUris: [], -      exchangeStatus: ExchangeEntryStatus.Unknown, -      permanent: r.permanent, -      ageRestrictionOptions: [], -      lastUpdateErrorInfo, -    }; + +  let exchangeUpdateStatus: ExchangeUpdateStatus; +  switch (r.updateStatus) { +    case ExchangeEntryDbUpdateStatus.Failed: +      exchangeUpdateStatus = ExchangeUpdateStatus.Failed; +      break; +    case ExchangeEntryDbUpdateStatus.Initial: +      exchangeUpdateStatus = ExchangeUpdateStatus.Initial; +      break; +    case ExchangeEntryDbUpdateStatus.InitialUpdate: +      exchangeUpdateStatus = ExchangeUpdateStatus.InitialUpdate; +      break; +    case ExchangeEntryDbUpdateStatus.OutdatedUpdate: +      exchangeUpdateStatus = ExchangeUpdateStatus.OutdatedUpdate; +      break; +    case ExchangeEntryDbUpdateStatus.Ready: +      exchangeUpdateStatus = ExchangeUpdateStatus.Ready; +      break; +    case ExchangeEntryDbUpdateStatus.ReadyUpdate: +      exchangeUpdateStatus = ExchangeUpdateStatus.ReadyUpdate; +      break; +    case ExchangeEntryDbUpdateStatus.Suspended: +      exchangeUpdateStatus = ExchangeUpdateStatus.Suspended; +      break; +  } + +  let exchangeEntryStatus: ExchangeEntryStatus; +  switch (r.entryStatus) { +    case ExchangeEntryDbRecordStatus.Ephemeral: +      exchangeEntryStatus = ExchangeEntryStatus.Ephemeral; +      break; +    case ExchangeEntryDbRecordStatus.Preset: +      exchangeEntryStatus = ExchangeEntryStatus.Preset; +      break; +    case ExchangeEntryDbRecordStatus.Used: +      exchangeEntryStatus = ExchangeEntryStatus.Used; +      break;    } -  let exchangeStatus; -  exchangeStatus = ExchangeEntryStatus.Ok; +    return {      exchangeBaseUrl: r.baseUrl, -    currency: exchangeDetails.currency, -    tosStatus: getExchangeTosStatus(exchangeDetails), -    paytoUris: exchangeDetails.wireInfo.accounts.map((x) => x.payto_uri), -    exchangeStatus, -    permanent: r.permanent, -    ageRestrictionOptions: exchangeDetails.ageMask +    currency: exchangeDetails?.currency, +    exchangeUpdateStatus, +    exchangeEntryStatus, +    tosStatus: exchangeDetails +      ? getExchangeTosStatus(exchangeDetails) +      : ExchangeTosStatus.Pending, +    ageRestrictionOptions: exchangeDetails?.ageMask        ? AgeRestriction.getAgeGroupsFromMask(exchangeDetails.ageMask)        : [], +    paytoUris: exchangeDetails?.wireInfo.accounts.map((x) => x.payto_uri) ?? [],      lastUpdateErrorInfo,    };  } @@ -892,13 +922,13 @@ export namespace TaskIdentifiers {    export function forWithdrawal(wg: WithdrawalGroupRecord): TaskId {      return `${PendingTaskType.Withdraw}:${wg.withdrawalGroupId}` as TaskId;    } -  export function forExchangeUpdate(exch: ExchangeRecord): TaskId { +  export function forExchangeUpdate(exch: ExchangeEntryRecord): TaskId {      return `${PendingTaskType.ExchangeUpdate}:${exch.baseUrl}` as TaskId;    }    export function forExchangeUpdateFromUrl(exchBaseUrl: string): TaskId {      return `${PendingTaskType.ExchangeUpdate}:${exchBaseUrl}` as TaskId;    } -  export function forExchangeCheckRefresh(exch: ExchangeRecord): TaskId { +  export function forExchangeCheckRefresh(exch: ExchangeEntryRecord): TaskId {      return `${PendingTaskType.ExchangeCheckRefresh}:${exch.baseUrl}` as TaskId;    }    export function forTipPickup(tipRecord: RewardRecord): TaskId { diff --git a/packages/taler-wallet-core/src/operations/exchanges.ts b/packages/taler-wallet-core/src/operations/exchanges.ts index c6b46e360..311a71a6e 100644 --- a/packages/taler-wallet-core/src/operations/exchanges.ts +++ b/packages/taler-wallet-core/src/operations/exchanges.ts @@ -32,6 +32,7 @@ import {    encodeCrock,    ExchangeAuditor,    ExchangeDenomination, +  ExchangeEntryStatus,    ExchangeGlobalFees,    ExchangeSignKeyJson,    ExchangeWireJson, @@ -66,10 +67,15 @@ import {    DenominationRecord,    DenominationVerificationStatus,    ExchangeDetailsRecord, -  ExchangeRecord, +  ExchangeEntryRecord,    WalletStoresV1,  } from "../db.js"; -import { isWithdrawableDenom } from "../index.js"; +import { +  ExchangeEntryDbRecordStatus, +  ExchangeEntryDbUpdateStatus, +  isWithdrawableDenom, +  WalletDbReadWriteTransaction, +} from "../index.js";  import { InternalWalletState, TrustInfo } from "../internal-wallet-state.js";  import { checkDbInvariant } from "../util/invariants.js";  import { @@ -326,6 +332,26 @@ export async function downloadExchangeInfo(    };  } +export async function addPresetExchangeEntry( +  tx: WalletDbReadWriteTransaction<"exchanges">, +  exchangeBaseUrl: string, +): Promise<void> { +  let exchange = await tx.exchanges.get(exchangeBaseUrl); +  if (!exchange) { +    const r: ExchangeEntryRecord = { +      entryStatus: ExchangeEntryDbRecordStatus.Preset, +      updateStatus: ExchangeEntryDbUpdateStatus.Initial, +      baseUrl: exchangeBaseUrl, +      detailsPointer: undefined, +      lastUpdate: undefined, +      lastKeysEtag: undefined, +      nextRefreshCheckStampMs: AbsoluteTime.getStampMsNever(), +      nextUpdateStampMs: AbsoluteTime.getStampMsNever(), +    }; +    await tx.exchanges.put(r); +  } +} +  export async function provideExchangeRecordInTx(    ws: InternalWalletState,    tx: GetReadWriteAccess<{ @@ -335,20 +361,20 @@ export async function provideExchangeRecordInTx(    baseUrl: string,    now: AbsoluteTime,  ): Promise<{ -  exchange: ExchangeRecord; +  exchange: ExchangeEntryRecord;    exchangeDetails: ExchangeDetailsRecord | undefined;  }> {    let exchange = await tx.exchanges.get(baseUrl);    if (!exchange) { -    const r: ExchangeRecord = { -      permanent: true, +    const r: ExchangeEntryRecord = { +      entryStatus: ExchangeEntryDbRecordStatus.Ephemeral, +      updateStatus: ExchangeEntryDbUpdateStatus.InitialUpdate,        baseUrl: baseUrl,        detailsPointer: undefined,        lastUpdate: undefined, -      nextUpdate: AbsoluteTime.toPreciseTimestamp(now), -      nextRefreshCheck: AbsoluteTime.toPreciseTimestamp(now), +      nextUpdateStampMs: AbsoluteTime.getStampMsNever(), +      nextRefreshCheckStampMs: AbsoluteTime.getStampMsNever(),        lastKeysEtag: undefined, -      lastWireEtag: undefined,      };      await tx.exchanges.put(r);      exchange = r; @@ -534,6 +560,10 @@ export async function downloadTosFromAcceptedFormat(    );  } +/** + * FIXME: Split this into two parts: (a) triggering the exchange + * to be updated and (b) waiting for the update to finish. + */  export async function updateExchangeFromUrl(    ws: InternalWalletState,    baseUrl: string, @@ -543,7 +573,7 @@ export async function updateExchangeFromUrl(      cancellationToken?: CancellationToken;    } = {},  ): Promise<{ -  exchange: ExchangeRecord; +  exchange: ExchangeEntryRecord;    exchangeDetails: ExchangeDetailsRecord;  }> {    const canonUrl = canonicalizeBaseUrl(baseUrl); @@ -613,7 +643,7 @@ export async function updateExchangeFromUrlHandler(      !forceNow &&      exchangeDetails !== undefined &&      !AbsoluteTime.isExpired( -      AbsoluteTime.fromPreciseTimestamp(exchange.nextUpdate), +      AbsoluteTime.fromStampMs(exchange.nextUpdateStampMs),      )    ) {      logger.trace("using existing exchange info"); @@ -755,11 +785,11 @@ export async function updateExchangeFromUrlHandler(          newDetails.rowId = existingDetails.rowId;        }        r.lastUpdate = TalerPreciseTimestamp.now(); -      r.nextUpdate = AbsoluteTime.toPreciseTimestamp( +      r.nextUpdateStampMs = AbsoluteTime.toStampMs(          AbsoluteTime.fromProtocolTimestamp(keysInfo.expiry),        );        // New denominations might be available. -      r.nextRefreshCheck = TalerPreciseTimestamp.now(); +      r.nextRefreshCheckStampMs = AbsoluteTime.getStampMsNow();        if (detailsPointerChanged) {          r.detailsPointer = {            currency: newDetails.currency, @@ -948,7 +978,7 @@ export async function getExchangePaytoUri(   */  export async function getExchangeTrust(    ws: InternalWalletState, -  exchangeInfo: ExchangeRecord, +  exchangeInfo: ExchangeEntryRecord,  ): Promise<TrustInfo> {    let isTrusted = false;    let isAudited = false; diff --git a/packages/taler-wallet-core/src/operations/pending.ts b/packages/taler-wallet-core/src/operations/pending.ts index 6c6546f83..e37e45c16 100644 --- a/packages/taler-wallet-core/src/operations/pending.ts +++ b/packages/taler-wallet-core/src/operations/pending.ts @@ -45,6 +45,7 @@ import {    PeerPushPaymentIncomingRecord,    RefundGroupRecord,    RefundGroupStatus, +  ExchangeEntryDbUpdateStatus,  } from "../db.js";  import {    PendingOperationsResponse, @@ -81,19 +82,25 @@ async function gatherExchangePending(    ws: InternalWalletState,    tx: GetReadOnlyAccess<{      exchanges: typeof WalletStoresV1.exchanges; -    exchangeDetails: typeof WalletStoresV1.exchangeDetails;      operationRetries: typeof WalletStoresV1.operationRetries;    }>,    now: AbsoluteTime,    resp: PendingOperationsResponse,  ): Promise<void> { -  // FIXME: We should do a range query here based on the update time. +  // FIXME: We should do a range query here based on the update time +  // and/or the entry state.    await tx.exchanges.iter().forEachAsync(async (exch) => { +    switch (exch.updateStatus) { +      case ExchangeEntryDbUpdateStatus.Initial: +      case ExchangeEntryDbUpdateStatus.Suspended: +      case ExchangeEntryDbUpdateStatus.Failed: +        return; +    }      const opTag = TaskIdentifiers.forExchangeUpdate(exch);      let opr = await tx.operationRetries.get(opTag);      const timestampDue =        opr?.retryInfo.nextRetry ?? -      AbsoluteTime.fromPreciseTimestamp(exch.nextUpdate); +      AbsoluteTime.fromStampMs(exch.nextUpdateStampMs);      resp.pendingOperations.push({        type: PendingTaskType.ExchangeUpdate,        ...getPendingCommon(ws, opTag, timestampDue), @@ -108,7 +115,7 @@ async function gatherExchangePending(        resp.pendingOperations.push({          type: PendingTaskType.ExchangeCheckRefresh,          ...getPendingCommon(ws, opTag, timestampDue), -        timestampDue: AbsoluteTime.fromPreciseTimestamp(exch.nextRefreshCheck), +        timestampDue: AbsoluteTime.fromStampMs(exch.nextRefreshCheckStampMs),          givesLifeness: false,          exchangeBaseUrl: exch.baseUrl,        }); @@ -184,8 +191,9 @@ export async function iterRecordsForWithdrawal(        WithdrawalGroupStatus.PendingRegisteringBank,        WithdrawalGroupStatus.PendingAml,      ); -    withdrawalGroupRecords = -      await tx.withdrawalGroups.indexes.byStatus.getAll(range); +    withdrawalGroupRecords = await tx.withdrawalGroups.indexes.byStatus.getAll( +      range, +    );    } else {      withdrawalGroupRecords =        await tx.withdrawalGroups.indexes.byStatus.getAll(); @@ -344,12 +352,8 @@ export async function iterRecordsForRefund(    f: (r: RefundGroupRecord) => Promise<void>,  ): Promise<void> {    if (filter.onlyState === "nonfinal") { -    const keyRange = GlobalIDB.KeyRange.only( -      RefundGroupStatus.Pending -    ); -    await tx.refundGroups.indexes.byStatus -      .iter(keyRange) -      .forEachAsync(f); +    const keyRange = GlobalIDB.KeyRange.only(RefundGroupStatus.Pending); +    await tx.refundGroups.indexes.byStatus.iter(keyRange).forEachAsync(f);    } else {      await tx.refundGroups.iter().forEachAsync(f);    } diff --git a/packages/taler-wallet-core/src/operations/refresh.ts b/packages/taler-wallet-core/src/operations/refresh.ts index 72d1a2725..fb356f0fc 100644 --- a/packages/taler-wallet-core/src/operations/refresh.ts +++ b/packages/taler-wallet-core/src/operations/refresh.ts @@ -1190,14 +1190,14 @@ export async function autoRefresh(            `created refresh group for auto-refresh (${res.refreshGroupId})`,          );        } -//      logger.trace( -//        `current wallet time: ${AbsoluteTime.toIsoString(AbsoluteTime.now())}`, -//      ); +      //      logger.trace( +      //        `current wallet time: ${AbsoluteTime.toIsoString(AbsoluteTime.now())}`, +      //      );        logger.trace(          `next refresh check at ${AbsoluteTime.toIsoString(minCheckThreshold)}`,        ); -      exchange.nextRefreshCheck = -        AbsoluteTime.toPreciseTimestamp(minCheckThreshold); +      exchange.nextRefreshCheckStampMs = +        AbsoluteTime.toStampMs(minCheckThreshold);        await tx.exchanges.put(exchange);      });    return TaskRunResult.finished(); diff --git a/packages/taler-wallet-core/src/operations/reward.ts b/packages/taler-wallet-core/src/operations/reward.ts index 69c888d7a..6f9d3ce85 100644 --- a/packages/taler-wallet-core/src/operations/reward.ts +++ b/packages/taler-wallet-core/src/operations/reward.ts @@ -150,14 +150,14 @@ export async function prepareTip(      .mktx((x) => [x.rewards])      .runReadOnly(async (tx) => {        return tx.rewards.indexes.byMerchantTipIdAndBaseUrl.get([ -        res.merchantTipId, +        res.merchantRewardId,          res.merchantBaseUrl,        ]);      });    if (!tipRecord) {      const tipStatusUrl = new URL( -      `tips/${res.merchantTipId}`, +      `rewards/${res.merchantRewardId}`,        res.merchantBaseUrl,      );      logger.trace("checking tip status from", tipStatusUrl.href); @@ -204,7 +204,7 @@ export async function prepareTip(        next_url: tipPickupStatus.next_url,        merchantBaseUrl: res.merchantBaseUrl,        createdTimestamp: TalerPreciseTimestamp.now(), -      merchantRewardId: res.merchantTipId, +      merchantRewardId: res.merchantRewardId,        rewardAmountEffective: Amounts.stringify(selectedDenoms.totalCoinValue),        denomsSel: selectedDenoms,        pickedUpTimestamp: undefined, diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts b/packages/taler-wallet-core/src/operations/withdraw.ts index 44817b389..a3d95fb5c 100644 --- a/packages/taler-wallet-core/src/operations/withdraw.ts +++ b/packages/taler-wallet-core/src/operations/withdraw.ts @@ -132,6 +132,8 @@ import {  } from "../util/coinSelection.js";  import {    ExchangeDetailsRecord, +  ExchangeEntryDbRecordStatus, +  ExchangeEntryDbUpdateStatus,    PendingTaskType,    isWithdrawableDenom,  } from "../index.js"; @@ -2346,10 +2348,6 @@ export async function internalPerformCreateWithdrawalGroup(    }>,    prep: PrepareCreateWithdrawalGroupResult,  ): Promise<PerformCreateWithdrawalGroupResult> { -  const transactionId = constructTransactionIdentifier({ -    tag: TransactionType.Withdrawal, -    withdrawalGroupId: prep.withdrawalGroup.withdrawalGroupId, -  });    const { withdrawalGroup } = prep;    if (!prep.creationInfo) {      return { withdrawalGroup, transitionInfo: undefined }; @@ -2366,6 +2364,7 @@ export async function internalPerformCreateWithdrawalGroup(    const exchange = await tx.exchanges.get(withdrawalGroup.exchangeBaseUrl);    if (exchange) {      exchange.lastWithdrawal = TalerPreciseTimestamp.now(); +    exchange.entryStatus = ExchangeEntryDbRecordStatus.Used;      await tx.exchanges.put(exchange);    } diff --git a/packages/taler-wallet-core/src/util/query.ts b/packages/taler-wallet-core/src/util/query.ts index 71f80f8aa..1c3ff6a2a 100644 --- a/packages/taler-wallet-core/src/util/query.ts +++ b/packages/taler-wallet-core/src/util/query.ts @@ -239,7 +239,7 @@ class ResultStream<T> {  export function openDatabase(    idbFactory: IDBFactory,    databaseName: string, -  databaseVersion: number, +  databaseVersion: number | undefined,    onVersionChange: () => void,    onUpgradeNeeded: (      db: IDBDatabase, @@ -257,7 +257,7 @@ export function openDatabase(      req.onsuccess = (e) => {        req.result.onversionchange = (evt: IDBVersionChangeEvent) => {          logger.info( -          `handling live db version change from ${evt.oldVersion} to ${evt.newVersion}`, +          `handling versionchange on ${databaseName} from ${evt.oldVersion} to ${evt.newVersion}`,          );          req.result.close();          onVersionChange(); @@ -274,6 +274,9 @@ export function openDatabase(        if (!transaction) {          throw Error("no transaction handle available in upgrade handler");        } +      logger.info( +        `handling upgradeneeded event on ${databaseName} from ${e.oldVersion} to ${e.newVersion}`, +      );        onUpgradeNeeded(db, e.oldVersion, newVersion, transaction);      };    }); @@ -376,8 +379,8 @@ export interface InsertResponse {  export interface StoreReadWriteAccessor<RecordType, IndexMap> {    get(key: IDBValidKey): Promise<RecordType | undefined>;    iter(query?: IDBValidKey): ResultStream<RecordType>; -  put(r: RecordType): Promise<InsertResponse>; -  add(r: RecordType): Promise<InsertResponse>; +  put(r: RecordType, key?: IDBValidKey): Promise<InsertResponse>; +  add(r: RecordType, key?: IDBValidKey): Promise<InsertResponse>;    delete(key: IDBValidKey): Promise<void>;    indexes: GetIndexReadWriteAccess<RecordType, IndexMap>;  } @@ -652,15 +655,15 @@ function makeWriteContext(          const req = tx.objectStore(storeName).openCursor(query);          return new ResultStream<any>(req);        }, -      async add(r) { -        const req = tx.objectStore(storeName).add(r); +      async add(r, k) { +        const req = tx.objectStore(storeName).add(r, k);          const key = await requestToPromise(req);          return {            key: key,          };        }, -      async put(r) { -        const req = tx.objectStore(storeName).put(r); +      async put(r, k) { +        const req = tx.objectStore(storeName).put(r, k);          const key = await requestToPromise(req);          return {            key: key, 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 194894e52..626409dd6 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,8 @@ import {    clearDatabase,    exportDb,    importDb, +  openStoredBackupsDatabase, +  openTalerDatabase,  } from "./db.js";  import { DevExperimentHttpLib, applyDevExperiment } from "./dev-experiments.js";  import { @@ -157,7 +160,6 @@ import {    getUserAttentionsUnreadCount,    markAttentionRequestAsRead,  } from "./operations/attention.js"; -import { exportBackup } from "./operations/backup/export.js";  import {    addBackupProvider,    codecForAddBackupProviderRequest, @@ -165,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, @@ -189,6 +190,7 @@ import {  } from "./operations/deposits.js";  import {    acceptExchangeTermsOfService, +  addPresetExchangeEntry,    downloadTosFromAcceptedFormat,    getExchangeDetails,    getExchangeRequestTimeout, @@ -314,6 +316,7 @@ import {    getMaxPeerPushAmount,    convertWithdrawalAmount,  } from "./util/instructedAmountConversion.js"; +import { IDBFactory } from "@gnu-taler/idb-bridge";  const logger = new Logger("wallet.ts"); @@ -340,9 +343,8 @@ async function callOperationHandler(        return await processRecoupGroup(ws, pending.recoupGroupId);      case PendingTaskType.ExchangeCheckRefresh:        return await autoRefresh(ws, pending.exchangeBaseUrl); -    case PendingTaskType.Deposit: { +    case PendingTaskType.Deposit:        return await processDepositGroup(ws, pending.depositGroupId); -    }      case PendingTaskType.Backup:        return await processBackupForProvider(ws, pending.backupProviderBaseUrl);      case PendingTaskType.PeerPushDebit: @@ -533,6 +535,7 @@ async function fillDefaults(ws: InternalWalletState): Promise<void> {          await tx.auditorTrust.put(c);        }        for (const baseUrl of ws.config.builtin.exchanges) { +        await addPresetExchangeEntry(tx, baseUrl);          const now = AbsoluteTime.now();          provideExchangeRecordInTx(ws, tx, baseUrl, now);        } @@ -1021,6 +1024,23 @@ 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()}`; +  await backupsDb.mktxAll().runReadWrite(async (tx) => { +    await tx.backupMeta.add({ +      name, +    }); +    await tx.backupData.add(backup, name); +  }); +  return { +    name, +  }; +} +  /**   * Implementation of the "wallet-core" API.   */ @@ -1037,6 +1057,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; @@ -1378,9 +1406,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); @@ -1531,13 +1556,11 @@ 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: { -      const dbDump = await exportDb(ws.db.idbHandle()); +      const dbDump = await exportDb(ws.idb);        return dbDump;      }      case WalletApiOperation.ImportDb: { @@ -1616,7 +1639,7 @@ export function getVersion(ws: InternalWalletState): WalletCoreVersion {  /**   * Handle a request to the wallet-core API.   */ -export async function handleCoreApiRequest( +async function handleCoreApiRequest(    ws: InternalWalletState,    operation: string,    id: string, @@ -1652,14 +1675,14 @@ export class Wallet {    private _client: WalletCoreApiClient | undefined;    private constructor( -    db: DbAccess<typeof WalletStoresV1>, +    idb: IDBFactory,      http: HttpRequestLibrary,      timer: TimerAPI,      cryptoWorkerFactory: CryptoWorkerFactory,      config?: WalletConfigParameter,    ) {      this.ws = new InternalWalletStateImpl( -      db, +      idb,        http,        timer,        cryptoWorkerFactory, @@ -1675,21 +1698,20 @@ export class Wallet {    }    static async create( -    db: DbAccess<typeof WalletStoresV1>, +    idb: IDBFactory,      http: HttpRequestLibrary,      timer: TimerAPI,      cryptoWorkerFactory: CryptoWorkerFactory,      config?: WalletConfigParameter,    ): Promise<Wallet> { -    const w = new Wallet(db, http, timer, cryptoWorkerFactory, config); +    const w = new Wallet(idb, http, timer, cryptoWorkerFactory, config);      w._client = await getClientFromWalletState(w.ws);      return w;    }    public static defaultConfig: Readonly<WalletConfig> = {      builtin: { -      //exchanges: ["https://exchange.demo.taler.net/"], -      exchanges: [], +      exchanges: ["https://exchange.demo.taler.net/"],        auditors: [          {            currency: "KUDOS", @@ -1724,19 +1746,22 @@ export class Wallet {      this.ws.stop();    } -  runPending(): Promise<void> { +  async runPending(): Promise<void> { +    await this.ws.ensureWalletDbOpen();      return runPending(this.ws);    } -  runTaskLoop(opts?: RetryLoopOpts): Promise<TaskLoopResult> { +  async runTaskLoop(opts?: RetryLoopOpts): Promise<TaskLoopResult> { +    await this.ws.ensureWalletDbOpen();      return runTaskLoop(this.ws, opts);    } -  handleCoreApiRequest( +  async handleCoreApiRequest(      operation: string,      id: string,      payload: unknown,    ): Promise<CoreApiResponse> { +    await this.ws.ensureWalletDbOpen();      return handleCoreApiRequest(this.ws, operation, id, payload);    }  } @@ -1800,12 +1825,17 @@ class InternalWalletStateImpl implements InternalWalletState {    config: Readonly<WalletConfig>; +  private _db: DbAccess<typeof WalletStoresV1> | undefined = undefined; + +  get db(): DbAccess<typeof WalletStoresV1> { +    if (!this._db) { +      throw Error("db not initialized"); +    } +    return this._db; +  } +    constructor( -    // FIXME: Make this a getter and make -    // the actual value nullable. -    // Check if we are in a DB migration / garbage collection -    // and throw an error in that case. -    public db: DbAccess<typeof WalletStoresV1>, +    public idb: IDBFactory,      public http: HttpRequestLibrary,      public timer: TimerAPI,      cryptoWorkerFactory: CryptoWorkerFactory, @@ -1820,6 +1850,17 @@ class InternalWalletStateImpl implements InternalWalletState {      }    } +  async ensureWalletDbOpen(): Promise<void> { +    if (this._db) { +      return; +    } +    const myVersionChange = async (): Promise<void> => { +      logger.info("version change requested for Taler DB"); +    }; +    const myDb = await openTalerDatabase(this.idb, myVersionChange); +    this._db = myDb; +  } +    async getTransactionState(      ws: InternalWalletState,      tx: GetReadOnlyAccess<typeof WalletStoresV1>, diff --git a/packages/taler-wallet-webextension/src/components/TermsOfService/views.tsx b/packages/taler-wallet-webextension/src/components/TermsOfService/views.tsx index e52add756..214c4d792 100644 --- a/packages/taler-wallet-webextension/src/components/TermsOfService/views.tsx +++ b/packages/taler-wallet-webextension/src/components/TermsOfService/views.tsx @@ -69,28 +69,28 @@ export function ShowButtonsNonAcceptedTosView({    terms,  }: State.ShowButtonsNotAccepted): VNode {    const { i18n } = useTranslationContext(); -  const ableToReviewTermsOfService = -    showingTermsOfService.button.onClick !== undefined; +  // const ableToReviewTermsOfService = +  //   showingTermsOfService.button.onClick !== undefined; -  if (!ableToReviewTermsOfService) { -    return ( -      <Fragment> -        {terms.status === ExchangeTosStatus.NotFound && ( -          <section style={{ justifyContent: "space-around", display: "flex" }}> -            <WarningText> -              <i18n.Translate> -                Exchange doesn't have terms of service -              </i18n.Translate> -            </WarningText> -          </section> -        )} -      </Fragment> -    ); -  } +  // if (!ableToReviewTermsOfService) { +  //   return ( +  //     <Fragment> +  //       {terms.status === ExchangeTosStatus.Pending && ( +  //         <section style={{ justifyContent: "space-around", display: "flex" }}> +  //           <WarningText> +  //             <i18n.Translate> +  //               Exchange doesn't have terms of service +  //             </i18n.Translate> +  //           </WarningText> +  //         </section> +  //       )} +  //     </Fragment> +  //   ); +  // }    return (      <Fragment> -      {terms.status === ExchangeTosStatus.NotFound && ( +      {/* {terms.status === ExchangeTosStatus.NotFound && (          <section style={{ justifyContent: "space-around", display: "flex" }}>            <WarningText>              <i18n.Translate> @@ -98,8 +98,8 @@ export function ShowButtonsNonAcceptedTosView({              </i18n.Translate>            </WarningText>          </section> -      )} -      {terms.status === "new" && ( +      )} */} +      {terms.status === ExchangeTosStatus.Pending && (          <section style={{ justifyContent: "space-around", display: "flex" }}>            <Button              variant="contained" @@ -110,19 +110,6 @@ export function ShowButtonsNonAcceptedTosView({            </Button>          </section>        )} -      {terms.status === "changed" && ( -        <section style={{ justifyContent: "space-around", display: "flex" }}> -          <Button -            variant="contained" -            color="success" -            onClick={showingTermsOfService.button.onClick} -          > -            <i18n.Translate> -              Review new version of terms of service -            </i18n.Translate> -          </Button> -        </section> -      )}      </Fragment>    );  } @@ -138,7 +125,7 @@ export function ShowTosContentView({    return (      <Fragment> -      {terms.status !== ExchangeTosStatus.NotFound && !terms.content && ( +      {!terms.content && (          <section style={{ justifyContent: "space-around", display: "flex" }}>            <WarningBox>              <i18n.Translate> @@ -194,7 +181,7 @@ export function ShowTosContentView({            </LinkSuccess>          </section>        )} -      {termsAccepted && terms.status !== ExchangeTosStatus.NotFound && ( +      {termsAccepted && terms.status !== ExchangeTosStatus.Proposed && (          <section style={{ justifyContent: "space-around", display: "flex" }}>            <CheckboxOutlined              name="terms" diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw/state.ts b/packages/taler-wallet-webextension/src/cta/Withdraw/state.ts index 46a72ac87..72cabe5a4 100644 --- a/packages/taler-wallet-webextension/src/cta/Withdraw/state.ts +++ b/packages/taler-wallet-webextension/src/cta/Withdraw/state.ts @@ -141,8 +141,8 @@ export function useComponentStateFromParams({          confirm: {            onClick: isValid              ? pushAlertOnError(async () => { -                onAmountChanged(Amounts.stringify(amount)); -              }) +              onAmountChanged(Amounts.stringify(amount)); +            })              : undefined,          },          amount: { @@ -304,8 +304,8 @@ function exchangeSelectionState(      const [ageRestricted, setAgeRestricted] = useState(0);      const currentExchange = selectedExchange.selected;      const tosNeedToBeAccepted = -      currentExchange.tosStatus == ExchangeTosStatus.New || -      currentExchange.tosStatus == ExchangeTosStatus.Changed; +      currentExchange.tosStatus == ExchangeTosStatus.Pending || +      currentExchange.tosStatus == ExchangeTosStatus.Proposed;      /**       * With the exchange and amount, ask the wallet the information @@ -393,12 +393,12 @@ function exchangeSelectionState(      //TODO: calculate based on exchange info      const ageRestriction = ageRestrictionEnabled        ? { -          list: ageRestrictionOptions, -          value: String(ageRestricted), -          onChange: pushAlertOnError(async (v: string) => -            setAgeRestricted(parseInt(v, 10)), -          ), -        } +        list: ageRestrictionOptions, +        value: String(ageRestricted), +        onChange: pushAlertOnError(async (v: string) => +          setAgeRestricted(parseInt(v, 10)), +        ), +      }        : undefined;      return { diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw/test.ts b/packages/taler-wallet-webextension/src/cta/Withdraw/test.ts index c7af160e4..ab3b2e316 100644 --- a/packages/taler-wallet-webextension/src/cta/Withdraw/test.ts +++ b/packages/taler-wallet-webextension/src/cta/Withdraw/test.ts @@ -37,7 +37,7 @@ const exchanges: ExchangeListItem[] = [      exchangeBaseUrl: "http://exchange.demo.taler.net",      paytoUris: [],      tosStatus: ExchangeTosStatus.Accepted, -    exchangeStatus: ExchangeEntryStatus.Ok, +    exchangeStatus: ExchangeEntryStatus.Used,      permanent: true,      auditors: [        { @@ -202,7 +202,7 @@ describe("Withdraw CTA states", () => {      const exchangeWithNewTos = exchanges.map((e) => ({        ...e, -      tosStatus: ExchangeTosStatus.New, +      tosStatus: ExchangeTosStatus.Proposed,      }));      handler.addWalletCallResponse( diff --git a/packages/taler-wallet-webextension/src/wallet/DestinationSelection/test.ts b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/test.ts index 99b5ec176..7ef475805 100644 --- a/packages/taler-wallet-webextension/src/wallet/DestinationSelection/test.ts +++ b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/test.ts @@ -24,6 +24,7 @@ import {    ExchangeEntryStatus,    ExchangeListItem,    ExchangeTosStatus, +  ExchangeUpdateStatus,  } from "@gnu-taler/taler-util";  import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";  import { expect } from "chai"; @@ -36,9 +37,9 @@ const exchangeArs: ExchangeListItem = {    currency: "ARS",    exchangeBaseUrl: "http://",    tosStatus: ExchangeTosStatus.Accepted, -  exchangeStatus: ExchangeEntryStatus.Ok, +  exchangeEntryStatus: ExchangeEntryStatus.Used, +  exchangeUpdateStatus: ExchangeUpdateStatus.Initial,    paytoUris: [], -  permanent: true,    ageRestrictionOptions: [],  }; diff --git a/packages/taler-wallet-webextension/src/wallet/Settings.tsx b/packages/taler-wallet-webextension/src/wallet/Settings.tsx index 071d2a594..0aa46d615 100644 --- a/packages/taler-wallet-webextension/src/wallet/Settings.tsx +++ b/packages/taler-wallet-webextension/src/wallet/Settings.tsx @@ -163,20 +163,16 @@ export function SettingsView({                              <i18n.Translate>ok</i18n.Translate>                            </SuccessText>                          ); -                      case ExchangeTosStatus.Changed: +                      case ExchangeTosStatus.Pending:                          return (                            <WarningText> -                            <i18n.Translate>changed</i18n.Translate> +                            <i18n.Translate>pending</i18n.Translate>                            </WarningText>                          ); -                      case ExchangeTosStatus.New: -                      case ExchangeTosStatus.NotFound: +                      case ExchangeTosStatus.Proposed:                          return ( -                          <DestructiveText> -                            <i18n.Translate>not accepted</i18n.Translate> -                          </DestructiveText> +                          <i18n.Translate>proposed</i18n.Translate>                          ); -                      case ExchangeTosStatus.Unknown:                        default:                          return (                            <DestructiveText> diff --git a/packages/taler-wallet-webextension/src/wxBackend.ts b/packages/taler-wallet-webextension/src/wxBackend.ts index 95af1a3a4..e7385abe5 100644 --- a/packages/taler-wallet-webextension/src/wxBackend.ts +++ b/packages/taler-wallet-webextension/src/wxBackend.ts @@ -50,7 +50,6 @@ import {    exportDb,    importDb,    openPromise, -  openTalerDatabase,  } from "@gnu-taler/taler-wallet-core";  import {    MessageFromBackend, @@ -139,7 +138,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"); @@ -298,13 +297,6 @@ async function reinitWallet(): Promise<void> {    }    currentDatabase = undefined;    // setBadgeText({ text: "" }); -  try { -    currentDatabase = await openTalerDatabase(indexedDB as any, reinitWallet); -  } catch (e) { -    logger.error("could not open database", e); -    walletInit.reject(e); -    return; -  }    let httpLib;    let cryptoWorker;    let timer; @@ -325,7 +317,7 @@ async function reinitWallet(): Promise<void> {    const settings = await platform.getSettingsFromStorage();    logger.info("Setting up wallet");    const wallet = await Wallet.create( -    currentDatabase, +    indexedDB as any,      httpLib,      timer,      cryptoWorker, diff --git a/packages/web-util/src/hooks/useLocalStorage.ts b/packages/web-util/src/hooks/useLocalStorage.ts index fae5537a4..b460144a6 100644 --- a/packages/web-util/src/hooks/useLocalStorage.ts +++ b/packages/web-util/src/hooks/useLocalStorage.ts @@ -106,7 +106,7 @@ export function useLocalStorage<Type = string>(        const newValue = storage.get(key.id);        setStoredValue(convert(newValue));      }); -  }, []); +  }, [key.id]);    const setValue = (value?: Type): void => {      if (value === undefined) { diff --git a/packages/web-util/src/hooks/useMemoryStorage.ts b/packages/web-util/src/hooks/useMemoryStorage.ts index 1dd263797..ef186392f 100644 --- a/packages/web-util/src/hooks/useMemoryStorage.ts +++ b/packages/web-util/src/hooks/useMemoryStorage.ts @@ -51,7 +51,7 @@ export function useMemoryStorage<Type = string>(        const newValue = storage.get(key);        setStoredValue(newValue === undefined ? defaultValue : newValue);      }); -  }, []); +  }, [key]);    const setValue = (value?: Type): void => {      if (value === undefined) { | 
