aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2023-09-12 12:24:42 +0200
committerFlorian Dold <florian@dold.me>2023-09-12 12:25:09 +0200
commitee8993f11cf81721cc30b4473e40124c2fee0dff (patch)
tree7978bc61a12c88368e59fd7b46a72865db9547de
parenta437605ebaf4267e60ae26f50ac107cdf11a8876 (diff)
wallet-core: use batch deposit API
-rw-r--r--packages/taler-util/src/taler-types.ts116
-rw-r--r--packages/taler-wallet-core/src/db.ts2
-rw-r--r--packages/taler-wallet-core/src/operations/deposits.ts178
3 files changed, 181 insertions, 115 deletions
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 <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
// 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 <sign-key-pub>` 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<DepositSuccess> =>
- buildCodecForObject<DepositSuccess>()
+export const codecForDepositConfirmationSignature =
+ (): Codec<DepositConfirmationSignature> =>
+ buildCodecForObject<DepositConfirmationSignature>()
+ .property("exchange_sig", codecForString())
+ .build("DepositConfirmationSignature");
+
+export const codecForBatchDepositSuccess = (): Codec<BatchDepositSuccess> =>
+ buildCodecForObject<BatchDepositSuccess>()
.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 <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 {
// 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,
@@ -407,46 +412,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
* 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<string> = 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,