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 */ -/** - * 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 - */ - -/** - * 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 ":" - */ -export type Tombstone = string; - -/** - * Detailed error report. - * - * For auditor-relevant reports with attached cryptographic proof, - * the error report also should contain the submission status to - * the auditor(s). - */ -interface BackupErrorReport { - // FIXME: specify! -} - -/** - * Trust declaration for an auditor. - * - * The trust applies based on the public key of - * the auditor, irrespective of what base URL the exchange - * is referencing. - */ -export interface BackupTrustAuditor { - /** - * Base URL of the auditor. - */ - auditor_base_url: string; - - /** - * Public key of the auditor. - */ - auditor_pub: string; - - /** - * UIDs for the operation of adding this auditor - * as a trusted auditor. - */ - uids: OperationUid; -} - -/** - * Trust declaration for an exchange. - * - * The trust only applies for the combination of base URL - * and public key. If the master public key changes while the base - * URL stays the same, the exchange has to be re-added by a wallet update - * or by the user. - */ -export interface BackupTrustExchange { - /** - * Canonicalized exchange base URL. - */ - exchange_base_url: string; - - /** - * Master public key of the exchange. - */ - exchange_master_pub: string; - - /** - * UIDs for the operation of adding this exchange - * as trusted. - */ - uids: OperationUid; -} - -export class BackupBackupProviderTerms { - /** - * Last known supported protocol version. - */ - supported_protocol_version: string; - - /** - * Last known annual fee. - */ - annual_fee: BackupAmountString; - - /** - * Last known storage limit. - */ - storage_limit_in_megabytes: number; -} - -/** - * Backup information about one backup storage provider. - */ -export class BackupBackupProvider { - /** - * Canonicalized base URL of the provider. - */ - base_url: string; - - /** - * Last known terms. Might be unavailable in some situations, such - * as directly after restoring form a backup recovery document. - */ - terms?: BackupBackupProviderTerms; - - /** - * Proposal IDs for payments to this provider. - */ - pay_proposal_ids: string[]; - - /** - * UIDs for adding this backup provider. - */ - uids: OperationUid[]; -} - -/** - * Status of recoup operations that were grouped together. - * - * The remaining amount of the corresponding coins must be set to - * zero when the recoup group is created/imported. - */ -export interface BackupRecoupGroup { - /** - * Unique identifier for the recoup group record. - */ - recoup_group_id: string; - - /** - * Timestamp when the recoup was started. - */ - timestamp_created: TalerPreciseTimestamp; - - timestamp_finish?: TalerPreciseTimestamp; - finish_clock?: TalerProtocolTimestamp; - // FIXME: Use some enum here! - finish_is_failure?: boolean; - - /** - * Information about each coin being recouped. - */ - coins: { - coin_pub: string; - recoup_finished: boolean; - }[]; -} - -/** - * Types of coin sources. - */ -export enum BackupCoinSourceType { - Withdraw = "withdraw", - Refresh = "refresh", - Reward = "reward", -} - -/** - * Metadata about a coin obtained via withdrawing. - */ -export interface BackupWithdrawCoinSource { - type: BackupCoinSourceType.Withdraw; - - /** - * Can be the empty string for orphaned coins. - */ - withdrawal_group_id: string; - - /** - * Index of the coin in the withdrawal session. - */ - coin_index: number; - - /** - * Reserve public key for the reserve we got this coin from. - */ - reserve_pub: string; -} - -/** - * Metadata about a coin obtained from refreshing. - * - * FIXME: Currently does not link to the refreshGroupId because - * the wallet DB doesn't do this. Not really necessary, - * but would be more consistent. - */ -export interface BackupRefreshCoinSource { - type: BackupCoinSourceType.Refresh; - - /** - * Public key of the coin that was refreshed into this coin. - */ - old_coin_pub: string; - - refresh_group_id: string; -} - -/** - * Metadata about a coin obtained from a tip. - */ -export interface BackupTipCoinSource { - type: BackupCoinSourceType.Reward; - - /** - * Wallet's identifier for the tip that this coin - * originates from. - */ - wallet_tip_id: string; - - /** - * Index in the tip planchets of the tip. - */ - coin_index: number; -} - -/** - * Metadata about a coin depending on the origin. - */ -export type BackupCoinSource = - | BackupWithdrawCoinSource - | BackupRefreshCoinSource - | BackupTipCoinSource; - -/** - * Backup information about a coin. - * - * (Always part of a BackupExchange/BackupDenom) - */ -export interface BackupCoin { - /** - * Where did the coin come from? Used for recouping coins. - */ - coin_source: BackupCoinSource; - - /** - * Private key to authorize operations on the coin. - */ - coin_priv: string; - - /** - * Unblinded signature by the exchange. - */ - denom_sig: UnblindedSignature; - - /** - * Information about where and how the coin was spent. - */ - spend_allocation: - | { - id: string; - amount: BackupAmountString; - } - | undefined; - - /** - * Blinding key used when withdrawing the coin. - * Potentionally used again during payback. - */ - blinding_key: string; - - /** - * Does the wallet think that the coin is still fresh? - * - * Note that even if a fresh coin is imported, it should still - * be refreshed in most situations. - */ - fresh: boolean; -} - -/** - * Status of a tip we got from a merchant. - */ -export interface BackupTip { - /** - * Tip ID chosen by the wallet. - */ - wallet_tip_id: string; - - /** - * The merchant's identifier for this tip. - */ - merchant_tip_id: string; - - /** - * Secret seed used for the tipping planchets. - */ - secret_seed: string; - - /** - * Has the user accepted the tip? Only after the tip has been accepted coins - * withdrawn from the tip may be used. - */ - timestamp_accepted: TalerPreciseTimestamp | undefined; - - /** - * When was the tip first scanned by the wallet? - */ - timestamp_created: TalerPreciseTimestamp; - - timestamp_finished?: TalerPreciseTimestamp; - finish_is_failure?: boolean; - - /** - * The tipped amount. - */ - tip_amount_raw: BackupAmountString; - - /** - * Timestamp, the tip can't be picked up anymore after this deadline. - */ - timestamp_expiration: TalerProtocolTimestamp; - - /** - * The exchange that will sign our coins, chosen by the merchant. - */ - exchange_base_url: string; - - /** - * Base URL of the merchant that is giving us the tip. - */ - merchant_base_url: string; - - /** - * Selected denominations. Determines the effective tip amount. - */ - selected_denoms: BackupDenomSel; - - /** - * The url to be redirected after the tip is accepted. - */ - next_url: string | undefined; - - /** - * UID for the denomination selection. - * Used to disambiguate when merging. - */ - selected_denoms_uid: OperationUid; -} - -/** - * Reasons for why a coin is being refreshed. - */ -export enum BackupRefreshReason { - Manual = "manual", - Pay = "pay", - Refund = "refund", - AbortPay = "abort-pay", - Recoup = "recoup", - BackupRestored = "backup-restored", - Scheduled = "scheduled", -} - -/** - * Information about one refresh session, always part - * of a refresh group. - * - * (Public key of the old coin is stored in the refresh group.) - */ -export interface BackupRefreshSession { - /** - * Hashed denominations of the newly requested coins. - */ - new_denoms: BackupDenomSel; - - /** - * Seed used to derive the planchets and - * transfer private keys for this refresh session. - */ - session_secret_seed: string; - - /** - * The no-reveal-index after we've done the melting. - */ - noreveal_index?: number; -} - -/** - * Refresh session for one coin inside a refresh group. - */ -export interface BackupRefreshOldCoin { - /** - * Public key of the old coin, - */ - coin_pub: string; - - /** - * Requested amount to refresh. Must be subtracted from the coin's remaining - * amount as soon as the coin is added to the refresh group. - */ - input_amount: BackupAmountString; - - /** - * Estimated output (may change if it takes a long time to create the - * actual session). - */ - estimated_output_amount: BackupAmountString; - - /** - * Did the refresh session finish (or was it unnecessary/impossible to create - * one) - */ - finished: boolean; - - /** - * Refresh session (if created) or undefined it not created yet. - */ - refresh_session: BackupRefreshSession | undefined; -} - -/** - * Information about one refresh group. - * - * May span more than one exchange, but typically doesn't - */ -export interface BackupRefreshGroup { - refresh_group_id: string; - - reason: BackupRefreshReason; - - /** - * Details per old coin. - */ - old_coins: BackupRefreshOldCoin[]; - - timestamp_created: TalerPreciseTimestamp; - - timestamp_finish?: TalerPreciseTimestamp; - finish_is_failure?: boolean; -} - -export enum BackupRefundState { - Failed = "failed", - Applied = "applied", - Pending = "pending", -} - -/** - * Common information about a refund. - */ -export interface BackupRefundItemCommon { - /** - * Execution time as claimed by the merchant - */ - execution_time: TalerProtocolTimestamp; - - /** - * Time when the wallet became aware of the refund. - */ - obtained_time: TalerProtocolTimestamp; - - /** - * Amount refunded for the coin. - */ - refund_amount: BackupAmountString; - - /** - * Coin being refunded. - */ - coin_pub: string; - - /** - * The refund transaction ID for the refund. - */ - rtransaction_id: number; - - /** - * Upper bound on the refresh cost incurred by - * applying this refund. - * - * Might be lower in practice when two refunds on the same - * coin are refreshed in the same refresh operation. - * - * Used to display fees, and stored since it's expensive to recompute - * accurately. - */ - total_refresh_cost_bound: BackupAmountString; -} - -/** - * Failed refund, either because the merchant did - * something wrong or it expired. - */ -export interface BackupRefundFailedItem extends BackupRefundItemCommon { - type: BackupRefundState.Failed; -} - -export interface BackupRefundPendingItem extends BackupRefundItemCommon { - type: BackupRefundState.Pending; -} - -export interface BackupRefundAppliedItem extends BackupRefundItemCommon { - type: BackupRefundState.Applied; -} - -/** - * State of one refund from the merchant, maintained by the wallet. - */ -export type BackupRefundItem = - | BackupRefundFailedItem - | BackupRefundPendingItem - | BackupRefundAppliedItem; - -/** - * Data we store when the payment was accepted. - */ -export interface BackupPayInfo { - pay_coins: { - /** - * Public keys of the coins that were selected. - */ - coin_pub: string; - - /** - * Amount that each coin contributes. - */ - contribution: BackupAmountString; - }[]; - - /** - * Unique ID to disambiguate pay coin selection on merge. - */ - pay_coins_uid: OperationUid; - - /** - * Total cost initially shown to the user. - * - * This includes the amount taken by the merchant, fees (wire/deposit) contributed - * by the customer, refreshing fees, fees for withdraw-after-refresh and "trimmings" - * of coins that are too small to spend. - * - * Note that in rare situations, this cost might not be accurate (e.g. - * when the payment or refresh gets re-denominated). - * We might show adjustments to this later, but currently we don't do so. - */ - total_pay_cost: BackupAmountString; -} - -export interface BackupPurchase { - /** - * Proposal ID for this purchase. Uniquely identifies the - * purchase and the proposal. - */ - proposal_id: string; - - /** - * Status of the proposal. - */ - proposal_status: BackupProposalStatus; - - /** - * Proposal that this one got "redirected" to as part of - * the repurchase detection. - */ - repurchase_proposal_id: string | undefined; - - /** - * Session ID we got when downloading the contract. - */ - download_session_id?: string; - - /** - * Merchant-assigned order ID of the proposal. - */ - order_id: string; - - /** - * Base URL of the merchant that proposed the purchase. - */ - merchant_base_url: string; - - /** - * Claim token initially given by the merchant. - */ - claim_token: string | undefined; - - /** - * Contract terms we got from the merchant. - */ - contract_terms_raw?: RawContractTerms; - - /** - * Signature on the contract terms. - * - * FIXME: Better name needed. - */ - merchant_sig?: string; - - /** - * Private key for the nonce. Might eventually be used - * to prove ownership of the contract. - */ - nonce_priv: string; - - pay_info: BackupPayInfo | undefined; - - /** - * Timestamp of the first time that sending a payment to the merchant - * for this purchase was successful. - */ - timestamp_first_successful_pay: TalerPreciseTimestamp | undefined; - - /** - * Signature by the merchant confirming the payment. - */ - merchant_pay_sig: string | undefined; - - /** - * Text to be shown to the point-of-sale staff as a proof of payment. - */ - pos_confirmation: string | undefined; - - timestamp_proposed: TalerPreciseTimestamp; - - /** - * When was the purchase made? - * Refers to the time that the user accepted. - */ - timestamp_accepted: TalerPreciseTimestamp | undefined; - - /** - * Pending refunds for the purchase. A refund is pending - * when the merchant reports a transient error from the exchange. - */ - refunds: BackupRefundItem[]; - - /** - * Continue querying the refund status until this deadline has expired. - */ - auto_refund_deadline: TalerProtocolTimestamp | undefined; - - shared: boolean; -} - -/** - * Info about one denomination in the backup. - * - * Note that the wallet only backs up validated denominations. - */ -export interface BackupDenomination { - /** - * Value of one coin of the denomination. - */ - value: BackupAmountString; - - /** - * The denomination public key. - */ - denom_pub: DenominationPubKey; - - /** - * Fee for withdrawing. - */ - fee_withdraw: BackupAmountString; - - /** - * Fee for depositing. - */ - fee_deposit: BackupAmountString; - - /** - * Fee for refreshing. - */ - fee_refresh: BackupAmountString; - - /** - * Fee for refunding. - */ - fee_refund: BackupAmountString; - - /** - * Validity start date of the denomination. - */ - stamp_start: TalerProtocolTimestamp; - - /** - * Date after which the currency can't be withdrawn anymore. - */ - stamp_expire_withdraw: TalerProtocolTimestamp; - - /** - * Date after the denomination officially doesn't exist anymore. - */ - stamp_expire_legal: TalerProtocolTimestamp; - - /** - * Data after which coins of this denomination can't be deposited anymore. - */ - stamp_expire_deposit: TalerProtocolTimestamp; - - /** - * Signature by the exchange's master key over the denomination - * information. - */ - master_sig: string; - - /** - * Was this denomination still offered by the exchange the last time - * we checked? - * Only false when the exchange redacts a previously published denomination. - */ - is_offered: boolean; - - /** - * Did the exchange revoke the denomination? - * When this field is set to true in the database, the same transaction - * should also mark all affected coins as revoked. - */ - is_revoked: boolean; - - /** - * Coins of this denomination. - */ - coins: BackupCoin[]; - - /** - * The list issue date of the exchange "/keys" response - * that this denomination was last seen in. - */ - list_issue_date: TalerProtocolTimestamp; -} - -/** - * Denomination selection. - */ -export type BackupDenomSel = { - denom_pub_hash: string; - count: number; -}[]; - -/** - * Wire fee for one wire payment target type as stored in the - * wallet's database. - * - * (Flattened to a list to make the declaration simpler). - */ -export interface BackupExchangeWireFee { - wire_type: string; - - /** - * Fee for wire transfers. - */ - wire_fee: string; - - /** - * Fees to close and refund a reserve. - */ - closing_fee: string; - - /** - * Start date of the fee. - */ - start_stamp: TalerProtocolTimestamp; - - /** - * End date of the fee. - */ - end_stamp: TalerProtocolTimestamp; - - /** - * Signature made by the exchange master key. - */ - sig: string; -} - -/** - * Global fee as stored in the wallet's database. - * - */ -export interface BackupExchangeGlobalFees { - startDate: TalerProtocolTimestamp; - endDate: TalerProtocolTimestamp; - - historyFee: BackupAmountString; - accountFee: BackupAmountString; - purseFee: BackupAmountString; - - historyTimeout: TalerProtocolDuration; - purseTimeout: TalerProtocolDuration; - - purseLimit: number; - - signature: string; -} -/** - * Structure of one exchange signing key in the /keys response. - */ -export class BackupExchangeSignKey { - stamp_start: TalerProtocolTimestamp; - stamp_expire: TalerProtocolTimestamp; - stamp_end: TalerProtocolTimestamp; - key: string; - master_sig: string; -} - -/** - * Signature by the auditor that a particular denomination key is audited. - */ -export class BackupAuditorDenomSig { - /** - * Denomination public key's hash. - */ - denom_pub_h: string; - - /** - * The signature. - */ - auditor_sig: string; -} - -/** - * Auditor information as given by the exchange in /keys. - */ -export class BackupExchangeAuditor { - /** - * Auditor's public key. - */ - auditor_pub: string; - - /** - * Base URL of the auditor. - */ - auditor_url: string; - - /** - * List of signatures for denominations by the auditor. - */ - denomination_keys: BackupAuditorDenomSig[]; -} - -/** - * Backup information for an exchange. Serves effectively - * as a pointer to the exchange details identified by - * the base URL, master public key and currency. - */ -export interface BackupExchange { - base_url: string; - - master_public_key: string; - - currency: string; - - /** - * Time when the pointer to the exchange details - * was last updated. - * - * Used to facilitate automatic merging. - */ - update_clock: TalerPreciseTimestamp; -} - -/** - * Backup information about an exchange's details. - * - * Note that one base URL can have multiple exchange - * details. The BackupExchange stores a pointer - * to the current exchange details. - */ -export interface BackupExchangeDetails { - /** - * Canonicalized base url of the exchange. - */ - base_url: string; - - /** - * Master public key of the exchange. - */ - master_public_key: string; - - /** - * Auditors (partially) auditing the exchange. - */ - auditors: BackupExchangeAuditor[]; - - /** - * Currency that the exchange offers. - */ - currency: string; - - /** - * Denominations offered by the exchange. - */ - denominations: BackupDenomination[]; - - /** - * Last observed protocol version. - */ - protocol_version: string; - - /** - * Closing delay of reserves. - */ - reserve_closing_delay: TalerProtocolDuration; - - /** - * Signing keys we got from the exchange, can also contain - * older signing keys that are not returned by /keys anymore. - */ - signing_keys: BackupExchangeSignKey[]; - - wire_fees: BackupExchangeWireFee[]; - - global_fees: BackupExchangeGlobalFees[]; - - /** - * Bank accounts offered by the exchange; - */ - accounts: { - payto_uri: string; - master_sig: string; - }[]; - - /** - * ETag for last terms of service download. - */ - tos_accepted_etag: string | undefined; - - /** - * Timestamp when the ToS has been accepted. - */ - tos_accepted_timestamp: TalerPreciseTimestamp | undefined; -} - -export enum BackupProposalStatus { - /** - * Proposed (and either downloaded or not, - * depending on whether contract terms are present), - * but the user needs to accept/reject it. - */ - Proposed = "proposed", - /** - * Proposed, other wallet may also have - * the purchase - */ - Shared = "shared", - /** - * The user has rejected the proposal. - */ - Refused = "refused", - /** - * Downloading or processing the proposal has failed permanently. - * - * FIXME: Should this be modeled as a "misbehavior report" instead? - */ - PermanentlyFailed = "permanently-failed", - /** - * Downloaded proposal was detected as a re-purchase. - */ - Repurchase = "repurchase", - - Paid = "paid", -} - export interface BackupRecovery { walletRootPriv: string; providers: { diff --git a/packages/taler-util/src/wallet-types.ts b/packages/taler-util/src/wallet-types.ts index 01c1838d5..accab746f 100644 --- a/packages/taler-util/src/wallet-types.ts +++ b/packages/taler-util/src/wallet-types.ts @@ -2655,3 +2655,21 @@ export interface TransactionRecordFilter { onlyState?: TransactionStateFilter; onlyCurrency?: string; } + +export interface StoredBackupList { + storedBackups: { + name: string; + }[]; +} + +export interface CreateStoredBackupResponse { + name: string; +} + +export interface RecoverStoredBackupRequest { + name: string; +} + +export interface DeleteStoredBackupRequest { + name: string; +} diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts index e68385267..1255e8c71 100644 --- a/packages/taler-wallet-core/src/db.ts +++ b/packages/taler-wallet-core/src/db.ts @@ -2769,6 +2769,24 @@ export const walletMetadataStore = { ), }; +export interface StoredBackupMeta { + name: string; +} + +export interface StoredBackupData { + name: string; + data: any; +} + +export const StoredBackupStores = { + backupMeta: describeStore( + "backupMeta", + describeContents({ keyPath: "name" }), + {}, + ), + backupData: describeStore("backupData", describeContents({}), {}), +}; + export interface DbDumpRecord { /** * Key, serialized with structuredEncapsulated. @@ -2831,6 +2849,7 @@ export async function exportSingleDb( return new Promise((resolve, reject) => { const tx = myDb.transaction(Array.from(myDb.objectStoreNames)); tx.addEventListener("complete", () => { + myDb.close(); resolve(singleDbDump); }); // tslint:disable-next-line:prefer-for-of @@ -3211,6 +3230,36 @@ function onMetaDbUpgradeNeeded( ); } +function onStoredBackupsDbUpgradeNeeded( + db: IDBDatabase, + oldVersion: number, + newVersion: number, + upgradeTransaction: IDBTransaction, +) { + upgradeFromStoreMap( + StoredBackupStores, + db, + oldVersion, + newVersion, + upgradeTransaction, + ); +} + +export async function openStoredBackupsDatabase( + idbFactory: IDBFactory, +): Promise> { + const backupsDbHandle = await openDatabase( + idbFactory, + TALER_WALLET_META_DB_NAME, + 1, + () => {}, + onStoredBackupsDbUpgradeNeeded, + ); + + const handle = new DbAccess(backupsDbHandle, StoredBackupStores); + return handle; +} + /** * Return a promise that resolves * to the taler wallet db. diff --git a/packages/taler-wallet-core/src/operations/backup/export.ts b/packages/taler-wallet-core/src/operations/backup/export.ts deleted file mode 100644 index c9446a05f..000000000 --- a/packages/taler-wallet-core/src/operations/backup/export.ts +++ /dev/null @@ -1,586 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2020 Taler Systems SA - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see - */ - -/** - * Implementation of wallet backups (export/import/upload) and sync - * server management. - * - * @author Florian Dold - */ - -/** - * 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 { - 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(); - - await tx.purchases.iter().forEachAsync(async (purch) => { - const refunds: BackupRefundItem[] = []; - purchaseProposalIdSet.add(purch.proposalId); - // for (const refundKey of Object.keys(purch.refunds)) { - // const ri = purch.refunds[refundKey]; - // const common = { - // coin_pub: ri.coinPub, - // execution_time: ri.executionTime, - // obtained_time: ri.obtainedTime, - // refund_amount: Amounts.stringify(ri.refundAmount), - // rtransaction_id: ri.rtransactionId, - // total_refresh_cost_bound: Amounts.stringify( - // ri.totalRefreshCostBound, - // ), - // }; - // switch (ri.type) { - // case RefundState.Applied: - // refunds.push({ type: BackupRefundState.Applied, ...common }); - // break; - // case RefundState.Failed: - // refunds.push({ type: BackupRefundState.Failed, ...common }); - // break; - // case RefundState.Pending: - // refunds.push({ type: BackupRefundState.Pending, ...common }); - // break; - // } - // } - - let propStatus: BackupProposalStatus; - switch (purch.purchaseStatus) { - case PurchaseStatus.Done: - case PurchaseStatus.PendingQueryingAutoRefund: - case PurchaseStatus.PendingQueryingRefund: - propStatus = BackupProposalStatus.Paid; - break; - case PurchaseStatus.PendingPayingReplay: - case PurchaseStatus.PendingDownloadingProposal: - case PurchaseStatus.DialogProposed: - case PurchaseStatus.PendingPaying: - propStatus = BackupProposalStatus.Proposed; - break; - case PurchaseStatus.DialogShared: - propStatus = BackupProposalStatus.Shared; - break; - case PurchaseStatus.FailedClaim: - case PurchaseStatus.AbortedIncompletePayment: - propStatus = BackupProposalStatus.PermanentlyFailed; - break; - case PurchaseStatus.AbortingWithRefund: - case PurchaseStatus.AbortedProposalRefused: - propStatus = BackupProposalStatus.Refused; - break; - case PurchaseStatus.RepurchaseDetected: - propStatus = BackupProposalStatus.Repurchase; - break; - default: { - const error = purch.purchaseStatus; - throw Error(`purchase status ${error} is not handled`); - } - } - - const payInfo = purch.payInfo; - let backupPayInfo: BackupPayInfo | undefined = undefined; - if (payInfo) { - backupPayInfo = { - pay_coins: payInfo.payCoinSelection.coinPubs.map((x, i) => ({ - coin_pub: x, - contribution: Amounts.stringify( - payInfo.payCoinSelection.coinContributions[i], - ), - })), - total_pay_cost: Amounts.stringify(payInfo.totalPayCost), - pay_coins_uid: payInfo.payCoinSelectionUid, - }; - } - - let contractTermsRaw = undefined; - if (purch.download) { - const contractTermsRecord = await tx.contractTerms.get( - purch.download.contractTermsHash, - ); - if (contractTermsRecord) { - contractTermsRaw = contractTermsRecord.contractTermsRaw; - } - } - - backupPurchases.push({ - contract_terms_raw: contractTermsRaw, - auto_refund_deadline: purch.autoRefundDeadline, - merchant_pay_sig: purch.merchantPaySig, - pos_confirmation: purch.posConfirmation, - pay_info: backupPayInfo, - proposal_id: purch.proposalId, - refunds, - timestamp_accepted: purch.timestampAccept, - timestamp_first_successful_pay: purch.timestampFirstSuccessfulPay, - nonce_priv: purch.noncePriv, - merchant_sig: purch.download?.contractTermsMerchantSig, - claim_token: purch.claimToken, - merchant_base_url: purch.merchantBaseUrl, - order_id: purch.orderId, - proposal_status: propStatus, - repurchase_proposal_id: purch.repurchaseProposalId, - download_session_id: purch.downloadSessionId, - timestamp_proposed: purch.timestamp, - shared: purch.shared, - }); - }); - - await tx.refreshGroups.iter().forEach((rg) => { - const oldCoins: BackupRefreshOldCoin[] = []; - - for (let i = 0; i < rg.oldCoinPubs.length; i++) { - let refreshSession: BackupRefreshSession | undefined; - const s = rg.refreshSessionPerCoin[i]; - if (s) { - refreshSession = { - new_denoms: s.newDenoms.map((x) => ({ - count: x.count, - denom_pub_hash: x.denomPubHash, - })), - session_secret_seed: s.sessionSecretSeed, - noreveal_index: s.norevealIndex, - }; - } - oldCoins.push({ - coin_pub: rg.oldCoinPubs[i], - estimated_output_amount: Amounts.stringify( - rg.estimatedOutputPerCoin[i], - ), - finished: rg.statusPerCoin[i] === RefreshCoinStatus.Finished, - input_amount: Amounts.stringify(rg.inputPerCoin[i]), - refresh_session: refreshSession, - }); - } - - backupRefreshGroups.push({ - reason: rg.reason as any, - refresh_group_id: rg.refreshGroupId, - timestamp_created: rg.timestampCreated, - timestamp_finish: rg.timestampFinished, - old_coins: oldCoins, - }); - }); - - const ts = TalerPreciseTimestamp.now(); - - if (!bs.lastBackupTimestamp) { - bs.lastBackupTimestamp = ts; - } - - const backupBlob: WalletBackupContentV1 = { - schema_id: "gnu-taler-wallet-backup-content", - schema_version: BACKUP_VERSION_MAJOR, - minor_version: BACKUP_VERSION_MINOR, - exchanges: backupExchanges, - exchange_details: backupExchangeDetails, - wallet_root_pub: bs.walletRootPub, - backup_providers: backupBackupProviders, - current_device_id: bs.deviceId, - purchases: backupPurchases, - recoup_groups: backupRecoupGroups, - refresh_groups: backupRefreshGroups, - tips: backupTips, - timestamp: bs.lastBackupTimestamp, - trusted_auditors: {}, - trusted_exchanges: {}, - intern_table: {}, - error_reports: [], - tombstones: [], - // FIXME! - withdrawal_groups: backupWithdrawalGroups, - }; - - // If the backup changed, we change our nonce and timestamp. - - let h = encodeCrock(hash(stringToBytes(canonicalJson(backupBlob)))); - if (h !== bs.lastBackupPlainHash) { - logger.trace( - `plain backup hash changed (from ${bs.lastBackupPlainHash}to ${h})`, - ); - bs.lastBackupTimestamp = ts; - backupBlob.timestamp = ts; - bs.lastBackupPlainHash = encodeCrock( - hash(stringToBytes(canonicalJson(backupBlob))), - ); - bs.lastBackupNonce = encodeCrock(getRandomBytes(32)); - logger.trace( - `setting timestamp to ${AbsoluteTime.toIsoString( - AbsoluteTime.fromPreciseTimestamp(ts), - )} and nonce to ${bs.lastBackupNonce}`, - ); - await tx.config.put({ - key: ConfigRecordKey.WalletBackupState, - value: bs, - }); - } else { - logger.trace("backup hash did not change"); - } - - return backupBlob; - }); -} diff --git a/packages/taler-wallet-core/src/operations/backup/import.ts b/packages/taler-wallet-core/src/operations/backup/import.ts deleted file mode 100644 index 836c65643..000000000 --- a/packages/taler-wallet-core/src/operations/backup/import.ts +++ /dev/null @@ -1,874 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2020 Taler Systems SA - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see - */ - -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 { - 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 = 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 { - 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; - coinPrivToCompletedCoin: Record; - proposalNoncePrivToPub: { [priv: string]: string }; - proposalIdToContractTermsHash: { [proposalId: string]: string }; - reservePrivToPub: Record; -} - -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 { - 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 { - await provideBackupState(ws); - - logger.info(`importing backup ${j2s(backupBlobArg)}`); - - return ws.db - .mktx((x) => [ - x.config, - x.exchangeDetails, - x.exchanges, - x.coins, - x.coinAvailability, - x.denominations, - x.purchases, - x.refreshGroups, - x.backupProviders, - x.rewards, - x.recoupGroups, - x.withdrawalGroups, - x.tombstones, - x.depositGroups, - ]) - .runReadWrite(async (tx) => { - // FIXME: validate schema! - const backupBlob = backupBlobArg as WalletBackupContentV1; - - // FIXME: validate version - - for (const tombstone of backupBlob.tombstones) { - await tx.tombstones.put({ - id: tombstone, - }); - } - - const tombstoneSet = new Set( - (await tx.tombstones.iter().toArray()).map((x) => x.id), - ); - - // FIXME: Validate that the "details pointer" is correct - - for (const backupExchange of backupBlob.exchanges) { - const existingExchange = await tx.exchanges.get( - backupExchange.base_url, - ); - if (existingExchange) { - continue; - } - // await tx.exchanges.put({ - // baseUrl: backupExchange.base_url, - // detailsPointer: { - // currency: backupExchange.currency, - // masterPublicKey: backupExchange.master_public_key, - // updateClock: backupExchange.update_clock, - // }, - // lastUpdate: undefined, - // nextUpdate: TalerPreciseTimestamp.now(), - // nextRefreshCheck: TalerPreciseTimestamp.now(), - // lastKeysEtag: undefined, - // lastWireEtag: undefined, - // }); - } - - for (const backupExchangeDetails of backupBlob.exchange_details) { - const existingExchangeDetails = - await tx.exchangeDetails.indexes.byPointer.get([ - backupExchangeDetails.base_url, - backupExchangeDetails.currency, - backupExchangeDetails.master_public_key, - ]); - - if (!existingExchangeDetails) { - const wireInfo: WireInfo = { - accounts: backupExchangeDetails.accounts.map((x) => ({ - master_sig: x.master_sig, - payto_uri: x.payto_uri, - })), - feesForType: {}, - }; - for (const fee of backupExchangeDetails.wire_fees) { - const w = (wireInfo.feesForType[fee.wire_type] ??= []); - w.push({ - closingFee: Amounts.stringify(fee.closing_fee), - endStamp: fee.end_stamp, - sig: fee.sig, - startStamp: fee.start_stamp, - wireFee: Amounts.stringify(fee.wire_fee), - }); - } - let tosAccepted = undefined; - if ( - backupExchangeDetails.tos_accepted_etag && - backupExchangeDetails.tos_accepted_timestamp - ) { - tosAccepted = { - etag: backupExchangeDetails.tos_accepted_etag, - timestamp: backupExchangeDetails.tos_accepted_timestamp, - }; - } - await tx.exchangeDetails.put({ - exchangeBaseUrl: backupExchangeDetails.base_url, - wireInfo, - currency: backupExchangeDetails.currency, - auditors: backupExchangeDetails.auditors.map((x) => ({ - auditor_pub: x.auditor_pub, - auditor_url: x.auditor_url, - denomination_keys: x.denomination_keys, - })), - masterPublicKey: backupExchangeDetails.master_public_key, - protocolVersionRange: backupExchangeDetails.protocol_version, - reserveClosingDelay: backupExchangeDetails.reserve_closing_delay, - tosCurrentEtag: backupExchangeDetails.tos_accepted_etag || "", - tosAccepted, - globalFees: backupExchangeDetails.global_fees.map((x) => ({ - accountFee: Amounts.stringify(x.accountFee), - historyFee: Amounts.stringify(x.historyFee), - purseFee: Amounts.stringify(x.purseFee), - endDate: x.endDate, - historyTimeout: x.historyTimeout, - signature: x.signature, - purseLimit: x.purseLimit, - purseTimeout: x.purseTimeout, - startDate: x.startDate, - })), - }); - } - - for (const backupDenomination of backupExchangeDetails.denominations) { - if (backupDenomination.denom_pub.cipher !== DenomKeyType.Rsa) { - throw Error("unsupported cipher"); - } - const denomPubHash = - cryptoComp.rsaDenomPubToHash[ - backupDenomination.denom_pub.rsa_public_key - ]; - checkLogicInvariant(!!denomPubHash); - const existingDenom = await tx.denominations.get([ - backupExchangeDetails.base_url, - denomPubHash, - ]); - if (!existingDenom) { - const value = Amounts.parseOrThrow(backupDenomination.value); - - await tx.denominations.put({ - denomPub: backupDenomination.denom_pub, - denomPubHash: denomPubHash, - exchangeBaseUrl: backupExchangeDetails.base_url, - exchangeMasterPub: backupExchangeDetails.master_public_key, - fees: { - feeDeposit: Amounts.stringify(backupDenomination.fee_deposit), - feeRefresh: Amounts.stringify(backupDenomination.fee_refresh), - feeRefund: Amounts.stringify(backupDenomination.fee_refund), - feeWithdraw: Amounts.stringify(backupDenomination.fee_withdraw), - }, - isOffered: backupDenomination.is_offered, - isRevoked: backupDenomination.is_revoked, - masterSig: backupDenomination.master_sig, - stampExpireDeposit: backupDenomination.stamp_expire_deposit, - stampExpireLegal: backupDenomination.stamp_expire_legal, - stampExpireWithdraw: backupDenomination.stamp_expire_withdraw, - stampStart: backupDenomination.stamp_start, - verificationStatus: DenominationVerificationStatus.VerifiedGood, - currency: value.currency, - amountFrac: value.fraction, - amountVal: value.value, - listIssueDate: backupDenomination.list_issue_date, - }); - } - for (const backupCoin of backupDenomination.coins) { - await importCoin(ws, tx, cryptoComp, { - backupCoin, - denomPubHash, - exchangeBaseUrl: backupExchangeDetails.base_url, - }); - } - } - } - - for (const backupWg of backupBlob.withdrawal_groups) { - const reservePub = cryptoComp.reservePrivToPub[backupWg.reserve_priv]; - checkLogicInvariant(!!reservePub); - const ts = constructTombstone({ - tag: TombstoneTag.DeleteReserve, - reservePub, - }); - if (tombstoneSet.has(ts)) { - continue; - } - const existingWg = await tx.withdrawalGroups.get( - backupWg.withdrawal_group_id, - ); - if (existingWg) { - continue; - } - let wgInfo: WgInfo; - switch (backupWg.info.type) { - case BackupWgType.BankIntegrated: - wgInfo = { - withdrawalType: WithdrawalRecordType.BankIntegrated, - bankInfo: { - exchangePaytoUri: backupWg.info.exchange_payto_uri, - talerWithdrawUri: backupWg.info.taler_withdraw_uri, - confirmUrl: backupWg.info.confirm_url, - timestampBankConfirmed: backupWg.info.timestamp_bank_confirmed, - timestampReserveInfoPosted: - backupWg.info.timestamp_reserve_info_posted, - }, - }; - break; - case BackupWgType.BankManual: - wgInfo = { - withdrawalType: WithdrawalRecordType.BankManual, - }; - break; - case BackupWgType.PeerPullCredit: - wgInfo = { - withdrawalType: WithdrawalRecordType.PeerPullCredit, - contractTerms: backupWg.info.contract_terms, - contractPriv: backupWg.info.contract_priv, - }; - break; - case BackupWgType.PeerPushCredit: - wgInfo = { - withdrawalType: WithdrawalRecordType.PeerPushCredit, - contractTerms: backupWg.info.contract_terms, - }; - break; - case BackupWgType.Recoup: - wgInfo = { - withdrawalType: WithdrawalRecordType.Recoup, - }; - break; - default: - assertUnreachable(backupWg.info); - } - const instructedAmount = Amounts.parseOrThrow( - backupWg.instructed_amount, - ); - await tx.withdrawalGroups.put({ - withdrawalGroupId: backupWg.withdrawal_group_id, - exchangeBaseUrl: backupWg.exchange_base_url, - instructedAmount: Amounts.stringify(instructedAmount), - secretSeed: backupWg.secret_seed, - denomsSel: await getDenomSelStateFromBackup( - tx, - instructedAmount.currency, - backupWg.exchange_base_url, - backupWg.selected_denoms, - ), - denomSelUid: backupWg.selected_denoms_uid, - rawWithdrawalAmount: Amounts.stringify( - backupWg.raw_withdrawal_amount, - ), - effectiveWithdrawalAmount: Amounts.stringify( - backupWg.effective_withdrawal_amount, - ), - reservePriv: backupWg.reserve_priv, - reservePub, - status: backupWg.timestamp_finish - ? WithdrawalGroupStatus.Finished - : WithdrawalGroupStatus.PendingQueryingStatus, // FIXME! - timestampStart: backupWg.timestamp_created, - wgInfo, - restrictAge: backupWg.restrict_age, - senderWire: undefined, // FIXME! - timestampFinish: backupWg.timestamp_finish, - }); - } - - for (const backupPurchase of backupBlob.purchases) { - const ts = constructTombstone({ - tag: TombstoneTag.DeletePayment, - proposalId: backupPurchase.proposal_id, - }); - if (tombstoneSet.has(ts)) { - continue; - } - const existingPurchase = await tx.purchases.get( - backupPurchase.proposal_id, - ); - let proposalStatus: PurchaseStatus; - switch (backupPurchase.proposal_status) { - case BackupProposalStatus.Paid: - proposalStatus = PurchaseStatus.Done; - break; - case BackupProposalStatus.Shared: - proposalStatus = PurchaseStatus.DialogShared; - break; - case BackupProposalStatus.Proposed: - proposalStatus = PurchaseStatus.DialogProposed; - break; - case BackupProposalStatus.PermanentlyFailed: - proposalStatus = PurchaseStatus.AbortedIncompletePayment; - break; - case BackupProposalStatus.Refused: - proposalStatus = PurchaseStatus.AbortedProposalRefused; - break; - case BackupProposalStatus.Repurchase: - proposalStatus = PurchaseStatus.RepurchaseDetected; - break; - default: { - const error: never = backupPurchase.proposal_status; - throw Error(`backup status ${error} is not handled`); - } - } - if (!existingPurchase) { - //const refunds: { [refundKey: string]: WalletRefundItem } = {}; - // for (const backupRefund of backupPurchase.refunds) { - // const key = `${backupRefund.coin_pub}-${backupRefund.rtransaction_id}`; - // const coin = await tx.coins.get(backupRefund.coin_pub); - // checkBackupInvariant(!!coin); - // const denom = await tx.denominations.get([ - // coin.exchangeBaseUrl, - // coin.denomPubHash, - // ]); - // checkBackupInvariant(!!denom); - // const common = { - // coinPub: backupRefund.coin_pub, - // executionTime: backupRefund.execution_time, - // obtainedTime: backupRefund.obtained_time, - // refundAmount: Amounts.stringify(backupRefund.refund_amount), - // refundFee: Amounts.stringify(denom.fees.feeRefund), - // rtransactionId: backupRefund.rtransaction_id, - // totalRefreshCostBound: Amounts.stringify( - // backupRefund.total_refresh_cost_bound, - // ), - // }; - // switch (backupRefund.type) { - // case BackupRefundState.Applied: - // refunds[key] = { - // type: RefundState.Applied, - // ...common, - // }; - // break; - // case BackupRefundState.Failed: - // refunds[key] = { - // type: RefundState.Failed, - // ...common, - // }; - // break; - // case BackupRefundState.Pending: - // refunds[key] = { - // type: RefundState.Pending, - // ...common, - // }; - // break; - // } - // } - const parsedContractTerms = codecForMerchantContractTerms().decode( - backupPurchase.contract_terms_raw, - ); - const amount = Amounts.parseOrThrow(parsedContractTerms.amount); - const contractTermsHash = - cryptoComp.proposalIdToContractTermsHash[ - backupPurchase.proposal_id - ]; - let maxWireFee: AmountJson; - if (parsedContractTerms.max_wire_fee) { - maxWireFee = Amounts.parseOrThrow(parsedContractTerms.max_wire_fee); - } else { - maxWireFee = Amounts.zeroOfCurrency(amount.currency); - } - const download: ProposalDownloadInfo = { - contractTermsHash, - contractTermsMerchantSig: backupPurchase.merchant_sig!, - currency: amount.currency, - fulfillmentUrl: backupPurchase.contract_terms_raw.fulfillment_url, - }; - - const contractData = extractContractData( - backupPurchase.contract_terms_raw, - contractTermsHash, - download.contractTermsMerchantSig, - ); - - let payInfo: PurchasePayInfo | undefined = undefined; - if (backupPurchase.pay_info) { - payInfo = { - payCoinSelection: await recoverPayCoinSelection( - tx, - contractData, - backupPurchase.pay_info, - ), - payCoinSelectionUid: backupPurchase.pay_info.pay_coins_uid, - totalPayCost: Amounts.stringify( - backupPurchase.pay_info.total_pay_cost, - ), - }; - } - - await tx.purchases.put({ - proposalId: backupPurchase.proposal_id, - noncePriv: backupPurchase.nonce_priv, - noncePub: - cryptoComp.proposalNoncePrivToPub[backupPurchase.nonce_priv], - autoRefundDeadline: TalerProtocolTimestamp.never(), - timestampAccept: backupPurchase.timestamp_accepted, - timestampFirstSuccessfulPay: - backupPurchase.timestamp_first_successful_pay, - timestampLastRefundStatus: undefined, - merchantPaySig: backupPurchase.merchant_pay_sig, - posConfirmation: backupPurchase.pos_confirmation, - lastSessionId: undefined, - download, - //refunds, - claimToken: backupPurchase.claim_token, - downloadSessionId: backupPurchase.download_session_id, - merchantBaseUrl: backupPurchase.merchant_base_url, - orderId: backupPurchase.order_id, - payInfo, - refundAmountAwaiting: undefined, - repurchaseProposalId: backupPurchase.repurchase_proposal_id, - purchaseStatus: proposalStatus, - timestamp: backupPurchase.timestamp_proposed, - shared: backupPurchase.shared, - }); - } - } - - for (const backupRefreshGroup of backupBlob.refresh_groups) { - const ts = constructTombstone({ - tag: TombstoneTag.DeleteRefreshGroup, - refreshGroupId: backupRefreshGroup.refresh_group_id, - }); - if (tombstoneSet.has(ts)) { - continue; - } - const existingRg = await tx.refreshGroups.get( - backupRefreshGroup.refresh_group_id, - ); - if (!existingRg) { - let reason: RefreshReason; - switch (backupRefreshGroup.reason) { - case BackupRefreshReason.AbortPay: - reason = RefreshReason.AbortPay; - break; - case BackupRefreshReason.BackupRestored: - reason = RefreshReason.BackupRestored; - break; - case BackupRefreshReason.Manual: - reason = RefreshReason.Manual; - break; - case BackupRefreshReason.Pay: - reason = RefreshReason.PayMerchant; - break; - case BackupRefreshReason.Recoup: - reason = RefreshReason.Recoup; - break; - case BackupRefreshReason.Refund: - reason = RefreshReason.Refund; - break; - case BackupRefreshReason.Scheduled: - reason = RefreshReason.Scheduled; - break; - } - const refreshSessionPerCoin: (RefreshSessionRecord | undefined)[] = - []; - for (const oldCoin of backupRefreshGroup.old_coins) { - const c = await tx.coins.get(oldCoin.coin_pub); - checkBackupInvariant(!!c); - const d = await tx.denominations.get([ - c.exchangeBaseUrl, - c.denomPubHash, - ]); - checkBackupInvariant(!!d); - - if (oldCoin.refresh_session) { - const denomSel = await getDenomSelStateFromBackup( - tx, - d.currency, - c.exchangeBaseUrl, - oldCoin.refresh_session.new_denoms, - ); - refreshSessionPerCoin.push({ - sessionSecretSeed: oldCoin.refresh_session.session_secret_seed, - norevealIndex: oldCoin.refresh_session.noreveal_index, - newDenoms: oldCoin.refresh_session.new_denoms.map((x) => ({ - count: x.count, - denomPubHash: x.denom_pub_hash, - })), - amountRefreshOutput: Amounts.stringify(denomSel.totalCoinValue), - }); - } else { - refreshSessionPerCoin.push(undefined); - } - } - await tx.refreshGroups.put({ - timestampFinished: backupRefreshGroup.timestamp_finish, - timestampCreated: backupRefreshGroup.timestamp_created, - refreshGroupId: backupRefreshGroup.refresh_group_id, - currency: Amounts.currencyOf( - backupRefreshGroup.old_coins[0].input_amount, - ), - reason, - lastErrorPerCoin: {}, - oldCoinPubs: backupRefreshGroup.old_coins.map((x) => x.coin_pub), - statusPerCoin: backupRefreshGroup.old_coins.map((x) => - x.finished - ? RefreshCoinStatus.Finished - : RefreshCoinStatus.Pending, - ), - operationStatus: backupRefreshGroup.timestamp_finish - ? RefreshOperationStatus.Finished - : RefreshOperationStatus.Pending, - inputPerCoin: backupRefreshGroup.old_coins.map( - (x) => x.input_amount, - ), - estimatedOutputPerCoin: backupRefreshGroup.old_coins.map( - (x) => x.estimated_output_amount, - ), - refreshSessionPerCoin, - }); - } - } - - for (const backupTip of backupBlob.tips) { - const ts = constructTombstone({ - tag: TombstoneTag.DeleteReward, - walletTipId: backupTip.wallet_tip_id, - }); - if (tombstoneSet.has(ts)) { - continue; - } - const existingTip = await tx.rewards.get(backupTip.wallet_tip_id); - if (!existingTip) { - const tipAmountRaw = Amounts.parseOrThrow(backupTip.tip_amount_raw); - const denomsSel = await getDenomSelStateFromBackup( - tx, - tipAmountRaw.currency, - backupTip.exchange_base_url, - backupTip.selected_denoms, - ); - await tx.rewards.put({ - acceptedTimestamp: backupTip.timestamp_accepted, - createdTimestamp: backupTip.timestamp_created, - denomsSel, - next_url: backupTip.next_url, - exchangeBaseUrl: backupTip.exchange_base_url, - merchantBaseUrl: backupTip.exchange_base_url, - merchantRewardId: backupTip.merchant_tip_id, - pickedUpTimestamp: backupTip.timestamp_finished, - secretSeed: backupTip.secret_seed, - rewardAmountEffective: Amounts.stringify(denomsSel.totalCoinValue), - rewardAmountRaw: Amounts.stringify(tipAmountRaw), - rewardExpiration: backupTip.timestamp_expiration, - walletRewardId: backupTip.wallet_tip_id, - denomSelUid: backupTip.selected_denoms_uid, - status: RewardRecordStatus.Done, // FIXME! - }); - } - } - - // We now process tombstones. - // The import code above should already prevent - // importing things that are tombstoned, - // but we do tombstone processing last just to be sure. - - for (const tombstone of tombstoneSet) { - const [type, ...rest] = tombstone.split(":"); - if (type === TombstoneTag.DeleteDepositGroup) { - await tx.depositGroups.delete(rest[0]); - } else if (type === TombstoneTag.DeletePayment) { - await tx.purchases.delete(rest[0]); - } else if (type === TombstoneTag.DeleteRefreshGroup) { - await tx.refreshGroups.delete(rest[0]); - } else if (type === TombstoneTag.DeleteRefund) { - // Nothing required, will just prevent display - // in the transactions list - } else if (type === TombstoneTag.DeleteReward) { - await tx.rewards.delete(rest[0]); - } else if (type === TombstoneTag.DeleteWithdrawalGroup) { - await tx.withdrawalGroups.delete(rest[0]); - } else { - logger.warn(`unable to process tombstone of type '${type}'`); - } - } - }); -} diff --git a/packages/taler-wallet-core/src/operations/backup/index.ts b/packages/taler-wallet-core/src/operations/backup/index.ts index e35765165..a5e8dbd42 100644 --- a/packages/taler-wallet-core/src/operations/backup/index.ts +++ b/packages/taler-wallet-core/src/operations/backup/index.ts @@ -43,7 +43,6 @@ import { TalerErrorDetail, TalerPreciseTimestamp, URL, - WalletBackupContentV1, buildCodecForObject, buildCodecForUnion, bytesToString, @@ -99,9 +98,8 @@ import { TaskIdentifiers, } from "../common.js"; import { checkPaymentByProposalId, preparePayForUri } from "../pay-merchant.js"; -import { exportBackup } from "./export.js"; -import { BackupCryptoPrecomputedData, importBackup } from "./import.js"; -import { getWalletBackupState, provideBackupState } from "./state.js"; +import { WalletStoresV1 } from "../../db.js"; +import { GetReadOnlyAccess } from "../../util/query.js"; const logger = new Logger("operations/backup.ts"); @@ -131,7 +129,7 @@ const magic = "TLRWBK01"; */ export async function encryptBackup( config: WalletBackupConfState, - blob: WalletBackupContentV1, + blob: any, ): Promise { 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 { - 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 { - // 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 { - 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 { +): Promise { 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 { - const backupConfig = await provideBackupState(ws); - const blob = await decryptBackup(backupConfig, data); - const cryptoData = await computeBackupCryptoData(ws.cryptoApi, blob); - await importBackup(ws, blob, cryptoData); +): Promise { + 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 { + 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 { + 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 { + 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 */ -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 { - 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 { - 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 { - 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 { - const bs = await provideBackupState(ws); - return bs.deviceId; -} diff --git a/packages/taler-wallet-core/src/wallet-api-types.ts b/packages/taler-wallet-core/src/wallet-api-types.ts index 06ccdf6f3..4d9d40c43 100644 --- a/packages/taler-wallet-core/src/wallet-api-types.ts +++ b/packages/taler-wallet-core/src/wallet-api-types.ts @@ -106,7 +106,6 @@ import { UserAttentionsResponse, ValidateIbanRequest, ValidateIbanResponse, - WalletBackupContentV1, WalletCoreVersion, WalletCurrencyInfo, WithdrawFakebankRequest, @@ -116,6 +115,10 @@ import { SharePaymentResult, GetCurrencyInfoRequest, GetCurrencyInfoResponse, + StoredBackupList, + CreateStoredBackupResponse, + RecoverStoredBackupRequest, + DeleteStoredBackupRequest, } from "@gnu-taler/taler-util"; import { AuditorTrustRecord, WalletContractData } from "./db.js"; import { @@ -195,7 +198,6 @@ export enum WalletApiOperation { GenerateDepositGroupTxId = "generateDepositGroupTxId", CreateDepositGroup = "createDepositGroup", SetWalletDeviceId = "setWalletDeviceId", - ExportBackupPlain = "exportBackupPlain", WithdrawFakebank = "withdrawFakebank", ImportDb = "importDb", ExportDb = "exportDb", @@ -214,6 +216,10 @@ export enum WalletApiOperation { TestingWaitTransactionsFinal = "testingWaitTransactionsFinal", TestingWaitRefreshesFinal = "testingWaitRefreshesFinal", GetScopedCurrencyInfo = "getScopedCurrencyInfo", + ListStoredBackups = "listStoredBackups", + CreateStoredBackup = "createStoredBackup", + DeleteStoredBackup = "deleteStoredBackup", + RecoverStoredBackup = "recoverStoredBackup", } // group: Initialization @@ -713,13 +719,28 @@ export type SetWalletDeviceIdOp = { response: EmptyObject; }; -/** - * Export a backup JSON, mostly useful for testing. - */ -export type ExportBackupPlainOp = { - op: WalletApiOperation.ExportBackupPlain; +export type ListStoredBackupsOp = { + op: WalletApiOperation.ListStoredBackups; request: EmptyObject; - response: WalletBackupContentV1; + response: StoredBackupList; +}; + +export type CreateStoredBackupsOp = { + op: WalletApiOperation.CreateStoredBackup; + request: EmptyObject; + response: CreateStoredBackupResponse; +}; + +export type RecoverStoredBackupsOp = { + op: WalletApiOperation.RecoverStoredBackup; + request: RecoverStoredBackupRequest; + response: EmptyObject; +}; + +export type DeleteStoredBackupOp = { + op: WalletApiOperation.DeleteStoredBackup; + request: DeleteStoredBackupRequest; + response: EmptyObject; }; // group: Peer Payments @@ -1062,7 +1083,6 @@ export type WalletOperations = { [WalletApiOperation.GenerateDepositGroupTxId]: GenerateDepositGroupTxIdOp; [WalletApiOperation.CreateDepositGroup]: CreateDepositGroupOp; [WalletApiOperation.SetWalletDeviceId]: SetWalletDeviceIdOp; - [WalletApiOperation.ExportBackupPlain]: ExportBackupPlainOp; [WalletApiOperation.ExportBackupRecovery]: ExportBackupRecoveryOp; [WalletApiOperation.ImportBackupRecovery]: ImportBackupRecoveryOp; [WalletApiOperation.RunBackupCycle]: RunBackupCycleOp; @@ -1092,6 +1112,10 @@ export type WalletOperations = { [WalletApiOperation.TestingWaitTransactionsFinal]: TestingWaitTransactionsFinal; [WalletApiOperation.TestingWaitRefreshesFinal]: TestingWaitRefreshesFinal; [WalletApiOperation.GetScopedCurrencyInfo]: GetScopedCurrencyInfoOp; + [WalletApiOperation.CreateStoredBackup]: CreateStoredBackupsOp; + [WalletApiOperation.ListStoredBackups]: ListStoredBackupsOp; + [WalletApiOperation.DeleteStoredBackup]: DeleteStoredBackupOp; + [WalletApiOperation.RecoverStoredBackup]: RecoverStoredBackupsOp; }; export type WalletCoreRequestType< diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts index 9f754ed69..283539a08 100644 --- a/packages/taler-wallet-core/src/wallet.ts +++ b/packages/taler-wallet-core/src/wallet.ts @@ -120,6 +120,7 @@ import { codecForSharePaymentRequest, GetCurrencyInfoResponse, codecForGetCurrencyInfoRequest, + CreateStoredBackupResponse, } from "@gnu-taler/taler-util"; import { HttpRequestLibrary, @@ -139,6 +140,7 @@ import { clearDatabase, exportDb, importDb, + openStoredBackupsDatabase, openTalerDatabase, } from "./db.js"; import { DevExperimentHttpLib, applyDevExperiment } from "./dev-experiments.js"; @@ -158,7 +160,6 @@ import { getUserAttentionsUnreadCount, markAttentionRequestAsRead, } from "./operations/attention.js"; -import { exportBackup } from "./operations/backup/export.js"; import { addBackupProvider, codecForAddBackupProviderRequest, @@ -166,13 +167,12 @@ import { codecForRunBackupCycle, getBackupInfo, getBackupRecovery, - importBackupPlain, loadBackupRecovery, processBackupForProvider, removeBackupProvider, runBackupCycle, + setWalletDeviceId, } from "./operations/backup/index.js"; -import { setWalletDeviceId } from "./operations/backup/state.js"; import { getBalanceDetail, getBalances } from "./operations/balance.js"; import { TaskIdentifiers, @@ -1025,6 +1025,17 @@ export async function getClientFromWalletState( return client; } +async function createStoredBackup( + ws: InternalWalletState, +): Promise { + const backup = await exportDb(ws.idb); + const backupsDb = await openStoredBackupsDatabase(ws.idb); + const name = `backup-${new Date().getTime()}`; + backupsDb.mktxAll().runReadWrite(async (tx) => {}); + + throw Error("not implemented"); +} + /** * Implementation of the "wallet-core" API. */ @@ -1041,6 +1052,14 @@ async function dispatchRequestInternal( // FIXME: Can we make this more type-safe by using the request/response type // definitions we already have? switch (operation) { + case WalletApiOperation.CreateStoredBackup: + return createStoredBackup(ws); + case WalletApiOperation.DeleteStoredBackup: + return {}; + case WalletApiOperation.ListStoredBackups: + return {}; + case WalletApiOperation.RecoverStoredBackup: + return {}; case WalletApiOperation.InitWallet: { logger.trace("initializing wallet"); ws.initCalled = true; @@ -1382,9 +1401,6 @@ async function dispatchRequestInternal( const req = codecForAcceptTipRequest().decode(payload); return await acceptTip(ws, req.walletRewardId); } - case WalletApiOperation.ExportBackupPlain: { - return exportBackup(ws); - } case WalletApiOperation.AddBackupProvider: { const req = codecForAddBackupProviderRequest().decode(payload); return await addBackupProvider(ws, req); @@ -1535,9 +1551,7 @@ async function dispatchRequestInternal( await clearDatabase(ws.db.idbHandle()); return {}; case WalletApiOperation.Recycle: { - const backup = await exportBackup(ws); - await clearDatabase(ws.db.idbHandle()); - await importBackupPlain(ws, backup); + throw Error("not implemented"); return {}; } case WalletApiOperation.ExportDb: { diff --git a/packages/taler-wallet-webextension/src/wxBackend.ts b/packages/taler-wallet-webextension/src/wxBackend.ts index f071d78df..b7484164d 100644 --- a/packages/taler-wallet-webextension/src/wxBackend.ts +++ b/packages/taler-wallet-webextension/src/wxBackend.ts @@ -139,7 +139,7 @@ async function runGarbageCollector(): Promise { 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");