diff options
author | Florian Dold <florian@dold.me> | 2021-01-04 13:30:38 +0100 |
---|---|---|
committer | Florian Dold <florian@dold.me> | 2021-01-04 13:30:38 +0100 |
commit | 03810fd2485f51966a1b805e4aaaedccad5a5f60 (patch) | |
tree | 663fc5904f0b41a029b37378096575cf931780b0 | |
parent | 95568395ce5817028046a96d95bd3399995154d5 (diff) |
backup import
-rw-r--r-- | packages/taler-wallet-core/src/operations/backup.ts | 294 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/operations/pay.ts | 95 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/operations/refund.ts | 36 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/operations/transactions.ts | 27 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/types/backupTypes.ts | 29 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/types/dbTypes.ts | 4 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/util/amounts.ts | 1 | ||||
-rw-r--r-- | 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 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, |