diff --git a/packages/taler-util/src/taler-types.ts b/packages/taler-util/src/taler-types.ts index a78df7452..eaba1ae3d 100644 --- a/packages/taler-util/src/taler-types.ts +++ b/packages/taler-util/src/taler-types.ts @@ -1889,42 +1889,58 @@ export interface ExchangeRefreshRevealRequest { old_age_commitment?: Edx25519PublicKeyEnc[]; } -export interface DepositSuccess { +interface DepositConfirmationSignature { + // 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: EddsaSignatureString; +} + +export interface BatchDepositSuccess { // 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 + // 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. + // Timestamp when the deposit was received by the exchange. exchange_timestamp: TalerProtocolTimestamp; - // 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 + // `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 + // 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; + exchange_pub: EddsaPublicKeyString; + + // Array of deposit confirmation signatures from the exchange + // Entries must be in the same order the coins were given + // in the batch deposit request. + exchange_sigs: DepositConfirmationSignature[]; } -export const codecForDepositSuccess = (): Codec => - buildCodecForObject() +export const codecForDepositConfirmationSignature = + (): Codec => + buildCodecForObject() + .property("exchange_sig", codecForString()) + .build("DepositConfirmationSignature"); + +export const codecForBatchDepositSuccess = (): Codec => + buildCodecForObject() .property("exchange_pub", codecForString()) - .property("exchange_sig", codecForString()) + .property( + "exchange_sigs", + codecForList(codecForDepositConfirmationSignature()), + ) .property("exchange_timestamp", codecForTimestamp) .property("transaction_base_url", codecOptional(codecForString())) - .build("DepositSuccess"); + .build("BatchDepositSuccess"); export interface TrackTransactionWired { // Raw wire transfer identifier of the deposit. @@ -2148,6 +2164,9 @@ export interface ExchangePurseDeposits { deposits: PurseDeposit[]; } +/** + * @deprecated batch deposit should be used. + */ export interface ExchangeDepositRequest { // Amount to be deposited, can be a fraction of the // coin's total value. @@ -2210,6 +2229,67 @@ export interface ExchangeDepositRequest { h_age_commitment?: string; } +export type WireSalt = string; + +export interface ExchangeBatchDepositRequest { + // The merchant's account details. + merchant_payto_uri: string; + + // The salt is used to hide the ``payto_uri`` from customers + // when computing the ``h_wire`` of the merchant. + wire_salt: WireSalt; + + // SHA-512 hash of the contract of the merchant with the customer. Further + // details are never disclosed to the exchange. + h_contract_terms: HashCodeString; + + // The list of coins that are going to be deposited with this Request. + coins: BatchDepositRequestCoin[]; + + // Timestamp when the contract was finalized. + timestamp: TalerProtocolTimestamp; + + // Indicative time by which the exchange undertakes to transfer the funds to + // the merchant, in case of successful payment. A wire transfer deadline of 'never' + // is not allowed. + wire_transfer_deadline: TalerProtocolTimestamp; + + // EdDSA `public key of the merchant `, so that the client can identify the + // merchant for refund requests. + merchant_pub: EddsaPublicKeyString; + + // Date until which the merchant can issue a refund to the customer via the + // exchange, to be omitted if refunds are not allowed. + // + // THIS FIELD WILL BE DEPRICATED, once the refund mechanism becomes a + // policy via extension. + refund_deadline?: TalerProtocolTimestamp; + + // CAVEAT: THIS IS WORK IN PROGRESS + // (Optional) policy for the batch-deposit. + // This might be a refund, auction or escrow policy. + policy?: any; +} + +export interface BatchDepositRequestCoin { + // EdDSA public key of the coin being deposited. + coin_pub: EddsaPublicKeyString; + + // Hash of denomination RSA key with which the coin is signed. + denom_pub_hash: HashCodeString; + + // Exchange's unblinded RSA signature of the coin. + ub_sig: UnblindedSignature; + + // Amount to be deposited, can be a fraction of the + // coin's total value. + contribution: Amounts; + + // Signature over `TALER_DepositRequestPS`, made by the customer with the + // `coin's private key `. + coin_sig: EddsaSignatureString; +} + export interface WalletKycUuid { // UUID that the wallet should use when initiating // the KYC check. diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts index ba1f5b8c0..04c3ce723 100644 --- a/packages/taler-wallet-core/src/db.ts +++ b/packages/taler-wallet-core/src/db.ts @@ -1657,6 +1657,8 @@ export interface DepositGroupRecord { /** * Verbatim contract terms. + * + * FIXME: Move this to the contract terms object store! */ contractTermsRaw: MerchantContractTerms; diff --git a/packages/taler-wallet-core/src/operations/deposits.ts b/packages/taler-wallet-core/src/operations/deposits.ts index 8ea792d91..a3483a332 100644 --- a/packages/taler-wallet-core/src/operations/deposits.ts +++ b/packages/taler-wallet-core/src/operations/deposits.ts @@ -21,70 +21,69 @@ import { AbsoluteTime, AmountJson, Amounts, + BatchDepositRequestCoin, CancellationToken, - canonicalJson, - codecForDepositSuccess, - codecForTackTransactionAccepted, - codecForTackTransactionWired, CoinRefreshRequest, CreateDepositGroupRequest, CreateDepositGroupResponse, DepositGroupFees, - durationFromSpec, - encodeCrock, - ExchangeDepositRequest, + Duration, + ExchangeBatchDepositRequest, ExchangeRefundRequest, - getRandomBytes, - hashTruncate32, - hashWire, HttpStatusCode, - j2s, Logger, MerchantContractTerms, NotificationType, - parsePaytoUri, PayCoinSelection, PrepareDepositRequest, PrepareDepositResponse, RefreshReason, - stringToBytes, + TalerError, TalerErrorCode, - TalerProtocolTimestamp, TalerPreciseTimestamp, + TalerProtocolTimestamp, TrackTransaction, + TransactionAction, TransactionMajorState, TransactionMinorState, TransactionState, TransactionType, URL, WireFee, - TransactionAction, - Duration, + canonicalJson, + codecForBatchDepositSuccess, + codecForTackTransactionAccepted, + codecForTackTransactionWired, + durationFromSpec, + encodeCrock, + getRandomBytes, + hashTruncate32, + hashWire, + j2s, + parsePaytoUri, + stringToBytes, } from "@gnu-taler/taler-util"; +import { readSuccessResponseJsonOrThrow } from "@gnu-taler/taler-util/http"; +import { DepositElementStatus, DepositGroupRecord } from "../db.js"; import { - DenominationRecord, - DepositGroupRecord, - DepositElementStatus, -} from "../db.js"; -import { TalerError } from "@gnu-taler/taler-util"; -import { - createRefreshGroup, DepositOperationStatus, DepositTrackingInfo, - getTotalRefreshCost, KycPendingInfo, - KycUserType, PendingTaskType, RefreshOperationStatus, + createRefreshGroup, + getTotalRefreshCost, } from "../index.js"; import { InternalWalletState } from "../internal-wallet-state.js"; -import { readSuccessResponseJsonOrThrow } from "@gnu-taler/taler-util/http"; +import { assertUnreachable } from "../util/assertUnreachable.js"; +import { selectPayCoinsNew } from "../util/coinSelection.js"; +import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js"; import { - constructTaskIdentifier, TaskRunResult, + TombstoneTag, + constructTaskIdentifier, runLongpollAsync, spendCoins, - TombstoneTag, } from "./common.js"; import { getExchangeDetails } from "./exchanges.js"; import { @@ -92,15 +91,12 @@ import { generateDepositPermissions, getTotalPaymentCost, } from "./pay-merchant.js"; -import { selectPayCoinsNew } from "../util/coinSelection.js"; import { constructTransactionIdentifier, notifyTransition, parseTransactionIdentifier, stopLongpolling, } from "./transactions.js"; -import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js"; -import { assertUnreachable } from "../util/assertUnreachable.js"; /** * Logger. @@ -169,6 +165,10 @@ export function computeDepositTransactionStatus( } } +/** + * Compute the possible actions possible on a deposit transaction + * based on the current transaction state. + */ export function computeDepositTransactionActions( dg: DepositGroupRecord, ): TransactionAction[] { @@ -200,6 +200,11 @@ export function computeDepositTransactionActions( } } +/** + * Put a deposit group in a suspended state. + * While the deposit group is suspended, no network requests + * will be made to advance the transaction status. + */ export async function suspendDepositGroup( ws: InternalWalletState, depositGroupId: string, @@ -406,46 +411,6 @@ export async function deleteDepositGroup( }); } -/** - * Check KYC status with the exchange, throw an appropriate exception when KYC - * is required. - * - * FIXME: Why does this throw an exception when KYC is required? - * Should we not return some proper result record here? - */ -async function checkDepositKycStatus( - ws: InternalWalletState, - exchangeUrl: string, - kycInfo: KycPendingInfo, - userType: KycUserType, -): Promise { - const url = new URL( - `kyc-check/${kycInfo.requirementRow}/${kycInfo.paytoHash}/${userType}`, - exchangeUrl, - ); - logger.info(`kyc url ${url.href}`); - const kycStatusReq = await ws.http.fetch(url.href, { - method: "GET", - }); - if (kycStatusReq.status === HttpStatusCode.Ok) { - logger.warn("kyc requested, but already fulfilled"); - return; - } else if (kycStatusReq.status === HttpStatusCode.Accepted) { - const kycStatus = await kycStatusReq.json(); - logger.info(`kyc status: ${j2s(kycStatus)}`); - // FIXME: This error code is totally wrong - throw TalerError.fromDetail( - TalerErrorCode.WALLET_WITHDRAWAL_KYC_REQUIRED, - { - kycUrl: kycStatus.kyc_url, - }, - `KYC check required for deposit`, - ); - } else { - throw Error(`unexpected response from kyc-check (${kycStatusReq.status})`); - } -} - /** * Check whether the refresh associated with the * aborting deposit group is done. @@ -940,38 +905,58 @@ async function processDepositGroupPendingDeposit( contractData, ); - for (let i = 0; i < depositPermissions.length; i++) { - const perm = depositPermissions[i]; + // Exchanges involved in the deposit + const exchanges: Set = new Set(); - if (depositGroup.statusPerCoin[i] !== DepositElementStatus.DepositPending) { - continue; - } + for (const dp of depositPermissions) { + exchanges.add(dp.exchange_url); + } - const requestBody: ExchangeDepositRequest = { - contribution: Amounts.stringify(perm.contribution), - merchant_payto_uri: depositGroup.wire.payto_uri, - wire_salt: depositGroup.wire.salt, + // We need to do one batch per exchange. + for (const exchangeUrl of exchanges.values()) { + const coins: BatchDepositRequestCoin[] = []; + const batchIndexes: number[] = []; + + const batchReq: ExchangeBatchDepositRequest = { + coins, h_contract_terms: depositGroup.contractTermsHash, - ub_sig: perm.ub_sig, + merchant_payto_uri: depositGroup.wire.payto_uri, + merchant_pub: depositGroup.contractTermsRaw.merchant_pub, timestamp: depositGroup.contractTermsRaw.timestamp, + wire_salt: depositGroup.wire.salt, 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, - h_age_commitment: perm.h_age_commitment, }; + + for (let i = 0; i < depositPermissions.length; i++) { + const perm = depositPermissions[i]; + if (perm.exchange_url != exchangeUrl) { + continue; + } + coins.push({ + coin_pub: perm.coin_pub, + coin_sig: perm.coin_sig, + contribution: Amounts.stringify(perm.contribution), + denom_pub_hash: perm.h_denom, + ub_sig: perm.ub_sig, + }); + batchIndexes.push(i); + } + // Check for cancellation before making network request. cancellationToken?.throwIfCancelled(); - const url = new URL(`coins/${perm.coin_pub}/deposit`, perm.exchange_url); + const url = new URL(`batch-deposit`, exchangeUrl); logger.info(`depositing to ${url}`); const httpResp = await ws.http.fetch(url.href, { method: "POST", - body: requestBody, + body: batchReq, cancellationToken: cancellationToken, }); - await readSuccessResponseJsonOrThrow(httpResp, codecForDepositSuccess()); + await readSuccessResponseJsonOrThrow( + httpResp, + codecForBatchDepositSuccess(), + ); await ws.db .mktx((x) => [x.depositGroups]) @@ -980,11 +965,13 @@ async function processDepositGroupPendingDeposit( if (!dg) { return; } - const coinStatus = dg.statusPerCoin[i]; - switch (coinStatus) { - case DepositElementStatus.DepositPending: - dg.statusPerCoin[i] = DepositElementStatus.Tracking; - await tx.depositGroups.put(dg); + for (const batchIndex of batchIndexes) { + const coinStatus = dg.statusPerCoin[batchIndex]; + switch (coinStatus) { + case DepositElementStatus.DepositPending: + dg.statusPerCoin[batchIndex] = DepositElementStatus.Tracking; + await tx.depositGroups.put(dg); + } } }); } @@ -1538,10 +1525,7 @@ async function getTotalFeesForDepositAmount( const allDenoms = await tx.denominations.indexes.byExchangeBaseUrl .iter(coin.exchangeBaseUrl) .filter((x) => - Amounts.isSameCurrency( - x.value, - pcs.coinContributions[i], - ), + Amounts.isSameCurrency(x.value, pcs.coinContributions[i]), ); const amountLeft = Amounts.sub( denom.value,