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/taleruri.test.ts b/packages/taler-util/src/taleruri.test.ts index 0dcf92252..19b94e191 100644 --- a/packages/taler-util/src/taleruri.test.ts +++ b/packages/taler-util/src/taleruri.test.ts @@ -159,8 +159,8 @@ test("taler refund uri parsing with instance", (t) => { t.is(r1.merchantBaseUrl, "https://merchant.example.com/instances/myinst/"); }); -test("taler tip pickup uri", (t) => { - const url1 = "taler://tip/merchant.example.com/tipid"; +test("taler reward pickup uri", (t) => { + const url1 = "taler://reward/merchant.example.com/tipid"; const r1 = parseRewardUri(url1); if (!r1) { t.fail(); @@ -169,26 +169,26 @@ test("taler tip pickup uri", (t) => { t.is(r1.merchantBaseUrl, "https://merchant.example.com/"); }); -test("taler tip pickup uri with instance", (t) => { - const url1 = "taler://tip/merchant.example.com/instances/tipm/tipid"; +test("taler reward pickup uri with instance", (t) => { + const url1 = "taler://reward/merchant.example.com/instances/tipm/tipid"; const r1 = parseRewardUri(url1); if (!r1) { t.fail(); return; } t.is(r1.merchantBaseUrl, "https://merchant.example.com/instances/tipm/"); - t.is(r1.merchantTipId, "tipid"); + t.is(r1.merchantRewardId, "tipid"); }); -test("taler tip pickup uri with instance and prefix", (t) => { - const url1 = "taler://tip/merchant.example.com/my/pfx/tipm/tipid"; +test("taler reward pickup uri with instance and prefix", (t) => { + const url1 = "taler://reward/merchant.example.com/my/pfx/tipm/tipid"; const r1 = parseRewardUri(url1); if (!r1) { t.fail(); return; } t.is(r1.merchantBaseUrl, "https://merchant.example.com/my/pfx/tipm/"); - t.is(r1.merchantTipId, "tipid"); + t.is(r1.merchantRewardId, "tipid"); }); test("taler peer to peer push URI", (t) => { diff --git a/packages/taler-util/src/taleruri.ts b/packages/taler-util/src/taleruri.ts index fff1ca833..310986eaf 100644 --- a/packages/taler-util/src/taleruri.ts +++ b/packages/taler-util/src/taleruri.ts @@ -63,7 +63,7 @@ export interface RefundUriResult { export interface RewardUriResult { type: TalerUriAction.Reward; merchantBaseUrl: string; - merchantTipId: string; + merchantRewardId: string; } export interface ExchangeUri { @@ -408,7 +408,7 @@ export function parseRewardUri(s: string): RewardUriResult | undefined { return undefined; } const host = parts[0].toLowerCase(); - const tipId = parts[parts.length - 1]; + const rewardId = parts[parts.length - 1]; const pathSegments = parts.slice(1, parts.length - 1); const hostAndSegments = [host, ...pathSegments].join("/"); const merchantBaseUrl = canonicalizeBaseUrl( @@ -418,7 +418,7 @@ export function parseRewardUri(s: string): RewardUriResult | undefined { return { type: TalerUriAction.Reward, merchantBaseUrl, - merchantTipId: tipId, + merchantRewardId: rewardId, }; } @@ -701,10 +701,10 @@ export function stringifyRefundUri({ } export function stringifyRewardUri({ merchantBaseUrl, - merchantTipId, + merchantRewardId, }: Omit): string { const { proto, path } = getUrlInfo(merchantBaseUrl); - return `${proto}://reward/${path}${merchantTipId}`; + return `${proto}://reward/${path}${merchantRewardId}`; } export function stringifyExchangeUri({ diff --git a/packages/taler-util/src/time.ts b/packages/taler-util/src/time.ts index 75b3c7e94..55cda08a5 100644 --- a/packages/taler-util/src/time.ts +++ b/packages/taler-util/src/time.ts @@ -312,6 +312,14 @@ export namespace Duration { } export namespace AbsoluteTime { + export function getStampMsNow(): number { + return new Date().getTime(); + } + + export function getStampMsNever(): number { + return Number.MAX_SAFE_INTEGER; + } + export function now(): AbsoluteTime { return { t_ms: new Date().getTime() + timeshift, @@ -398,6 +406,13 @@ export namespace AbsoluteTime { }; } + export function fromStampMs(stampMs: number): AbsoluteTime { + return { + t_ms: stampMs, + [opaque_AbsoluteTime]: true, + }; + } + export function fromPreciseTimestamp(t: TalerPreciseTimestamp): AbsoluteTime { if (t.t_s === "never") { return { t_ms: "never", [opaque_AbsoluteTime]: true }; @@ -409,6 +424,13 @@ export namespace AbsoluteTime { }; } + export function toStampMs(at: AbsoluteTime): number { + if (at.t_ms === "never") { + return Number.MAX_SAFE_INTEGER; + } + return at.t_ms; + } + export function toPreciseTimestamp(at: AbsoluteTime): TalerPreciseTimestamp { if (at.t_ms == "never") { return { diff --git a/packages/taler-util/src/wallet-types.ts b/packages/taler-util/src/wallet-types.ts index 04fb43ec6..accab746f 100644 --- a/packages/taler-util/src/wallet-types.ts +++ b/packages/taler-util/src/wallet-types.ts @@ -1262,17 +1262,25 @@ export interface ExchangeFullDetails { } export enum ExchangeTosStatus { - New = "new", + Pending = "pending", + Proposed = "proposed", Accepted = "accepted", - Changed = "changed", - NotFound = "not-found", - Unknown = "unknown", } export enum ExchangeEntryStatus { - Unknown = "unknown", - Outdated = "outdated", - Ok = "ok", + Preset = "preset", + Ephemeral = "ephemeral", + Used = "used", +} + +export enum ExchangeUpdateStatus { + Initial = "initial", + InitialUpdate = "initial(update)", + Suspended = "suspended", + Failed = "failed", + OutdatedUpdate = "outdated(update)", + Ready = "ready", + ReadyUpdate = "ready(update)", } export interface OperationErrorInfo { @@ -1285,13 +1293,9 @@ export interface ExchangeListItem { currency: string | undefined; paytoUris: string[]; tosStatus: ExchangeTosStatus; - exchangeStatus: ExchangeEntryStatus; + exchangeEntryStatus: ExchangeEntryStatus; + exchangeUpdateStatus: ExchangeUpdateStatus; ageRestrictionOptions: number[]; - /** - * Permanently added to the wallet, as opposed to just - * temporarily queried. - */ - permanent: boolean; /** * Information about the last error that occurred when trying @@ -1370,8 +1374,8 @@ export const codecForExchangeListItem = (): Codec => .property("exchangeBaseUrl", codecForString()) .property("paytoUris", codecForList(codecForString())) .property("tosStatus", codecForAny()) - .property("exchangeStatus", codecForAny()) - .property("permanent", codecForBoolean()) + .property("exchangeEntryStatus", codecForAny()) + .property("exchangeUpdateStatus", codecForAny()) .property("ageRestrictionOptions", codecForList(codecForNumber())) .build("ExchangeListItem"); @@ -2651,3 +2655,21 @@ export interface TransactionRecordFilter { onlyState?: TransactionStateFilter; onlyCurrency?: string; } + +export interface StoredBackupList { + storedBackups: { + name: string; + }[]; +} + +export interface CreateStoredBackupResponse { + name: string; +} + +export interface RecoverStoredBackupRequest { + name: string; +} + +export interface DeleteStoredBackupRequest { + name: string; +} diff --git a/packages/taler-wallet-cli/src/index.ts b/packages/taler-wallet-cli/src/index.ts index 9d840e5bb..36e7f7768 100644 --- a/packages/taler-wallet-cli/src/index.ts +++ b/packages/taler-wallet-cli/src/index.ts @@ -257,8 +257,7 @@ async function createLocalWallet( }, cryptoWorkerType: walletCliArgs.wallet.cryptoWorker as any, config: { - features: { - }, + features: {}, testing: { devModeActive: checkEnvFlag("TALER_WALLET_DEV_MODE"), denomselAllowLate: checkEnvFlag( @@ -651,9 +650,12 @@ walletCli }); break; case TalerUriAction.Reward: { - const res = await wallet.client.call(WalletApiOperation.PrepareReward, { - talerRewardUri: uri, - }); + const res = await wallet.client.call( + WalletApiOperation.PrepareReward, + { + talerRewardUri: uri, + }, + ); console.log("tip status", res); await wallet.client.call(WalletApiOperation.AcceptReward, { walletRewardId: res.walletRewardId, @@ -874,95 +876,32 @@ const backupCli = walletCli.subcommand("backupArgs", "backup", { help: "Subcommands for backups", }); -backupCli - .subcommand("setDeviceId", "set-device-id") - .requiredArgument("deviceId", clk.STRING, { - help: "new device ID", - }) - .action(async (args) => { - await withWallet(args, async (wallet) => { - await wallet.client.call(WalletApiOperation.SetWalletDeviceId, { - walletDeviceId: args.setDeviceId.deviceId, - }); - }); - }); - -backupCli.subcommand("exportPlain", "export-plain").action(async (args) => { +backupCli.subcommand("exportDb", "export-db").action(async (args) => { await withWallet(args, async (wallet) => { - const backup = await wallet.client.call( - WalletApiOperation.ExportBackupPlain, - {}, - ); + const backup = await wallet.client.call(WalletApiOperation.ExportDb, {}); console.log(JSON.stringify(backup, undefined, 2)); }); }); -backupCli.subcommand("recoverySave", "save-recovery").action(async (args) => { +backupCli.subcommand("storeBackup", "store-backup").action(async (args) => { await withWallet(args, async (wallet) => { - const recoveryJson = await wallet.client.call( - WalletApiOperation.ExportBackupRecovery, + const resp = await wallet.client.call( + WalletApiOperation.CreateStoredBackup, {}, ); - console.log(JSON.stringify(recoveryJson, undefined, 2)); + console.log(JSON.stringify(resp, undefined, 2)); }); }); -backupCli.subcommand("run", "run").action(async (args) => { +backupCli.subcommand("importDb", "import-db").action(async (args) => { await withWallet(args, async (wallet) => { - await wallet.client.call(WalletApiOperation.RunBackupCycle, {}); - }); -}); - -backupCli.subcommand("status", "status").action(async (args) => { - await withWallet(args, async (wallet) => { - const status = await wallet.client.call( - WalletApiOperation.GetBackupInfo, - {}, - ); - console.log(JSON.stringify(status, undefined, 2)); - }); -}); - -backupCli - .subcommand("recoveryLoad", "load-recovery") - .maybeOption("strategy", ["--strategy"], clk.STRING, { - help: "Strategy for resolving a conflict with the existing wallet key ('theirs' or 'ours')", - }) - .action(async (args) => { - await withWallet(args, async (wallet) => { - const data = JSON.parse(await read(process.stdin)); - let strategy: RecoveryMergeStrategy | undefined; - const stratStr = args.recoveryLoad.strategy; - if (stratStr) { - if (stratStr === "theirs") { - strategy = RecoveryMergeStrategy.Theirs; - } else if (stratStr === "ours") { - strategy = RecoveryMergeStrategy.Theirs; - } else { - throw Error("invalid recovery strategy"); - } - } - await wallet.client.call(WalletApiOperation.ImportBackupRecovery, { - recovery: data, - strategy, - }); - }); - }); - -backupCli - .subcommand("addProvider", "add-provider") - .requiredArgument("url", clk.STRING) - .maybeArgument("name", clk.STRING) - .flag("activate", ["--activate"]) - .action(async (args) => { - await withWallet(args, async (wallet) => { - await wallet.client.call(WalletApiOperation.AddBackupProvider, { - backupProviderBaseUrl: args.addProvider.url, - activate: args.addProvider.activate, - name: args.addProvider.name || args.addProvider.url, - }); + const dumpRaw = await read(process.stdin); + const dump = JSON.parse(dumpRaw); + await wallet.client.call(WalletApiOperation.ImportDb, { + dump, }); }); +}); const depositCli = walletCli.subcommand("depositArgs", "deposit", { help: "Subcommands for depositing money to payto:// accounts", @@ -1681,6 +1620,3 @@ async function read(stream: NodeJS.ReadStream) { export function main() { walletCli.run(); } -function classifyTalerUri(uri: string) { - throw new Error("Function not implemented."); -} diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts index 0fad66d92..b52a503bc 100644 --- a/packages/taler-wallet-core/src/db.ts +++ b/packages/taler-wallet-core/src/db.ts @@ -23,6 +23,7 @@ import { IDBFactory, IDBObjectStore, IDBTransaction, + structuredEncapsulate, } from "@gnu-taler/idb-bridge"; import { AgeCommitmentProof, @@ -103,7 +104,7 @@ import { RetryInfo, TaskIdentifiers } from "./operations/common.js"; * for all previous versions must be written, which should be * avoided. */ -export const TALER_DB_NAME = "taler-wallet-main-v9"; +export const TALER_WALLET_MAIN_DB_NAME = "taler-wallet-main-v9"; /** * Name of the metadata database. This database is used @@ -111,7 +112,12 @@ export const TALER_DB_NAME = "taler-wallet-main-v9"; * * (Minor migrations are handled via upgrade transactions.) */ -export const TALER_META_DB_NAME = "taler-wallet-meta"; +export const TALER_WALLET_META_DB_NAME = "taler-wallet-meta"; + +/** + * Stored backups, mainly created when manually importing a backup. + */ +export const TALER_WALLET_STORED_BACKUPS_DB_NAME = "taler-wallet-stored-backups"; export const CURRENT_DB_CONFIG_KEY = "currentMainDbName"; @@ -566,10 +572,31 @@ export interface ExchangeDetailsPointer { updateClock: TalerPreciseTimestamp; } +export enum ExchangeEntryDbRecordStatus { + Preset = 1, + Ephemeral = 2, + Used = 3, +} + +export enum ExchangeEntryDbUpdateStatus { + Initial = 1, + InitialUpdate = 2, + Suspended = 3, + Failed = 4, + OutdatedUpdate = 5, + Ready = 6, + ReadyUpdate = 7, +} + +/** + * Timestamp stored as a IEEE 754 double, in milliseconds. + */ +export type DbIndexableTimestampMs = number; + /** * Exchange record as stored in the wallet's database. */ -export interface ExchangeRecord { +export interface ExchangeEntryRecord { /** * Base url of the exchange. */ @@ -594,13 +621,12 @@ export interface ExchangeRecord { */ detailsPointer: ExchangeDetailsPointer | undefined; - /** - * Is this a permanent or temporary exchange record? - */ - permanent: boolean; + entryStatus: ExchangeEntryDbRecordStatus; + + updateStatus: ExchangeEntryDbUpdateStatus; /** - * Last time when the exchange was updated (both /keys and /wire). + * Last time when the exchange /keys info was updated. */ lastUpdate: TalerPreciseTimestamp | undefined; @@ -608,20 +634,21 @@ export interface ExchangeRecord { * Next scheduled update for the exchange. * * (This field must always be present, so we can index on the timestamp.) + * + * FIXME: To index on the timestamp, this needs to be a number of + * binary timestamp! */ - nextUpdate: TalerPreciseTimestamp; + nextUpdateStampMs: DbIndexableTimestampMs; lastKeysEtag: string | undefined; - lastWireEtag: string | undefined; - /** * Next time that we should check if coins need to be refreshed. * * Updated whenever the exchange's denominations are updated or when * the refresh check has been done. */ - nextRefreshCheck: TalerPreciseTimestamp; + nextRefreshCheckStampMs: DbIndexableTimestampMs; /** * Public key of the reserve that we're currently using for @@ -2431,7 +2458,7 @@ export const WalletStoresV1 = { ), exchanges: describeStore( "exchanges", - describeContents({ + describeContents({ keyPath: "baseUrl", }), {}, @@ -2725,11 +2752,10 @@ export type WalletDbReadOnlyTransaction< Stores extends StoreNames & string, > = DbReadOnlyTransaction; -export type WalletReadWriteTransaction< +export type WalletDbReadWriteTransaction< Stores extends StoreNames & string, > = DbReadWriteTransaction; - /** * An applied migration. */ @@ -2760,45 +2786,144 @@ export const walletMetadataStore = { ), }; -export function exportDb(db: IDBDatabase): Promise { - const dump = { - name: db.name, - stores: {} as { [s: string]: any }, - version: db.version, +export interface StoredBackupMeta { + name: string; +} + +export const StoredBackupStores = { + backupMeta: describeStore( + "backupMeta", + describeContents({ keyPath: "name" }), + {}, + ), + backupData: describeStore("backupData", describeContents({}), {}), +}; + +export interface DbDumpRecord { + /** + * Key, serialized with structuredEncapsulated. + * + * Only present for out-of-line keys (i.e. no key path). + */ + key?: any; + /** + * Value, serialized with structuredEncapsulated. + */ + value: any; +} + +export interface DbIndexDump { + keyPath: string | string[]; + multiEntry: boolean; + unique: boolean; +} + +export interface DbStoreDump { + keyPath?: string | string[]; + autoIncrement: boolean; + indexes: { [indexName: string]: DbIndexDump }; + records: DbDumpRecord[]; +} + +export interface DbDumpDatabase { + version: number; + stores: { [storeName: string]: DbStoreDump }; +} + +export interface DbDump { + databases: { + [name: string]: DbDumpDatabase; + }; +} + +export async function exportSingleDb( + idb: IDBFactory, + dbName: string, +): Promise { + const myDb = await openDatabase( + idb, + dbName, + undefined, + () => { + // May not happen, since we're not requesting a specific version + throw Error("unexpected version change"); + }, + () => { + logger.info("unexpected onupgradeneeded"); + }, + ); + + const singleDbDump: DbDumpDatabase = { + version: myDb.version, + stores: {}, }; return new Promise((resolve, reject) => { - const tx = db.transaction(Array.from(db.objectStoreNames)); + const tx = myDb.transaction(Array.from(myDb.objectStoreNames)); tx.addEventListener("complete", () => { - resolve(dump); + myDb.close(); + resolve(singleDbDump); }); // tslint:disable-next-line:prefer-for-of - for (let i = 0; i < db.objectStoreNames.length; i++) { - const name = db.objectStoreNames[i]; - const storeDump = {} as { [s: string]: any }; - dump.stores[name] = storeDump; - tx.objectStore(name) - .openCursor() - .addEventListener("success", (e: Event) => { - const cursor = (e.target as any).result; - if (cursor) { - storeDump[cursor.key] = cursor.value; - cursor.continue(); + for (let i = 0; i < myDb.objectStoreNames.length; i++) { + const name = myDb.objectStoreNames[i]; + const store = tx.objectStore(name); + const storeDump: DbStoreDump = { + autoIncrement: store.autoIncrement, + keyPath: store.keyPath, + indexes: {}, + records: [], + }; + const indexNames = store.indexNames; + for (let j = 0; j < indexNames.length; j++) { + const idxName = indexNames[j]; + const index = store.index(idxName); + storeDump.indexes[idxName] = { + keyPath: index.keyPath, + multiEntry: index.multiEntry, + unique: index.unique, + }; + } + singleDbDump.stores[name] = storeDump; + store.openCursor().addEventListener("success", (e: Event) => { + const cursor = (e.target as any).result; + if (cursor) { + const rec: DbDumpRecord = { + value: structuredEncapsulate(cursor.value), + }; + // Only store key if necessary, i.e. when + // the key is not stored as part of the object via + // a key path. + if (store.keyPath == null) { + rec.key = structuredEncapsulate(cursor.key); } - }); + cursor.continue(); + } + }); } }); } -export interface DatabaseDump { - name: string; - stores: { [s: string]: any }; - version: string; +export async function exportDb(idb: IDBFactory): Promise { + const dbDump: DbDump = { + databases: {}, + }; + + dbDump.databases[TALER_WALLET_META_DB_NAME] = await exportSingleDb( + idb, + TALER_WALLET_META_DB_NAME, + ); + dbDump.databases[TALER_WALLET_MAIN_DB_NAME] = await exportSingleDb( + idb, + TALER_WALLET_MAIN_DB_NAME, + ); + + return dbDump; } async function recoverFromDump( db: IDBDatabase, - dump: DatabaseDump, + dbDump: DbDumpDatabase, ): Promise { return new Promise((resolve, reject) => { const tx = db.transaction(Array.from(db.objectStoreNames), "readwrite"); @@ -2807,67 +2932,33 @@ async function recoverFromDump( }); for (let i = 0; i < db.objectStoreNames.length; i++) { const name = db.objectStoreNames[i]; - const storeDump = dump.stores[name]; + const storeDump = dbDump.stores[name]; if (!storeDump) continue; - Object.keys(storeDump).forEach(async (key) => { - const value = storeDump[key]; - if (!value) return; - tx.objectStore(name).put(value); - }); + for (let rec of storeDump.records) { + tx.objectStore(name).put(rec.value, rec.key); + } } tx.commit(); }); } -export async function importDb(db: IDBDatabase, object: any): Promise { - if ("name" in object && "stores" in object && "version" in object) { - // looks like a database dump - const dump = object as DatabaseDump; - return recoverFromDump(db, dump); - } +function checkDbDump(x: any): x is DbDump { + return "databases" in x; +} - if ("databases" in object && "$types" in object) { - // looks like a IDBDatabase - const someDatabase = object.databases; - - if (TALER_META_DB_NAME in someDatabase) { - //looks like a taler database - const currentMainDbValue = - someDatabase[TALER_META_DB_NAME].objectStores.metaConfig.records[0] - .value.value; - - if (currentMainDbValue !== TALER_DB_NAME) { - console.log("not the current database version"); - } - - const talerDb = someDatabase[currentMainDbValue]; - - const objectStoreNames = Object.keys(talerDb.objectStores); - - const dump: DatabaseDump = { - name: talerDb.schema.databaseName, - version: talerDb.schema.databaseVersion, - stores: {}, - }; - - for (let i = 0; i < objectStoreNames.length; i++) { - const name = objectStoreNames[i]; - const storeDump = {} as { [s: string]: any }; - dump.stores[name] = storeDump; - talerDb.objectStores[name].records.map((r: any) => { - const pkey = r.primaryKey; - const key = - typeof pkey === "string" || typeof pkey === "number" - ? pkey - : pkey.join(","); - storeDump[key] = r.value; - }); - } - - return recoverFromDump(db, dump); +export async function importDb(db: IDBDatabase, dumpJson: any): Promise { + const d = dumpJson; + if (checkDbDump(d)) { + const walletDb = d.databases[TALER_WALLET_MAIN_DB_NAME]; + if (!walletDb) { + throw Error( + `unable to import, main wallet database (${TALER_WALLET_MAIN_DB_NAME}) not found`, + ); } + await recoverFromDump(db, walletDb); + } else { + throw Error("unable to import, doesn't look like a valid DB dump"); } - throw Error("could not import database"); } export interface FixupDescription { @@ -3151,6 +3242,36 @@ function onMetaDbUpgradeNeeded( ); } +function onStoredBackupsDbUpgradeNeeded( + db: IDBDatabase, + oldVersion: number, + newVersion: number, + upgradeTransaction: IDBTransaction, +) { + upgradeFromStoreMap( + StoredBackupStores, + db, + oldVersion, + newVersion, + upgradeTransaction, + ); +} + +export async function openStoredBackupsDatabase( + idbFactory: IDBFactory, +): Promise> { + const backupsDbHandle = await openDatabase( + idbFactory, + TALER_WALLET_STORED_BACKUPS_DB_NAME, + 1, + () => {}, + onStoredBackupsDbUpgradeNeeded, + ); + + const handle = new DbAccess(backupsDbHandle, StoredBackupStores); + return handle; +} + /** * Return a promise that resolves * to the taler wallet db. @@ -3164,7 +3285,7 @@ export async function openTalerDatabase( ): Promise> { const metaDbHandle = await openDatabase( idbFactory, - TALER_META_DB_NAME, + TALER_WALLET_META_DB_NAME, 1, () => {}, onMetaDbUpgradeNeeded, @@ -3177,17 +3298,17 @@ export async function openTalerDatabase( .runReadWrite(async (tx) => { const dbVersionRecord = await tx.metaConfig.get(CURRENT_DB_CONFIG_KEY); if (!dbVersionRecord) { - currentMainVersion = TALER_DB_NAME; + currentMainVersion = TALER_WALLET_MAIN_DB_NAME; await tx.metaConfig.put({ key: CURRENT_DB_CONFIG_KEY, - value: TALER_DB_NAME, + value: TALER_WALLET_MAIN_DB_NAME, }); } else { currentMainVersion = dbVersionRecord.value; } }); - if (currentMainVersion !== TALER_DB_NAME) { + if (currentMainVersion !== TALER_WALLET_MAIN_DB_NAME) { switch (currentMainVersion) { case "taler-wallet-main-v2": case "taler-wallet-main-v3": @@ -3203,7 +3324,7 @@ export async function openTalerDatabase( .runReadWrite(async (tx) => { await tx.metaConfig.put({ key: CURRENT_DB_CONFIG_KEY, - value: TALER_DB_NAME, + value: TALER_WALLET_MAIN_DB_NAME, }); }); break; @@ -3216,7 +3337,7 @@ export async function openTalerDatabase( const mainDbHandle = await openDatabase( idbFactory, - TALER_DB_NAME, + TALER_WALLET_MAIN_DB_NAME, WALLET_DB_MINOR_VERSION, onVersionChange, onTalerDbUpgradeNeeded, @@ -3233,7 +3354,7 @@ export async function deleteTalerDatabase( idbFactory: IDBFactory, ): Promise { return new Promise((resolve, reject) => { - const req = idbFactory.deleteDatabase(TALER_DB_NAME); + const req = idbFactory.deleteDatabase(TALER_WALLET_MAIN_DB_NAME); req.onerror = () => reject(req.error); req.onsuccess = () => resolve(); }); diff --git a/packages/taler-wallet-core/src/host-impl.node.ts b/packages/taler-wallet-core/src/host-impl.node.ts index 6a4f21d79..0b6539306 100644 --- a/packages/taler-wallet-core/src/host-impl.node.ts +++ b/packages/taler-wallet-core/src/host-impl.node.ts @@ -139,13 +139,6 @@ export async function createNativeWalletHost2( }); } - const myVersionChange = (): Promise => { - logger.error("version change requested, should not happen"); - throw Error( - "BUG: wallet DB version change event can't happen with memory IDB", - ); - }; - let dbResp: MakeDbResult; if (args.persistentStoragePath &&args.persistentStoragePath.endsWith(".json")) { @@ -160,8 +153,6 @@ export async function createNativeWalletHost2( shimIndexedDB(dbResp.idbFactory); - const myDb = await openTalerDatabase(myIdbFactory, myVersionChange); - let workerFactory; const cryptoWorkerType = args.cryptoWorkerType ?? "node-worker-thread"; if (cryptoWorkerType === "sync") { @@ -189,7 +180,7 @@ export async function createNativeWalletHost2( const timer = new SetTimeoutTimerAPI(); const w = await Wallet.create( - myDb, + myIdbFactory, myHttpLib, timer, workerFactory, diff --git a/packages/taler-wallet-core/src/host-impl.qtart.ts b/packages/taler-wallet-core/src/host-impl.qtart.ts index 720f5affb..81dbe0acd 100644 --- a/packages/taler-wallet-core/src/host-impl.qtart.ts +++ b/packages/taler-wallet-core/src/host-impl.qtart.ts @@ -110,7 +110,7 @@ async function makeSqliteDb( return { ...myBackend.accessStats, primitiveStatements: numStmt, - } + }; }, idbFactory: myBridgeIdbFactory, }; @@ -167,12 +167,15 @@ export async function createNativeWalletHost2( let dbResp: MakeDbResult; - if (args.persistentStoragePath && args.persistentStoragePath.endsWith(".json")) { + if ( + args.persistentStoragePath && + args.persistentStoragePath.endsWith(".json") + ) { logger.info("using JSON file DB backend (slow!)"); dbResp = await makeFileDb(args); } else { logger.info("using sqlite3 DB backend (experimental!)"); - dbResp = await makeSqliteDb(args) + dbResp = await makeSqliteDb(args); } const myIdbFactory: IDBFactory = dbResp.idbFactory as any as IDBFactory; @@ -189,22 +192,13 @@ export async function createNativeWalletHost2( }); } - const myVersionChange = (): Promise => { - logger.error("version change requested, should not happen"); - throw Error( - "BUG: wallet DB version change event can't happen with memory IDB", - ); - }; - - const myDb = await openTalerDatabase(myIdbFactory, myVersionChange); - let workerFactory; workerFactory = new SynchronousCryptoWorkerFactoryPlain(); const timer = new SetTimeoutTimerAPI(); const w = await Wallet.create( - myDb, + myIdbFactory, myHttpLib, timer, workerFactory, diff --git a/packages/taler-wallet-core/src/internal-wallet-state.ts b/packages/taler-wallet-core/src/internal-wallet-state.ts index 742af89a8..a189c9cb3 100644 --- a/packages/taler-wallet-core/src/internal-wallet-state.ts +++ b/packages/taler-wallet-core/src/internal-wallet-state.ts @@ -42,7 +42,7 @@ import { HttpRequestLibrary } from "@gnu-taler/taler-util/http"; import { TalerCryptoInterface } from "./crypto/cryptoImplementation.js"; import { ExchangeDetailsRecord, - ExchangeRecord, + ExchangeEntryRecord, RefreshReasonDetails, WalletStoresV1, } from "./db.js"; @@ -54,6 +54,7 @@ import { } from "./util/query.js"; import { TimerGroup } from "./util/timer.js"; import { WalletConfig } from "./wallet-api-types.js"; +import { IDBFactory } from "@gnu-taler/idb-bridge"; export const EXCHANGE_COINS_LOCK = "exchange-coins-lock"; export const EXCHANGE_RESERVES_LOCK = "exchange-reserves-lock"; @@ -108,7 +109,7 @@ export interface ExchangeOperations { ): Promise; getExchangeTrust( ws: InternalWalletState, - exchangeInfo: ExchangeRecord, + exchangeInfo: ExchangeEntryRecord, ): Promise; updateExchangeFromUrl( ws: InternalWalletState, @@ -118,7 +119,7 @@ export interface ExchangeOperations { cancellationToken?: CancellationToken; }, ): Promise<{ - exchange: ExchangeRecord; + exchange: ExchangeEntryRecord; exchangeDetails: ExchangeDetailsRecord; }>; } @@ -203,6 +204,9 @@ export interface InternalWalletState { denomPubHash: string, ): Promise; + ensureWalletDbOpen(): Promise; + + idb: IDBFactory; db: DbAccess; http: HttpRequestLibrary; diff --git a/packages/taler-wallet-core/src/operations/backup/export.ts b/packages/taler-wallet-core/src/operations/backup/export.ts deleted file mode 100644 index c9446a05f..000000000 --- a/packages/taler-wallet-core/src/operations/backup/export.ts +++ /dev/null @@ -1,586 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2020 Taler Systems SA - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see - */ - -/** - * 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 a53b624e8..000000000 --- a/packages/taler-wallet-core/src/operations/backup/import.ts +++ /dev/null @@ -1,875 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2020 Taler Systems SA - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see - */ - -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, - }, - permanent: true, - lastUpdate: undefined, - nextUpdate: TalerPreciseTimestamp.now(), - nextRefreshCheck: TalerPreciseTimestamp.now(), - lastKeysEtag: undefined, - lastWireEtag: undefined, - }); - } - - for (const backupExchangeDetails of backupBlob.exchange_details) { - const existingExchangeDetails = - await tx.exchangeDetails.indexes.byPointer.get([ - backupExchangeDetails.base_url, - backupExchangeDetails.currency, - backupExchangeDetails.master_public_key, - ]); - - if (!existingExchangeDetails) { - const wireInfo: WireInfo = { - accounts: backupExchangeDetails.accounts.map((x) => ({ - master_sig: x.master_sig, - payto_uri: x.payto_uri, - })), - feesForType: {}, - }; - for (const fee of backupExchangeDetails.wire_fees) { - const w = (wireInfo.feesForType[fee.wire_type] ??= []); - w.push({ - closingFee: Amounts.stringify(fee.closing_fee), - endStamp: fee.end_stamp, - sig: fee.sig, - startStamp: fee.start_stamp, - wireFee: Amounts.stringify(fee.wire_fee), - }); - } - let tosAccepted = undefined; - if ( - backupExchangeDetails.tos_accepted_etag && - backupExchangeDetails.tos_accepted_timestamp - ) { - tosAccepted = { - etag: backupExchangeDetails.tos_accepted_etag, - timestamp: backupExchangeDetails.tos_accepted_timestamp, - }; - } - await tx.exchangeDetails.put({ - exchangeBaseUrl: backupExchangeDetails.base_url, - wireInfo, - currency: backupExchangeDetails.currency, - auditors: backupExchangeDetails.auditors.map((x) => ({ - auditor_pub: x.auditor_pub, - auditor_url: x.auditor_url, - denomination_keys: x.denomination_keys, - })), - masterPublicKey: backupExchangeDetails.master_public_key, - protocolVersionRange: backupExchangeDetails.protocol_version, - reserveClosingDelay: backupExchangeDetails.reserve_closing_delay, - tosCurrentEtag: backupExchangeDetails.tos_accepted_etag || "", - tosAccepted, - globalFees: backupExchangeDetails.global_fees.map((x) => ({ - accountFee: Amounts.stringify(x.accountFee), - historyFee: Amounts.stringify(x.historyFee), - purseFee: Amounts.stringify(x.purseFee), - endDate: x.endDate, - historyTimeout: x.historyTimeout, - signature: x.signature, - purseLimit: x.purseLimit, - purseTimeout: x.purseTimeout, - startDate: x.startDate, - })), - }); - } - - for (const backupDenomination of backupExchangeDetails.denominations) { - if (backupDenomination.denom_pub.cipher !== DenomKeyType.Rsa) { - throw Error("unsupported cipher"); - } - const denomPubHash = - cryptoComp.rsaDenomPubToHash[ - backupDenomination.denom_pub.rsa_public_key - ]; - checkLogicInvariant(!!denomPubHash); - const existingDenom = await tx.denominations.get([ - backupExchangeDetails.base_url, - denomPubHash, - ]); - if (!existingDenom) { - const value = Amounts.parseOrThrow(backupDenomination.value); - - await tx.denominations.put({ - denomPub: backupDenomination.denom_pub, - denomPubHash: denomPubHash, - exchangeBaseUrl: backupExchangeDetails.base_url, - exchangeMasterPub: backupExchangeDetails.master_public_key, - fees: { - feeDeposit: Amounts.stringify(backupDenomination.fee_deposit), - feeRefresh: Amounts.stringify(backupDenomination.fee_refresh), - feeRefund: Amounts.stringify(backupDenomination.fee_refund), - feeWithdraw: Amounts.stringify(backupDenomination.fee_withdraw), - }, - isOffered: backupDenomination.is_offered, - isRevoked: backupDenomination.is_revoked, - masterSig: backupDenomination.master_sig, - stampExpireDeposit: backupDenomination.stamp_expire_deposit, - stampExpireLegal: backupDenomination.stamp_expire_legal, - stampExpireWithdraw: backupDenomination.stamp_expire_withdraw, - stampStart: backupDenomination.stamp_start, - verificationStatus: DenominationVerificationStatus.VerifiedGood, - currency: value.currency, - amountFrac: value.fraction, - amountVal: value.value, - listIssueDate: backupDenomination.list_issue_date, - }); - } - for (const backupCoin of backupDenomination.coins) { - await importCoin(ws, tx, cryptoComp, { - backupCoin, - denomPubHash, - exchangeBaseUrl: backupExchangeDetails.base_url, - }); - } - } - } - - for (const backupWg of backupBlob.withdrawal_groups) { - const reservePub = cryptoComp.reservePrivToPub[backupWg.reserve_priv]; - checkLogicInvariant(!!reservePub); - const ts = constructTombstone({ - tag: TombstoneTag.DeleteReserve, - reservePub, - }); - if (tombstoneSet.has(ts)) { - continue; - } - const existingWg = await tx.withdrawalGroups.get( - backupWg.withdrawal_group_id, - ); - if (existingWg) { - continue; - } - let wgInfo: WgInfo; - switch (backupWg.info.type) { - case BackupWgType.BankIntegrated: - wgInfo = { - withdrawalType: WithdrawalRecordType.BankIntegrated, - bankInfo: { - exchangePaytoUri: backupWg.info.exchange_payto_uri, - talerWithdrawUri: backupWg.info.taler_withdraw_uri, - confirmUrl: backupWg.info.confirm_url, - timestampBankConfirmed: backupWg.info.timestamp_bank_confirmed, - timestampReserveInfoPosted: - backupWg.info.timestamp_reserve_info_posted, - }, - }; - break; - case BackupWgType.BankManual: - wgInfo = { - withdrawalType: WithdrawalRecordType.BankManual, - }; - break; - case BackupWgType.PeerPullCredit: - wgInfo = { - withdrawalType: WithdrawalRecordType.PeerPullCredit, - contractTerms: backupWg.info.contract_terms, - contractPriv: backupWg.info.contract_priv, - }; - break; - case BackupWgType.PeerPushCredit: - wgInfo = { - withdrawalType: WithdrawalRecordType.PeerPushCredit, - contractTerms: backupWg.info.contract_terms, - }; - break; - case BackupWgType.Recoup: - wgInfo = { - withdrawalType: WithdrawalRecordType.Recoup, - }; - break; - default: - assertUnreachable(backupWg.info); - } - const instructedAmount = Amounts.parseOrThrow( - backupWg.instructed_amount, - ); - await tx.withdrawalGroups.put({ - withdrawalGroupId: backupWg.withdrawal_group_id, - exchangeBaseUrl: backupWg.exchange_base_url, - instructedAmount: Amounts.stringify(instructedAmount), - secretSeed: backupWg.secret_seed, - denomsSel: await getDenomSelStateFromBackup( - tx, - instructedAmount.currency, - backupWg.exchange_base_url, - backupWg.selected_denoms, - ), - denomSelUid: backupWg.selected_denoms_uid, - rawWithdrawalAmount: Amounts.stringify( - backupWg.raw_withdrawal_amount, - ), - effectiveWithdrawalAmount: Amounts.stringify( - backupWg.effective_withdrawal_amount, - ), - reservePriv: backupWg.reserve_priv, - reservePub, - status: backupWg.timestamp_finish - ? WithdrawalGroupStatus.Finished - : WithdrawalGroupStatus.PendingQueryingStatus, // FIXME! - timestampStart: backupWg.timestamp_created, - wgInfo, - restrictAge: backupWg.restrict_age, - senderWire: undefined, // FIXME! - timestampFinish: backupWg.timestamp_finish, - }); - } - - for (const backupPurchase of backupBlob.purchases) { - const ts = constructTombstone({ - tag: TombstoneTag.DeletePayment, - proposalId: backupPurchase.proposal_id, - }); - if (tombstoneSet.has(ts)) { - continue; - } - const existingPurchase = await tx.purchases.get( - backupPurchase.proposal_id, - ); - let proposalStatus: PurchaseStatus; - switch (backupPurchase.proposal_status) { - case BackupProposalStatus.Paid: - proposalStatus = PurchaseStatus.Done; - break; - case BackupProposalStatus.Shared: - proposalStatus = PurchaseStatus.DialogShared; - break; - case BackupProposalStatus.Proposed: - proposalStatus = PurchaseStatus.DialogProposed; - break; - case BackupProposalStatus.PermanentlyFailed: - proposalStatus = PurchaseStatus.AbortedIncompletePayment; - break; - case BackupProposalStatus.Refused: - proposalStatus = PurchaseStatus.AbortedProposalRefused; - break; - case BackupProposalStatus.Repurchase: - proposalStatus = PurchaseStatus.RepurchaseDetected; - break; - default: { - const error: never = backupPurchase.proposal_status; - throw Error(`backup status ${error} is not handled`); - } - } - if (!existingPurchase) { - //const refunds: { [refundKey: string]: WalletRefundItem } = {}; - // for (const backupRefund of backupPurchase.refunds) { - // const key = `${backupRefund.coin_pub}-${backupRefund.rtransaction_id}`; - // const coin = await tx.coins.get(backupRefund.coin_pub); - // checkBackupInvariant(!!coin); - // const denom = await tx.denominations.get([ - // coin.exchangeBaseUrl, - // coin.denomPubHash, - // ]); - // checkBackupInvariant(!!denom); - // const common = { - // coinPub: backupRefund.coin_pub, - // executionTime: backupRefund.execution_time, - // obtainedTime: backupRefund.obtained_time, - // refundAmount: Amounts.stringify(backupRefund.refund_amount), - // refundFee: Amounts.stringify(denom.fees.feeRefund), - // rtransactionId: backupRefund.rtransaction_id, - // totalRefreshCostBound: Amounts.stringify( - // backupRefund.total_refresh_cost_bound, - // ), - // }; - // switch (backupRefund.type) { - // case BackupRefundState.Applied: - // refunds[key] = { - // type: RefundState.Applied, - // ...common, - // }; - // break; - // case BackupRefundState.Failed: - // refunds[key] = { - // type: RefundState.Failed, - // ...common, - // }; - // break; - // case BackupRefundState.Pending: - // refunds[key] = { - // type: RefundState.Pending, - // ...common, - // }; - // break; - // } - // } - const parsedContractTerms = codecForMerchantContractTerms().decode( - backupPurchase.contract_terms_raw, - ); - const amount = Amounts.parseOrThrow(parsedContractTerms.amount); - const contractTermsHash = - cryptoComp.proposalIdToContractTermsHash[ - backupPurchase.proposal_id - ]; - let maxWireFee: AmountJson; - if (parsedContractTerms.max_wire_fee) { - maxWireFee = Amounts.parseOrThrow(parsedContractTerms.max_wire_fee); - } else { - maxWireFee = Amounts.zeroOfCurrency(amount.currency); - } - const download: ProposalDownloadInfo = { - contractTermsHash, - contractTermsMerchantSig: backupPurchase.merchant_sig!, - currency: amount.currency, - fulfillmentUrl: backupPurchase.contract_terms_raw.fulfillment_url, - }; - - const contractData = extractContractData( - backupPurchase.contract_terms_raw, - contractTermsHash, - download.contractTermsMerchantSig, - ); - - let payInfo: PurchasePayInfo | undefined = undefined; - if (backupPurchase.pay_info) { - payInfo = { - payCoinSelection: await recoverPayCoinSelection( - tx, - contractData, - backupPurchase.pay_info, - ), - payCoinSelectionUid: backupPurchase.pay_info.pay_coins_uid, - totalPayCost: Amounts.stringify( - backupPurchase.pay_info.total_pay_cost, - ), - }; - } - - await tx.purchases.put({ - proposalId: backupPurchase.proposal_id, - noncePriv: backupPurchase.nonce_priv, - noncePub: - cryptoComp.proposalNoncePrivToPub[backupPurchase.nonce_priv], - autoRefundDeadline: TalerProtocolTimestamp.never(), - timestampAccept: backupPurchase.timestamp_accepted, - timestampFirstSuccessfulPay: - backupPurchase.timestamp_first_successful_pay, - timestampLastRefundStatus: undefined, - merchantPaySig: backupPurchase.merchant_pay_sig, - posConfirmation: backupPurchase.pos_confirmation, - lastSessionId: undefined, - download, - //refunds, - claimToken: backupPurchase.claim_token, - downloadSessionId: backupPurchase.download_session_id, - merchantBaseUrl: backupPurchase.merchant_base_url, - orderId: backupPurchase.order_id, - payInfo, - refundAmountAwaiting: undefined, - repurchaseProposalId: backupPurchase.repurchase_proposal_id, - purchaseStatus: proposalStatus, - timestamp: backupPurchase.timestamp_proposed, - shared: backupPurchase.shared, - }); - } - } - - for (const backupRefreshGroup of backupBlob.refresh_groups) { - const ts = constructTombstone({ - tag: TombstoneTag.DeleteRefreshGroup, - refreshGroupId: backupRefreshGroup.refresh_group_id, - }); - if (tombstoneSet.has(ts)) { - continue; - } - const existingRg = await tx.refreshGroups.get( - backupRefreshGroup.refresh_group_id, - ); - if (!existingRg) { - let reason: RefreshReason; - switch (backupRefreshGroup.reason) { - case BackupRefreshReason.AbortPay: - reason = RefreshReason.AbortPay; - break; - case BackupRefreshReason.BackupRestored: - reason = RefreshReason.BackupRestored; - break; - case BackupRefreshReason.Manual: - reason = RefreshReason.Manual; - break; - case BackupRefreshReason.Pay: - reason = RefreshReason.PayMerchant; - break; - case BackupRefreshReason.Recoup: - reason = RefreshReason.Recoup; - break; - case BackupRefreshReason.Refund: - reason = RefreshReason.Refund; - break; - case BackupRefreshReason.Scheduled: - reason = RefreshReason.Scheduled; - break; - } - const refreshSessionPerCoin: (RefreshSessionRecord | undefined)[] = - []; - for (const oldCoin of backupRefreshGroup.old_coins) { - const c = await tx.coins.get(oldCoin.coin_pub); - checkBackupInvariant(!!c); - const d = await tx.denominations.get([ - c.exchangeBaseUrl, - c.denomPubHash, - ]); - checkBackupInvariant(!!d); - - if (oldCoin.refresh_session) { - const denomSel = await getDenomSelStateFromBackup( - tx, - d.currency, - c.exchangeBaseUrl, - oldCoin.refresh_session.new_denoms, - ); - refreshSessionPerCoin.push({ - sessionSecretSeed: oldCoin.refresh_session.session_secret_seed, - norevealIndex: oldCoin.refresh_session.noreveal_index, - newDenoms: oldCoin.refresh_session.new_denoms.map((x) => ({ - count: x.count, - denomPubHash: x.denom_pub_hash, - })), - amountRefreshOutput: Amounts.stringify(denomSel.totalCoinValue), - }); - } else { - refreshSessionPerCoin.push(undefined); - } - } - await tx.refreshGroups.put({ - timestampFinished: backupRefreshGroup.timestamp_finish, - timestampCreated: backupRefreshGroup.timestamp_created, - refreshGroupId: backupRefreshGroup.refresh_group_id, - currency: Amounts.currencyOf( - backupRefreshGroup.old_coins[0].input_amount, - ), - reason, - lastErrorPerCoin: {}, - oldCoinPubs: backupRefreshGroup.old_coins.map((x) => x.coin_pub), - statusPerCoin: backupRefreshGroup.old_coins.map((x) => - x.finished - ? RefreshCoinStatus.Finished - : RefreshCoinStatus.Pending, - ), - operationStatus: backupRefreshGroup.timestamp_finish - ? RefreshOperationStatus.Finished - : RefreshOperationStatus.Pending, - inputPerCoin: backupRefreshGroup.old_coins.map( - (x) => x.input_amount, - ), - estimatedOutputPerCoin: backupRefreshGroup.old_coins.map( - (x) => x.estimated_output_amount, - ), - refreshSessionPerCoin, - }); - } - } - - for (const backupTip of backupBlob.tips) { - const ts = constructTombstone({ - tag: TombstoneTag.DeleteReward, - walletTipId: backupTip.wallet_tip_id, - }); - if (tombstoneSet.has(ts)) { - continue; - } - const existingTip = await tx.rewards.get(backupTip.wallet_tip_id); - if (!existingTip) { - const tipAmountRaw = Amounts.parseOrThrow(backupTip.tip_amount_raw); - const denomsSel = await getDenomSelStateFromBackup( - tx, - tipAmountRaw.currency, - backupTip.exchange_base_url, - backupTip.selected_denoms, - ); - await tx.rewards.put({ - acceptedTimestamp: backupTip.timestamp_accepted, - createdTimestamp: backupTip.timestamp_created, - denomsSel, - next_url: backupTip.next_url, - exchangeBaseUrl: backupTip.exchange_base_url, - merchantBaseUrl: backupTip.exchange_base_url, - merchantRewardId: backupTip.merchant_tip_id, - pickedUpTimestamp: backupTip.timestamp_finished, - secretSeed: backupTip.secret_seed, - rewardAmountEffective: Amounts.stringify(denomsSel.totalCoinValue), - rewardAmountRaw: Amounts.stringify(tipAmountRaw), - rewardExpiration: backupTip.timestamp_expiration, - walletRewardId: backupTip.wallet_tip_id, - denomSelUid: backupTip.selected_denoms_uid, - status: RewardRecordStatus.Done, // FIXME! - }); - } - } - - // We now process tombstones. - // The import code above should already prevent - // importing things that are tombstoned, - // but we do tombstone processing last just to be sure. - - for (const tombstone of tombstoneSet) { - const [type, ...rest] = tombstone.split(":"); - if (type === TombstoneTag.DeleteDepositGroup) { - await tx.depositGroups.delete(rest[0]); - } else if (type === TombstoneTag.DeletePayment) { - await tx.purchases.delete(rest[0]); - } else if (type === TombstoneTag.DeleteRefreshGroup) { - await tx.refreshGroups.delete(rest[0]); - } else if (type === TombstoneTag.DeleteRefund) { - // Nothing required, will just prevent display - // in the transactions list - } else if (type === TombstoneTag.DeleteReward) { - await tx.rewards.delete(rest[0]); - } else if (type === TombstoneTag.DeleteWithdrawalGroup) { - await tx.withdrawalGroups.delete(rest[0]); - } else { - logger.warn(`unable to process tombstone of type '${type}'`); - } - } - }); -} diff --git a/packages/taler-wallet-core/src/operations/backup/index.ts b/packages/taler-wallet-core/src/operations/backup/index.ts index e35765165..a5e8dbd42 100644 --- a/packages/taler-wallet-core/src/operations/backup/index.ts +++ b/packages/taler-wallet-core/src/operations/backup/index.ts @@ -43,7 +43,6 @@ import { TalerErrorDetail, TalerPreciseTimestamp, URL, - WalletBackupContentV1, buildCodecForObject, buildCodecForUnion, bytesToString, @@ -99,9 +98,8 @@ import { TaskIdentifiers, } from "../common.js"; import { checkPaymentByProposalId, preparePayForUri } from "../pay-merchant.js"; -import { exportBackup } from "./export.js"; -import { BackupCryptoPrecomputedData, importBackup } from "./import.js"; -import { getWalletBackupState, provideBackupState } from "./state.js"; +import { WalletStoresV1 } from "../../db.js"; +import { GetReadOnlyAccess } from "../../util/query.js"; const logger = new Logger("operations/backup.ts"); @@ -131,7 +129,7 @@ const magic = "TLRWBK01"; */ export async function encryptBackup( config: WalletBackupConfState, - blob: WalletBackupContentV1, + blob: any, ): Promise { 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/operations/common.ts b/packages/taler-wallet-core/src/operations/common.ts index 7a8b78b53..e96beb5b2 100644 --- a/packages/taler-wallet-core/src/operations/common.ts +++ b/packages/taler-wallet-core/src/operations/common.ts @@ -30,6 +30,7 @@ import { ExchangeEntryStatus, ExchangeListItem, ExchangeTosStatus, + ExchangeUpdateStatus, getErrorDetailFromException, j2s, Logger, @@ -47,7 +48,7 @@ import { WalletStoresV1, CoinRecord, ExchangeDetailsRecord, - ExchangeRecord, + ExchangeEntryRecord, BackupProviderRecord, DepositGroupRecord, PeerPullPaymentIncomingRecord, @@ -59,6 +60,8 @@ import { RefreshGroupRecord, RewardRecord, WithdrawalGroupRecord, + ExchangeEntryDbUpdateStatus, + ExchangeEntryDbRecordStatus, } from "../db.js"; import { makeErrorDetail, TalerError } from "@gnu-taler/taler-util"; import { InternalWalletState } from "../internal-wallet-state.js"; @@ -529,16 +532,16 @@ export function getExchangeTosStatus( exchangeDetails: ExchangeDetailsRecord, ): ExchangeTosStatus { if (!exchangeDetails.tosAccepted) { - return ExchangeTosStatus.New; + return ExchangeTosStatus.Proposed; } if (exchangeDetails.tosAccepted?.etag == exchangeDetails.tosCurrentEtag) { return ExchangeTosStatus.Accepted; } - return ExchangeTosStatus.Changed; + return ExchangeTosStatus.Proposed; } export function makeExchangeListItem( - r: ExchangeRecord, + r: ExchangeEntryRecord, exchangeDetails: ExchangeDetailsRecord | undefined, lastError: TalerErrorDetail | undefined, ): ExchangeListItem { @@ -547,30 +550,57 @@ export function makeExchangeListItem( error: lastError, } : undefined; - if (!exchangeDetails) { - return { - exchangeBaseUrl: r.baseUrl, - currency: undefined, - tosStatus: ExchangeTosStatus.Unknown, - paytoUris: [], - exchangeStatus: ExchangeEntryStatus.Unknown, - permanent: r.permanent, - ageRestrictionOptions: [], - lastUpdateErrorInfo, - }; + + let exchangeUpdateStatus: ExchangeUpdateStatus; + switch (r.updateStatus) { + case ExchangeEntryDbUpdateStatus.Failed: + exchangeUpdateStatus = ExchangeUpdateStatus.Failed; + break; + case ExchangeEntryDbUpdateStatus.Initial: + exchangeUpdateStatus = ExchangeUpdateStatus.Initial; + break; + case ExchangeEntryDbUpdateStatus.InitialUpdate: + exchangeUpdateStatus = ExchangeUpdateStatus.InitialUpdate; + break; + case ExchangeEntryDbUpdateStatus.OutdatedUpdate: + exchangeUpdateStatus = ExchangeUpdateStatus.OutdatedUpdate; + break; + case ExchangeEntryDbUpdateStatus.Ready: + exchangeUpdateStatus = ExchangeUpdateStatus.Ready; + break; + case ExchangeEntryDbUpdateStatus.ReadyUpdate: + exchangeUpdateStatus = ExchangeUpdateStatus.ReadyUpdate; + break; + case ExchangeEntryDbUpdateStatus.Suspended: + exchangeUpdateStatus = ExchangeUpdateStatus.Suspended; + break; } - let exchangeStatus; - exchangeStatus = ExchangeEntryStatus.Ok; + + let exchangeEntryStatus: ExchangeEntryStatus; + switch (r.entryStatus) { + case ExchangeEntryDbRecordStatus.Ephemeral: + exchangeEntryStatus = ExchangeEntryStatus.Ephemeral; + break; + case ExchangeEntryDbRecordStatus.Preset: + exchangeEntryStatus = ExchangeEntryStatus.Preset; + break; + case ExchangeEntryDbRecordStatus.Used: + exchangeEntryStatus = ExchangeEntryStatus.Used; + break; + } + return { exchangeBaseUrl: r.baseUrl, - currency: exchangeDetails.currency, - tosStatus: getExchangeTosStatus(exchangeDetails), - paytoUris: exchangeDetails.wireInfo.accounts.map((x) => x.payto_uri), - exchangeStatus, - permanent: r.permanent, - ageRestrictionOptions: exchangeDetails.ageMask + currency: exchangeDetails?.currency, + exchangeUpdateStatus, + exchangeEntryStatus, + tosStatus: exchangeDetails + ? getExchangeTosStatus(exchangeDetails) + : ExchangeTosStatus.Pending, + ageRestrictionOptions: exchangeDetails?.ageMask ? AgeRestriction.getAgeGroupsFromMask(exchangeDetails.ageMask) : [], + paytoUris: exchangeDetails?.wireInfo.accounts.map((x) => x.payto_uri) ?? [], lastUpdateErrorInfo, }; } @@ -892,13 +922,13 @@ export namespace TaskIdentifiers { export function forWithdrawal(wg: WithdrawalGroupRecord): TaskId { return `${PendingTaskType.Withdraw}:${wg.withdrawalGroupId}` as TaskId; } - export function forExchangeUpdate(exch: ExchangeRecord): TaskId { + export function forExchangeUpdate(exch: ExchangeEntryRecord): TaskId { return `${PendingTaskType.ExchangeUpdate}:${exch.baseUrl}` as TaskId; } export function forExchangeUpdateFromUrl(exchBaseUrl: string): TaskId { return `${PendingTaskType.ExchangeUpdate}:${exchBaseUrl}` as TaskId; } - export function forExchangeCheckRefresh(exch: ExchangeRecord): TaskId { + export function forExchangeCheckRefresh(exch: ExchangeEntryRecord): TaskId { return `${PendingTaskType.ExchangeCheckRefresh}:${exch.baseUrl}` as TaskId; } export function forTipPickup(tipRecord: RewardRecord): TaskId { diff --git a/packages/taler-wallet-core/src/operations/exchanges.ts b/packages/taler-wallet-core/src/operations/exchanges.ts index c6b46e360..311a71a6e 100644 --- a/packages/taler-wallet-core/src/operations/exchanges.ts +++ b/packages/taler-wallet-core/src/operations/exchanges.ts @@ -32,6 +32,7 @@ import { encodeCrock, ExchangeAuditor, ExchangeDenomination, + ExchangeEntryStatus, ExchangeGlobalFees, ExchangeSignKeyJson, ExchangeWireJson, @@ -66,10 +67,15 @@ import { DenominationRecord, DenominationVerificationStatus, ExchangeDetailsRecord, - ExchangeRecord, + ExchangeEntryRecord, WalletStoresV1, } from "../db.js"; -import { isWithdrawableDenom } from "../index.js"; +import { + ExchangeEntryDbRecordStatus, + ExchangeEntryDbUpdateStatus, + isWithdrawableDenom, + WalletDbReadWriteTransaction, +} from "../index.js"; import { InternalWalletState, TrustInfo } from "../internal-wallet-state.js"; import { checkDbInvariant } from "../util/invariants.js"; import { @@ -326,6 +332,26 @@ export async function downloadExchangeInfo( }; } +export async function addPresetExchangeEntry( + tx: WalletDbReadWriteTransaction<"exchanges">, + exchangeBaseUrl: string, +): Promise { + let exchange = await tx.exchanges.get(exchangeBaseUrl); + if (!exchange) { + const r: ExchangeEntryRecord = { + entryStatus: ExchangeEntryDbRecordStatus.Preset, + updateStatus: ExchangeEntryDbUpdateStatus.Initial, + baseUrl: exchangeBaseUrl, + detailsPointer: undefined, + lastUpdate: undefined, + lastKeysEtag: undefined, + nextRefreshCheckStampMs: AbsoluteTime.getStampMsNever(), + nextUpdateStampMs: AbsoluteTime.getStampMsNever(), + }; + await tx.exchanges.put(r); + } +} + export async function provideExchangeRecordInTx( ws: InternalWalletState, tx: GetReadWriteAccess<{ @@ -335,20 +361,20 @@ export async function provideExchangeRecordInTx( baseUrl: string, now: AbsoluteTime, ): Promise<{ - exchange: ExchangeRecord; + exchange: ExchangeEntryRecord; exchangeDetails: ExchangeDetailsRecord | undefined; }> { let exchange = await tx.exchanges.get(baseUrl); if (!exchange) { - const r: ExchangeRecord = { - permanent: true, + const r: ExchangeEntryRecord = { + entryStatus: ExchangeEntryDbRecordStatus.Ephemeral, + updateStatus: ExchangeEntryDbUpdateStatus.InitialUpdate, baseUrl: baseUrl, detailsPointer: undefined, lastUpdate: undefined, - nextUpdate: AbsoluteTime.toPreciseTimestamp(now), - nextRefreshCheck: AbsoluteTime.toPreciseTimestamp(now), + nextUpdateStampMs: AbsoluteTime.getStampMsNever(), + nextRefreshCheckStampMs: AbsoluteTime.getStampMsNever(), lastKeysEtag: undefined, - lastWireEtag: undefined, }; await tx.exchanges.put(r); exchange = r; @@ -534,6 +560,10 @@ export async function downloadTosFromAcceptedFormat( ); } +/** + * FIXME: Split this into two parts: (a) triggering the exchange + * to be updated and (b) waiting for the update to finish. + */ export async function updateExchangeFromUrl( ws: InternalWalletState, baseUrl: string, @@ -543,7 +573,7 @@ export async function updateExchangeFromUrl( cancellationToken?: CancellationToken; } = {}, ): Promise<{ - exchange: ExchangeRecord; + exchange: ExchangeEntryRecord; exchangeDetails: ExchangeDetailsRecord; }> { const canonUrl = canonicalizeBaseUrl(baseUrl); @@ -613,7 +643,7 @@ export async function updateExchangeFromUrlHandler( !forceNow && exchangeDetails !== undefined && !AbsoluteTime.isExpired( - AbsoluteTime.fromPreciseTimestamp(exchange.nextUpdate), + AbsoluteTime.fromStampMs(exchange.nextUpdateStampMs), ) ) { logger.trace("using existing exchange info"); @@ -755,11 +785,11 @@ export async function updateExchangeFromUrlHandler( newDetails.rowId = existingDetails.rowId; } r.lastUpdate = TalerPreciseTimestamp.now(); - r.nextUpdate = AbsoluteTime.toPreciseTimestamp( + r.nextUpdateStampMs = AbsoluteTime.toStampMs( AbsoluteTime.fromProtocolTimestamp(keysInfo.expiry), ); // New denominations might be available. - r.nextRefreshCheck = TalerPreciseTimestamp.now(); + r.nextRefreshCheckStampMs = AbsoluteTime.getStampMsNow(); if (detailsPointerChanged) { r.detailsPointer = { currency: newDetails.currency, @@ -948,7 +978,7 @@ export async function getExchangePaytoUri( */ export async function getExchangeTrust( ws: InternalWalletState, - exchangeInfo: ExchangeRecord, + exchangeInfo: ExchangeEntryRecord, ): Promise { let isTrusted = false; let isAudited = false; diff --git a/packages/taler-wallet-core/src/operations/pending.ts b/packages/taler-wallet-core/src/operations/pending.ts index 6c6546f83..e37e45c16 100644 --- a/packages/taler-wallet-core/src/operations/pending.ts +++ b/packages/taler-wallet-core/src/operations/pending.ts @@ -45,6 +45,7 @@ import { PeerPushPaymentIncomingRecord, RefundGroupRecord, RefundGroupStatus, + ExchangeEntryDbUpdateStatus, } from "../db.js"; import { PendingOperationsResponse, @@ -81,19 +82,25 @@ async function gatherExchangePending( ws: InternalWalletState, tx: GetReadOnlyAccess<{ exchanges: typeof WalletStoresV1.exchanges; - exchangeDetails: typeof WalletStoresV1.exchangeDetails; operationRetries: typeof WalletStoresV1.operationRetries; }>, now: AbsoluteTime, resp: PendingOperationsResponse, ): Promise { - // FIXME: We should do a range query here based on the update time. + // FIXME: We should do a range query here based on the update time + // and/or the entry state. await tx.exchanges.iter().forEachAsync(async (exch) => { + switch (exch.updateStatus) { + case ExchangeEntryDbUpdateStatus.Initial: + case ExchangeEntryDbUpdateStatus.Suspended: + case ExchangeEntryDbUpdateStatus.Failed: + return; + } const opTag = TaskIdentifiers.forExchangeUpdate(exch); let opr = await tx.operationRetries.get(opTag); const timestampDue = opr?.retryInfo.nextRetry ?? - AbsoluteTime.fromPreciseTimestamp(exch.nextUpdate); + AbsoluteTime.fromStampMs(exch.nextUpdateStampMs); resp.pendingOperations.push({ type: PendingTaskType.ExchangeUpdate, ...getPendingCommon(ws, opTag, timestampDue), @@ -108,7 +115,7 @@ async function gatherExchangePending( resp.pendingOperations.push({ type: PendingTaskType.ExchangeCheckRefresh, ...getPendingCommon(ws, opTag, timestampDue), - timestampDue: AbsoluteTime.fromPreciseTimestamp(exch.nextRefreshCheck), + timestampDue: AbsoluteTime.fromStampMs(exch.nextRefreshCheckStampMs), givesLifeness: false, exchangeBaseUrl: exch.baseUrl, }); @@ -184,8 +191,9 @@ export async function iterRecordsForWithdrawal( WithdrawalGroupStatus.PendingRegisteringBank, WithdrawalGroupStatus.PendingAml, ); - withdrawalGroupRecords = - await tx.withdrawalGroups.indexes.byStatus.getAll(range); + withdrawalGroupRecords = await tx.withdrawalGroups.indexes.byStatus.getAll( + range, + ); } else { withdrawalGroupRecords = await tx.withdrawalGroups.indexes.byStatus.getAll(); @@ -344,12 +352,8 @@ export async function iterRecordsForRefund( f: (r: RefundGroupRecord) => Promise, ): Promise { if (filter.onlyState === "nonfinal") { - const keyRange = GlobalIDB.KeyRange.only( - RefundGroupStatus.Pending - ); - await tx.refundGroups.indexes.byStatus - .iter(keyRange) - .forEachAsync(f); + const keyRange = GlobalIDB.KeyRange.only(RefundGroupStatus.Pending); + await tx.refundGroups.indexes.byStatus.iter(keyRange).forEachAsync(f); } else { await tx.refundGroups.iter().forEachAsync(f); } diff --git a/packages/taler-wallet-core/src/operations/refresh.ts b/packages/taler-wallet-core/src/operations/refresh.ts index 72d1a2725..fb356f0fc 100644 --- a/packages/taler-wallet-core/src/operations/refresh.ts +++ b/packages/taler-wallet-core/src/operations/refresh.ts @@ -1190,14 +1190,14 @@ export async function autoRefresh( `created refresh group for auto-refresh (${res.refreshGroupId})`, ); } -// logger.trace( -// `current wallet time: ${AbsoluteTime.toIsoString(AbsoluteTime.now())}`, -// ); + // logger.trace( + // `current wallet time: ${AbsoluteTime.toIsoString(AbsoluteTime.now())}`, + // ); logger.trace( `next refresh check at ${AbsoluteTime.toIsoString(minCheckThreshold)}`, ); - exchange.nextRefreshCheck = - AbsoluteTime.toPreciseTimestamp(minCheckThreshold); + exchange.nextRefreshCheckStampMs = + AbsoluteTime.toStampMs(minCheckThreshold); await tx.exchanges.put(exchange); }); return TaskRunResult.finished(); diff --git a/packages/taler-wallet-core/src/operations/reward.ts b/packages/taler-wallet-core/src/operations/reward.ts index 69c888d7a..6f9d3ce85 100644 --- a/packages/taler-wallet-core/src/operations/reward.ts +++ b/packages/taler-wallet-core/src/operations/reward.ts @@ -150,14 +150,14 @@ export async function prepareTip( .mktx((x) => [x.rewards]) .runReadOnly(async (tx) => { return tx.rewards.indexes.byMerchantTipIdAndBaseUrl.get([ - res.merchantTipId, + res.merchantRewardId, res.merchantBaseUrl, ]); }); if (!tipRecord) { const tipStatusUrl = new URL( - `tips/${res.merchantTipId}`, + `rewards/${res.merchantRewardId}`, res.merchantBaseUrl, ); logger.trace("checking tip status from", tipStatusUrl.href); @@ -204,7 +204,7 @@ export async function prepareTip( next_url: tipPickupStatus.next_url, merchantBaseUrl: res.merchantBaseUrl, createdTimestamp: TalerPreciseTimestamp.now(), - merchantRewardId: res.merchantTipId, + merchantRewardId: res.merchantRewardId, rewardAmountEffective: Amounts.stringify(selectedDenoms.totalCoinValue), denomsSel: selectedDenoms, pickedUpTimestamp: undefined, diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts b/packages/taler-wallet-core/src/operations/withdraw.ts index 44817b389..a3d95fb5c 100644 --- a/packages/taler-wallet-core/src/operations/withdraw.ts +++ b/packages/taler-wallet-core/src/operations/withdraw.ts @@ -132,6 +132,8 @@ import { } from "../util/coinSelection.js"; import { ExchangeDetailsRecord, + ExchangeEntryDbRecordStatus, + ExchangeEntryDbUpdateStatus, PendingTaskType, isWithdrawableDenom, } from "../index.js"; @@ -2346,10 +2348,6 @@ export async function internalPerformCreateWithdrawalGroup( }>, prep: PrepareCreateWithdrawalGroupResult, ): Promise { - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.Withdrawal, - withdrawalGroupId: prep.withdrawalGroup.withdrawalGroupId, - }); const { withdrawalGroup } = prep; if (!prep.creationInfo) { return { withdrawalGroup, transitionInfo: undefined }; @@ -2366,6 +2364,7 @@ export async function internalPerformCreateWithdrawalGroup( const exchange = await tx.exchanges.get(withdrawalGroup.exchangeBaseUrl); if (exchange) { exchange.lastWithdrawal = TalerPreciseTimestamp.now(); + exchange.entryStatus = ExchangeEntryDbRecordStatus.Used; await tx.exchanges.put(exchange); } diff --git a/packages/taler-wallet-core/src/util/query.ts b/packages/taler-wallet-core/src/util/query.ts index 71f80f8aa..1c3ff6a2a 100644 --- a/packages/taler-wallet-core/src/util/query.ts +++ b/packages/taler-wallet-core/src/util/query.ts @@ -239,7 +239,7 @@ class ResultStream { export function openDatabase( idbFactory: IDBFactory, databaseName: string, - databaseVersion: number, + databaseVersion: number | undefined, onVersionChange: () => void, onUpgradeNeeded: ( db: IDBDatabase, @@ -257,7 +257,7 @@ export function openDatabase( req.onsuccess = (e) => { req.result.onversionchange = (evt: IDBVersionChangeEvent) => { logger.info( - `handling live db version change from ${evt.oldVersion} to ${evt.newVersion}`, + `handling versionchange on ${databaseName} from ${evt.oldVersion} to ${evt.newVersion}`, ); req.result.close(); onVersionChange(); @@ -274,6 +274,9 @@ export function openDatabase( if (!transaction) { throw Error("no transaction handle available in upgrade handler"); } + logger.info( + `handling upgradeneeded event on ${databaseName} from ${e.oldVersion} to ${e.newVersion}`, + ); onUpgradeNeeded(db, e.oldVersion, newVersion, transaction); }; }); @@ -376,8 +379,8 @@ export interface InsertResponse { export interface StoreReadWriteAccessor { get(key: IDBValidKey): Promise; iter(query?: IDBValidKey): ResultStream; - put(r: RecordType): Promise; - add(r: RecordType): Promise; + put(r: RecordType, key?: IDBValidKey): Promise; + add(r: RecordType, key?: IDBValidKey): Promise; delete(key: IDBValidKey): Promise; indexes: GetIndexReadWriteAccess; } @@ -652,15 +655,15 @@ function makeWriteContext( const req = tx.objectStore(storeName).openCursor(query); return new ResultStream(req); }, - async add(r) { - const req = tx.objectStore(storeName).add(r); + async add(r, k) { + const req = tx.objectStore(storeName).add(r, k); const key = await requestToPromise(req); return { key: key, }; }, - async put(r) { - const req = tx.objectStore(storeName).put(r); + async put(r, k) { + const req = tx.objectStore(storeName).put(r, k); const key = await requestToPromise(req); return { key: key, diff --git a/packages/taler-wallet-core/src/wallet-api-types.ts b/packages/taler-wallet-core/src/wallet-api-types.ts index 06ccdf6f3..4d9d40c43 100644 --- a/packages/taler-wallet-core/src/wallet-api-types.ts +++ b/packages/taler-wallet-core/src/wallet-api-types.ts @@ -106,7 +106,6 @@ import { UserAttentionsResponse, ValidateIbanRequest, ValidateIbanResponse, - WalletBackupContentV1, WalletCoreVersion, WalletCurrencyInfo, WithdrawFakebankRequest, @@ -116,6 +115,10 @@ import { SharePaymentResult, GetCurrencyInfoRequest, GetCurrencyInfoResponse, + StoredBackupList, + CreateStoredBackupResponse, + RecoverStoredBackupRequest, + DeleteStoredBackupRequest, } from "@gnu-taler/taler-util"; import { AuditorTrustRecord, WalletContractData } from "./db.js"; import { @@ -195,7 +198,6 @@ export enum WalletApiOperation { GenerateDepositGroupTxId = "generateDepositGroupTxId", CreateDepositGroup = "createDepositGroup", SetWalletDeviceId = "setWalletDeviceId", - ExportBackupPlain = "exportBackupPlain", WithdrawFakebank = "withdrawFakebank", ImportDb = "importDb", ExportDb = "exportDb", @@ -214,6 +216,10 @@ export enum WalletApiOperation { TestingWaitTransactionsFinal = "testingWaitTransactionsFinal", TestingWaitRefreshesFinal = "testingWaitRefreshesFinal", GetScopedCurrencyInfo = "getScopedCurrencyInfo", + ListStoredBackups = "listStoredBackups", + CreateStoredBackup = "createStoredBackup", + DeleteStoredBackup = "deleteStoredBackup", + RecoverStoredBackup = "recoverStoredBackup", } // group: Initialization @@ -713,13 +719,28 @@ export type SetWalletDeviceIdOp = { response: EmptyObject; }; -/** - * Export a backup JSON, mostly useful for testing. - */ -export type ExportBackupPlainOp = { - op: WalletApiOperation.ExportBackupPlain; +export type ListStoredBackupsOp = { + op: WalletApiOperation.ListStoredBackups; request: EmptyObject; - response: 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 194894e52..626409dd6 100644 --- a/packages/taler-wallet-core/src/wallet.ts +++ b/packages/taler-wallet-core/src/wallet.ts @@ -120,6 +120,7 @@ import { codecForSharePaymentRequest, GetCurrencyInfoResponse, codecForGetCurrencyInfoRequest, + CreateStoredBackupResponse, } from "@gnu-taler/taler-util"; import { HttpRequestLibrary, @@ -139,6 +140,8 @@ import { clearDatabase, exportDb, importDb, + openStoredBackupsDatabase, + openTalerDatabase, } from "./db.js"; import { DevExperimentHttpLib, applyDevExperiment } from "./dev-experiments.js"; import { @@ -157,7 +160,6 @@ import { getUserAttentionsUnreadCount, markAttentionRequestAsRead, } from "./operations/attention.js"; -import { exportBackup } from "./operations/backup/export.js"; import { addBackupProvider, codecForAddBackupProviderRequest, @@ -165,13 +167,12 @@ import { codecForRunBackupCycle, getBackupInfo, getBackupRecovery, - importBackupPlain, loadBackupRecovery, processBackupForProvider, removeBackupProvider, runBackupCycle, + setWalletDeviceId, } from "./operations/backup/index.js"; -import { setWalletDeviceId } from "./operations/backup/state.js"; import { getBalanceDetail, getBalances } from "./operations/balance.js"; import { TaskIdentifiers, @@ -189,6 +190,7 @@ import { } from "./operations/deposits.js"; import { acceptExchangeTermsOfService, + addPresetExchangeEntry, downloadTosFromAcceptedFormat, getExchangeDetails, getExchangeRequestTimeout, @@ -314,6 +316,7 @@ import { getMaxPeerPushAmount, convertWithdrawalAmount, } from "./util/instructedAmountConversion.js"; +import { IDBFactory } from "@gnu-taler/idb-bridge"; const logger = new Logger("wallet.ts"); @@ -340,9 +343,8 @@ async function callOperationHandler( return await processRecoupGroup(ws, pending.recoupGroupId); case PendingTaskType.ExchangeCheckRefresh: return await autoRefresh(ws, pending.exchangeBaseUrl); - case PendingTaskType.Deposit: { + case PendingTaskType.Deposit: return await processDepositGroup(ws, pending.depositGroupId); - } case PendingTaskType.Backup: return await processBackupForProvider(ws, pending.backupProviderBaseUrl); case PendingTaskType.PeerPushDebit: @@ -533,6 +535,7 @@ async function fillDefaults(ws: InternalWalletState): Promise { await tx.auditorTrust.put(c); } for (const baseUrl of ws.config.builtin.exchanges) { + await addPresetExchangeEntry(tx, baseUrl); const now = AbsoluteTime.now(); provideExchangeRecordInTx(ws, tx, baseUrl, now); } @@ -1021,6 +1024,23 @@ export async function getClientFromWalletState( return client; } +async function createStoredBackup( + ws: InternalWalletState, +): Promise { + const backup = await exportDb(ws.idb); + const backupsDb = await openStoredBackupsDatabase(ws.idb); + const name = `backup-${new Date().getTime()}`; + await backupsDb.mktxAll().runReadWrite(async (tx) => { + await tx.backupMeta.add({ + name, + }); + await tx.backupData.add(backup, name); + }); + return { + name, + }; +} + /** * Implementation of the "wallet-core" API. */ @@ -1037,6 +1057,14 @@ async function dispatchRequestInternal( // FIXME: Can we make this more type-safe by using the request/response type // definitions we already have? switch (operation) { + case WalletApiOperation.CreateStoredBackup: + return createStoredBackup(ws); + case WalletApiOperation.DeleteStoredBackup: + return {}; + case WalletApiOperation.ListStoredBackups: + return {}; + case WalletApiOperation.RecoverStoredBackup: + return {}; case WalletApiOperation.InitWallet: { logger.trace("initializing wallet"); ws.initCalled = true; @@ -1378,9 +1406,6 @@ async function dispatchRequestInternal( const req = codecForAcceptTipRequest().decode(payload); return await acceptTip(ws, req.walletRewardId); } - case WalletApiOperation.ExportBackupPlain: { - return exportBackup(ws); - } case WalletApiOperation.AddBackupProvider: { const req = codecForAddBackupProviderRequest().decode(payload); return await addBackupProvider(ws, req); @@ -1531,13 +1556,11 @@ async function dispatchRequestInternal( await clearDatabase(ws.db.idbHandle()); return {}; case WalletApiOperation.Recycle: { - const backup = await exportBackup(ws); - await clearDatabase(ws.db.idbHandle()); - await importBackupPlain(ws, backup); + throw Error("not implemented"); return {}; } case WalletApiOperation.ExportDb: { - const dbDump = await exportDb(ws.db.idbHandle()); + const dbDump = await exportDb(ws.idb); return dbDump; } case WalletApiOperation.ImportDb: { @@ -1616,7 +1639,7 @@ export function getVersion(ws: InternalWalletState): WalletCoreVersion { /** * Handle a request to the wallet-core API. */ -export async function handleCoreApiRequest( +async function handleCoreApiRequest( ws: InternalWalletState, operation: string, id: string, @@ -1652,14 +1675,14 @@ export class Wallet { private _client: WalletCoreApiClient | undefined; private constructor( - db: DbAccess, + idb: IDBFactory, http: HttpRequestLibrary, timer: TimerAPI, cryptoWorkerFactory: CryptoWorkerFactory, config?: WalletConfigParameter, ) { this.ws = new InternalWalletStateImpl( - db, + idb, http, timer, cryptoWorkerFactory, @@ -1675,21 +1698,20 @@ export class Wallet { } static async create( - db: DbAccess, + idb: IDBFactory, http: HttpRequestLibrary, timer: TimerAPI, cryptoWorkerFactory: CryptoWorkerFactory, config?: WalletConfigParameter, ): Promise { - const w = new Wallet(db, http, timer, cryptoWorkerFactory, config); + const w = new Wallet(idb, http, timer, cryptoWorkerFactory, config); w._client = await getClientFromWalletState(w.ws); return w; } public static defaultConfig: Readonly = { builtin: { - //exchanges: ["https://exchange.demo.taler.net/"], - exchanges: [], + exchanges: ["https://exchange.demo.taler.net/"], auditors: [ { currency: "KUDOS", @@ -1724,19 +1746,22 @@ export class Wallet { this.ws.stop(); } - runPending(): Promise { + async runPending(): Promise { + await this.ws.ensureWalletDbOpen(); return runPending(this.ws); } - runTaskLoop(opts?: RetryLoopOpts): Promise { + async runTaskLoop(opts?: RetryLoopOpts): Promise { + await this.ws.ensureWalletDbOpen(); return runTaskLoop(this.ws, opts); } - handleCoreApiRequest( + async handleCoreApiRequest( operation: string, id: string, payload: unknown, ): Promise { + await this.ws.ensureWalletDbOpen(); return handleCoreApiRequest(this.ws, operation, id, payload); } } @@ -1800,12 +1825,17 @@ class InternalWalletStateImpl implements InternalWalletState { config: Readonly; + private _db: DbAccess | undefined = undefined; + + get db(): DbAccess { + if (!this._db) { + throw Error("db not initialized"); + } + return this._db; + } + constructor( - // FIXME: Make this a getter and make - // the actual value nullable. - // Check if we are in a DB migration / garbage collection - // and throw an error in that case. - public db: DbAccess, + public idb: IDBFactory, public http: HttpRequestLibrary, public timer: TimerAPI, cryptoWorkerFactory: CryptoWorkerFactory, @@ -1820,6 +1850,17 @@ class InternalWalletStateImpl implements InternalWalletState { } } + async ensureWalletDbOpen(): Promise { + if (this._db) { + return; + } + const myVersionChange = async (): Promise => { + logger.info("version change requested for Taler DB"); + }; + const myDb = await openTalerDatabase(this.idb, myVersionChange); + this._db = myDb; + } + async getTransactionState( ws: InternalWalletState, tx: GetReadOnlyAccess, diff --git a/packages/taler-wallet-webextension/src/components/TermsOfService/views.tsx b/packages/taler-wallet-webextension/src/components/TermsOfService/views.tsx index e52add756..214c4d792 100644 --- a/packages/taler-wallet-webextension/src/components/TermsOfService/views.tsx +++ b/packages/taler-wallet-webextension/src/components/TermsOfService/views.tsx @@ -69,28 +69,28 @@ export function ShowButtonsNonAcceptedTosView({ terms, }: State.ShowButtonsNotAccepted): VNode { const { i18n } = useTranslationContext(); - const ableToReviewTermsOfService = - showingTermsOfService.button.onClick !== undefined; + // const ableToReviewTermsOfService = + // showingTermsOfService.button.onClick !== undefined; - if (!ableToReviewTermsOfService) { - return ( - - {terms.status === ExchangeTosStatus.NotFound && ( -
- - - Exchange doesn't have terms of service - - -
- )} -
- ); - } + // if (!ableToReviewTermsOfService) { + // return ( + // + // {terms.status === ExchangeTosStatus.Pending && ( + //
+ // + // + // Exchange doesn't have terms of service + // + // + //
+ // )} + //
+ // ); + // } return ( - {terms.status === ExchangeTosStatus.NotFound && ( + {/* {terms.status === ExchangeTosStatus.NotFound && (
@@ -98,8 +98,8 @@ export function ShowButtonsNonAcceptedTosView({
- )} - {terms.status === "new" && ( + )} */} + {terms.status === ExchangeTosStatus.Pending && (
)} - {terms.status === "changed" && ( -
- -
- )}
); } @@ -138,7 +125,7 @@ export function ShowTosContentView({ return ( - {terms.status !== ExchangeTosStatus.NotFound && !terms.content && ( + {!terms.content && (
@@ -194,7 +181,7 @@ export function ShowTosContentView({
)} - {termsAccepted && terms.status !== ExchangeTosStatus.NotFound && ( + {termsAccepted && terms.status !== ExchangeTosStatus.Proposed && (
{ - onAmountChanged(Amounts.stringify(amount)); - }) + onAmountChanged(Amounts.stringify(amount)); + }) : undefined, }, amount: { @@ -304,8 +304,8 @@ function exchangeSelectionState( const [ageRestricted, setAgeRestricted] = useState(0); const currentExchange = selectedExchange.selected; const tosNeedToBeAccepted = - currentExchange.tosStatus == ExchangeTosStatus.New || - currentExchange.tosStatus == ExchangeTosStatus.Changed; + currentExchange.tosStatus == ExchangeTosStatus.Pending || + currentExchange.tosStatus == ExchangeTosStatus.Proposed; /** * With the exchange and amount, ask the wallet the information @@ -393,12 +393,12 @@ function exchangeSelectionState( //TODO: calculate based on exchange info const ageRestriction = ageRestrictionEnabled ? { - list: ageRestrictionOptions, - value: String(ageRestricted), - onChange: pushAlertOnError(async (v: string) => - setAgeRestricted(parseInt(v, 10)), - ), - } + list: ageRestrictionOptions, + value: String(ageRestricted), + onChange: pushAlertOnError(async (v: string) => + setAgeRestricted(parseInt(v, 10)), + ), + } : undefined; return { diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw/test.ts b/packages/taler-wallet-webextension/src/cta/Withdraw/test.ts index c7af160e4..ab3b2e316 100644 --- a/packages/taler-wallet-webextension/src/cta/Withdraw/test.ts +++ b/packages/taler-wallet-webextension/src/cta/Withdraw/test.ts @@ -37,7 +37,7 @@ const exchanges: ExchangeListItem[] = [ exchangeBaseUrl: "http://exchange.demo.taler.net", paytoUris: [], tosStatus: ExchangeTosStatus.Accepted, - exchangeStatus: ExchangeEntryStatus.Ok, + exchangeStatus: ExchangeEntryStatus.Used, permanent: true, auditors: [ { @@ -202,7 +202,7 @@ describe("Withdraw CTA states", () => { const exchangeWithNewTos = exchanges.map((e) => ({ ...e, - tosStatus: ExchangeTosStatus.New, + tosStatus: ExchangeTosStatus.Proposed, })); handler.addWalletCallResponse( diff --git a/packages/taler-wallet-webextension/src/wallet/DestinationSelection/test.ts b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/test.ts index 99b5ec176..7ef475805 100644 --- a/packages/taler-wallet-webextension/src/wallet/DestinationSelection/test.ts +++ b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/test.ts @@ -24,6 +24,7 @@ import { ExchangeEntryStatus, ExchangeListItem, ExchangeTosStatus, + ExchangeUpdateStatus, } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { expect } from "chai"; @@ -36,9 +37,9 @@ const exchangeArs: ExchangeListItem = { currency: "ARS", exchangeBaseUrl: "http://", tosStatus: ExchangeTosStatus.Accepted, - exchangeStatus: ExchangeEntryStatus.Ok, + exchangeEntryStatus: ExchangeEntryStatus.Used, + exchangeUpdateStatus: ExchangeUpdateStatus.Initial, paytoUris: [], - permanent: true, ageRestrictionOptions: [], }; diff --git a/packages/taler-wallet-webextension/src/wallet/Settings.tsx b/packages/taler-wallet-webextension/src/wallet/Settings.tsx index 071d2a594..0aa46d615 100644 --- a/packages/taler-wallet-webextension/src/wallet/Settings.tsx +++ b/packages/taler-wallet-webextension/src/wallet/Settings.tsx @@ -163,20 +163,16 @@ export function SettingsView({ ok ); - case ExchangeTosStatus.Changed: + case ExchangeTosStatus.Pending: return ( - changed + pending ); - case ExchangeTosStatus.New: - case ExchangeTosStatus.NotFound: + case ExchangeTosStatus.Proposed: return ( - - not accepted - + proposed ); - case ExchangeTosStatus.Unknown: default: return ( diff --git a/packages/taler-wallet-webextension/src/wxBackend.ts b/packages/taler-wallet-webextension/src/wxBackend.ts index 95af1a3a4..e7385abe5 100644 --- a/packages/taler-wallet-webextension/src/wxBackend.ts +++ b/packages/taler-wallet-webextension/src/wxBackend.ts @@ -50,7 +50,6 @@ import { exportDb, importDb, openPromise, - openTalerDatabase, } from "@gnu-taler/taler-wallet-core"; import { MessageFromBackend, @@ -139,7 +138,7 @@ async function runGarbageCollector(): Promise { if (!dbBeforeGc) { throw Error("no current db before running gc"); } - const dump = await exportDb(dbBeforeGc.idbHandle()); + const dump = await exportDb(indexedDB as any); await deleteTalerDatabase(indexedDB as any); logger.info("cleaned"); @@ -298,13 +297,6 @@ async function reinitWallet(): Promise { } currentDatabase = undefined; // setBadgeText({ text: "" }); - try { - currentDatabase = await openTalerDatabase(indexedDB as any, reinitWallet); - } catch (e) { - logger.error("could not open database", e); - walletInit.reject(e); - return; - } let httpLib; let cryptoWorker; let timer; @@ -325,7 +317,7 @@ async function reinitWallet(): Promise { const settings = await platform.getSettingsFromStorage(); logger.info("Setting up wallet"); const wallet = await Wallet.create( - currentDatabase, + indexedDB as any, httpLib, timer, cryptoWorker, diff --git a/packages/web-util/src/hooks/useLocalStorage.ts b/packages/web-util/src/hooks/useLocalStorage.ts index fae5537a4..b460144a6 100644 --- a/packages/web-util/src/hooks/useLocalStorage.ts +++ b/packages/web-util/src/hooks/useLocalStorage.ts @@ -106,7 +106,7 @@ export function useLocalStorage( const newValue = storage.get(key.id); setStoredValue(convert(newValue)); }); - }, []); + }, [key.id]); const setValue = (value?: Type): void => { if (value === undefined) { diff --git a/packages/web-util/src/hooks/useMemoryStorage.ts b/packages/web-util/src/hooks/useMemoryStorage.ts index 1dd263797..ef186392f 100644 --- a/packages/web-util/src/hooks/useMemoryStorage.ts +++ b/packages/web-util/src/hooks/useMemoryStorage.ts @@ -51,7 +51,7 @@ export function useMemoryStorage( const newValue = storage.get(key); setStoredValue(newValue === undefined ? defaultValue : newValue); }); - }, []); + }, [key]); const setValue = (value?: Type): void => { if (value === undefined) {