aboutsummaryrefslogtreecommitdiff
path: root/packages/taler-wallet-core/src/operations
diff options
context:
space:
mode:
authorÖzgür Kesim <oec-taler@kesim.org>2023-08-31 13:12:06 +0200
committerÖzgür Kesim <oec-taler@kesim.org>2023-08-31 13:12:06 +0200
commit94cfcc875065f988815c31aaf8ebf36f75ac5983 (patch)
tree8389a638c67a3ae2d57fb354da1df6378e82113f /packages/taler-wallet-core/src/operations
parente02a4eb990c8c54662fbb658695312bdfce492e0 (diff)
parent79973a63dd31c0d84b677a2a1511b1dffc6218b8 (diff)
Merge branch 'master' into age-withdraw
Diffstat (limited to 'packages/taler-wallet-core/src/operations')
-rw-r--r--packages/taler-wallet-core/src/operations/backup/export.ts586
-rw-r--r--packages/taler-wallet-core/src/operations/backup/import.ts875
-rw-r--r--packages/taler-wallet-core/src/operations/backup/index.ts188
-rw-r--r--packages/taler-wallet-core/src/operations/backup/state.ts92
-rw-r--r--packages/taler-wallet-core/src/operations/common.ts80
-rw-r--r--packages/taler-wallet-core/src/operations/exchanges.ts56
-rw-r--r--packages/taler-wallet-core/src/operations/pending.ts28
-rw-r--r--packages/taler-wallet-core/src/operations/refresh.ts10
-rw-r--r--packages/taler-wallet-core/src/operations/reward.ts6
-rw-r--r--packages/taler-wallet-core/src/operations/withdraw.ts7
10 files changed, 214 insertions, 1714 deletions
diff --git a/packages/taler-wallet-core/src/operations/backup/export.ts b/packages/taler-wallet-core/src/operations/backup/export.ts
deleted file mode 100644
index c9446a05f..000000000
--- a/packages/taler-wallet-core/src/operations/backup/export.ts
+++ /dev/null
@@ -1,586 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2020 Taler Systems SA
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- * Implementation of wallet backups (export/import/upload) and sync
- * server management.
- *
- * @author Florian Dold <dold@taler.net>
- */
-
-/**
- * Imports.
- */
-import {
- AbsoluteTime,
- Amounts,
- BackupBackupProvider,
- BackupBackupProviderTerms,
- BackupCoin,
- BackupCoinSource,
- BackupCoinSourceType,
- BackupDenomination,
- BackupExchange,
- BackupExchangeDetails,
- BackupExchangeSignKey,
- BackupExchangeWireFee,
- BackupOperationStatus,
- BackupPayInfo,
- BackupProposalStatus,
- BackupPurchase,
- BackupRecoupGroup,
- BackupRefreshGroup,
- BackupRefreshOldCoin,
- BackupRefreshSession,
- BackupRefundItem,
- BackupRefundState,
- BackupTip,
- BackupWgInfo,
- BackupWgType,
- BackupWithdrawalGroup,
- BACKUP_VERSION_MAJOR,
- BACKUP_VERSION_MINOR,
- canonicalizeBaseUrl,
- canonicalJson,
- CoinStatus,
- encodeCrock,
- getRandomBytes,
- hash,
- Logger,
- stringToBytes,
- WalletBackupContentV1,
- TalerPreciseTimestamp,
-} from "@gnu-taler/taler-util";
-import {
- CoinSourceType,
- ConfigRecordKey,
- DenominationRecord,
- PurchaseStatus,
- RefreshCoinStatus,
- WithdrawalGroupStatus,
- WithdrawalRecordType,
-} from "../../db.js";
-import { InternalWalletState } from "../../internal-wallet-state.js";
-import { assertUnreachable } from "../../util/assertUnreachable.js";
-import { checkDbInvariant } from "../../util/invariants.js";
-import { getWalletBackupState, provideBackupState } from "./state.js";
-
-const logger = new Logger("backup/export.ts");
-
-export async function exportBackup(
- ws: InternalWalletState,
-): Promise<WalletBackupContentV1> {
- await provideBackupState(ws);
- return ws.db
- .mktx((x) => [
- x.config,
- x.exchanges,
- x.exchangeDetails,
- x.exchangeSignKeys,
- x.coins,
- x.contractTerms,
- x.denominations,
- x.purchases,
- x.refreshGroups,
- x.backupProviders,
- x.rewards,
- x.recoupGroups,
- x.withdrawalGroups,
- ])
- .runReadWrite(async (tx) => {
- const bs = await getWalletBackupState(ws, tx);
-
- const backupExchangeDetails: BackupExchangeDetails[] = [];
- const backupExchanges: BackupExchange[] = [];
- const backupCoinsByDenom: { [dph: string]: BackupCoin[] } = {};
- const backupDenominationsByExchange: {
- [url: string]: BackupDenomination[];
- } = {};
- const backupPurchases: BackupPurchase[] = [];
- const backupRefreshGroups: BackupRefreshGroup[] = [];
- const backupBackupProviders: BackupBackupProvider[] = [];
- const backupTips: BackupTip[] = [];
- const backupRecoupGroups: BackupRecoupGroup[] = [];
- const backupWithdrawalGroups: BackupWithdrawalGroup[] = [];
-
- await tx.withdrawalGroups.iter().forEachAsync(async (wg) => {
- let info: BackupWgInfo;
- switch (wg.wgInfo.withdrawalType) {
- case WithdrawalRecordType.BankIntegrated:
- info = {
- type: BackupWgType.BankIntegrated,
- exchange_payto_uri: wg.wgInfo.bankInfo.exchangePaytoUri,
- taler_withdraw_uri: wg.wgInfo.bankInfo.talerWithdrawUri,
- confirm_url: wg.wgInfo.bankInfo.confirmUrl,
- timestamp_bank_confirmed:
- wg.wgInfo.bankInfo.timestampBankConfirmed,
- timestamp_reserve_info_posted:
- wg.wgInfo.bankInfo.timestampReserveInfoPosted,
- };
- break;
- case WithdrawalRecordType.BankManual:
- info = {
- type: BackupWgType.BankManual,
- };
- break;
- case WithdrawalRecordType.PeerPullCredit:
- info = {
- type: BackupWgType.PeerPullCredit,
- contract_priv: wg.wgInfo.contractPriv,
- contract_terms: wg.wgInfo.contractTerms,
- };
- break;
- case WithdrawalRecordType.PeerPushCredit:
- info = {
- type: BackupWgType.PeerPushCredit,
- contract_terms: wg.wgInfo.contractTerms,
- };
- break;
- case WithdrawalRecordType.Recoup:
- info = {
- type: BackupWgType.Recoup,
- };
- break;
- default:
- assertUnreachable(wg.wgInfo);
- }
- backupWithdrawalGroups.push({
- raw_withdrawal_amount: Amounts.stringify(wg.rawWithdrawalAmount),
- info,
- timestamp_created: wg.timestampStart,
- timestamp_finish: wg.timestampFinish,
- withdrawal_group_id: wg.withdrawalGroupId,
- secret_seed: wg.secretSeed,
- exchange_base_url: wg.exchangeBaseUrl,
- instructed_amount: Amounts.stringify(wg.instructedAmount),
- effective_withdrawal_amount: Amounts.stringify(
- wg.effectiveWithdrawalAmount,
- ),
- reserve_priv: wg.reservePriv,
- restrict_age: wg.restrictAge,
- // FIXME: proper status conversion!
- operation_status:
- wg.status == WithdrawalGroupStatus.Finished
- ? BackupOperationStatus.Finished
- : BackupOperationStatus.Pending,
- selected_denoms_uid: wg.denomSelUid,
- selected_denoms: wg.denomsSel.selectedDenoms.map((x) => ({
- count: x.count,
- denom_pub_hash: x.denomPubHash,
- })),
- });
- });
-
- await tx.rewards.iter().forEach((tip) => {
- backupTips.push({
- exchange_base_url: tip.exchangeBaseUrl,
- merchant_base_url: tip.merchantBaseUrl,
- merchant_tip_id: tip.merchantRewardId,
- wallet_tip_id: tip.walletRewardId,
- next_url: tip.next_url,
- secret_seed: tip.secretSeed,
- selected_denoms: tip.denomsSel.selectedDenoms.map((x) => ({
- count: x.count,
- denom_pub_hash: x.denomPubHash,
- })),
- timestamp_finished: tip.pickedUpTimestamp,
- timestamp_accepted: tip.acceptedTimestamp,
- timestamp_created: tip.createdTimestamp,
- timestamp_expiration: tip.rewardExpiration,
- tip_amount_raw: Amounts.stringify(tip.rewardAmountRaw),
- selected_denoms_uid: tip.denomSelUid,
- });
- });
-
- await tx.recoupGroups.iter().forEach((recoupGroup) => {
- backupRecoupGroups.push({
- recoup_group_id: recoupGroup.recoupGroupId,
- timestamp_created: recoupGroup.timestampStarted,
- timestamp_finish: recoupGroup.timestampFinished,
- coins: recoupGroup.coinPubs.map((x, i) => ({
- coin_pub: x,
- recoup_finished: recoupGroup.recoupFinishedPerCoin[i],
- })),
- });
- });
-
- await tx.backupProviders.iter().forEach((bp) => {
- let terms: BackupBackupProviderTerms | undefined;
- if (bp.terms) {
- terms = {
- annual_fee: Amounts.stringify(bp.terms.annualFee),
- storage_limit_in_megabytes: bp.terms.storageLimitInMegabytes,
- supported_protocol_version: bp.terms.supportedProtocolVersion,
- };
- }
- backupBackupProviders.push({
- terms,
- base_url: canonicalizeBaseUrl(bp.baseUrl),
- pay_proposal_ids: bp.paymentProposalIds,
- uids: bp.uids,
- });
- });
-
- await tx.coins.iter().forEach((coin) => {
- let bcs: BackupCoinSource;
- switch (coin.coinSource.type) {
- case CoinSourceType.Refresh:
- bcs = {
- type: BackupCoinSourceType.Refresh,
- old_coin_pub: coin.coinSource.oldCoinPub,
- refresh_group_id: coin.coinSource.refreshGroupId,
- };
- break;
- case CoinSourceType.Reward:
- bcs = {
- type: BackupCoinSourceType.Reward,
- coin_index: coin.coinSource.coinIndex,
- wallet_tip_id: coin.coinSource.walletRewardId,
- };
- break;
- case CoinSourceType.Withdraw:
- bcs = {
- type: BackupCoinSourceType.Withdraw,
- coin_index: coin.coinSource.coinIndex,
- reserve_pub: coin.coinSource.reservePub,
- withdrawal_group_id: coin.coinSource.withdrawalGroupId,
- };
- break;
- }
-
- const coins = (backupCoinsByDenom[coin.denomPubHash] ??= []);
- coins.push({
- blinding_key: coin.blindingKey,
- coin_priv: coin.coinPriv,
- coin_source: bcs,
- fresh: coin.status === CoinStatus.Fresh,
- spend_allocation: coin.spendAllocation
- ? {
- amount: coin.spendAllocation.amount,
- id: coin.spendAllocation.id,
- }
- : undefined,
- denom_sig: coin.denomSig,
- });
- });
-
- await tx.denominations.iter().forEach((denom) => {
- const backupDenoms = (backupDenominationsByExchange[
- denom.exchangeBaseUrl
- ] ??= []);
- backupDenoms.push({
- coins: backupCoinsByDenom[denom.denomPubHash] ?? [],
- denom_pub: denom.denomPub,
- fee_deposit: Amounts.stringify(denom.fees.feeDeposit),
- fee_refresh: Amounts.stringify(denom.fees.feeRefresh),
- fee_refund: Amounts.stringify(denom.fees.feeRefund),
- fee_withdraw: Amounts.stringify(denom.fees.feeWithdraw),
- is_offered: denom.isOffered,
- is_revoked: denom.isRevoked,
- master_sig: denom.masterSig,
- stamp_expire_deposit: denom.stampExpireDeposit,
- stamp_expire_legal: denom.stampExpireLegal,
- stamp_expire_withdraw: denom.stampExpireWithdraw,
- stamp_start: denom.stampStart,
- value: Amounts.stringify(DenominationRecord.getValue(denom)),
- list_issue_date: denom.listIssueDate,
- });
- });
-
- await tx.exchanges.iter().forEachAsync(async (ex) => {
- const dp = ex.detailsPointer;
- if (!dp) {
- return;
- }
- backupExchanges.push({
- base_url: ex.baseUrl,
- currency: dp.currency,
- master_public_key: dp.masterPublicKey,
- update_clock: dp.updateClock,
- });
- });
-
- await tx.exchangeDetails.iter().forEachAsync(async (ex) => {
- // Only back up permanently added exchanges.
-
- const wi = ex.wireInfo;
- const wireFees: BackupExchangeWireFee[] = [];
-
- Object.keys(wi.feesForType).forEach((x) => {
- for (const f of wi.feesForType[x]) {
- wireFees.push({
- wire_type: x,
- closing_fee: Amounts.stringify(f.closingFee),
- end_stamp: f.endStamp,
- sig: f.sig,
- start_stamp: f.startStamp,
- wire_fee: Amounts.stringify(f.wireFee),
- });
- }
- });
- checkDbInvariant(ex.rowId != null);
- const exchangeSk =
- await tx.exchangeSignKeys.indexes.byExchangeDetailsRowId.getAll(
- ex.rowId,
- );
- let signingKeys: BackupExchangeSignKey[] = exchangeSk.map((x) => ({
- key: x.signkeyPub,
- master_sig: x.masterSig,
- stamp_end: x.stampEnd,
- stamp_expire: x.stampExpire,
- stamp_start: x.stampStart,
- }));
-
- backupExchangeDetails.push({
- base_url: ex.exchangeBaseUrl,
- reserve_closing_delay: ex.reserveClosingDelay,
- accounts: ex.wireInfo.accounts.map((x) => ({
- payto_uri: x.payto_uri,
- master_sig: x.master_sig,
- })),
- auditors: ex.auditors.map((x) => ({
- auditor_pub: x.auditor_pub,
- auditor_url: x.auditor_url,
- denomination_keys: x.denomination_keys,
- })),
- master_public_key: ex.masterPublicKey,
- currency: ex.currency,
- protocol_version: ex.protocolVersionRange,
- wire_fees: wireFees,
- signing_keys: signingKeys,
- global_fees: ex.globalFees.map((x) => ({
- accountFee: Amounts.stringify(x.accountFee),
- historyFee: Amounts.stringify(x.historyFee),
- purseFee: Amounts.stringify(x.purseFee),
- endDate: x.endDate,
- historyTimeout: x.historyTimeout,
- signature: x.signature,
- purseLimit: x.purseLimit,
- purseTimeout: x.purseTimeout,
- startDate: x.startDate,
- })),
- tos_accepted_etag: ex.tosAccepted?.etag,
- tos_accepted_timestamp: ex.tosAccepted?.timestamp,
- denominations:
- backupDenominationsByExchange[ex.exchangeBaseUrl] ?? [],
- });
- });
-
- const purchaseProposalIdSet = new Set<string>();
-
- await tx.purchases.iter().forEachAsync(async (purch) => {
- const refunds: BackupRefundItem[] = [];
- purchaseProposalIdSet.add(purch.proposalId);
- // for (const refundKey of Object.keys(purch.refunds)) {
- // const ri = purch.refunds[refundKey];
- // const common = {
- // coin_pub: ri.coinPub,
- // execution_time: ri.executionTime,
- // obtained_time: ri.obtainedTime,
- // refund_amount: Amounts.stringify(ri.refundAmount),
- // rtransaction_id: ri.rtransactionId,
- // total_refresh_cost_bound: Amounts.stringify(
- // ri.totalRefreshCostBound,
- // ),
- // };
- // switch (ri.type) {
- // case RefundState.Applied:
- // refunds.push({ type: BackupRefundState.Applied, ...common });
- // break;
- // case RefundState.Failed:
- // refunds.push({ type: BackupRefundState.Failed, ...common });
- // break;
- // case RefundState.Pending:
- // refunds.push({ type: BackupRefundState.Pending, ...common });
- // break;
- // }
- // }
-
- let propStatus: BackupProposalStatus;
- switch (purch.purchaseStatus) {
- case PurchaseStatus.Done:
- case PurchaseStatus.PendingQueryingAutoRefund:
- case PurchaseStatus.PendingQueryingRefund:
- propStatus = BackupProposalStatus.Paid;
- break;
- case PurchaseStatus.PendingPayingReplay:
- case PurchaseStatus.PendingDownloadingProposal:
- case PurchaseStatus.DialogProposed:
- case PurchaseStatus.PendingPaying:
- propStatus = BackupProposalStatus.Proposed;
- break;
- case PurchaseStatus.DialogShared:
- propStatus = BackupProposalStatus.Shared;
- break;
- case PurchaseStatus.FailedClaim:
- case PurchaseStatus.AbortedIncompletePayment:
- propStatus = BackupProposalStatus.PermanentlyFailed;
- break;
- case PurchaseStatus.AbortingWithRefund:
- case PurchaseStatus.AbortedProposalRefused:
- propStatus = BackupProposalStatus.Refused;
- break;
- case PurchaseStatus.RepurchaseDetected:
- propStatus = BackupProposalStatus.Repurchase;
- break;
- default: {
- const error = purch.purchaseStatus;
- throw Error(`purchase status ${error} is not handled`);
- }
- }
-
- const payInfo = purch.payInfo;
- let backupPayInfo: BackupPayInfo | undefined = undefined;
- if (payInfo) {
- backupPayInfo = {
- pay_coins: payInfo.payCoinSelection.coinPubs.map((x, i) => ({
- coin_pub: x,
- contribution: Amounts.stringify(
- payInfo.payCoinSelection.coinContributions[i],
- ),
- })),
- total_pay_cost: Amounts.stringify(payInfo.totalPayCost),
- pay_coins_uid: payInfo.payCoinSelectionUid,
- };
- }
-
- let contractTermsRaw = undefined;
- if (purch.download) {
- const contractTermsRecord = await tx.contractTerms.get(
- purch.download.contractTermsHash,
- );
- if (contractTermsRecord) {
- contractTermsRaw = contractTermsRecord.contractTermsRaw;
- }
- }
-
- backupPurchases.push({
- contract_terms_raw: contractTermsRaw,
- auto_refund_deadline: purch.autoRefundDeadline,
- merchant_pay_sig: purch.merchantPaySig,
- pos_confirmation: purch.posConfirmation,
- pay_info: backupPayInfo,
- proposal_id: purch.proposalId,
- refunds,
- timestamp_accepted: purch.timestampAccept,
- timestamp_first_successful_pay: purch.timestampFirstSuccessfulPay,
- nonce_priv: purch.noncePriv,
- merchant_sig: purch.download?.contractTermsMerchantSig,
- claim_token: purch.claimToken,
- merchant_base_url: purch.merchantBaseUrl,
- order_id: purch.orderId,
- proposal_status: propStatus,
- repurchase_proposal_id: purch.repurchaseProposalId,
- download_session_id: purch.downloadSessionId,
- timestamp_proposed: purch.timestamp,
- shared: purch.shared,
- });
- });
-
- await tx.refreshGroups.iter().forEach((rg) => {
- const oldCoins: BackupRefreshOldCoin[] = [];
-
- for (let i = 0; i < rg.oldCoinPubs.length; i++) {
- let refreshSession: BackupRefreshSession | undefined;
- const s = rg.refreshSessionPerCoin[i];
- if (s) {
- refreshSession = {
- new_denoms: s.newDenoms.map((x) => ({
- count: x.count,
- denom_pub_hash: x.denomPubHash,
- })),
- session_secret_seed: s.sessionSecretSeed,
- noreveal_index: s.norevealIndex,
- };
- }
- oldCoins.push({
- coin_pub: rg.oldCoinPubs[i],
- estimated_output_amount: Amounts.stringify(
- rg.estimatedOutputPerCoin[i],
- ),
- finished: rg.statusPerCoin[i] === RefreshCoinStatus.Finished,
- input_amount: Amounts.stringify(rg.inputPerCoin[i]),
- refresh_session: refreshSession,
- });
- }
-
- backupRefreshGroups.push({
- reason: rg.reason as any,
- refresh_group_id: rg.refreshGroupId,
- timestamp_created: rg.timestampCreated,
- timestamp_finish: rg.timestampFinished,
- old_coins: oldCoins,
- });
- });
-
- const ts = TalerPreciseTimestamp.now();
-
- if (!bs.lastBackupTimestamp) {
- bs.lastBackupTimestamp = ts;
- }
-
- const backupBlob: WalletBackupContentV1 = {
- schema_id: "gnu-taler-wallet-backup-content",
- schema_version: BACKUP_VERSION_MAJOR,
- minor_version: BACKUP_VERSION_MINOR,
- exchanges: backupExchanges,
- exchange_details: backupExchangeDetails,
- wallet_root_pub: bs.walletRootPub,
- backup_providers: backupBackupProviders,
- current_device_id: bs.deviceId,
- purchases: backupPurchases,
- recoup_groups: backupRecoupGroups,
- refresh_groups: backupRefreshGroups,
- tips: backupTips,
- timestamp: bs.lastBackupTimestamp,
- trusted_auditors: {},
- trusted_exchanges: {},
- intern_table: {},
- error_reports: [],
- tombstones: [],
- // FIXME!
- withdrawal_groups: backupWithdrawalGroups,
- };
-
- // If the backup changed, we change our nonce and timestamp.
-
- let h = encodeCrock(hash(stringToBytes(canonicalJson(backupBlob))));
- if (h !== bs.lastBackupPlainHash) {
- logger.trace(
- `plain backup hash changed (from ${bs.lastBackupPlainHash}to ${h})`,
- );
- bs.lastBackupTimestamp = ts;
- backupBlob.timestamp = ts;
- bs.lastBackupPlainHash = encodeCrock(
- hash(stringToBytes(canonicalJson(backupBlob))),
- );
- bs.lastBackupNonce = encodeCrock(getRandomBytes(32));
- logger.trace(
- `setting timestamp to ${AbsoluteTime.toIsoString(
- AbsoluteTime.fromPreciseTimestamp(ts),
- )} and nonce to ${bs.lastBackupNonce}`,
- );
- await tx.config.put({
- key: ConfigRecordKey.WalletBackupState,
- value: bs,
- });
- } else {
- logger.trace("backup hash did not change");
- }
-
- return backupBlob;
- });
-}
diff --git a/packages/taler-wallet-core/src/operations/backup/import.ts b/packages/taler-wallet-core/src/operations/backup/import.ts
deleted file mode 100644
index a53b624e8..000000000
--- a/packages/taler-wallet-core/src/operations/backup/import.ts
+++ /dev/null
@@ -1,875 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2020 Taler Systems SA
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-import {
- AgeRestriction,
- AmountJson,
- Amounts,
- BackupCoin,
- BackupCoinSourceType,
- BackupDenomSel,
- BackupPayInfo,
- BackupProposalStatus,
- BackupRefreshReason,
- BackupRefundState,
- BackupWgType,
- codecForMerchantContractTerms,
- CoinStatus,
- DenomKeyType,
- DenomSelectionState,
- j2s,
- Logger,
- PayCoinSelection,
- RefreshReason,
- TalerProtocolTimestamp,
- TalerPreciseTimestamp,
- WalletBackupContentV1,
- WireInfo,
-} from "@gnu-taler/taler-util";
-import {
- CoinRecord,
- CoinSource,
- CoinSourceType,
- DenominationRecord,
- DenominationVerificationStatus,
- ProposalDownloadInfo,
- PurchaseStatus,
- PurchasePayInfo,
- RefreshCoinStatus,
- RefreshSessionRecord,
- WalletContractData,
- WalletStoresV1,
- WgInfo,
- WithdrawalGroupStatus,
- WithdrawalRecordType,
- RefreshOperationStatus,
- RewardRecordStatus,
-} from "../../db.js";
-import { InternalWalletState } from "../../internal-wallet-state.js";
-import { assertUnreachable } from "../../util/assertUnreachable.js";
-import { checkLogicInvariant } from "../../util/invariants.js";
-import { GetReadOnlyAccess, GetReadWriteAccess } from "../../util/query.js";
-import {
- constructTombstone,
- makeCoinAvailable,
- TombstoneTag,
-} from "../common.js";
-import { getExchangeDetails } from "../exchanges.js";
-import { extractContractData } from "../pay-merchant.js";
-import { provideBackupState } from "./state.js";
-
-const logger = new Logger("operations/backup/import.ts");
-
-function checkBackupInvariant(b: boolean, m?: string): asserts b {
- if (!b) {
- if (m) {
- throw Error(`BUG: backup invariant failed (${m})`);
- } else {
- throw Error("BUG: backup invariant failed");
- }
- }
-}
-
-/**
- * Re-compute information about the coin selection for a payment.
- */
-async function recoverPayCoinSelection(
- tx: GetReadWriteAccess<{
- exchanges: typeof WalletStoresV1.exchanges;
- exchangeDetails: typeof WalletStoresV1.exchangeDetails;
- coins: typeof WalletStoresV1.coins;
- denominations: typeof WalletStoresV1.denominations;
- }>,
- contractData: WalletContractData,
- payInfo: BackupPayInfo,
-): Promise<PayCoinSelection> {
- const coinPubs: string[] = payInfo.pay_coins.map((x) => x.coin_pub);
- const coinContributions: AmountJson[] = payInfo.pay_coins.map((x) =>
- Amounts.parseOrThrow(x.contribution),
- );
-
- const coveredExchanges: Set<string> = new Set();
-
- let totalWireFee: AmountJson = Amounts.zeroOfAmount(contractData.amount);
- let totalDepositFees: AmountJson = Amounts.zeroOfAmount(contractData.amount);
-
- for (const coinPub of coinPubs) {
- const coinRecord = await tx.coins.get(coinPub);
- checkBackupInvariant(!!coinRecord);
- const denom = await tx.denominations.get([
- coinRecord.exchangeBaseUrl,
- coinRecord.denomPubHash,
- ]);
- checkBackupInvariant(!!denom);
- totalDepositFees = Amounts.add(
- totalDepositFees,
- denom.fees.feeDeposit,
- ).amount;
-
- if (!coveredExchanges.has(coinRecord.exchangeBaseUrl)) {
- const exchangeDetails = await getExchangeDetails(
- tx,
- coinRecord.exchangeBaseUrl,
- );
- checkBackupInvariant(!!exchangeDetails);
- let wireFee: AmountJson | undefined;
- const feesForType = exchangeDetails.wireInfo.feesForType;
- checkBackupInvariant(!!feesForType);
- for (const fee of feesForType[contractData.wireMethod] || []) {
- if (
- fee.startStamp <= contractData.timestamp &&
- fee.endStamp >= contractData.timestamp
- ) {
- wireFee = Amounts.parseOrThrow(fee.wireFee);
- break;
- }
- }
- if (wireFee) {
- totalWireFee = Amounts.add(totalWireFee, wireFee).amount;
- }
- coveredExchanges.add(coinRecord.exchangeBaseUrl);
- }
- }
-
- let customerWireFee: AmountJson;
-
- const amortizedWireFee = Amounts.divide(
- totalWireFee,
- contractData.wireFeeAmortization,
- );
- if (Amounts.cmp(contractData.maxWireFee, amortizedWireFee) < 0) {
- customerWireFee = amortizedWireFee;
- } else {
- customerWireFee = Amounts.zeroOfAmount(contractData.amount);
- }
-
- const customerDepositFees = Amounts.sub(
- totalDepositFees,
- contractData.maxDepositFee,
- ).amount;
-
- return {
- coinPubs,
- coinContributions: coinContributions.map((x) => Amounts.stringify(x)),
- paymentAmount: Amounts.stringify(contractData.amount),
- customerWireFees: Amounts.stringify(customerWireFee),
- customerDepositFees: Amounts.stringify(customerDepositFees),
- };
-}
-
-async function getDenomSelStateFromBackup(
- tx: GetReadOnlyAccess<{ denominations: typeof WalletStoresV1.denominations }>,
- currency: string,
- exchangeBaseUrl: string,
- sel: BackupDenomSel,
-): Promise<DenomSelectionState> {
- const selectedDenoms: {
- denomPubHash: string;
- count: number;
- }[] = [];
- let totalCoinValue = Amounts.zeroOfCurrency(currency);
- let totalWithdrawCost = Amounts.zeroOfCurrency(currency);
- for (const s of sel) {
- const d = await tx.denominations.get([exchangeBaseUrl, s.denom_pub_hash]);
- checkBackupInvariant(!!d);
- totalCoinValue = Amounts.add(
- totalCoinValue,
- DenominationRecord.getValue(d),
- ).amount;
- totalWithdrawCost = Amounts.add(
- totalWithdrawCost,
- DenominationRecord.getValue(d),
- d.fees.feeWithdraw,
- ).amount;
- }
- return {
- selectedDenoms,
- totalCoinValue: Amounts.stringify(totalCoinValue),
- totalWithdrawCost: Amounts.stringify(totalWithdrawCost),
- };
-}
-
-export interface CompletedCoin {
- coinPub: string;
- coinEvHash: string;
-}
-
-/**
- * Precomputed cryptographic material for a backup import.
- *
- * We separate this data from the backup blob as we want the backup
- * blob to be small, and we can't compute it during the database transaction,
- * as the async crypto worker communication would auto-close the database transaction.
- */
-export interface BackupCryptoPrecomputedData {
- rsaDenomPubToHash: Record<string, string>;
- coinPrivToCompletedCoin: Record<string, CompletedCoin>;
- proposalNoncePrivToPub: { [priv: string]: string };
- proposalIdToContractTermsHash: { [proposalId: string]: string };
- reservePrivToPub: Record<string, string>;
-}
-
-export async function importCoin(
- ws: InternalWalletState,
- tx: GetReadWriteAccess<{
- coins: typeof WalletStoresV1.coins;
- coinAvailability: typeof WalletStoresV1.coinAvailability;
- denominations: typeof WalletStoresV1.denominations;
- }>,
- cryptoComp: BackupCryptoPrecomputedData,
- args: {
- backupCoin: BackupCoin;
- exchangeBaseUrl: string;
- denomPubHash: string;
- },
-): Promise<void> {
- const { backupCoin, exchangeBaseUrl, denomPubHash } = args;
- const compCoin = cryptoComp.coinPrivToCompletedCoin[backupCoin.coin_priv];
- checkLogicInvariant(!!compCoin);
- const existingCoin = await tx.coins.get(compCoin.coinPub);
- if (!existingCoin) {
- let coinSource: CoinSource;
- switch (backupCoin.coin_source.type) {
- case BackupCoinSourceType.Refresh:
- coinSource = {
- type: CoinSourceType.Refresh,
- oldCoinPub: backupCoin.coin_source.old_coin_pub,
- refreshGroupId: backupCoin.coin_source.refresh_group_id,
- };
- break;
- case BackupCoinSourceType.Reward:
- coinSource = {
- type: CoinSourceType.Reward,
- coinIndex: backupCoin.coin_source.coin_index,
- walletRewardId: backupCoin.coin_source.wallet_tip_id,
- };
- break;
- case BackupCoinSourceType.Withdraw:
- coinSource = {
- type: CoinSourceType.Withdraw,
- coinIndex: backupCoin.coin_source.coin_index,
- reservePub: backupCoin.coin_source.reserve_pub,
- withdrawalGroupId: backupCoin.coin_source.withdrawal_group_id,
- };
- break;
- }
- const coinRecord: CoinRecord = {
- blindingKey: backupCoin.blinding_key,
- coinEvHash: compCoin.coinEvHash,
- coinPriv: backupCoin.coin_priv,
- denomSig: backupCoin.denom_sig,
- coinPub: compCoin.coinPub,
- exchangeBaseUrl,
- denomPubHash,
- status: backupCoin.fresh ? CoinStatus.Fresh : CoinStatus.Dormant,
- coinSource,
- // FIXME!
- maxAge: AgeRestriction.AGE_UNRESTRICTED,
- // FIXME!
- ageCommitmentProof: undefined,
- // FIXME!
- spendAllocation: undefined,
- };
- if (coinRecord.status === CoinStatus.Fresh) {
- await makeCoinAvailable(ws, tx, coinRecord);
- } else {
- await tx.coins.put(coinRecord);
- }
- }
-}
-
-export async function importBackup(
- ws: InternalWalletState,
- backupBlobArg: any,
- cryptoComp: BackupCryptoPrecomputedData,
-): Promise<void> {
- await provideBackupState(ws);
-
- logger.info(`importing backup ${j2s(backupBlobArg)}`);
-
- return ws.db
- .mktx((x) => [
- x.config,
- x.exchangeDetails,
- x.exchanges,
- x.coins,
- x.coinAvailability,
- x.denominations,
- x.purchases,
- x.refreshGroups,
- x.backupProviders,
- x.rewards,
- x.recoupGroups,
- x.withdrawalGroups,
- x.tombstones,
- x.depositGroups,
- ])
- .runReadWrite(async (tx) => {
- // FIXME: validate schema!
- const backupBlob = backupBlobArg as WalletBackupContentV1;
-
- // FIXME: validate version
-
- for (const tombstone of backupBlob.tombstones) {
- await tx.tombstones.put({
- id: tombstone,
- });
- }
-
- const tombstoneSet = new Set(
- (await tx.tombstones.iter().toArray()).map((x) => x.id),
- );
-
- // FIXME: Validate that the "details pointer" is correct
-
- for (const backupExchange of backupBlob.exchanges) {
- const existingExchange = await tx.exchanges.get(
- backupExchange.base_url,
- );
- if (existingExchange) {
- continue;
- }
- await tx.exchanges.put({
- baseUrl: backupExchange.base_url,
- detailsPointer: {
- currency: backupExchange.currency,
- masterPublicKey: backupExchange.master_public_key,
- updateClock: backupExchange.update_clock,
- },
- permanent: true,
- lastUpdate: undefined,
- nextUpdate: TalerPreciseTimestamp.now(),
- nextRefreshCheck: TalerPreciseTimestamp.now(),
- lastKeysEtag: undefined,
- lastWireEtag: undefined,
- });
- }
-
- for (const backupExchangeDetails of backupBlob.exchange_details) {
- const existingExchangeDetails =
- await tx.exchangeDetails.indexes.byPointer.get([
- backupExchangeDetails.base_url,
- backupExchangeDetails.currency,
- backupExchangeDetails.master_public_key,
- ]);
-
- if (!existingExchangeDetails) {
- const wireInfo: WireInfo = {
- accounts: backupExchangeDetails.accounts.map((x) => ({
- master_sig: x.master_sig,
- payto_uri: x.payto_uri,
- })),
- feesForType: {},
- };
- for (const fee of backupExchangeDetails.wire_fees) {
- const w = (wireInfo.feesForType[fee.wire_type] ??= []);
- w.push({
- closingFee: Amounts.stringify(fee.closing_fee),
- endStamp: fee.end_stamp,
- sig: fee.sig,
- startStamp: fee.start_stamp,
- wireFee: Amounts.stringify(fee.wire_fee),
- });
- }
- let tosAccepted = undefined;
- if (
- backupExchangeDetails.tos_accepted_etag &&
- backupExchangeDetails.tos_accepted_timestamp
- ) {
- tosAccepted = {
- etag: backupExchangeDetails.tos_accepted_etag,
- timestamp: backupExchangeDetails.tos_accepted_timestamp,
- };
- }
- await tx.exchangeDetails.put({
- exchangeBaseUrl: backupExchangeDetails.base_url,
- wireInfo,
- currency: backupExchangeDetails.currency,
- auditors: backupExchangeDetails.auditors.map((x) => ({
- auditor_pub: x.auditor_pub,
- auditor_url: x.auditor_url,
- denomination_keys: x.denomination_keys,
- })),
- masterPublicKey: backupExchangeDetails.master_public_key,
- protocolVersionRange: backupExchangeDetails.protocol_version,
- reserveClosingDelay: backupExchangeDetails.reserve_closing_delay,
- tosCurrentEtag: backupExchangeDetails.tos_accepted_etag || "",
- tosAccepted,
- globalFees: backupExchangeDetails.global_fees.map((x) => ({
- accountFee: Amounts.stringify(x.accountFee),
- historyFee: Amounts.stringify(x.historyFee),
- purseFee: Amounts.stringify(x.purseFee),
- endDate: x.endDate,
- historyTimeout: x.historyTimeout,
- signature: x.signature,
- purseLimit: x.purseLimit,
- purseTimeout: x.purseTimeout,
- startDate: x.startDate,
- })),
- });
- }
-
- for (const backupDenomination of backupExchangeDetails.denominations) {
- if (backupDenomination.denom_pub.cipher !== DenomKeyType.Rsa) {
- throw Error("unsupported cipher");
- }
- const denomPubHash =
- cryptoComp.rsaDenomPubToHash[
- backupDenomination.denom_pub.rsa_public_key
- ];
- checkLogicInvariant(!!denomPubHash);
- const existingDenom = await tx.denominations.get([
- backupExchangeDetails.base_url,
- denomPubHash,
- ]);
- if (!existingDenom) {
- const value = Amounts.parseOrThrow(backupDenomination.value);
-
- await tx.denominations.put({
- denomPub: backupDenomination.denom_pub,
- denomPubHash: denomPubHash,
- exchangeBaseUrl: backupExchangeDetails.base_url,
- exchangeMasterPub: backupExchangeDetails.master_public_key,
- fees: {
- feeDeposit: Amounts.stringify(backupDenomination.fee_deposit),
- feeRefresh: Amounts.stringify(backupDenomination.fee_refresh),
- feeRefund: Amounts.stringify(backupDenomination.fee_refund),
- feeWithdraw: Amounts.stringify(backupDenomination.fee_withdraw),
- },
- isOffered: backupDenomination.is_offered,
- isRevoked: backupDenomination.is_revoked,
- masterSig: backupDenomination.master_sig,
- stampExpireDeposit: backupDenomination.stamp_expire_deposit,
- stampExpireLegal: backupDenomination.stamp_expire_legal,
- stampExpireWithdraw: backupDenomination.stamp_expire_withdraw,
- stampStart: backupDenomination.stamp_start,
- verificationStatus: DenominationVerificationStatus.VerifiedGood,
- currency: value.currency,
- amountFrac: value.fraction,
- amountVal: value.value,
- listIssueDate: backupDenomination.list_issue_date,
- });
- }
- for (const backupCoin of backupDenomination.coins) {
- await importCoin(ws, tx, cryptoComp, {
- backupCoin,
- denomPubHash,
- exchangeBaseUrl: backupExchangeDetails.base_url,
- });
- }
- }
- }
-
- for (const backupWg of backupBlob.withdrawal_groups) {
- const reservePub = cryptoComp.reservePrivToPub[backupWg.reserve_priv];
- checkLogicInvariant(!!reservePub);
- const ts = constructTombstone({
- tag: TombstoneTag.DeleteReserve,
- reservePub,
- });
- if (tombstoneSet.has(ts)) {
- continue;
- }
- const existingWg = await tx.withdrawalGroups.get(
- backupWg.withdrawal_group_id,
- );
- if (existingWg) {
- continue;
- }
- let wgInfo: WgInfo;
- switch (backupWg.info.type) {
- case BackupWgType.BankIntegrated:
- wgInfo = {
- withdrawalType: WithdrawalRecordType.BankIntegrated,
- bankInfo: {
- exchangePaytoUri: backupWg.info.exchange_payto_uri,
- talerWithdrawUri: backupWg.info.taler_withdraw_uri,
- confirmUrl: backupWg.info.confirm_url,
- timestampBankConfirmed: backupWg.info.timestamp_bank_confirmed,
- timestampReserveInfoPosted:
- backupWg.info.timestamp_reserve_info_posted,
- },
- };
- break;
- case BackupWgType.BankManual:
- wgInfo = {
- withdrawalType: WithdrawalRecordType.BankManual,
- };
- break;
- case BackupWgType.PeerPullCredit:
- wgInfo = {
- withdrawalType: WithdrawalRecordType.PeerPullCredit,
- contractTerms: backupWg.info.contract_terms,
- contractPriv: backupWg.info.contract_priv,
- };
- break;
- case BackupWgType.PeerPushCredit:
- wgInfo = {
- withdrawalType: WithdrawalRecordType.PeerPushCredit,
- contractTerms: backupWg.info.contract_terms,
- };
- break;
- case BackupWgType.Recoup:
- wgInfo = {
- withdrawalType: WithdrawalRecordType.Recoup,
- };
- break;
- default:
- assertUnreachable(backupWg.info);
- }
- const instructedAmount = Amounts.parseOrThrow(
- backupWg.instructed_amount,
- );
- await tx.withdrawalGroups.put({
- withdrawalGroupId: backupWg.withdrawal_group_id,
- exchangeBaseUrl: backupWg.exchange_base_url,
- instructedAmount: Amounts.stringify(instructedAmount),
- secretSeed: backupWg.secret_seed,
- denomsSel: await getDenomSelStateFromBackup(
- tx,
- instructedAmount.currency,
- backupWg.exchange_base_url,
- backupWg.selected_denoms,
- ),
- denomSelUid: backupWg.selected_denoms_uid,
- rawWithdrawalAmount: Amounts.stringify(
- backupWg.raw_withdrawal_amount,
- ),
- effectiveWithdrawalAmount: Amounts.stringify(
- backupWg.effective_withdrawal_amount,
- ),
- reservePriv: backupWg.reserve_priv,
- reservePub,
- status: backupWg.timestamp_finish
- ? WithdrawalGroupStatus.Finished
- : WithdrawalGroupStatus.PendingQueryingStatus, // FIXME!
- timestampStart: backupWg.timestamp_created,
- wgInfo,
- restrictAge: backupWg.restrict_age,
- senderWire: undefined, // FIXME!
- timestampFinish: backupWg.timestamp_finish,
- });
- }
-
- for (const backupPurchase of backupBlob.purchases) {
- const ts = constructTombstone({
- tag: TombstoneTag.DeletePayment,
- proposalId: backupPurchase.proposal_id,
- });
- if (tombstoneSet.has(ts)) {
- continue;
- }
- const existingPurchase = await tx.purchases.get(
- backupPurchase.proposal_id,
- );
- let proposalStatus: PurchaseStatus;
- switch (backupPurchase.proposal_status) {
- case BackupProposalStatus.Paid:
- proposalStatus = PurchaseStatus.Done;
- break;
- case BackupProposalStatus.Shared:
- proposalStatus = PurchaseStatus.DialogShared;
- break;
- case BackupProposalStatus.Proposed:
- proposalStatus = PurchaseStatus.DialogProposed;
- break;
- case BackupProposalStatus.PermanentlyFailed:
- proposalStatus = PurchaseStatus.AbortedIncompletePayment;
- break;
- case BackupProposalStatus.Refused:
- proposalStatus = PurchaseStatus.AbortedProposalRefused;
- break;
- case BackupProposalStatus.Repurchase:
- proposalStatus = PurchaseStatus.RepurchaseDetected;
- break;
- default: {
- const error: never = backupPurchase.proposal_status;
- throw Error(`backup status ${error} is not handled`);
- }
- }
- if (!existingPurchase) {
- //const refunds: { [refundKey: string]: WalletRefundItem } = {};
- // for (const backupRefund of backupPurchase.refunds) {
- // const key = `${backupRefund.coin_pub}-${backupRefund.rtransaction_id}`;
- // const coin = await tx.coins.get(backupRefund.coin_pub);
- // checkBackupInvariant(!!coin);
- // const denom = await tx.denominations.get([
- // coin.exchangeBaseUrl,
- // coin.denomPubHash,
- // ]);
- // checkBackupInvariant(!!denom);
- // const common = {
- // coinPub: backupRefund.coin_pub,
- // executionTime: backupRefund.execution_time,
- // obtainedTime: backupRefund.obtained_time,
- // refundAmount: Amounts.stringify(backupRefund.refund_amount),
- // refundFee: Amounts.stringify(denom.fees.feeRefund),
- // rtransactionId: backupRefund.rtransaction_id,
- // totalRefreshCostBound: Amounts.stringify(
- // backupRefund.total_refresh_cost_bound,
- // ),
- // };
- // switch (backupRefund.type) {
- // case BackupRefundState.Applied:
- // refunds[key] = {
- // type: RefundState.Applied,
- // ...common,
- // };
- // break;
- // case BackupRefundState.Failed:
- // refunds[key] = {
- // type: RefundState.Failed,
- // ...common,
- // };
- // break;
- // case BackupRefundState.Pending:
- // refunds[key] = {
- // type: RefundState.Pending,
- // ...common,
- // };
- // break;
- // }
- // }
- const parsedContractTerms = codecForMerchantContractTerms().decode(
- backupPurchase.contract_terms_raw,
- );
- const amount = Amounts.parseOrThrow(parsedContractTerms.amount);
- const contractTermsHash =
- cryptoComp.proposalIdToContractTermsHash[
- backupPurchase.proposal_id
- ];
- let maxWireFee: AmountJson;
- if (parsedContractTerms.max_wire_fee) {
- maxWireFee = Amounts.parseOrThrow(parsedContractTerms.max_wire_fee);
- } else {
- maxWireFee = Amounts.zeroOfCurrency(amount.currency);
- }
- const download: ProposalDownloadInfo = {
- contractTermsHash,
- contractTermsMerchantSig: backupPurchase.merchant_sig!,
- currency: amount.currency,
- fulfillmentUrl: backupPurchase.contract_terms_raw.fulfillment_url,
- };
-
- const contractData = extractContractData(
- backupPurchase.contract_terms_raw,
- contractTermsHash,
- download.contractTermsMerchantSig,
- );
-
- let payInfo: PurchasePayInfo | undefined = undefined;
- if (backupPurchase.pay_info) {
- payInfo = {
- payCoinSelection: await recoverPayCoinSelection(
- tx,
- contractData,
- backupPurchase.pay_info,
- ),
- payCoinSelectionUid: backupPurchase.pay_info.pay_coins_uid,
- totalPayCost: Amounts.stringify(
- backupPurchase.pay_info.total_pay_cost,
- ),
- };
- }
-
- await tx.purchases.put({
- proposalId: backupPurchase.proposal_id,
- noncePriv: backupPurchase.nonce_priv,
- noncePub:
- cryptoComp.proposalNoncePrivToPub[backupPurchase.nonce_priv],
- autoRefundDeadline: TalerProtocolTimestamp.never(),
- timestampAccept: backupPurchase.timestamp_accepted,
- timestampFirstSuccessfulPay:
- backupPurchase.timestamp_first_successful_pay,
- timestampLastRefundStatus: undefined,
- merchantPaySig: backupPurchase.merchant_pay_sig,
- posConfirmation: backupPurchase.pos_confirmation,
- lastSessionId: undefined,
- download,
- //refunds,
- claimToken: backupPurchase.claim_token,
- downloadSessionId: backupPurchase.download_session_id,
- merchantBaseUrl: backupPurchase.merchant_base_url,
- orderId: backupPurchase.order_id,
- payInfo,
- refundAmountAwaiting: undefined,
- repurchaseProposalId: backupPurchase.repurchase_proposal_id,
- purchaseStatus: proposalStatus,
- timestamp: backupPurchase.timestamp_proposed,
- shared: backupPurchase.shared,
- });
- }
- }
-
- for (const backupRefreshGroup of backupBlob.refresh_groups) {
- const ts = constructTombstone({
- tag: TombstoneTag.DeleteRefreshGroup,
- refreshGroupId: backupRefreshGroup.refresh_group_id,
- });
- if (tombstoneSet.has(ts)) {
- continue;
- }
- const existingRg = await tx.refreshGroups.get(
- backupRefreshGroup.refresh_group_id,
- );
- if (!existingRg) {
- let reason: RefreshReason;
- switch (backupRefreshGroup.reason) {
- case BackupRefreshReason.AbortPay:
- reason = RefreshReason.AbortPay;
- break;
- case BackupRefreshReason.BackupRestored:
- reason = RefreshReason.BackupRestored;
- break;
- case BackupRefreshReason.Manual:
- reason = RefreshReason.Manual;
- break;
- case BackupRefreshReason.Pay:
- reason = RefreshReason.PayMerchant;
- break;
- case BackupRefreshReason.Recoup:
- reason = RefreshReason.Recoup;
- break;
- case BackupRefreshReason.Refund:
- reason = RefreshReason.Refund;
- break;
- case BackupRefreshReason.Scheduled:
- reason = RefreshReason.Scheduled;
- break;
- }
- const refreshSessionPerCoin: (RefreshSessionRecord | undefined)[] =
- [];
- for (const oldCoin of backupRefreshGroup.old_coins) {
- const c = await tx.coins.get(oldCoin.coin_pub);
- checkBackupInvariant(!!c);
- const d = await tx.denominations.get([
- c.exchangeBaseUrl,
- c.denomPubHash,
- ]);
- checkBackupInvariant(!!d);
-
- if (oldCoin.refresh_session) {
- const denomSel = await getDenomSelStateFromBackup(
- tx,
- d.currency,
- c.exchangeBaseUrl,
- oldCoin.refresh_session.new_denoms,
- );
- refreshSessionPerCoin.push({
- sessionSecretSeed: oldCoin.refresh_session.session_secret_seed,
- norevealIndex: oldCoin.refresh_session.noreveal_index,
- newDenoms: oldCoin.refresh_session.new_denoms.map((x) => ({
- count: x.count,
- denomPubHash: x.denom_pub_hash,
- })),
- amountRefreshOutput: Amounts.stringify(denomSel.totalCoinValue),
- });
- } else {
- refreshSessionPerCoin.push(undefined);
- }
- }
- await tx.refreshGroups.put({
- timestampFinished: backupRefreshGroup.timestamp_finish,
- timestampCreated: backupRefreshGroup.timestamp_created,
- refreshGroupId: backupRefreshGroup.refresh_group_id,
- currency: Amounts.currencyOf(
- backupRefreshGroup.old_coins[0].input_amount,
- ),
- reason,
- lastErrorPerCoin: {},
- oldCoinPubs: backupRefreshGroup.old_coins.map((x) => x.coin_pub),
- statusPerCoin: backupRefreshGroup.old_coins.map((x) =>
- x.finished
- ? RefreshCoinStatus.Finished
- : RefreshCoinStatus.Pending,
- ),
- operationStatus: backupRefreshGroup.timestamp_finish
- ? RefreshOperationStatus.Finished
- : RefreshOperationStatus.Pending,
- inputPerCoin: backupRefreshGroup.old_coins.map(
- (x) => x.input_amount,
- ),
- estimatedOutputPerCoin: backupRefreshGroup.old_coins.map(
- (x) => x.estimated_output_amount,
- ),
- refreshSessionPerCoin,
- });
- }
- }
-
- for (const backupTip of backupBlob.tips) {
- const ts = constructTombstone({
- tag: TombstoneTag.DeleteReward,
- walletTipId: backupTip.wallet_tip_id,
- });
- if (tombstoneSet.has(ts)) {
- continue;
- }
- const existingTip = await tx.rewards.get(backupTip.wallet_tip_id);
- if (!existingTip) {
- const tipAmountRaw = Amounts.parseOrThrow(backupTip.tip_amount_raw);
- const denomsSel = await getDenomSelStateFromBackup(
- tx,
- tipAmountRaw.currency,
- backupTip.exchange_base_url,
- backupTip.selected_denoms,
- );
- await tx.rewards.put({
- acceptedTimestamp: backupTip.timestamp_accepted,
- createdTimestamp: backupTip.timestamp_created,
- denomsSel,
- next_url: backupTip.next_url,
- exchangeBaseUrl: backupTip.exchange_base_url,
- merchantBaseUrl: backupTip.exchange_base_url,
- merchantRewardId: backupTip.merchant_tip_id,
- pickedUpTimestamp: backupTip.timestamp_finished,
- secretSeed: backupTip.secret_seed,
- rewardAmountEffective: Amounts.stringify(denomsSel.totalCoinValue),
- rewardAmountRaw: Amounts.stringify(tipAmountRaw),
- rewardExpiration: backupTip.timestamp_expiration,
- walletRewardId: backupTip.wallet_tip_id,
- denomSelUid: backupTip.selected_denoms_uid,
- status: RewardRecordStatus.Done, // FIXME!
- });
- }
- }
-
- // We now process tombstones.
- // The import code above should already prevent
- // importing things that are tombstoned,
- // but we do tombstone processing last just to be sure.
-
- for (const tombstone of tombstoneSet) {
- const [type, ...rest] = tombstone.split(":");
- if (type === TombstoneTag.DeleteDepositGroup) {
- await tx.depositGroups.delete(rest[0]);
- } else if (type === TombstoneTag.DeletePayment) {
- await tx.purchases.delete(rest[0]);
- } else if (type === TombstoneTag.DeleteRefreshGroup) {
- await tx.refreshGroups.delete(rest[0]);
- } else if (type === TombstoneTag.DeleteRefund) {
- // Nothing required, will just prevent display
- // in the transactions list
- } else if (type === TombstoneTag.DeleteReward) {
- await tx.rewards.delete(rest[0]);
- } else if (type === TombstoneTag.DeleteWithdrawalGroup) {
- await tx.withdrawalGroups.delete(rest[0]);
- } else {
- logger.warn(`unable to process tombstone of type '${type}'`);
- }
- }
- });
-}
diff --git a/packages/taler-wallet-core/src/operations/backup/index.ts b/packages/taler-wallet-core/src/operations/backup/index.ts
index e35765165..a5e8dbd42 100644
--- a/packages/taler-wallet-core/src/operations/backup/index.ts
+++ b/packages/taler-wallet-core/src/operations/backup/index.ts
@@ -43,7 +43,6 @@ import {
TalerErrorDetail,
TalerPreciseTimestamp,
URL,
- WalletBackupContentV1,
buildCodecForObject,
buildCodecForUnion,
bytesToString,
@@ -99,9 +98,8 @@ import {
TaskIdentifiers,
} from "../common.js";
import { checkPaymentByProposalId, preparePayForUri } from "../pay-merchant.js";
-import { exportBackup } from "./export.js";
-import { BackupCryptoPrecomputedData, importBackup } from "./import.js";
-import { getWalletBackupState, provideBackupState } from "./state.js";
+import { WalletStoresV1 } from "../../db.js";
+import { GetReadOnlyAccess } from "../../util/query.js";
const logger = new Logger("operations/backup.ts");
@@ -131,7 +129,7 @@ const magic = "TLRWBK01";
*/
export async function encryptBackup(
config: WalletBackupConfState,
- blob: WalletBackupContentV1,
+ blob: any,
): Promise<Uint8Array> {
const chunks: Uint8Array[] = [];
chunks.push(stringToBytes(magic));
@@ -150,64 +148,6 @@ export async function encryptBackup(
return concatArrays(chunks);
}
-/**
- * Compute cryptographic values for a backup blob.
- *
- * FIXME: Take data that we already know from the DB.
- * FIXME: Move computations into crypto worker.
- */
-async function computeBackupCryptoData(
- cryptoApi: TalerCryptoInterface,
- backupContent: WalletBackupContentV1,
-): Promise<BackupCryptoPrecomputedData> {
- const cryptoData: BackupCryptoPrecomputedData = {
- coinPrivToCompletedCoin: {},
- rsaDenomPubToHash: {},
- proposalIdToContractTermsHash: {},
- proposalNoncePrivToPub: {},
- reservePrivToPub: {},
- };
- for (const backupExchangeDetails of backupContent.exchange_details) {
- for (const backupDenom of backupExchangeDetails.denominations) {
- if (backupDenom.denom_pub.cipher !== DenomKeyType.Rsa) {
- throw Error("unsupported cipher");
- }
- for (const backupCoin of backupDenom.coins) {
- const coinPub = encodeCrock(
- eddsaGetPublic(decodeCrock(backupCoin.coin_priv)),
- );
- const blindedCoin = rsaBlind(
- hash(decodeCrock(backupCoin.coin_priv)),
- decodeCrock(backupCoin.blinding_key),
- decodeCrock(backupDenom.denom_pub.rsa_public_key),
- );
- cryptoData.coinPrivToCompletedCoin[backupCoin.coin_priv] = {
- coinEvHash: encodeCrock(hash(blindedCoin)),
- coinPub,
- };
- }
- cryptoData.rsaDenomPubToHash[backupDenom.denom_pub.rsa_public_key] =
- encodeCrock(hashDenomPub(backupDenom.denom_pub));
- }
- }
- for (const backupWg of backupContent.withdrawal_groups) {
- cryptoData.reservePrivToPub[backupWg.reserve_priv] = encodeCrock(
- eddsaGetPublic(decodeCrock(backupWg.reserve_priv)),
- );
- }
- for (const purch of backupContent.purchases) {
- if (!purch.contract_terms_raw) continue;
- const { h: contractTermsHash } = await cryptoApi.hashString({
- str: canonicalJson(purch.contract_terms_raw),
- });
- const noncePub = encodeCrock(eddsaGetPublic(decodeCrock(purch.nonce_priv)));
- cryptoData.proposalNoncePrivToPub[purch.nonce_priv] = noncePub;
- cryptoData.proposalIdToContractTermsHash[purch.proposal_id] =
- contractTermsHash;
- }
- return cryptoData;
-}
-
function deriveAccountKeyPair(
bc: WalletBackupConfState,
providerUrl: string,
@@ -262,7 +202,9 @@ async function runBackupCycleForProvider(
return TaskRunResult.finished();
}
- const backupJson = await exportBackup(ws);
+ //const backupJson = await exportBackup(ws);
+ // FIXME: re-implement backup
+ const backupJson = {};
const backupConfig = await provideBackupState(ws);
const encBackup = await encryptBackup(backupConfig, backupJson);
const currentBackupHash = hash(encBackup);
@@ -441,9 +383,9 @@ async function runBackupCycleForProvider(
logger.info("conflicting backup found");
const backupEnc = new Uint8Array(await resp.bytes());
const backupConfig = await provideBackupState(ws);
- const blob = await decryptBackup(backupConfig, backupEnc);
- const cryptoData = await computeBackupCryptoData(ws.cryptoApi, blob);
- await importBackup(ws, blob, cryptoData);
+ // const blob = await decryptBackup(backupConfig, backupEnc);
+ // FIXME: Re-implement backup import with merging
+ // await importBackup(ws, blob, cryptoData);
await ws.db
.mktx((x) => [x.backupProviders, x.operationRetries])
.runReadWrite(async (tx) => {
@@ -789,18 +731,6 @@ export interface BackupInfo {
providers: ProviderInfo[];
}
-export async function importBackupPlain(
- ws: InternalWalletState,
- blob: any,
-): Promise<void> {
- // FIXME: parse
- const backup: WalletBackupContentV1 = blob;
-
- const cryptoData = await computeBackupCryptoData(ws.cryptoApi, backup);
-
- await importBackup(ws, blob, cryptoData);
-}
-
export enum ProviderPaymentType {
Unpaid = "unpaid",
Pending = "pending",
@@ -1036,23 +966,10 @@ export async function loadBackupRecovery(
}
}
-export async function exportBackupEncrypted(
- ws: InternalWalletState,
-): Promise<Uint8Array> {
- await provideBackupState(ws);
- const blob = await exportBackup(ws);
- const bs = await ws.db
- .mktx((x) => [x.config])
- .runReadOnly(async (tx) => {
- return await getWalletBackupState(ws, tx);
- });
- return encryptBackup(bs, blob);
-}
-
export async function decryptBackup(
backupConfig: WalletBackupConfState,
data: Uint8Array,
-): Promise<WalletBackupContentV1> {
+): Promise<any> {
const rMagic = bytesToString(data.slice(0, 8));
if (rMagic != magic) {
throw Error("invalid backup file (magic tag mismatch)");
@@ -1068,12 +985,85 @@ export async function decryptBackup(
return JSON.parse(bytesToString(gunzipSync(dataCompressed)));
}
-export async function importBackupEncrypted(
+export async function provideBackupState(
ws: InternalWalletState,
- data: Uint8Array,
+): Promise<WalletBackupConfState> {
+ const bs: ConfigRecord | undefined = await ws.db
+ .mktx((stores) => [stores.config])
+ .runReadOnly(async (tx) => {
+ return await tx.config.get(ConfigRecordKey.WalletBackupState);
+ });
+ if (bs) {
+ checkDbInvariant(bs.key === ConfigRecordKey.WalletBackupState);
+ return bs.value;
+ }
+ // We need to generate the key outside of the transaction
+ // due to how IndexedDB works.
+ const k = await ws.cryptoApi.createEddsaKeypair({});
+ const d = getRandomBytes(5);
+ // FIXME: device ID should be configured when wallet is initialized
+ // and be based on hostname
+ const deviceId = `wallet-core-${encodeCrock(d)}`;
+ return await ws.db
+ .mktx((x) => [x.config])
+ .runReadWrite(async (tx) => {
+ let backupStateEntry: ConfigRecord | undefined = await tx.config.get(
+ ConfigRecordKey.WalletBackupState,
+ );
+ if (!backupStateEntry) {
+ backupStateEntry = {
+ key: ConfigRecordKey.WalletBackupState,
+ value: {
+ deviceId,
+ walletRootPub: k.pub,
+ walletRootPriv: k.priv,
+ lastBackupPlainHash: undefined,
+ },
+ };
+ await tx.config.put(backupStateEntry);
+ }
+ checkDbInvariant(
+ backupStateEntry.key === ConfigRecordKey.WalletBackupState,
+ );
+ return backupStateEntry.value;
+ });
+}
+
+export async function getWalletBackupState(
+ ws: InternalWalletState,
+ tx: GetReadOnlyAccess<{ config: typeof WalletStoresV1.config }>,
+): Promise<WalletBackupConfState> {
+ const bs = await tx.config.get(ConfigRecordKey.WalletBackupState);
+ checkDbInvariant(!!bs, "wallet backup state should be in DB");
+ checkDbInvariant(bs.key === ConfigRecordKey.WalletBackupState);
+ return bs.value;
+}
+
+export async function setWalletDeviceId(
+ ws: InternalWalletState,
+ deviceId: string,
): Promise<void> {
- const backupConfig = await provideBackupState(ws);
- const blob = await decryptBackup(backupConfig, data);
- const cryptoData = await computeBackupCryptoData(ws.cryptoApi, blob);
- await importBackup(ws, blob, cryptoData);
+ await provideBackupState(ws);
+ await ws.db
+ .mktx((x) => [x.config])
+ .runReadWrite(async (tx) => {
+ let backupStateEntry: ConfigRecord | undefined = await tx.config.get(
+ ConfigRecordKey.WalletBackupState,
+ );
+ if (
+ !backupStateEntry ||
+ backupStateEntry.key !== ConfigRecordKey.WalletBackupState
+ ) {
+ return;
+ }
+ backupStateEntry.value.deviceId = deviceId;
+ await tx.config.put(backupStateEntry);
+ });
+}
+
+export async function getWalletDeviceId(
+ ws: InternalWalletState,
+): Promise<string> {
+ const bs = await provideBackupState(ws);
+ return bs.deviceId;
}
diff --git a/packages/taler-wallet-core/src/operations/backup/state.ts b/packages/taler-wallet-core/src/operations/backup/state.ts
index fa632f44c..d02ead783 100644
--- a/packages/taler-wallet-core/src/operations/backup/state.ts
+++ b/packages/taler-wallet-core/src/operations/backup/state.ts
@@ -14,96 +14,4 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { encodeCrock, getRandomBytes } from "@gnu-taler/taler-util";
-import {
- ConfigRecord,
- ConfigRecordKey,
- WalletBackupConfState,
- WalletStoresV1,
-} from "../../db.js";
-import { checkDbInvariant } from "../../util/invariants.js";
-import { GetReadOnlyAccess } from "../../util/query.js";
-import { InternalWalletState } from "../../internal-wallet-state.js";
-export async function provideBackupState(
- ws: InternalWalletState,
-): Promise<WalletBackupConfState> {
- const bs: ConfigRecord | undefined = await ws.db
- .mktx((stores) => [stores.config])
- .runReadOnly(async (tx) => {
- return await tx.config.get(ConfigRecordKey.WalletBackupState);
- });
- if (bs) {
- checkDbInvariant(bs.key === ConfigRecordKey.WalletBackupState);
- return bs.value;
- }
- // We need to generate the key outside of the transaction
- // due to how IndexedDB works.
- const k = await ws.cryptoApi.createEddsaKeypair({});
- const d = getRandomBytes(5);
- // FIXME: device ID should be configured when wallet is initialized
- // and be based on hostname
- const deviceId = `wallet-core-${encodeCrock(d)}`;
- return await ws.db
- .mktx((x) => [x.config])
- .runReadWrite(async (tx) => {
- let backupStateEntry: ConfigRecord | undefined = await tx.config.get(
- ConfigRecordKey.WalletBackupState,
- );
- if (!backupStateEntry) {
- backupStateEntry = {
- key: ConfigRecordKey.WalletBackupState,
- value: {
- deviceId,
- walletRootPub: k.pub,
- walletRootPriv: k.priv,
- lastBackupPlainHash: undefined,
- },
- };
- await tx.config.put(backupStateEntry);
- }
- checkDbInvariant(
- backupStateEntry.key === ConfigRecordKey.WalletBackupState,
- );
- return backupStateEntry.value;
- });
-}
-
-export async function getWalletBackupState(
- ws: InternalWalletState,
- tx: GetReadOnlyAccess<{ config: typeof WalletStoresV1.config }>,
-): Promise<WalletBackupConfState> {
- const bs = await tx.config.get(ConfigRecordKey.WalletBackupState);
- checkDbInvariant(!!bs, "wallet backup state should be in DB");
- checkDbInvariant(bs.key === ConfigRecordKey.WalletBackupState);
- return bs.value;
-}
-
-export async function setWalletDeviceId(
- ws: InternalWalletState,
- deviceId: string,
-): Promise<void> {
- await provideBackupState(ws);
- await ws.db
- .mktx((x) => [x.config])
- .runReadWrite(async (tx) => {
- let backupStateEntry: ConfigRecord | undefined = await tx.config.get(
- ConfigRecordKey.WalletBackupState,
- );
- if (
- !backupStateEntry ||
- backupStateEntry.key !== ConfigRecordKey.WalletBackupState
- ) {
- return;
- }
- backupStateEntry.value.deviceId = deviceId;
- await tx.config.put(backupStateEntry);
- });
-}
-
-export async function getWalletDeviceId(
- ws: InternalWalletState,
-): Promise<string> {
- const bs = await provideBackupState(ws);
- return bs.deviceId;
-}
diff --git a/packages/taler-wallet-core/src/operations/common.ts b/packages/taler-wallet-core/src/operations/common.ts
index 7a8b78b53..e96beb5b2 100644
--- a/packages/taler-wallet-core/src/operations/common.ts
+++ b/packages/taler-wallet-core/src/operations/common.ts
@@ -30,6 +30,7 @@ import {
ExchangeEntryStatus,
ExchangeListItem,
ExchangeTosStatus,
+ ExchangeUpdateStatus,
getErrorDetailFromException,
j2s,
Logger,
@@ -47,7 +48,7 @@ import {
WalletStoresV1,
CoinRecord,
ExchangeDetailsRecord,
- ExchangeRecord,
+ ExchangeEntryRecord,
BackupProviderRecord,
DepositGroupRecord,
PeerPullPaymentIncomingRecord,
@@ -59,6 +60,8 @@ import {
RefreshGroupRecord,
RewardRecord,
WithdrawalGroupRecord,
+ ExchangeEntryDbUpdateStatus,
+ ExchangeEntryDbRecordStatus,
} from "../db.js";
import { makeErrorDetail, TalerError } from "@gnu-taler/taler-util";
import { InternalWalletState } from "../internal-wallet-state.js";
@@ -529,16 +532,16 @@ export function getExchangeTosStatus(
exchangeDetails: ExchangeDetailsRecord,
): ExchangeTosStatus {
if (!exchangeDetails.tosAccepted) {
- return ExchangeTosStatus.New;
+ return ExchangeTosStatus.Proposed;
}
if (exchangeDetails.tosAccepted?.etag == exchangeDetails.tosCurrentEtag) {
return ExchangeTosStatus.Accepted;
}
- return ExchangeTosStatus.Changed;
+ return ExchangeTosStatus.Proposed;
}
export function makeExchangeListItem(
- r: ExchangeRecord,
+ r: ExchangeEntryRecord,
exchangeDetails: ExchangeDetailsRecord | undefined,
lastError: TalerErrorDetail | undefined,
): ExchangeListItem {
@@ -547,30 +550,57 @@ export function makeExchangeListItem(
error: lastError,
}
: undefined;
- if (!exchangeDetails) {
- return {
- exchangeBaseUrl: r.baseUrl,
- currency: undefined,
- tosStatus: ExchangeTosStatus.Unknown,
- paytoUris: [],
- exchangeStatus: ExchangeEntryStatus.Unknown,
- permanent: r.permanent,
- ageRestrictionOptions: [],
- lastUpdateErrorInfo,
- };
+
+ let exchangeUpdateStatus: ExchangeUpdateStatus;
+ switch (r.updateStatus) {
+ case ExchangeEntryDbUpdateStatus.Failed:
+ exchangeUpdateStatus = ExchangeUpdateStatus.Failed;
+ break;
+ case ExchangeEntryDbUpdateStatus.Initial:
+ exchangeUpdateStatus = ExchangeUpdateStatus.Initial;
+ break;
+ case ExchangeEntryDbUpdateStatus.InitialUpdate:
+ exchangeUpdateStatus = ExchangeUpdateStatus.InitialUpdate;
+ break;
+ case ExchangeEntryDbUpdateStatus.OutdatedUpdate:
+ exchangeUpdateStatus = ExchangeUpdateStatus.OutdatedUpdate;
+ break;
+ case ExchangeEntryDbUpdateStatus.Ready:
+ exchangeUpdateStatus = ExchangeUpdateStatus.Ready;
+ break;
+ case ExchangeEntryDbUpdateStatus.ReadyUpdate:
+ exchangeUpdateStatus = ExchangeUpdateStatus.ReadyUpdate;
+ break;
+ case ExchangeEntryDbUpdateStatus.Suspended:
+ exchangeUpdateStatus = ExchangeUpdateStatus.Suspended;
+ break;
+ }
+
+ let exchangeEntryStatus: ExchangeEntryStatus;
+ switch (r.entryStatus) {
+ case ExchangeEntryDbRecordStatus.Ephemeral:
+ exchangeEntryStatus = ExchangeEntryStatus.Ephemeral;
+ break;
+ case ExchangeEntryDbRecordStatus.Preset:
+ exchangeEntryStatus = ExchangeEntryStatus.Preset;
+ break;
+ case ExchangeEntryDbRecordStatus.Used:
+ exchangeEntryStatus = ExchangeEntryStatus.Used;
+ break;
}
- let exchangeStatus;
- exchangeStatus = ExchangeEntryStatus.Ok;
+
return {
exchangeBaseUrl: r.baseUrl,
- currency: exchangeDetails.currency,
- tosStatus: getExchangeTosStatus(exchangeDetails),
- paytoUris: exchangeDetails.wireInfo.accounts.map((x) => x.payto_uri),
- exchangeStatus,
- permanent: r.permanent,
- ageRestrictionOptions: exchangeDetails.ageMask
+ currency: exchangeDetails?.currency,
+ exchangeUpdateStatus,
+ exchangeEntryStatus,
+ tosStatus: exchangeDetails
+ ? getExchangeTosStatus(exchangeDetails)
+ : ExchangeTosStatus.Pending,
+ ageRestrictionOptions: exchangeDetails?.ageMask
? AgeRestriction.getAgeGroupsFromMask(exchangeDetails.ageMask)
: [],
+ paytoUris: exchangeDetails?.wireInfo.accounts.map((x) => x.payto_uri) ?? [],
lastUpdateErrorInfo,
};
}
@@ -892,13 +922,13 @@ export namespace TaskIdentifiers {
export function forWithdrawal(wg: WithdrawalGroupRecord): TaskId {
return `${PendingTaskType.Withdraw}:${wg.withdrawalGroupId}` as TaskId;
}
- export function forExchangeUpdate(exch: ExchangeRecord): TaskId {
+ export function forExchangeUpdate(exch: ExchangeEntryRecord): TaskId {
return `${PendingTaskType.ExchangeUpdate}:${exch.baseUrl}` as TaskId;
}
export function forExchangeUpdateFromUrl(exchBaseUrl: string): TaskId {
return `${PendingTaskType.ExchangeUpdate}:${exchBaseUrl}` as TaskId;
}
- export function forExchangeCheckRefresh(exch: ExchangeRecord): TaskId {
+ export function forExchangeCheckRefresh(exch: ExchangeEntryRecord): TaskId {
return `${PendingTaskType.ExchangeCheckRefresh}:${exch.baseUrl}` as TaskId;
}
export function forTipPickup(tipRecord: RewardRecord): TaskId {
diff --git a/packages/taler-wallet-core/src/operations/exchanges.ts b/packages/taler-wallet-core/src/operations/exchanges.ts
index c6b46e360..311a71a6e 100644
--- a/packages/taler-wallet-core/src/operations/exchanges.ts
+++ b/packages/taler-wallet-core/src/operations/exchanges.ts
@@ -32,6 +32,7 @@ import {
encodeCrock,
ExchangeAuditor,
ExchangeDenomination,
+ ExchangeEntryStatus,
ExchangeGlobalFees,
ExchangeSignKeyJson,
ExchangeWireJson,
@@ -66,10 +67,15 @@ import {
DenominationRecord,
DenominationVerificationStatus,
ExchangeDetailsRecord,
- ExchangeRecord,
+ ExchangeEntryRecord,
WalletStoresV1,
} from "../db.js";
-import { isWithdrawableDenom } from "../index.js";
+import {
+ ExchangeEntryDbRecordStatus,
+ ExchangeEntryDbUpdateStatus,
+ isWithdrawableDenom,
+ WalletDbReadWriteTransaction,
+} from "../index.js";
import { InternalWalletState, TrustInfo } from "../internal-wallet-state.js";
import { checkDbInvariant } from "../util/invariants.js";
import {
@@ -326,6 +332,26 @@ export async function downloadExchangeInfo(
};
}
+export async function addPresetExchangeEntry(
+ tx: WalletDbReadWriteTransaction<"exchanges">,
+ exchangeBaseUrl: string,
+): Promise<void> {
+ let exchange = await tx.exchanges.get(exchangeBaseUrl);
+ if (!exchange) {
+ const r: ExchangeEntryRecord = {
+ entryStatus: ExchangeEntryDbRecordStatus.Preset,
+ updateStatus: ExchangeEntryDbUpdateStatus.Initial,
+ baseUrl: exchangeBaseUrl,
+ detailsPointer: undefined,
+ lastUpdate: undefined,
+ lastKeysEtag: undefined,
+ nextRefreshCheckStampMs: AbsoluteTime.getStampMsNever(),
+ nextUpdateStampMs: AbsoluteTime.getStampMsNever(),
+ };
+ await tx.exchanges.put(r);
+ }
+}
+
export async function provideExchangeRecordInTx(
ws: InternalWalletState,
tx: GetReadWriteAccess<{
@@ -335,20 +361,20 @@ export async function provideExchangeRecordInTx(
baseUrl: string,
now: AbsoluteTime,
): Promise<{
- exchange: ExchangeRecord;
+ exchange: ExchangeEntryRecord;
exchangeDetails: ExchangeDetailsRecord | undefined;
}> {
let exchange = await tx.exchanges.get(baseUrl);
if (!exchange) {
- const r: ExchangeRecord = {
- permanent: true,
+ const r: ExchangeEntryRecord = {
+ entryStatus: ExchangeEntryDbRecordStatus.Ephemeral,
+ updateStatus: ExchangeEntryDbUpdateStatus.InitialUpdate,
baseUrl: baseUrl,
detailsPointer: undefined,
lastUpdate: undefined,
- nextUpdate: AbsoluteTime.toPreciseTimestamp(now),
- nextRefreshCheck: AbsoluteTime.toPreciseTimestamp(now),
+ nextUpdateStampMs: AbsoluteTime.getStampMsNever(),
+ nextRefreshCheckStampMs: AbsoluteTime.getStampMsNever(),
lastKeysEtag: undefined,
- lastWireEtag: undefined,
};
await tx.exchanges.put(r);
exchange = r;
@@ -534,6 +560,10 @@ export async function downloadTosFromAcceptedFormat(
);
}
+/**
+ * FIXME: Split this into two parts: (a) triggering the exchange
+ * to be updated and (b) waiting for the update to finish.
+ */
export async function updateExchangeFromUrl(
ws: InternalWalletState,
baseUrl: string,
@@ -543,7 +573,7 @@ export async function updateExchangeFromUrl(
cancellationToken?: CancellationToken;
} = {},
): Promise<{
- exchange: ExchangeRecord;
+ exchange: ExchangeEntryRecord;
exchangeDetails: ExchangeDetailsRecord;
}> {
const canonUrl = canonicalizeBaseUrl(baseUrl);
@@ -613,7 +643,7 @@ export async function updateExchangeFromUrlHandler(
!forceNow &&
exchangeDetails !== undefined &&
!AbsoluteTime.isExpired(
- AbsoluteTime.fromPreciseTimestamp(exchange.nextUpdate),
+ AbsoluteTime.fromStampMs(exchange.nextUpdateStampMs),
)
) {
logger.trace("using existing exchange info");
@@ -755,11 +785,11 @@ export async function updateExchangeFromUrlHandler(
newDetails.rowId = existingDetails.rowId;
}
r.lastUpdate = TalerPreciseTimestamp.now();
- r.nextUpdate = AbsoluteTime.toPreciseTimestamp(
+ r.nextUpdateStampMs = AbsoluteTime.toStampMs(
AbsoluteTime.fromProtocolTimestamp(keysInfo.expiry),
);
// New denominations might be available.
- r.nextRefreshCheck = TalerPreciseTimestamp.now();
+ r.nextRefreshCheckStampMs = AbsoluteTime.getStampMsNow();
if (detailsPointerChanged) {
r.detailsPointer = {
currency: newDetails.currency,
@@ -948,7 +978,7 @@ export async function getExchangePaytoUri(
*/
export async function getExchangeTrust(
ws: InternalWalletState,
- exchangeInfo: ExchangeRecord,
+ exchangeInfo: ExchangeEntryRecord,
): Promise<TrustInfo> {
let isTrusted = false;
let isAudited = false;
diff --git a/packages/taler-wallet-core/src/operations/pending.ts b/packages/taler-wallet-core/src/operations/pending.ts
index 6c6546f83..e37e45c16 100644
--- a/packages/taler-wallet-core/src/operations/pending.ts
+++ b/packages/taler-wallet-core/src/operations/pending.ts
@@ -45,6 +45,7 @@ import {
PeerPushPaymentIncomingRecord,
RefundGroupRecord,
RefundGroupStatus,
+ ExchangeEntryDbUpdateStatus,
} from "../db.js";
import {
PendingOperationsResponse,
@@ -81,19 +82,25 @@ async function gatherExchangePending(
ws: InternalWalletState,
tx: GetReadOnlyAccess<{
exchanges: typeof WalletStoresV1.exchanges;
- exchangeDetails: typeof WalletStoresV1.exchangeDetails;
operationRetries: typeof WalletStoresV1.operationRetries;
}>,
now: AbsoluteTime,
resp: PendingOperationsResponse,
): Promise<void> {
- // FIXME: We should do a range query here based on the update time.
+ // FIXME: We should do a range query here based on the update time
+ // and/or the entry state.
await tx.exchanges.iter().forEachAsync(async (exch) => {
+ switch (exch.updateStatus) {
+ case ExchangeEntryDbUpdateStatus.Initial:
+ case ExchangeEntryDbUpdateStatus.Suspended:
+ case ExchangeEntryDbUpdateStatus.Failed:
+ return;
+ }
const opTag = TaskIdentifiers.forExchangeUpdate(exch);
let opr = await tx.operationRetries.get(opTag);
const timestampDue =
opr?.retryInfo.nextRetry ??
- AbsoluteTime.fromPreciseTimestamp(exch.nextUpdate);
+ AbsoluteTime.fromStampMs(exch.nextUpdateStampMs);
resp.pendingOperations.push({
type: PendingTaskType.ExchangeUpdate,
...getPendingCommon(ws, opTag, timestampDue),
@@ -108,7 +115,7 @@ async function gatherExchangePending(
resp.pendingOperations.push({
type: PendingTaskType.ExchangeCheckRefresh,
...getPendingCommon(ws, opTag, timestampDue),
- timestampDue: AbsoluteTime.fromPreciseTimestamp(exch.nextRefreshCheck),
+ timestampDue: AbsoluteTime.fromStampMs(exch.nextRefreshCheckStampMs),
givesLifeness: false,
exchangeBaseUrl: exch.baseUrl,
});
@@ -184,8 +191,9 @@ export async function iterRecordsForWithdrawal(
WithdrawalGroupStatus.PendingRegisteringBank,
WithdrawalGroupStatus.PendingAml,
);
- withdrawalGroupRecords =
- await tx.withdrawalGroups.indexes.byStatus.getAll(range);
+ withdrawalGroupRecords = await tx.withdrawalGroups.indexes.byStatus.getAll(
+ range,
+ );
} else {
withdrawalGroupRecords =
await tx.withdrawalGroups.indexes.byStatus.getAll();
@@ -344,12 +352,8 @@ export async function iterRecordsForRefund(
f: (r: RefundGroupRecord) => Promise<void>,
): Promise<void> {
if (filter.onlyState === "nonfinal") {
- const keyRange = GlobalIDB.KeyRange.only(
- RefundGroupStatus.Pending
- );
- await tx.refundGroups.indexes.byStatus
- .iter(keyRange)
- .forEachAsync(f);
+ const keyRange = GlobalIDB.KeyRange.only(RefundGroupStatus.Pending);
+ await tx.refundGroups.indexes.byStatus.iter(keyRange).forEachAsync(f);
} else {
await tx.refundGroups.iter().forEachAsync(f);
}
diff --git a/packages/taler-wallet-core/src/operations/refresh.ts b/packages/taler-wallet-core/src/operations/refresh.ts
index 72d1a2725..fb356f0fc 100644
--- a/packages/taler-wallet-core/src/operations/refresh.ts
+++ b/packages/taler-wallet-core/src/operations/refresh.ts
@@ -1190,14 +1190,14 @@ export async function autoRefresh(
`created refresh group for auto-refresh (${res.refreshGroupId})`,
);
}
-// logger.trace(
-// `current wallet time: ${AbsoluteTime.toIsoString(AbsoluteTime.now())}`,
-// );
+ // logger.trace(
+ // `current wallet time: ${AbsoluteTime.toIsoString(AbsoluteTime.now())}`,
+ // );
logger.trace(
`next refresh check at ${AbsoluteTime.toIsoString(minCheckThreshold)}`,
);
- exchange.nextRefreshCheck =
- AbsoluteTime.toPreciseTimestamp(minCheckThreshold);
+ exchange.nextRefreshCheckStampMs =
+ AbsoluteTime.toStampMs(minCheckThreshold);
await tx.exchanges.put(exchange);
});
return TaskRunResult.finished();
diff --git a/packages/taler-wallet-core/src/operations/reward.ts b/packages/taler-wallet-core/src/operations/reward.ts
index 69c888d7a..6f9d3ce85 100644
--- a/packages/taler-wallet-core/src/operations/reward.ts
+++ b/packages/taler-wallet-core/src/operations/reward.ts
@@ -150,14 +150,14 @@ export async function prepareTip(
.mktx((x) => [x.rewards])
.runReadOnly(async (tx) => {
return tx.rewards.indexes.byMerchantTipIdAndBaseUrl.get([
- res.merchantTipId,
+ res.merchantRewardId,
res.merchantBaseUrl,
]);
});
if (!tipRecord) {
const tipStatusUrl = new URL(
- `tips/${res.merchantTipId}`,
+ `rewards/${res.merchantRewardId}`,
res.merchantBaseUrl,
);
logger.trace("checking tip status from", tipStatusUrl.href);
@@ -204,7 +204,7 @@ export async function prepareTip(
next_url: tipPickupStatus.next_url,
merchantBaseUrl: res.merchantBaseUrl,
createdTimestamp: TalerPreciseTimestamp.now(),
- merchantRewardId: res.merchantTipId,
+ merchantRewardId: res.merchantRewardId,
rewardAmountEffective: Amounts.stringify(selectedDenoms.totalCoinValue),
denomsSel: selectedDenoms,
pickedUpTimestamp: undefined,
diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts b/packages/taler-wallet-core/src/operations/withdraw.ts
index 44817b389..a3d95fb5c 100644
--- a/packages/taler-wallet-core/src/operations/withdraw.ts
+++ b/packages/taler-wallet-core/src/operations/withdraw.ts
@@ -132,6 +132,8 @@ import {
} from "../util/coinSelection.js";
import {
ExchangeDetailsRecord,
+ ExchangeEntryDbRecordStatus,
+ ExchangeEntryDbUpdateStatus,
PendingTaskType,
isWithdrawableDenom,
} from "../index.js";
@@ -2346,10 +2348,6 @@ export async function internalPerformCreateWithdrawalGroup(
}>,
prep: PrepareCreateWithdrawalGroupResult,
): Promise<PerformCreateWithdrawalGroupResult> {
- const transactionId = constructTransactionIdentifier({
- tag: TransactionType.Withdrawal,
- withdrawalGroupId: prep.withdrawalGroup.withdrawalGroupId,
- });
const { withdrawalGroup } = prep;
if (!prep.creationInfo) {
return { withdrawalGroup, transitionInfo: undefined };
@@ -2366,6 +2364,7 @@ export async function internalPerformCreateWithdrawalGroup(
const exchange = await tx.exchanges.get(withdrawalGroup.exchangeBaseUrl);
if (exchange) {
exchange.lastWithdrawal = TalerPreciseTimestamp.now();
+ exchange.entryStatus = ExchangeEntryDbRecordStatus.Used;
await tx.exchanges.put(exchange);
}