gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[taler-wallet-core] branch master updated: backup import


From: gnunet
Subject: [taler-wallet-core] branch master updated: backup import
Date: Tue, 05 Jan 2021 11:19:22 +0100

This is an automated email from the git hooks/post-receive script.

dold pushed a commit to branch master
in repository wallet-core.

The following commit(s) were added to refs/heads/master by this push:
     new 03810fd2 backup import
03810fd2 is described below

commit 03810fd2485f51966a1b805e4aaaedccad5a5f60
Author: Florian Dold <florian@dold.me>
AuthorDate: Mon Jan 4 13:30:38 2021 +0100

    backup import
---
 .../taler-wallet-core/src/operations/backup.ts     | 294 ++++++++++++++++++++-
 packages/taler-wallet-core/src/operations/pay.ts   |  95 ++++---
 .../taler-wallet-core/src/operations/refund.ts     |  36 +--
 .../src/operations/transactions.ts                 |  27 +-
 .../taler-wallet-core/src/types/backupTypes.ts     |  29 +-
 packages/taler-wallet-core/src/types/dbTypes.ts    |   4 +-
 packages/taler-wallet-core/src/util/amounts.ts     |   1 +
 packages/taler-wallet-core/src/wallet.ts           |   2 +-
 8 files changed, 415 insertions(+), 73 deletions(-)

diff --git a/packages/taler-wallet-core/src/operations/backup.ts 
b/packages/taler-wallet-core/src/operations/backup.ts
index fdccd23c..b82e63ff 100644
--- a/packages/taler-wallet-core/src/operations/backup.ts
+++ b/packages/taler-wallet-core/src/operations/backup.ts
@@ -60,6 +60,7 @@ import {
   DenomSelectionState,
   ExchangeUpdateStatus,
   ExchangeWireInfo,
+  PayCoinSelection,
   ProposalDownload,
   ProposalStatus,
   RefreshSessionRecord,
@@ -67,6 +68,8 @@ import {
   ReserveBankInfo,
   ReserveRecordStatus,
   Stores,
+  WalletContractData,
+  WalletRefundItem,
 } from "../types/dbTypes";
 import { checkDbInvariant, checkLogicInvariant } from "../util/invariants";
 import { AmountJson, Amounts, codecForAmountString } from "../util/amounts";
@@ -77,6 +80,7 @@ import {
   encodeCrock,
   getRandomBytes,
   hash,
+  rsaBlind,
   stringToBytes,
 } from "../crypto/talerCrypto";
 import { canonicalizeBaseUrl, canonicalJson, j2s } from "../util/helpers";
@@ -102,6 +106,7 @@ import { gzipSync } from "fflate";
 import { kdf } from "../crypto/primitives/kdf";
 import { initRetryInfo } from "../util/retries";
 import { RefreshReason } from "../types/walletTypes";
+import { CryptoApi } from "../crypto/workers/cryptoApi";
 
 interface WalletBackupConfState {
   deviceId: string;
@@ -461,6 +466,8 @@ export async function exportBackup(
               ? undefined
               : purch.abortStatus,
           nonce_priv: purch.noncePriv,
+          merchant_sig: purch.download.contractData.merchantSig,
+          total_pay_cost: Amounts.stringify(purch.totalPayCost),
         });
       });
 
@@ -607,11 +614,77 @@ interface CompletedCoin {
 interface BackupCryptoPrecomputedData {
   denomPubToHash: Record<string, string>;
   coinPrivToCompletedCoin: Record<string, CompletedCoin>;
-  proposalNoncePrivToProposalPub: { [priv: string]: string };
+  proposalNoncePrivToPub: { [priv: string]: string };
   proposalIdToContractTermsHash: { [proposalId: string]: string };
   reservePrivToPub: Record<string, string>;
 }
 
+/**
+ * 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: CryptoApi,
+  backupContent: WalletBackupContentV1,
+): Promise<BackupCryptoPrecomputedData> {
+  const cryptoData: BackupCryptoPrecomputedData = {
+    coinPrivToCompletedCoin: {},
+    denomPubToHash: {},
+    proposalIdToContractTermsHash: {},
+    proposalNoncePrivToPub: {},
+    reservePrivToPub: {},
+  };
+  for (const backupExchange of backupContent.exchanges) {
+    for (const backupDenom of backupExchange.denominations) {
+      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),
+        );
+        cryptoData.coinPrivToCompletedCoin[backupCoin.coin_priv] = {
+          coinEvHash: encodeCrock(hash(blindedCoin)),
+          coinPub,
+        }
+      }
+      cryptoData.denomPubToHash[backupDenom.denom_pub] = encodeCrock(
+        hash(decodeCrock(backupDenom.denom_pub)),
+      );
+    }
+    for (const backupReserve of backupExchange.reserves) {
+      cryptoData.reservePrivToPub[backupReserve.reserve_priv] = encodeCrock(
+        eddsaGetPublic(decodeCrock(backupReserve.reserve_priv)),
+      );
+    }
+  }
+  for (const prop of backupContent.proposals) {
+    const contractTermsHash = await cryptoApi.hashString(
+      canonicalJson(prop.contract_terms_raw),
+    );
+    const noncePub = encodeCrock(eddsaGetPublic(decodeCrock(prop.nonce_priv)));
+    cryptoData.proposalNoncePrivToPub[prop.nonce_priv] = noncePub;
+    cryptoData.proposalIdToContractTermsHash[
+      prop.proposal_id
+    ] = contractTermsHash;
+  }
+  for (const purch of backupContent.purchases) {
+    const contractTermsHash = await cryptoApi.hashString(
+      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 checkBackupInvariant(b: boolean, m?: string): asserts b {
   if (!b) {
     if (m) {
@@ -622,6 +695,88 @@ function checkBackupInvariant(b: boolean, m?: string): 
asserts b {
   }
 }
 
+/**
+ * Re-compute information about the coin selection for a payment.
+ */
+async function recoverPayCoinSelection(
+  tx: TransactionHandle<
+    typeof Stores.exchanges | typeof Stores.coins | typeof Stores.denominations
+  >,
+  contractData: WalletContractData,
+  backupPurchase: BackupPurchase,
+): Promise<PayCoinSelection> {
+  const coinPubs: string[] = backupPurchase.pay_coins.map((x) => x.coin_pub);
+  const coinContributions: AmountJson[] = backupPurchase.pay_coins.map((x) =>
+    Amounts.parseOrThrow(x.contribution),
+  );
+
+  const coveredExchanges: Set<string> = new Set();
+
+  let totalWireFee: AmountJson = Amounts.getZero(contractData.amount.currency);
+  let totalDepositFees: AmountJson = Amounts.getZero(
+    contractData.amount.currency,
+  );
+
+  for (const coinPub of coinPubs) {
+    const coinRecord = await tx.get(Stores.coins, coinPub);
+    checkBackupInvariant(!!coinRecord);
+    const denom = await tx.get(Stores.denominations, [
+      coinRecord.exchangeBaseUrl,
+      coinRecord.denomPubHash,
+    ]);
+    checkBackupInvariant(!!denom);
+    totalDepositFees = Amounts.add(totalDepositFees, denom.feeDeposit).amount;
+
+    if (!coveredExchanges.has(coinRecord.exchangeBaseUrl)) {
+      const exchange = await tx.get(
+        Stores.exchanges,
+        coinRecord.exchangeBaseUrl,
+      );
+      checkBackupInvariant(!!exchange);
+      let wireFee: AmountJson | undefined;
+      const feesForType = exchange.wireInfo?.feesForType;
+      checkBackupInvariant(!!feesForType);
+      for (const fee of feesForType[contractData.wireMethod] || []) {
+        if (
+          fee.startStamp <= contractData.timestamp &&
+          fee.endStamp >= contractData.timestamp
+        ) {
+          wireFee = fee.wireFee;
+          break;
+        }
+      }
+      if (wireFee) {
+        totalWireFee = Amounts.add(totalWireFee, wireFee).amount;
+      }
+    }
+  }
+
+  let customerWireFee: AmountJson;
+
+  const amortizedWireFee = Amounts.divide(
+    totalWireFee,
+    contractData.wireFeeAmortization,
+  );
+  if (Amounts.cmp(contractData.maxWireFee, amortizedWireFee) < 0) {
+    customerWireFee = amortizedWireFee;
+  } else {
+    customerWireFee = Amounts.getZero(contractData.amount.currency);
+  }
+
+  const customerDepositFees = Amounts.sub(
+    totalDepositFees,
+    contractData.maxDepositFee,
+  ).amount;
+
+  return {
+    coinPubs,
+    coinContributions,
+    paymentAmount: contractData.amount,
+    customerWireFees: customerWireFee,
+    customerDepositFees,
+  };
+}
+
 function getDenomSelStateFromBackup(
   tx: TransactionHandle<typeof Stores.denominations>,
   sel: BackupDenomSel,
@@ -959,9 +1114,7 @@ export async function importBackup(
             orderId: backupProposal.order_id,
             noncePriv: backupProposal.nonce_priv,
             noncePub:
-              cryptoComp.proposalNoncePrivToProposalPub[
-                backupProposal.nonce_priv
-              ],
+              cryptoComp.proposalNoncePrivToPub[backupProposal.nonce_priv],
             proposalId: backupProposal.proposal_id,
             repurchaseProposalId: backupProposal.repurchase_proposal_id,
             retryInfo: initRetryInfo(false),
@@ -977,7 +1130,138 @@ export async function importBackup(
           backupPurchase.proposal_id,
         );
         if (!existingPurchase) {
-          await tx.put(Stores.purchases, {});
+          const refunds: { [refundKey: string]: WalletRefundItem } = {};
+          for (const backupRefund of backupPurchase.refunds) {
+            const key = 
`${backupRefund.coin_pub}-${backupRefund.rtransaction_id}`;
+            const coin = await tx.get(Stores.coins, backupRefund.coin_pub);
+            checkBackupInvariant(!!coin);
+            const denom = await tx.get(Stores.denominations, [
+              coin.exchangeBaseUrl,
+              coin.denomPubHash,
+            ]);
+            checkBackupInvariant(!!denom);
+            const common = {
+              coinPub: backupRefund.coin_pub,
+              executionTime: backupRefund.execution_time,
+              obtainedTime: backupRefund.obtained_time,
+              refundAmount: Amounts.parseOrThrow(backupRefund.refund_amount),
+              refundFee: denom.feeRefund,
+              rtransactionId: backupRefund.rtransaction_id,
+              totalRefreshCostBound: Amounts.parseOrThrow(
+                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;
+            }
+          }
+          let abortStatus: AbortStatus;
+          switch (backupPurchase.abort_status) {
+            case "abort-finished":
+              abortStatus = AbortStatus.AbortFinished;
+              break;
+            case "abort-refund":
+              abortStatus = AbortStatus.AbortRefund;
+              break;
+            default:
+              throw Error("not reachable");
+          }
+          const parsedContractTerms = codecForContractTerms().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.getZero(amount.currency);
+          }
+          const download: ProposalDownload = {
+            contractData: {
+              amount,
+              contractTermsHash: contractTermsHash,
+              fulfillmentUrl: parsedContractTerms.fulfillment_url ?? "",
+              merchantBaseUrl: parsedContractTerms.merchant_base_url,
+              merchantPub: parsedContractTerms.merchant_pub,
+              merchantSig: backupPurchase.merchant_sig,
+              orderId: parsedContractTerms.order_id,
+              summary: parsedContractTerms.summary,
+              autoRefund: parsedContractTerms.auto_refund,
+              maxWireFee,
+              payDeadline: parsedContractTerms.pay_deadline,
+              refundDeadline: parsedContractTerms.refund_deadline,
+              wireFeeAmortization:
+                parsedContractTerms.wire_fee_amortization || 1,
+              allowedAuditors: parsedContractTerms.auditors.map((x) => ({
+                auditorBaseUrl: x.url,
+                auditorPub: x.master_pub,
+              })),
+              allowedExchanges: parsedContractTerms.exchanges.map((x) => ({
+                exchangeBaseUrl: x.url,
+                exchangePub: x.master_pub,
+              })),
+              timestamp: parsedContractTerms.timestamp,
+              wireMethod: parsedContractTerms.wire_method,
+              wireInfoHash: parsedContractTerms.h_wire,
+              maxDepositFee: Amounts.parseOrThrow(parsedContractTerms.max_fee),
+              merchant: parsedContractTerms.merchant,
+              products: parsedContractTerms.products,
+              summaryI18n: parsedContractTerms.summary_i18n,
+            },
+            contractTermsRaw: backupPurchase.contract_terms_raw,
+          };
+          await tx.put(Stores.purchases, {
+            proposalId: backupPurchase.proposal_id,
+            noncePriv: backupPurchase.nonce_priv,
+            noncePub:
+              cryptoComp.proposalNoncePrivToPub[backupPurchase.nonce_priv],
+            lastPayError: undefined,
+            autoRefundDeadline: { t_ms: "never" },
+            refundStatusRetryInfo: initRetryInfo(false),
+            lastRefundStatusError: undefined,
+            timestampAccept: backupPurchase.timestamp_accept,
+            timestampFirstSuccessfulPay:
+              backupPurchase.timestamp_first_successful_pay,
+            timestampLastRefundStatus:
+              backupPurchase.timestamp_last_refund_status,
+            merchantPaySig: backupPurchase.merchant_pay_sig,
+            lastSessionId: undefined,
+            abortStatus,
+            // FIXME!
+            payRetryInfo: initRetryInfo(false),
+            download,
+            paymentSubmitPending: 
!backupPurchase.timestamp_first_successful_pay,
+            refundQueryRequested: false,
+            payCoinSelection: await recoverPayCoinSelection(
+              tx,
+              download.contractData,
+              backupPurchase,
+            ),
+            coinDepositPermissions: undefined,
+            totalPayCost: Amounts.parseOrThrow(backupPurchase.total_pay_cost),
+            refunds,
+          });
         }
       }
 
diff --git a/packages/taler-wallet-core/src/operations/pay.ts 
b/packages/taler-wallet-core/src/operations/pay.ts
index ecbe37a6..e9d642d3 100644
--- a/packages/taler-wallet-core/src/operations/pay.ts
+++ b/packages/taler-wallet-core/src/operations/pay.ts
@@ -941,8 +941,21 @@ async function submitPay(
       purchase.download.contractData.merchantBaseUrl,
     ).href;
 
+    let depositPermissions: CoinDepositPermission[];
+
+    if (purchase.coinDepositPermissions) {
+      depositPermissions = purchase.coinDepositPermissions;
+    } else {
+      // FIXME: also cache!
+      depositPermissions = await generateDepositPermissions(
+        ws,
+        purchase.payCoinSelection,
+        purchase.download.contractData,
+      );
+    }
+
     const reqBody = {
-      coins: purchase.coinDepositPermissions,
+      coins: depositPermissions,
       session_id: purchase.lastSessionId,
     };
 
@@ -1192,6 +1205,50 @@ export async function preparePayForUri(
   }
 }
 
+/**
+ * Generate deposit permissions for a purchase.
+ *
+ * Accesses the database and the crypto worker.
+ */
+async function generateDepositPermissions(
+  ws: InternalWalletState,
+  payCoinSel: PayCoinSelection,
+  contractData: WalletContractData,
+): Promise<CoinDepositPermission[]> {
+  const depositPermissions: CoinDepositPermission[] = [];
+  for (let i = 0; i < payCoinSel.coinPubs.length; i++) {
+    const coin = await ws.db.get(Stores.coins, payCoinSel.coinPubs[i]);
+    if (!coin) {
+      throw Error("can't pay, allocated coin not found anymore");
+    }
+    const denom = await ws.db.get(Stores.denominations, [
+      coin.exchangeBaseUrl,
+      coin.denomPubHash,
+    ]);
+    if (!denom) {
+      throw Error(
+        "can't pay, denomination of allocated coin not found anymore",
+      );
+    }
+    const dp = await ws.cryptoApi.signDepositPermission({
+      coinPriv: coin.coinPriv,
+      coinPub: coin.coinPub,
+      contractTermsHash: contractData.contractTermsHash,
+      denomPubHash: coin.denomPubHash,
+      denomSig: coin.denomSig,
+      exchangeBaseUrl: coin.exchangeBaseUrl,
+      feeDeposit: denom.feeDeposit,
+      merchantPub: contractData.merchantPub,
+      refundDeadline: contractData.refundDeadline,
+      spendAmount: payCoinSel.coinContributions[i],
+      timestamp: contractData.timestamp,
+      wireInfoHash: contractData.wireInfoHash,
+    });
+    depositPermissions.push(dp);
+  }
+  return depositPermissions;
+}
+
 /**
  * Add a contract to the wallet and sign coins, and send them.
  */
@@ -1248,37 +1305,11 @@ export async function confirmPay(
     throw Error("insufficient balance");
   }
 
-  const depositPermissions: CoinDepositPermission[] = [];
-  for (let i = 0; i < res.coinPubs.length; i++) {
-    const coin = await ws.db.get(Stores.coins, res.coinPubs[i]);
-    if (!coin) {
-      throw Error("can't pay, allocated coin not found anymore");
-    }
-    const denom = await ws.db.get(Stores.denominations, [
-      coin.exchangeBaseUrl,
-      coin.denomPubHash,
-    ]);
-    if (!denom) {
-      throw Error(
-        "can't pay, denomination of allocated coin not found anymore",
-      );
-    }
-    const dp = await ws.cryptoApi.signDepositPermission({
-      coinPriv: coin.coinPriv,
-      coinPub: coin.coinPub,
-      contractTermsHash: d.contractData.contractTermsHash,
-      denomPubHash: coin.denomPubHash,
-      denomSig: coin.denomSig,
-      exchangeBaseUrl: coin.exchangeBaseUrl,
-      feeDeposit: denom.feeDeposit,
-      merchantPub: d.contractData.merchantPub,
-      refundDeadline: d.contractData.refundDeadline,
-      spendAmount: res.coinContributions[i],
-      timestamp: d.contractData.timestamp,
-      wireInfoHash: d.contractData.wireInfoHash,
-    });
-    depositPermissions.push(dp);
-  }
+  const depositPermissions = await generateDepositPermissions(
+    ws,
+    res,
+    d.contractData,
+  );
   purchase = await recordConfirmPay(
     ws,
     proposal,
diff --git a/packages/taler-wallet-core/src/operations/refund.ts 
b/packages/taler-wallet-core/src/operations/refund.ts
index 367b644a..7ffcdb6d 100644
--- a/packages/taler-wallet-core/src/operations/refund.ts
+++ b/packages/taler-wallet-core/src/operations/refund.ts
@@ -501,9 +501,9 @@ export async function applyRefund(
   const p = purchase;
 
   let amountRefundGranted = Amounts.getZero(
-    purchase.contractData.amount.currency,
+    purchase.download.contractData.amount.currency,
   );
-  let amountRefundGone = 
Amounts.getZero(purchase.contractData.amount.currency);
+  let amountRefundGone = 
Amounts.getZero(purchase.download.contractData.amount.currency);
 
   let pendingAtExchange = false;
 
@@ -531,21 +531,21 @@ export async function applyRefund(
   });
 
   return {
-    contractTermsHash: purchase.contractData.contractTermsHash,
+    contractTermsHash: purchase.download.contractData.contractTermsHash,
     proposalId: purchase.proposalId,
     amountEffectivePaid: Amounts.stringify(purchase.totalPayCost),
     amountRefundGone: Amounts.stringify(amountRefundGone),
     amountRefundGranted: Amounts.stringify(amountRefundGranted),
     pendingAtExchange,
     info: {
-      contractTermsHash: purchase.contractData.contractTermsHash,
-      merchant: purchase.contractData.merchant,
-      orderId: purchase.contractData.orderId,
-      products: purchase.contractData.products,
-      summary: purchase.contractData.summary,
-      fulfillmentMessage: purchase.contractData.fulfillmentMessage,
-      summary_i18n: purchase.contractData.summaryI18n,
-      fulfillmentMessage_i18n: purchase.contractData.fulfillmentMessageI18n,
+      contractTermsHash: purchase.download.contractData.contractTermsHash,
+      merchant: purchase.download.contractData.merchant,
+      orderId: purchase.download.contractData.orderId,
+      products: purchase.download.contractData.products,
+      summary: purchase.download.contractData.summary,
+      fulfillmentMessage: purchase.download.contractData.fulfillmentMessage,
+      summary_i18n: purchase.download.contractData.summaryI18n,
+      fulfillmentMessage_i18n: 
purchase.download.contractData.fulfillmentMessageI18n,
     },
   };
 }
@@ -594,14 +594,14 @@ async function processPurchaseQueryRefundImpl(
 
   if (purchase.timestampFirstSuccessfulPay) {
     const requestUrl = new URL(
-      `orders/${purchase.contractData.orderId}/refund`,
-      purchase.contractData.merchantBaseUrl,
+      `orders/${purchase.download.contractData.orderId}/refund`,
+      purchase.download.contractData.merchantBaseUrl,
     );
 
     logger.trace(`making refund request to ${requestUrl.href}`);
 
     const request = await ws.http.postJson(requestUrl.href, {
-      h_contract: purchase.contractData.contractTermsHash,
+      h_contract: purchase.download.contractData.contractTermsHash,
     });
 
     logger.trace(
@@ -622,8 +622,8 @@ async function processPurchaseQueryRefundImpl(
     );
   } else if (purchase.abortStatus === AbortStatus.AbortRefund) {
     const requestUrl = new URL(
-      `orders/${purchase.contractData.orderId}/abort`,
-      purchase.contractData.merchantBaseUrl,
+      `orders/${purchase.download.contractData.orderId}/abort`,
+      purchase.download.contractData.merchantBaseUrl,
     );
 
     const abortingCoins: AbortingCoin[] = [];
@@ -641,7 +641,7 @@ async function processPurchaseQueryRefundImpl(
     }
 
     const abortReq: AbortRequest = {
-      h_contract: purchase.contractData.contractTermsHash,
+      h_contract: purchase.download.contractData.contractTermsHash,
       coins: abortingCoins,
     };
 
@@ -669,7 +669,7 @@ async function processPurchaseQueryRefundImpl(
           purchase.payCoinSelection.coinContributions[i],
         ),
         rtransaction_id: 0,
-        execution_time: timestampAddDuration(purchase.contractData.timestamp, {
+        execution_time: 
timestampAddDuration(purchase.download.contractData.timestamp, {
           d_ms: 1000,
         }),
       });
diff --git a/packages/taler-wallet-core/src/operations/transactions.ts 
b/packages/taler-wallet-core/src/operations/transactions.ts
index cf524db4..a862d24e 100644
--- a/packages/taler-wallet-core/src/operations/transactions.ts
+++ b/packages/taler-wallet-core/src/operations/transactions.ts
@@ -207,12 +207,13 @@ export async function getTransactions(
         if (
           shouldSkipCurrency(
             transactionsRequest,
-            pr.contractData.amount.currency,
+            pr.download.contractData.amount.currency,
           )
         ) {
           return;
         }
-        if (shouldSkipSearch(transactionsRequest, [pr.contractData.summary])) {
+        const contractData = pr.download.contractData;
+        if (shouldSkipSearch(transactionsRequest, [contractData.summary])) {
           return;
         }
         const proposal = await tx.get(Stores.proposals, pr.proposalId);
@@ -220,15 +221,15 @@ export async function getTransactions(
           return;
         }
         const info: OrderShortInfo = {
-          merchant: pr.contractData.merchant,
-          orderId: pr.contractData.orderId,
-          products: pr.contractData.products,
-          summary: pr.contractData.summary,
-          summary_i18n: pr.contractData.summaryI18n,
-          contractTermsHash: pr.contractData.contractTermsHash,
+          merchant: contractData.merchant,
+          orderId: contractData.orderId,
+          products: contractData.products,
+          summary: contractData.summary,
+          summary_i18n: contractData.summaryI18n,
+          contractTermsHash: contractData.contractTermsHash,
         };
-        if (pr.contractData.fulfillmentUrl !== "") {
-          info.fulfillmentUrl = pr.contractData.fulfillmentUrl;
+        if (contractData.fulfillmentUrl !== "") {
+          info.fulfillmentUrl = contractData.fulfillmentUrl;
         }
         const paymentTransactionId = makeEventId(
           TransactionType.Payment,
@@ -237,7 +238,7 @@ export async function getTransactions(
         const err = pr.lastPayError ?? pr.lastRefundStatusError;
         transactions.push({
           type: TransactionType.Payment,
-          amountRaw: Amounts.stringify(pr.contractData.amount),
+          amountRaw: Amounts.stringify(contractData.amount),
           amountEffective: Amounts.stringify(pr.totalPayCost),
           status: pr.timestampFirstSuccessfulPay
             ? PaymentStatus.Paid
@@ -267,9 +268,9 @@ export async function getTransactions(
             groupKey,
           );
           let r0: WalletRefundItem | undefined;
-          let amountRaw = Amounts.getZero(pr.contractData.amount.currency);
+          let amountRaw = Amounts.getZero(contractData.amount.currency);
           let amountEffective = Amounts.getZero(
-            pr.contractData.amount.currency,
+            contractData.amount.currency,
           );
           for (const rk of Object.keys(pr.refunds)) {
             const refund = pr.refunds[rk];
diff --git a/packages/taler-wallet-core/src/types/backupTypes.ts 
b/packages/taler-wallet-core/src/types/backupTypes.ts
index 0b7f93c6..fdc244d8 100644
--- a/packages/taler-wallet-core/src/types/backupTypes.ts
+++ b/packages/taler-wallet-core/src/types/backupTypes.ts
@@ -34,6 +34,11 @@
  * 6. Returning money to own bank account isn't supported/exported yet.
  * 7. Peer-to-peer payments aren't supported yet.
  * 8. Next update time / next refresh time isn't backed up yet.
+ * 9. Coin/denom selections should be forgettable once that information
+ *    becomes irrelevant.
+ * 10. Re-denominated payments/refreshes are not shown properly in the total
+ *     payment cost.
+ * 11. Failed refunds do not have any information about why they failed.
  *
  * Questions:
  * 1. What happens when two backups are merged that have
@@ -42,6 +47,10 @@
  * 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
@@ -78,6 +87,9 @@ type DeviceIdString = string;
  */
 type ClockValue = number;
 
+/**
+ * Contract terms JSON.
+ */
 type RawContractTerms = any;
 
 /**
@@ -751,10 +763,8 @@ export interface BackupPurchase {
 
   /**
    * Signature on the contract terms.
-   *
-   * Must be present if contract_terms_raw is present.
    */
-  merchant_sig?: string;
+  merchant_sig: string;
 
   /**
    * Private key for the nonce.  Might eventually be used
@@ -774,6 +784,19 @@ export interface BackupPurchase {
     contribution: BackupAmountString;
   }[];
 
+  /**
+   * 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;
+
   /**
    * Timestamp of the first time that sending a payment to the merchant
    * for this purchase was successful.
diff --git a/packages/taler-wallet-core/src/types/dbTypes.ts 
b/packages/taler-wallet-core/src/types/dbTypes.ts
index 5b05e287..2f9c0ec1 100644
--- a/packages/taler-wallet-core/src/types/dbTypes.ts
+++ b/packages/taler-wallet-core/src/types/dbTypes.ts
@@ -1206,8 +1206,10 @@ export interface PurchaseRecord {
 
   /**
    * Deposit permissions, available once the user has accepted the payment.
+   * 
+   * This value is cached and derived from payCoinSelection.
    */
-  coinDepositPermissions: CoinDepositPermission[];
+  coinDepositPermissions: CoinDepositPermission[] | undefined;
 
   payCoinSelection: PayCoinSelection;
 
diff --git a/packages/taler-wallet-core/src/util/amounts.ts 
b/packages/taler-wallet-core/src/util/amounts.ts
index e6bee2d1..801c3385 100644
--- a/packages/taler-wallet-core/src/util/amounts.ts
+++ b/packages/taler-wallet-core/src/util/amounts.ts
@@ -398,4 +398,5 @@ export const Amounts = {
   fromFloat: fromFloat,
   copy: copy,
   fractionalBase: fractionalBase,
+  divide: divide,
 };
diff --git a/packages/taler-wallet-core/src/wallet.ts 
b/packages/taler-wallet-core/src/wallet.ts
index baafc63d..a09bfcc0 100644
--- a/packages/taler-wallet-core/src/wallet.ts
+++ b/packages/taler-wallet-core/src/wallet.ts
@@ -840,7 +840,7 @@ export class Wallet {
     ]).amount;
     const totalFees = totalRefundFees;
     return {
-      contractTerms: JSON.parse(purchase.contractTermsRaw),
+      contractTerms: JSON.parse(purchase.download.contractTermsRaw),
       hasRefund: purchase.timestampLastRefundStatus !== undefined,
       totalRefundAmount: totalRefundAmount,
       totalRefundAndRefreshFees: totalFees,

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]