wallet-core: use batch deposit API

This commit is contained in:
Florian Dold 2023-09-12 12:24:42 +02:00
parent a437605eba
commit ee8993f11c
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
3 changed files with 181 additions and 115 deletions

View File

@ -1889,42 +1889,58 @@ export interface ExchangeRefreshRevealRequest {
old_age_commitment?: Edx25519PublicKeyEnc[]; old_age_commitment?: Edx25519PublicKeyEnc[];
} }
export interface DepositSuccess { interface DepositConfirmationSignature {
// The EdDSA signature of `TALER_DepositConfirmationPS` using a current
// `signing key of the exchange <sign-key-priv>` 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 // Optional base URL of the exchange for looking up wire transfers
// associated with this transaction. If not given, // associated with this transaction. If not given,
// the base URL is the same as the one used for this request. // 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 // Can be used if the base URL for ``/transactions/`` differs from that
// for /coins/, i.e. for load balancing. Clients SHOULD // for ``/coins/``, i.e. for load balancing. Clients SHOULD
// respect the transaction_base_url if provided. Any HTTP server // respect the ``transaction_base_url`` if provided. Any HTTP server
// belonging to an exchange MUST generate a 307 or 308 redirection // belonging to an exchange MUST generate a 307 or 308 redirection
// to the correct base URL should a client uses the wrong base // to the correct base URL should a client uses the wrong base
// URL, or if the base URL has changed since the deposit. // URL, or if the base URL has changed since the deposit.
transaction_base_url?: string; 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; exchange_timestamp: TalerProtocolTimestamp;
// the EdDSA signature of TALER_DepositConfirmationPS using a current // `Public EdDSA key of the exchange <sign-key-pub>` that was used to
// 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. // 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 // explicitly as the client might otherwise be confused by clock skew as to
// which signing key was used. // 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<DepositSuccess> => export const codecForDepositConfirmationSignature =
buildCodecForObject<DepositSuccess>() (): Codec<DepositConfirmationSignature> =>
buildCodecForObject<DepositConfirmationSignature>()
.property("exchange_sig", codecForString())
.build("DepositConfirmationSignature");
export const codecForBatchDepositSuccess = (): Codec<BatchDepositSuccess> =>
buildCodecForObject<BatchDepositSuccess>()
.property("exchange_pub", codecForString()) .property("exchange_pub", codecForString())
.property("exchange_sig", codecForString()) .property(
"exchange_sigs",
codecForList(codecForDepositConfirmationSignature()),
)
.property("exchange_timestamp", codecForTimestamp) .property("exchange_timestamp", codecForTimestamp)
.property("transaction_base_url", codecOptional(codecForString())) .property("transaction_base_url", codecOptional(codecForString()))
.build("DepositSuccess"); .build("BatchDepositSuccess");
export interface TrackTransactionWired { export interface TrackTransactionWired {
// Raw wire transfer identifier of the deposit. // Raw wire transfer identifier of the deposit.
@ -2148,6 +2164,9 @@ export interface ExchangePurseDeposits {
deposits: PurseDeposit[]; deposits: PurseDeposit[];
} }
/**
* @deprecated batch deposit should be used.
*/
export interface ExchangeDepositRequest { export interface ExchangeDepositRequest {
// Amount to be deposited, can be a fraction of the // Amount to be deposited, can be a fraction of the
// coin's total value. // coin's total value.
@ -2210,6 +2229,67 @@ export interface ExchangeDepositRequest {
h_age_commitment?: string; 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 <merchant-pub>`, 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-priv>`.
coin_sig: EddsaSignatureString;
}
export interface WalletKycUuid { export interface WalletKycUuid {
// UUID that the wallet should use when initiating // UUID that the wallet should use when initiating
// the KYC check. // the KYC check.

View File

@ -1657,6 +1657,8 @@ export interface DepositGroupRecord {
/** /**
* Verbatim contract terms. * Verbatim contract terms.
*
* FIXME: Move this to the contract terms object store!
*/ */
contractTermsRaw: MerchantContractTerms; contractTermsRaw: MerchantContractTerms;

View File

@ -21,70 +21,69 @@ import {
AbsoluteTime, AbsoluteTime,
AmountJson, AmountJson,
Amounts, Amounts,
BatchDepositRequestCoin,
CancellationToken, CancellationToken,
canonicalJson,
codecForDepositSuccess,
codecForTackTransactionAccepted,
codecForTackTransactionWired,
CoinRefreshRequest, CoinRefreshRequest,
CreateDepositGroupRequest, CreateDepositGroupRequest,
CreateDepositGroupResponse, CreateDepositGroupResponse,
DepositGroupFees, DepositGroupFees,
durationFromSpec, Duration,
encodeCrock, ExchangeBatchDepositRequest,
ExchangeDepositRequest,
ExchangeRefundRequest, ExchangeRefundRequest,
getRandomBytes,
hashTruncate32,
hashWire,
HttpStatusCode, HttpStatusCode,
j2s,
Logger, Logger,
MerchantContractTerms, MerchantContractTerms,
NotificationType, NotificationType,
parsePaytoUri,
PayCoinSelection, PayCoinSelection,
PrepareDepositRequest, PrepareDepositRequest,
PrepareDepositResponse, PrepareDepositResponse,
RefreshReason, RefreshReason,
stringToBytes, TalerError,
TalerErrorCode, TalerErrorCode,
TalerProtocolTimestamp,
TalerPreciseTimestamp, TalerPreciseTimestamp,
TalerProtocolTimestamp,
TrackTransaction, TrackTransaction,
TransactionAction,
TransactionMajorState, TransactionMajorState,
TransactionMinorState, TransactionMinorState,
TransactionState, TransactionState,
TransactionType, TransactionType,
URL, URL,
WireFee, WireFee,
TransactionAction, canonicalJson,
Duration, codecForBatchDepositSuccess,
codecForTackTransactionAccepted,
codecForTackTransactionWired,
durationFromSpec,
encodeCrock,
getRandomBytes,
hashTruncate32,
hashWire,
j2s,
parsePaytoUri,
stringToBytes,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { readSuccessResponseJsonOrThrow } from "@gnu-taler/taler-util/http";
import { DepositElementStatus, DepositGroupRecord } from "../db.js";
import { import {
DenominationRecord,
DepositGroupRecord,
DepositElementStatus,
} from "../db.js";
import { TalerError } from "@gnu-taler/taler-util";
import {
createRefreshGroup,
DepositOperationStatus, DepositOperationStatus,
DepositTrackingInfo, DepositTrackingInfo,
getTotalRefreshCost,
KycPendingInfo, KycPendingInfo,
KycUserType,
PendingTaskType, PendingTaskType,
RefreshOperationStatus, RefreshOperationStatus,
createRefreshGroup,
getTotalRefreshCost,
} from "../index.js"; } from "../index.js";
import { InternalWalletState } from "../internal-wallet-state.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 { import {
constructTaskIdentifier,
TaskRunResult, TaskRunResult,
TombstoneTag,
constructTaskIdentifier,
runLongpollAsync, runLongpollAsync,
spendCoins, spendCoins,
TombstoneTag,
} from "./common.js"; } from "./common.js";
import { getExchangeDetails } from "./exchanges.js"; import { getExchangeDetails } from "./exchanges.js";
import { import {
@ -92,15 +91,12 @@ import {
generateDepositPermissions, generateDepositPermissions,
getTotalPaymentCost, getTotalPaymentCost,
} from "./pay-merchant.js"; } from "./pay-merchant.js";
import { selectPayCoinsNew } from "../util/coinSelection.js";
import { import {
constructTransactionIdentifier, constructTransactionIdentifier,
notifyTransition, notifyTransition,
parseTransactionIdentifier, parseTransactionIdentifier,
stopLongpolling, stopLongpolling,
} from "./transactions.js"; } from "./transactions.js";
import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js";
import { assertUnreachable } from "../util/assertUnreachable.js";
/** /**
* Logger. * 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( export function computeDepositTransactionActions(
dg: DepositGroupRecord, dg: DepositGroupRecord,
): TransactionAction[] { ): 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( export async function suspendDepositGroup(
ws: InternalWalletState, ws: InternalWalletState,
depositGroupId: string, 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<void> {
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 * Check whether the refresh associated with the
* aborting deposit group is done. * aborting deposit group is done.
@ -940,38 +905,58 @@ async function processDepositGroupPendingDeposit(
contractData, contractData,
); );
for (let i = 0; i < depositPermissions.length; i++) { // Exchanges involved in the deposit
const perm = depositPermissions[i]; const exchanges: Set<string> = new Set();
if (depositGroup.statusPerCoin[i] !== DepositElementStatus.DepositPending) { for (const dp of depositPermissions) {
continue; exchanges.add(dp.exchange_url);
} }
const requestBody: ExchangeDepositRequest = { // We need to do one batch per exchange.
contribution: Amounts.stringify(perm.contribution), for (const exchangeUrl of exchanges.values()) {
merchant_payto_uri: depositGroup.wire.payto_uri, const coins: BatchDepositRequestCoin[] = [];
wire_salt: depositGroup.wire.salt, const batchIndexes: number[] = [];
const batchReq: ExchangeBatchDepositRequest = {
coins,
h_contract_terms: depositGroup.contractTermsHash, 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, timestamp: depositGroup.contractTermsRaw.timestamp,
wire_salt: depositGroup.wire.salt,
wire_transfer_deadline: wire_transfer_deadline:
depositGroup.contractTermsRaw.wire_transfer_deadline, depositGroup.contractTermsRaw.wire_transfer_deadline,
refund_deadline: depositGroup.contractTermsRaw.refund_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. // Check for cancellation before making network request.
cancellationToken?.throwIfCancelled(); 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}`); logger.info(`depositing to ${url}`);
const httpResp = await ws.http.fetch(url.href, { const httpResp = await ws.http.fetch(url.href, {
method: "POST", method: "POST",
body: requestBody, body: batchReq,
cancellationToken: cancellationToken, cancellationToken: cancellationToken,
}); });
await readSuccessResponseJsonOrThrow(httpResp, codecForDepositSuccess()); await readSuccessResponseJsonOrThrow(
httpResp,
codecForBatchDepositSuccess(),
);
await ws.db await ws.db
.mktx((x) => [x.depositGroups]) .mktx((x) => [x.depositGroups])
@ -980,11 +965,13 @@ async function processDepositGroupPendingDeposit(
if (!dg) { if (!dg) {
return; return;
} }
const coinStatus = dg.statusPerCoin[i]; for (const batchIndex of batchIndexes) {
switch (coinStatus) { const coinStatus = dg.statusPerCoin[batchIndex];
case DepositElementStatus.DepositPending: switch (coinStatus) {
dg.statusPerCoin[i] = DepositElementStatus.Tracking; case DepositElementStatus.DepositPending:
await tx.depositGroups.put(dg); dg.statusPerCoin[batchIndex] = DepositElementStatus.Tracking;
await tx.depositGroups.put(dg);
}
} }
}); });
} }
@ -1538,10 +1525,7 @@ async function getTotalFeesForDepositAmount(
const allDenoms = await tx.denominations.indexes.byExchangeBaseUrl const allDenoms = await tx.denominations.indexes.byExchangeBaseUrl
.iter(coin.exchangeBaseUrl) .iter(coin.exchangeBaseUrl)
.filter((x) => .filter((x) =>
Amounts.isSameCurrency( Amounts.isSameCurrency(x.value, pcs.coinContributions[i]),
x.value,
pcs.coinContributions[i],
),
); );
const amountLeft = Amounts.sub( const amountLeft = Amounts.sub(
denom.value, denom.value,