aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2021-01-04 13:30:38 +0100
committerFlorian Dold <florian@dold.me>2021-01-04 13:30:38 +0100
commit03810fd2485f51966a1b805e4aaaedccad5a5f60 (patch)
tree663fc5904f0b41a029b37378096575cf931780b0
parent95568395ce5817028046a96d95bd3399995154d5 (diff)
backup import
-rw-r--r--packages/taler-wallet-core/src/operations/backup.ts294
-rw-r--r--packages/taler-wallet-core/src/operations/pay.ts95
-rw-r--r--packages/taler-wallet-core/src/operations/refund.ts36
-rw-r--r--packages/taler-wallet-core/src/operations/transactions.ts27
-rw-r--r--packages/taler-wallet-core/src/types/backupTypes.ts29
-rw-r--r--packages/taler-wallet-core/src/types/dbTypes.ts4
-rw-r--r--packages/taler-wallet-core/src/util/amounts.ts1
-rw-r--r--packages/taler-wallet-core/src/wallet.ts2
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 fdccd23c1..b82e63ff2 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 ecbe37a64..e9d642d39 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,
};
@@ -1193,6 +1206,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.
*/
export async function confirmPay(
@@ -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 367b644a2..7ffcdb6d9 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 cf524db4e..a862d24ef 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 0b7f93c69..fdc244d8f 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
@@ -775,6 +785,19 @@ export interface BackupPurchase {
}[];
/**
+ * 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 5b05e2874..2f9c0ec19 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 e6bee2d1d..801c3385e 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 baafc63dd..a09bfcc0f 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,