diff options
author | Florian Dold <florian@dold.me> | 2021-01-18 23:35:41 +0100 |
---|---|---|
committer | Florian Dold <florian@dold.me> | 2021-01-18 23:35:41 +0100 |
commit | 5f3c02d31a223add55a32b20f4a289210cbb4f15 (patch) | |
tree | d91ded55692aea1294c0565328515f120559ab6a /packages/taler-wallet-core/src/operations | |
parent | f884193b1adf0861f710c6ab1bb94ea2073ade65 (diff) | |
download | wallet-core-5f3c02d31a223add55a32b20f4a289210cbb4f15.tar.gz wallet-core-5f3c02d31a223add55a32b20f4a289210cbb4f15.tar.bz2 wallet-core-5f3c02d31a223add55a32b20f4a289210cbb4f15.zip |
implement deposits
Diffstat (limited to 'packages/taler-wallet-core/src/operations')
6 files changed, 638 insertions, 76 deletions
diff --git a/packages/taler-wallet-core/src/operations/deposits.ts b/packages/taler-wallet-core/src/operations/deposits.ts new file mode 100644 index 000000000..50921a170 --- /dev/null +++ b/packages/taler-wallet-core/src/operations/deposits.ts @@ -0,0 +1,420 @@ +/* + This file is part of GNU Taler + (C) 2021 Taler Systems S.A. + + 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 { + Amounts, + CreateDepositGroupRequest, + guardOperationException, + Logger, + NotificationType, + TalerErrorDetails, +} from ".."; +import { kdf } from "../crypto/primitives/kdf"; +import { + encodeCrock, + getRandomBytes, + stringToBytes, +} from "../crypto/talerCrypto"; +import { DepositGroupRecord, Stores } from "../types/dbTypes"; +import { ContractTerms } from "../types/talerTypes"; +import { CreateDepositGroupResponse, TrackDepositGroupRequest, TrackDepositGroupResponse } from "../types/walletTypes"; +import { + buildCodecForObject, + Codec, + codecForString, + codecOptional, +} from "../util/codec"; +import { canonicalJson } from "../util/helpers"; +import { readSuccessResponseJsonOrThrow } from "../util/http"; +import { parsePaytoUri } from "../util/payto"; +import { initRetryInfo, updateRetryInfoTimeout } from "../util/retries"; +import { + codecForTimestamp, + durationFromSpec, + getTimestampNow, + Timestamp, + timestampAddDuration, + timestampTruncateToSecond, +} from "../util/time"; +import { URL } from "../util/url"; +import { + applyCoinSpend, + extractContractData, + generateDepositPermissions, + getCoinsForPayment, + getEffectiveDepositAmount, + getTotalPaymentCost, +} from "./pay"; +import { InternalWalletState } from "./state"; + +/** + * Logger. + */ +const logger = new Logger("deposits.ts"); + +interface DepositSuccess { + // Optional base URL of the exchange for looking up wire transfers + // associated with this transaction. If not given, + // the base URL is the same as the one used for this request. + // Can be used if the base URL for /transactions/ differs from that + // for /coins/, i.e. for load balancing. Clients SHOULD + // respect the transaction_base_url if provided. Any HTTP server + // belonging to an exchange MUST generate a 307 or 308 redirection + // to the correct base URL should a client uses the wrong base + // URL, or if the base URL has changed since the deposit. + transaction_base_url?: string; + + // timestamp when the deposit was received by the exchange. + exchange_timestamp: Timestamp; + + // the EdDSA signature of TALER_DepositConfirmationPS using a current + // signing key of the exchange affirming the successful + // deposit and that the exchange will transfer the funds after the refund + // deadline, or as soon as possible if the refund deadline is zero. + exchange_sig: string; + + // public EdDSA key of the exchange that was used to + // generate the signature. + // Should match one of the exchange's signing keys from /keys. It is given + // explicitly as the client might otherwise be confused by clock skew as to + // which signing key was used. + exchange_pub: string; +} + +const codecForDepositSuccess = (): Codec<DepositSuccess> => + buildCodecForObject<DepositSuccess>() + .property("exchange_pub", codecForString()) + .property("exchange_sig", codecForString()) + .property("exchange_timestamp", codecForTimestamp) + .property("transaction_base_url", codecOptional(codecForString())) + .build("DepositSuccess"); + +function hashWire(paytoUri: string, salt: string): string { + const r = kdf( + 64, + stringToBytes(paytoUri + "\0"), + stringToBytes(salt + "\0"), + stringToBytes("merchant-wire-signature"), + ); + return encodeCrock(r); +} + +async function resetDepositGroupRetry( + ws: InternalWalletState, + depositGroupId: string, +): Promise<void> { + await ws.db.mutate(Stores.depositGroups, depositGroupId, (x) => { + if (x.retryInfo.active) { + x.retryInfo = initRetryInfo(); + } + return x; + }); +} + +async function incrementDepositRetry( + ws: InternalWalletState, + depositGroupId: string, + err: TalerErrorDetails | undefined, +): Promise<void> { + await ws.db.runWithWriteTransaction([Stores.depositGroups], async (tx) => { + const r = await tx.get(Stores.depositGroups, depositGroupId); + if (!r) { + return; + } + if (!r.retryInfo) { + return; + } + r.retryInfo.retryCounter++; + updateRetryInfoTimeout(r.retryInfo); + r.lastError = err; + await tx.put(Stores.depositGroups, r); + }); + if (err) { + ws.notify({ type: NotificationType.DepositOperationError, error: err }); + } +} + +export async function processDepositGroup( + ws: InternalWalletState, + depositGroupId: string, + forceNow = false, +): Promise<void> { + await ws.memoProcessDeposit.memo(depositGroupId, async () => { + const onOpErr = (e: TalerErrorDetails): Promise<void> => + incrementDepositRetry(ws, depositGroupId, e); + return await guardOperationException( + async () => await processDepositGroupImpl(ws, depositGroupId, forceNow), + onOpErr, + ); + }); +} + +async function processDepositGroupImpl( + ws: InternalWalletState, + depositGroupId: string, + forceNow: boolean = false, +): Promise<void> { + if (forceNow) { + await resetDepositGroupRetry(ws, depositGroupId); + } + const depositGroup = await ws.db.get(Stores.depositGroups, depositGroupId); + if (!depositGroup) { + logger.warn(`deposit group ${depositGroupId} not found`); + return; + } + if (depositGroup.timestampFinished) { + logger.trace(`deposit group ${depositGroupId} already finished`); + return; + } + + const contractData = extractContractData( + depositGroup.contractTermsRaw, + depositGroup.contractTermsHash, + "", + ); + + const depositPermissions = await generateDepositPermissions( + ws, + depositGroup.payCoinSelection, + contractData, + ); + + for (let i = 0; i < depositPermissions.length; i++) { + if (depositGroup.depositedPerCoin[i]) { + continue; + } + const perm = depositPermissions[i]; + const url = new URL(`/coins/${perm.coin_pub}/deposit`, perm.exchange_url); + const httpResp = await ws.http.postJson(url.href, { + contribution: Amounts.stringify(perm.contribution), + wire: depositGroup.wire, + h_wire: depositGroup.contractTermsRaw.h_wire, + h_contract_terms: depositGroup.contractTermsHash, + ub_sig: perm.ub_sig, + timestamp: depositGroup.contractTermsRaw.timestamp, + wire_transfer_deadline: + depositGroup.contractTermsRaw.wire_transfer_deadline, + refund_deadline: depositGroup.contractTermsRaw.refund_deadline, + coin_sig: perm.coin_sig, + denom_pub_hash: perm.h_denom, + merchant_pub: depositGroup.merchantPub, + }); + await readSuccessResponseJsonOrThrow(httpResp, codecForDepositSuccess()); + await ws.db.runWithWriteTransaction([Stores.depositGroups], async (tx) => { + const dg = await tx.get(Stores.depositGroups, depositGroupId); + if (!dg) { + return; + } + dg.depositedPerCoin[i] = true; + await tx.put(Stores.depositGroups, dg); + }); + } + + await ws.db.runWithWriteTransaction([Stores.depositGroups], async (tx) => { + const dg = await tx.get(Stores.depositGroups, depositGroupId); + if (!dg) { + return; + } + let allDeposited = true; + for (const d of depositGroup.depositedPerCoin) { + if (!d) { + allDeposited = false; + } + } + if (allDeposited) { + dg.timestampFinished = getTimestampNow(); + await tx.put(Stores.depositGroups, dg); + } + }); +} + + +export async function trackDepositGroup( + ws: InternalWalletState, + req: TrackDepositGroupRequest, +): Promise<TrackDepositGroupResponse> { + const responses: { + status: number; + body: any; + }[] = []; + const depositGroup = await ws.db.get( + Stores.depositGroups, + req.depositGroupId, + ); + if (!depositGroup) { + throw Error("deposit group not found"); + } + const contractData = extractContractData( + depositGroup.contractTermsRaw, + depositGroup.contractTermsHash, + "", + ); + + const depositPermissions = await generateDepositPermissions( + ws, + depositGroup.payCoinSelection, + contractData, + ); + + const wireHash = depositGroup.contractTermsRaw.h_wire; + + for (const dp of depositPermissions) { + const url = new URL( + `/deposits/${wireHash}/${depositGroup.merchantPub}/${depositGroup.contractTermsHash}/${dp.coin_pub}`, + dp.exchange_url, + ); + const sig = await ws.cryptoApi.signTrackTransaction({ + coinPub: dp.coin_pub, + contractTermsHash: depositGroup.contractTermsHash, + merchantPriv: depositGroup.merchantPriv, + merchantPub: depositGroup.merchantPub, + wireHash, + }); + url.searchParams.set("merchant_sig", sig); + const httpResp = await ws.http.get(url.href); + const body = await httpResp.json(); + responses.push({ + body, + status: httpResp.status, + }); + } + return { + responses, + }; +} + +export async function createDepositGroup( + ws: InternalWalletState, + req: CreateDepositGroupRequest, +): Promise<CreateDepositGroupResponse> { + const p = parsePaytoUri(req.depositPaytoUri); + if (!p) { + throw Error("invalid payto URI"); + } + + const amount = Amounts.parseOrThrow(req.amount); + + const allExchanges = await ws.db.iter(Stores.exchanges).toArray(); + const exchangeInfos: { url: string; master_pub: string }[] = []; + for (const e of allExchanges) { + if (!e.details) { + continue; + } + if (e.details.currency != amount.currency) { + continue; + } + exchangeInfos.push({ + master_pub: e.details.masterPublicKey, + url: e.baseUrl, + }); + } + + const timestamp = getTimestampNow(); + const timestampRound = timestampTruncateToSecond(timestamp); + const noncePair = await ws.cryptoApi.createEddsaKeypair(); + const merchantPair = await ws.cryptoApi.createEddsaKeypair(); + const wireSalt = encodeCrock(getRandomBytes(64)); + const wireHash = hashWire(req.depositPaytoUri, wireSalt); + const contractTerms: ContractTerms = { + auditors: [], + exchanges: exchangeInfos, + amount: req.amount, + max_fee: Amounts.stringify(amount), + max_wire_fee: Amounts.stringify(amount), + wire_method: p.targetType, + timestamp: timestampRound, + merchant_base_url: "", + summary: "", + nonce: noncePair.pub, + wire_transfer_deadline: timestampRound, + order_id: "", + h_wire: wireHash, + pay_deadline: timestampAddDuration( + timestampRound, + durationFromSpec({ hours: 1 }), + ), + merchant: { + name: "", + }, + merchant_pub: merchantPair.pub, + refund_deadline: { t_ms: 0 }, + }; + + const contractTermsHash = await ws.cryptoApi.hashString( + canonicalJson(contractTerms), + ); + + const contractData = extractContractData( + contractTerms, + contractTermsHash, + "", + ); + + const payCoinSel = await getCoinsForPayment(ws, contractData); + + if (!payCoinSel) { + throw Error("insufficient funds"); + } + + const totalDepositCost = await getTotalPaymentCost(ws, payCoinSel); + + const depositGroupId = encodeCrock(getRandomBytes(32)); + + const effectiveDepositAmount = await getEffectiveDepositAmount( + ws, + p.targetType, + payCoinSel, + ); + + const depositGroup: DepositGroupRecord = { + contractTermsHash, + contractTermsRaw: contractTerms, + depositGroupId, + noncePriv: noncePair.priv, + noncePub: noncePair.pub, + timestampCreated: timestamp, + timestampFinished: undefined, + payCoinSelection: payCoinSel, + depositedPerCoin: payCoinSel.coinPubs.map((x) => false), + merchantPriv: merchantPair.priv, + merchantPub: merchantPair.pub, + totalPayCost: totalDepositCost, + effectiveDepositAmount, + wire: { + payto_uri: req.depositPaytoUri, + salt: wireSalt, + }, + retryInfo: initRetryInfo(true), + lastError: undefined, + }; + + await ws.db.runWithWriteTransaction( + [ + Stores.depositGroups, + Stores.coins, + Stores.refreshGroups, + Stores.denominations, + ], + async (tx) => { + await applyCoinSpend(ws, tx, payCoinSel); + await tx.put(Stores.depositGroups, depositGroup); + }, + ); + + await ws.db.put(Stores.depositGroups, depositGroup); + + return { depositGroupId }; +} diff --git a/packages/taler-wallet-core/src/operations/pay.ts b/packages/taler-wallet-core/src/operations/pay.ts index ee42d347e..d8168acdf 100644 --- a/packages/taler-wallet-core/src/operations/pay.ts +++ b/packages/taler-wallet-core/src/operations/pay.ts @@ -36,6 +36,8 @@ import { DenominationRecord, PayCoinSelection, AbortStatus, + AllowedExchangeInfo, + AllowedAuditorInfo, } from "../types/dbTypes"; import { NotificationType } from "../types/notifications"; import { @@ -43,6 +45,7 @@ import { codecForContractTerms, CoinDepositPermission, codecForMerchantPayResponse, + ContractTerms, } from "../types/talerTypes"; import { ConfirmPayResult, @@ -72,7 +75,8 @@ import { durationMin, isTimestampExpired, durationMul, - durationAdd, + Timestamp, + timestampIsBetween, } from "../util/time"; import { strcmp, canonicalJson } from "../util/helpers"; import { @@ -88,6 +92,7 @@ import { updateRetryInfoTimeout, getRetryDuration, } from "../util/retries"; +import { TransactionHandle } from "../util/query"; /** * Logger. @@ -163,6 +168,49 @@ export async function getTotalPaymentCost( } /** + * Get the amount that will be deposited on the merchant's bank + * account, not considering aggregation. + */ +export async function getEffectiveDepositAmount( + ws: InternalWalletState, + wireType: string, + pcs: PayCoinSelection, +): Promise<AmountJson> { + const amt: AmountJson[] = []; + const fees: AmountJson[] = []; + const exchangeSet: Set<string> = new Set(); + for (let i = 0; i < pcs.coinPubs.length; i++) { + const coin = await ws.db.get(Stores.coins, pcs.coinPubs[i]); + if (!coin) { + throw Error("can't calculate deposit amountt, coin not found"); + } + const denom = await ws.db.get(Stores.denominations, [ + coin.exchangeBaseUrl, + coin.denomPubHash, + ]); + if (!denom) { + throw Error("can't find denomination to calculate deposit amount"); + } + amt.push(pcs.coinContributions[i]); + fees.push(denom.feeDeposit); + exchangeSet.add(coin.exchangeBaseUrl); + } + for (const exchangeUrl of exchangeSet.values()) { + const exchange = await ws.db.get(Stores.exchanges, exchangeUrl); + if (!exchange?.wireInfo) { + continue; + } + const fee = exchange.wireInfo.feesForType[wireType].find((x) => { + return timestampIsBetween(getTimestampNow(), x.startStamp, x.endStamp); + })?.wireFee; + if (fee) { + fees.push(fee); + } + } + return Amounts.sub(Amounts.sum(amt).amount, Amounts.sum(fees).amount).amount; +} + +/** * Given a list of available coins, select coins to spend under the merchant's * constraints. * @@ -277,17 +325,36 @@ export function isSpendableCoin( return true; } +export interface CoinSelectionRequest { + amount: AmountJson; + allowedAuditors: AllowedAuditorInfo[]; + allowedExchanges: AllowedExchangeInfo[]; + + /** + * Timestamp of the contract. + */ + timestamp: Timestamp; + + wireMethod: string; + + wireFeeAmortization: number; + + maxWireFee: AmountJson; + + maxDepositFee: AmountJson; +} + /** * Select coins from the wallet's database that can be used * to pay for the given contract. * * If payment is impossible, undefined is returned. */ -async function getCoinsForPayment( +export async function getCoinsForPayment( ws: InternalWalletState, - contractData: WalletContractData, + req: CoinSelectionRequest, ): Promise<PayCoinSelection | undefined> { - const remainingAmount = contractData.amount; + const remainingAmount = req.amount; const exchanges = await ws.db.iter(Stores.exchanges).toArray(); @@ -303,7 +370,7 @@ async function getCoinsForPayment( } // is the exchange explicitly allowed? - for (const allowedExchange of contractData.allowedExchanges) { + for (const allowedExchange of req.allowedExchanges) { if (allowedExchange.exchangePub === exchangeDetails.masterPublicKey) { isOkay = true; break; @@ -312,7 +379,7 @@ async function getCoinsForPayment( // is the exchange allowed because of one of its auditors? if (!isOkay) { - for (const allowedAuditor of contractData.allowedAuditors) { + for (const allowedAuditor of req.allowedAuditors) { for (const auditor of exchangeDetails.auditors) { if (auditor.auditor_pub === allowedAuditor.auditorPub) { isOkay = true; @@ -374,11 +441,8 @@ async function getCoinsForPayment( } let wireFee: AmountJson | undefined; - for (const fee of exchangeFees.feesForType[contractData.wireMethod] || []) { - if ( - fee.startStamp <= contractData.timestamp && - fee.endStamp >= contractData.timestamp - ) { + for (const fee of exchangeFees.feesForType[req.wireMethod] || []) { + if (fee.startStamp <= req.timestamp && fee.endStamp >= req.timestamp) { wireFee = fee.wireFee; break; } @@ -386,12 +450,9 @@ async function getCoinsForPayment( let customerWireFee: AmountJson; - if (wireFee) { - const amortizedWireFee = Amounts.divide( - wireFee, - contractData.wireFeeAmortization, - ); - if (Amounts.cmp(contractData.maxWireFee, amortizedWireFee) < 0) { + if (wireFee && req.wireFeeAmortization) { + const amortizedWireFee = Amounts.divide(wireFee, req.wireFeeAmortization); + if (Amounts.cmp(req.maxWireFee, amortizedWireFee) < 0) { customerWireFee = amortizedWireFee; } else { customerWireFee = Amounts.getZero(currency); @@ -405,7 +466,7 @@ async function getCoinsForPayment( acis, remainingAmount, customerWireFee, - contractData.maxDepositFee, + req.maxDepositFee, ); if (res) { return res; @@ -414,6 +475,37 @@ async function getCoinsForPayment( return undefined; } +export async function applyCoinSpend( + ws: InternalWalletState, + tx: TransactionHandle< + | typeof Stores.coins + | typeof Stores.refreshGroups + | typeof Stores.denominations + >, + coinSelection: PayCoinSelection, +) { + for (let i = 0; i < coinSelection.coinPubs.length; i++) { + const coin = await tx.get(Stores.coins, coinSelection.coinPubs[i]); + if (!coin) { + throw Error("coin allocated for payment doesn't exist anymore"); + } + coin.status = CoinStatus.Dormant; + const remaining = Amounts.sub( + coin.currentAmount, + coinSelection.coinContributions[i], + ); + if (remaining.saturated) { + throw Error("not enough remaining balance on coin for payment"); + } + coin.currentAmount = remaining.amount; + await tx.put(Stores.coins, coin); + } + const refreshCoinPubs = coinSelection.coinPubs.map((x) => ({ + coinPub: x, + })); + await createRefreshGroup(ws, tx, refreshCoinPubs, RefreshReason.Pay); +} + /** * Record all information that is necessary to * pay for a proposal in the wallet's database. @@ -480,26 +572,7 @@ async function recordConfirmPay( await tx.put(Stores.proposals, p); } await tx.put(Stores.purchases, t); - for (let i = 0; i < coinSelection.coinPubs.length; i++) { - const coin = await tx.get(Stores.coins, coinSelection.coinPubs[i]); - if (!coin) { - throw Error("coin allocated for payment doesn't exist anymore"); - } - coin.status = CoinStatus.Dormant; - const remaining = Amounts.sub( - coin.currentAmount, - coinSelection.coinContributions[i], - ); - if (remaining.saturated) { - throw Error("not enough remaining balance on coin for payment"); - } - coin.currentAmount = remaining.amount; - await tx.put(Stores.coins, coin); - } - const refreshCoinPubs = coinSelection.coinPubs.map((x) => ({ - coinPub: x, - })); - await createRefreshGroup(ws, tx, refreshCoinPubs, RefreshReason.Pay); + await applyCoinSpend(ws, tx, coinSelection); }, ); @@ -609,6 +682,50 @@ function getPayRequestTimeout(purchase: PurchaseRecord): Duration { ); } +export function extractContractData( + parsedContractTerms: ContractTerms, + contractTermsHash: string, + merchantSig: string, +): WalletContractData { + const amount = Amounts.parseOrThrow(parsedContractTerms.amount); + let maxWireFee: AmountJson; + if (parsedContractTerms.max_wire_fee) { + maxWireFee = Amounts.parseOrThrow(parsedContractTerms.max_wire_fee); + } else { + maxWireFee = Amounts.getZero(amount.currency); + } + return { + amount, + contractTermsHash: contractTermsHash, + fulfillmentUrl: parsedContractTerms.fulfillment_url ?? "", + merchantBaseUrl: parsedContractTerms.merchant_base_url, + merchantPub: parsedContractTerms.merchant_pub, + merchantSig, + 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.auditor_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, + }; +} + async function processDownloadProposalImpl( ws: InternalWalletState, proposalId: string, @@ -714,6 +831,12 @@ async function processDownloadProposalImpl( throw new OperationFailedAndReportedError(err); } + const contractData = extractContractData( + parsedContractTerms, + contractTermsHash, + proposalResp.sig, + ); + await ws.db.runWithWriteTransaction( [Stores.proposals, Stores.purchases], async (tx) => { @@ -724,44 +847,8 @@ async function processDownloadProposalImpl( if (p.proposalStatus !== ProposalStatus.DOWNLOADING) { return; } - const amount = Amounts.parseOrThrow(parsedContractTerms.amount); - let maxWireFee: AmountJson; - if (parsedContractTerms.max_wire_fee) { - maxWireFee = Amounts.parseOrThrow(parsedContractTerms.max_wire_fee); - } else { - maxWireFee = Amounts.getZero(amount.currency); - } p.download = { - contractData: { - amount, - contractTermsHash: contractTermsHash, - fulfillmentUrl: parsedContractTerms.fulfillment_url ?? "", - merchantBaseUrl: parsedContractTerms.merchant_base_url, - merchantPub: parsedContractTerms.merchant_pub, - merchantSig: proposalResp.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.auditor_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, - }, + contractData, contractTermsRaw: proposalResp.contract_terms, }; if ( @@ -1210,7 +1297,7 @@ export async function preparePayForUri( * * Accesses the database and the crypto worker. */ -async function generateDepositPermissions( +export async function generateDepositPermissions( ws: InternalWalletState, payCoinSel: PayCoinSelection, contractData: WalletContractData, diff --git a/packages/taler-wallet-core/src/operations/pending.ts b/packages/taler-wallet-core/src/operations/pending.ts index cc693a49d..bae281937 100644 --- a/packages/taler-wallet-core/src/operations/pending.ts +++ b/packages/taler-wallet-core/src/operations/pending.ts @@ -445,6 +445,34 @@ async function gatherRecoupPending( }); } +async function gatherDepositPending( + tx: TransactionHandle<typeof Stores.depositGroups>, + now: Timestamp, + resp: PendingOperationsResponse, + onlyDue = false, +): Promise<void> { + await tx.iter(Stores.depositGroups).forEach((dg) => { + if (dg.timestampFinished) { + return; + } + resp.nextRetryDelay = updateRetryDelay( + resp.nextRetryDelay, + now, + dg.retryInfo.nextRetry, + ); + if (onlyDue && dg.retryInfo.nextRetry.t_ms > now.t_ms) { + return; + } + resp.pendingOperations.push({ + type: PendingOperationType.Deposit, + givesLifeness: true, + depositGroupId: dg.depositGroupId, + retryInfo: dg.retryInfo, + lastError: dg.lastError, + }); + }); +} + export async function getPendingOperations( ws: InternalWalletState, { onlyDue = false } = {}, @@ -462,6 +490,7 @@ export async function getPendingOperations( Stores.purchases, Stores.recoupGroups, Stores.planchets, + Stores.depositGroups, ], async (tx) => { const walletBalance = await getBalancesInsideTransaction(ws, tx); @@ -479,6 +508,7 @@ export async function getPendingOperations( await gatherTipPending(tx, now, resp, onlyDue); await gatherPurchasePending(tx, now, resp, onlyDue); await gatherRecoupPending(tx, now, resp, onlyDue); + await gatherDepositPending(tx, now, resp, onlyDue); return resp; }, ); diff --git a/packages/taler-wallet-core/src/operations/refund.ts b/packages/taler-wallet-core/src/operations/refund.ts index 13df438e4..28d48d5ba 100644 --- a/packages/taler-wallet-core/src/operations/refund.ts +++ b/packages/taler-wallet-core/src/operations/refund.ts @@ -600,6 +600,7 @@ async function processPurchaseQueryRefundImpl( `orders/${purchase.download.contractData.orderId}/refund`, purchase.download.contractData.merchantBaseUrl, ); + logger.trace(`making refund request to ${requestUrl.href}`); diff --git a/packages/taler-wallet-core/src/operations/state.ts b/packages/taler-wallet-core/src/operations/state.ts index 645ad8ad3..ce52affe4 100644 --- a/packages/taler-wallet-core/src/operations/state.ts +++ b/packages/taler-wallet-core/src/operations/state.ts @@ -41,6 +41,7 @@ export class InternalWalletState { memoGetBalance: AsyncOpMemoSingle<BalancesResponse> = new AsyncOpMemoSingle(); memoProcessRefresh: AsyncOpMemoMap<void> = new AsyncOpMemoMap(); memoProcessRecoup: AsyncOpMemoMap<void> = new AsyncOpMemoMap(); + memoProcessDeposit: AsyncOpMemoMap<void> = new AsyncOpMemoMap(); cryptoApi: CryptoApi; listeners: NotificationListener[] = []; diff --git a/packages/taler-wallet-core/src/operations/transactions.ts b/packages/taler-wallet-core/src/operations/transactions.ts index c7e6a9c53..d49031551 100644 --- a/packages/taler-wallet-core/src/operations/transactions.ts +++ b/packages/taler-wallet-core/src/operations/transactions.ts @@ -96,6 +96,7 @@ export async function getTransactions( Stores.withdrawalGroups, Stores.planchets, Stores.recoupGroups, + Stores.depositGroups, ], // Report withdrawals that are currently in progress. async (tx) => { @@ -203,6 +204,28 @@ export async function getTransactions( }); }); + tx.iter(Stores.depositGroups).forEachAsync(async (dg) => { + const amount = Amounts.parseOrThrow(dg.contractTermsRaw.amount); + if (shouldSkipCurrency(transactionsRequest, amount.currency)) { + return; + } + + transactions.push({ + type: TransactionType.Deposit, + amountRaw: Amounts.stringify(dg.effectiveDepositAmount), + amountEffective: Amounts.stringify(dg.totalPayCost), + pending: !dg.timestampFinished, + timestamp: dg.timestampCreated, + targetPaytoUri: dg.wire.payto_uri, + transactionId: makeEventId( + TransactionType.Deposit, + dg.depositGroupId, + ), + depositGroupId: dg.depositGroupId, + ...(dg.lastError ? { error: dg.lastError } : {}), + }); + }); + tx.iter(Stores.purchases).forEachAsync(async (pr) => { if ( shouldSkipCurrency( |