aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2023-06-26 12:48:20 +0200
committerFlorian Dold <florian@dold.me>2023-06-26 12:48:20 +0200
commitfca893038dc61267c9861186041e129c88b46da8 (patch)
tree0f7603fca5f426551cf29e4cc6484bb0ae223491
parent66432cdd0588836000a8114e360aea537713712c (diff)
downloadwallet-core-fca893038dc61267c9861186041e129c88b46da8.tar.gz
wallet-core-fca893038dc61267c9861186041e129c88b46da8.tar.bz2
wallet-core-fca893038dc61267c9861186041e129c88b46da8.zip
wallet-core: fix deposit tx states, long-poll on kyc
-rw-r--r--packages/taler-wallet-core/src/db.ts26
-rw-r--r--packages/taler-wallet-core/src/operations/balance.ts1
-rw-r--r--packages/taler-wallet-core/src/operations/deposits.ts732
-rw-r--r--packages/taler-wallet-core/src/operations/pay-peer-push-credit.ts2
-rw-r--r--packages/taler-wallet-core/src/operations/pending.ts6
-rw-r--r--packages/taler-wallet-core/src/operations/withdraw.ts12
6 files changed, 463 insertions, 316 deletions
diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts
index 9d0efbc6a..ab2e95c23 100644
--- a/packages/taler-wallet-core/src/db.ts
+++ b/packages/taler-wallet-core/src/db.ts
@@ -910,14 +910,6 @@ export enum RefreshOperationStatus {
Failed = 51 /* DORMANT_START + 1 */,
}
-export enum DepositGroupOperationStatus {
- Pending = 10,
- AbortingWithRefresh = 11,
-
- Finished = 50,
- Failed = 51,
-}
-
/**
* Status of a single element of a deposit group.
*/
@@ -1653,11 +1645,15 @@ export interface BackupProviderRecord {
}
export enum DepositOperationStatus {
- Pending = 10,
+ PendingDeposit = 10,
Aborting = 11,
+ PendingTrack = 12,
+ PendingKyc = 13,
- Suspended = 20,
+ SuspendedDeposit = 20,
SuspendedAborting = 21,
+ SuspendedTrack = 22,
+ SuspendedKyc = 23,
Finished = 50,
Failed = 51,
@@ -1737,12 +1733,22 @@ export interface DepositGroupRecord {
*/
abortRefreshGroupId?: string;
+ kycInfo?: DepositKycInfo;
+
// FIXME: Do we need this and should it be in this object store?
trackingState?: {
[signature: string]: DepositTrackingInfo;
};
}
+export interface DepositKycInfo {
+ kycUrl: string;
+ requirementRow: number;
+ paytoHash: string;
+ exchangeBaseUrl: string;
+}
+
+
/**
* Record for a deposits that the wallet observed
* as a result of double spending, but which is not
diff --git a/packages/taler-wallet-core/src/operations/balance.ts b/packages/taler-wallet-core/src/operations/balance.ts
index 59c49deaa..3ab6649d7 100644
--- a/packages/taler-wallet-core/src/operations/balance.ts
+++ b/packages/taler-wallet-core/src/operations/balance.ts
@@ -108,7 +108,6 @@ function computeRefreshGroupAvailableAmount(r: RefreshGroupRecord): AmountJson {
export async function getBalancesInsideTransaction(
ws: InternalWalletState,
tx: GetReadOnlyAccess<{
- coins: typeof WalletStoresV1.coins;
coinAvailability: typeof WalletStoresV1.coinAvailability;
refreshGroups: typeof WalletStoresV1.refreshGroups;
withdrawalGroups: typeof WalletStoresV1.withdrawalGroups;
diff --git a/packages/taler-wallet-core/src/operations/deposits.ts b/packages/taler-wallet-core/src/operations/deposits.ts
index 8fd49858f..b771fc009 100644
--- a/packages/taler-wallet-core/src/operations/deposits.ts
+++ b/packages/taler-wallet-core/src/operations/deposits.ts
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2021-2023 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
@@ -83,6 +83,7 @@ import { readSuccessResponseJsonOrThrow } from "@gnu-taler/taler-util/http";
import {
constructTaskIdentifier,
OperationAttemptResult,
+ runLongpollAsync,
spendCoins,
TombstoneTag,
} from "./common.js";
@@ -100,6 +101,7 @@ import {
stopLongpolling,
} from "./transactions.js";
import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js";
+import { assertUnreachable } from "../util/assertUnreachable.js";
/**
* Logger.
@@ -114,51 +116,36 @@ export function computeDepositTransactionStatus(
dg: DepositGroupRecord,
): TransactionState {
switch (dg.operationStatus) {
- case DepositOperationStatus.Finished: {
+ case DepositOperationStatus.Finished:
return {
major: TransactionMajorState.Done,
};
- }
- // FIXME: We should actually use separate pending states for this!
- case DepositOperationStatus.Pending: {
- const numTotal = dg.payCoinSelection.coinPubs.length;
- let numDeposited = 0;
- let numKycRequired = 0;
- let numWired = 0;
- for (let i = 0; i < numTotal; i++) {
- if (dg.depositedPerCoin[i]) {
- numDeposited++;
- }
- switch (dg.transactionPerCoin[i]) {
- case DepositElementStatus.KycRequired:
- numKycRequired++;
- break;
- case DepositElementStatus.Wired:
- numWired++;
- break;
- }
- }
-
- if (numKycRequired > 0) {
- return {
- major: TransactionMajorState.Pending,
- minor: TransactionMinorState.KycRequired,
- };
- }
-
- if (numDeposited == numTotal) {
- return {
- major: TransactionMajorState.Pending,
- minor: TransactionMinorState.Track,
- };
- }
-
+ case DepositOperationStatus.PendingDeposit:
return {
major: TransactionMajorState.Pending,
minor: TransactionMinorState.Deposit,
};
- }
- case DepositOperationStatus.Suspended:
+ case DepositOperationStatus.PendingKyc:
+ return {
+ major: TransactionMajorState.Pending,
+ minor: TransactionMinorState.KycRequired,
+ };
+ case DepositOperationStatus.PendingTrack:
+ return {
+ major: TransactionMajorState.Pending,
+ minor: TransactionMinorState.Track,
+ };
+ case DepositOperationStatus.SuspendedKyc:
+ return {
+ major: TransactionMajorState.Suspended,
+ minor: TransactionMinorState.KycRequired,
+ };
+ case DepositOperationStatus.SuspendedTrack:
+ return {
+ major: TransactionMajorState.Suspended,
+ minor: TransactionMinorState.Track,
+ };
+ case DepositOperationStatus.SuspendedDeposit:
return {
major: TransactionMajorState.Suspended,
};
@@ -179,7 +166,7 @@ export function computeDepositTransactionStatus(
major: TransactionMajorState.SuspendedAborting,
};
default:
- throw Error(`unexpected deposit group state (${dg.operationStatus})`);
+ assertUnreachable(dg.operationStatus);
}
}
@@ -187,39 +174,11 @@ export function computeDepositTransactionActions(
dg: DepositGroupRecord,
): TransactionAction[] {
switch (dg.operationStatus) {
- case DepositOperationStatus.Finished: {
+ case DepositOperationStatus.Finished:
return [TransactionAction.Delete];
- }
- case DepositOperationStatus.Pending: {
- const numTotal = dg.payCoinSelection.coinPubs.length;
- let numDeposited = 0;
- let numKycRequired = 0;
- let numWired = 0;
- for (let i = 0; i < numTotal; i++) {
- if (dg.depositedPerCoin[i]) {
- numDeposited++;
- }
- switch (dg.transactionPerCoin[i]) {
- case DepositElementStatus.KycRequired:
- numKycRequired++;
- break;
- case DepositElementStatus.Wired:
- numWired++;
- break;
- }
- }
-
- if (numKycRequired > 0) {
- return [TransactionAction.Suspend, TransactionAction.Fail];
- }
-
- if (numDeposited == numTotal) {
- return [TransactionAction.Suspend, TransactionAction.Fail];
- }
-
+ case DepositOperationStatus.PendingDeposit:
return [TransactionAction.Suspend, TransactionAction.Abort];
- }
- case DepositOperationStatus.Suspended:
+ case DepositOperationStatus.SuspendedDeposit:
return [TransactionAction.Resume];
case DepositOperationStatus.Aborting:
return [TransactionAction.Fail, TransactionAction.Suspend];
@@ -229,8 +188,16 @@ export function computeDepositTransactionActions(
return [TransactionAction.Delete];
case DepositOperationStatus.SuspendedAborting:
return [TransactionAction.Resume, TransactionAction.Fail];
+ case DepositOperationStatus.PendingKyc:
+ return [TransactionAction.Suspend, TransactionAction.Fail];
+ case DepositOperationStatus.PendingTrack:
+ return [TransactionAction.Suspend, TransactionAction.Abort];
+ case DepositOperationStatus.SuspendedKyc:
+ return [TransactionAction.Resume, TransactionAction.Fail];
+ case DepositOperationStatus.SuspendedTrack:
+ return [TransactionAction.Resume, TransactionAction.Abort];
default:
- throw Error(`unexpected deposit group state (${dg.operationStatus})`);
+ assertUnreachable(dg.operationStatus);
}
}
@@ -260,15 +227,15 @@ export async function suspendDepositGroup(
switch (dg.operationStatus) {
case DepositOperationStatus.Finished:
return undefined;
- case DepositOperationStatus.Pending: {
- dg.operationStatus = DepositOperationStatus.Suspended;
+ case DepositOperationStatus.PendingDeposit: {
+ dg.operationStatus = DepositOperationStatus.SuspendedDeposit;
await tx.depositGroups.put(dg);
return {
oldTxState: oldState,
newTxState: computeDepositTransactionStatus(dg),
};
}
- case DepositOperationStatus.Suspended:
+ case DepositOperationStatus.SuspendedDeposit:
return undefined;
}
return undefined;
@@ -299,11 +266,11 @@ export async function resumeDepositGroup(
switch (dg.operationStatus) {
case DepositOperationStatus.Finished:
return;
- case DepositOperationStatus.Pending: {
+ case DepositOperationStatus.PendingDeposit: {
return;
}
- case DepositOperationStatus.Suspended:
- dg.operationStatus = DepositOperationStatus.Pending;
+ case DepositOperationStatus.SuspendedDeposit:
+ dg.operationStatus = DepositOperationStatus.PendingDeposit;
await tx.depositGroups.put(dg);
return {
oldTxState: oldState,
@@ -342,7 +309,7 @@ export async function abortDepositGroup(
switch (dg.operationStatus) {
case DepositOperationStatus.Finished:
return undefined;
- case DepositOperationStatus.Pending: {
+ case DepositOperationStatus.PendingDeposit: {
dg.operationStatus = DepositOperationStatus.Aborting;
await tx.depositGroups.put(dg);
return {
@@ -350,7 +317,7 @@ export async function abortDepositGroup(
newTxState: computeDepositTransactionStatus(dg),
};
}
- case DepositOperationStatus.Suspended:
+ case DepositOperationStatus.SuspendedDeposit:
// FIXME: Can we abort a suspended transaction?!
return undefined;
}
@@ -633,251 +600,431 @@ async function refundDepositGroup(
return OperationAttemptResult.pendingEmpty();
}
-/**
- * Process a deposit group that is not in its final state yet.
- */
-export async function processDepositGroup(
+async function processDepositGroupAborting(
ws: InternalWalletState,
- depositGroupId: string,
- options: {
- cancellationToken?: CancellationToken;
- } = {},
+ depositGroup: DepositGroupRecord,
): Promise<OperationAttemptResult> {
- const depositGroup = await ws.db
- .mktx((x) => [x.depositGroups])
- .runReadOnly(async (tx) => {
- return tx.depositGroups.get(depositGroupId);
- });
- if (!depositGroup) {
- logger.warn(`deposit group ${depositGroupId} not found`);
- return OperationAttemptResult.finishedEmpty();
- }
- if (depositGroup.timestampFinished) {
- logger.trace(`deposit group ${depositGroupId} already finished`);
- return OperationAttemptResult.finishedEmpty();
+ logger.info("processing deposit tx in 'aborting'");
+ const abortRefreshGroupId = depositGroup.abortRefreshGroupId;
+ if (!abortRefreshGroupId) {
+ logger.info("refunding deposit group");
+ return refundDepositGroup(ws, depositGroup);
}
+ logger.info("waiting for refresh");
+ return waitForRefreshOnDepositGroup(ws, depositGroup);
+}
+async function processDepositGroupPendingKyc(
+ ws: InternalWalletState,
+ depositGroup: DepositGroupRecord,
+): Promise<OperationAttemptResult> {
+ const { depositGroupId } = depositGroup;
const transactionId = constructTransactionIdentifier({
tag: TransactionType.Deposit,
depositGroupId,
});
+ const retryTag = constructTaskIdentifier({
+ tag: PendingTaskType.Deposit,
+ depositGroupId,
+ });
- const txStateOld = computeDepositTransactionStatus(depositGroup);
+ const kycInfo = depositGroup.kycInfo;
+ const userType = "individual";
- if (depositGroup.operationStatus === DepositOperationStatus.Pending) {
- const contractData = extractContractData(
- depositGroup.contractTermsRaw,
- depositGroup.contractTermsHash,
- "",
- );
+ if (!kycInfo) {
+ throw Error("invalid DB state, in pending(kyc), but no kycInfo present");
+ }
- // Check for cancellation before expensive operations.
- options.cancellationToken?.throwIfCancelled();
- // FIXME: Cache these!
- const depositPermissions = await generateDepositPermissions(
- ws,
- depositGroup.payCoinSelection,
- contractData,
+ runLongpollAsync(ws, retryTag, async (ct) => {
+ const url = new URL(
+ `kyc-check/${kycInfo.requirementRow}/${kycInfo.paytoHash}/${userType}`,
+ kycInfo.exchangeBaseUrl,
);
-
- for (let i = 0; i < depositPermissions.length; i++) {
- const perm = depositPermissions[i];
-
- let didDeposit: boolean = false;
-
- if (!depositGroup.depositedPerCoin[i]) {
- const requestBody: ExchangeDepositRequest = {
- contribution: Amounts.stringify(perm.contribution),
- merchant_payto_uri: depositGroup.wire.payto_uri,
- wire_salt: depositGroup.wire.salt,
- 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,
- h_age_commitment: perm.h_age_commitment,
- };
- // Check for cancellation before making network request.
- options.cancellationToken?.throwIfCancelled();
- const url = new URL(
- `coins/${perm.coin_pub}/deposit`,
- perm.exchange_url,
- );
- logger.info(`depositing to ${url}`);
- const httpResp = await ws.http.fetch(url.href, {
- method: "POST",
- body: requestBody,
- cancellationToken: options.cancellationToken,
+ url.searchParams.set("timeout_ms", "10000");
+ logger.info(`kyc url ${url.href}`);
+ const kycStatusRes = await ws.http.fetch(url.href, {
+ method: "GET",
+ cancellationToken: ct,
+ });
+ if (
+ kycStatusRes.status === HttpStatusCode.Ok ||
+ //FIXME: NoContent is not expected https://docs.taler.net/core/api-exchange.html#post--purses-$PURSE_PUB-merge
+ // remove after the exchange is fixed or clarified
+ kycStatusRes.status === HttpStatusCode.NoContent
+ ) {
+ const transitionInfo = await ws.db
+ .mktx((x) => [x.depositGroups])
+ .runReadWrite(async (tx) => {
+ const newDg = await tx.depositGroups.get(depositGroupId);
+ if (!newDg) {
+ return;
+ }
+ if (newDg.operationStatus !== DepositOperationStatus.PendingKyc) {
+ return;
+ }
+ const oldTxState = computeDepositTransactionStatus(newDg);
+ newDg.operationStatus = DepositOperationStatus.PendingTrack;
+ const newTxState = computeDepositTransactionStatus(newDg);
+ await tx.depositGroups.put(newDg);
+ return { oldTxState, newTxState };
});
- await readSuccessResponseJsonOrThrow(
- httpResp,
- codecForDepositSuccess(),
- );
- didDeposit = true;
- }
+ notifyTransition(ws, transactionId, transitionInfo);
+ return { ready: true };
+ } else if (kycStatusRes.status === HttpStatusCode.Accepted) {
+ // FIXME: Do we have to update the URL here?
+ return { ready: false };
+ } else {
+ throw Error(
+ `unexpected response from kyc-check (${kycStatusRes.status})`,
+ );
+ }
+ });
+ return OperationAttemptResult.longpoll();
+}
- let updatedTxStatus: DepositElementStatus | undefined = undefined;
+/**
+ * Tracking information from the exchange indicated that
+ * KYC is required. We need to check the KYC info
+ * and transition the transaction to the KYC required state.
+ */
+async function transitionToKycRequired(
+ ws: InternalWalletState,
+ depositGroup: DepositGroupRecord,
+ kycInfo: KycPendingInfo,
+ exchangeUrl: string,
+): Promise<OperationAttemptResult> {
+ const { depositGroupId } = depositGroup;
+ const userType = "individual";
- let newWiredCoin:
- | {
- id: string;
- value: DepositTrackingInfo;
- }
- | undefined;
+ const transactionId = constructTransactionIdentifier({
+ tag: TransactionType.Deposit,
+ depositGroupId,
+ });
- if (depositGroup.transactionPerCoin[i] !== DepositElementStatus.Wired) {
- const track = await trackDeposit(ws, depositGroup, perm);
+ 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 OperationAttemptResult.finishedEmpty();
+ } else if (kycStatusReq.status === HttpStatusCode.Accepted) {
+ const kycStatus = await kycStatusReq.json();
+ logger.info(`kyc status: ${j2s(kycStatus)}`);
+ const transitionInfo = await ws.db
+ .mktx((x) => [x.depositGroups])
+ .runReadWrite(async (tx) => {
+ const dg = await tx.depositGroups.get(depositGroupId);
+ if (!dg) {
+ return undefined;
+ }
+ if (dg.operationStatus !== DepositOperationStatus.PendingTrack) {
+ return undefined;
+ }
+ const oldTxState = computeDepositTransactionStatus(dg);
+ dg.kycInfo = {
+ exchangeBaseUrl: exchangeUrl,
+ kycUrl: kycStatus.kyc_url,
+ paytoHash: kycInfo.paytoHash,
+ requirementRow: kycInfo.requirementRow,
+ };
+ await tx.depositGroups.put(dg);
+ const newTxState = computeDepositTransactionStatus(dg);
+ return { oldTxState, newTxState };
+ });
+ notifyTransition(ws, transactionId, transitionInfo);
+ return OperationAttemptResult.finishedEmpty();
+ } else {
+ throw Error(`unexpected response from kyc-check (${kycStatusReq.status})`);
+ }
+}
- if (track.type === "accepted") {
- if (!track.kyc_ok && track.requirement_row !== undefined) {
- updatedTxStatus = DepositElementStatus.KycRequired;
- const { requirement_row: requirementRow } = track;
- const paytoHash = encodeCrock(
- hashTruncate32(stringToBytes(depositGroup.wire.payto_uri + "\0")),
- );
- await checkDepositKycStatus(
- ws,
- perm.exchange_url,
- { paytoHash, requirementRow },
- "individual",
- );
- } else {
- updatedTxStatus = DepositElementStatus.Accepted;
- }
- } else if (track.type === "wired") {
- updatedTxStatus = DepositElementStatus.Wired;
+async function processDepositGroupPendingTrack(
+ ws: InternalWalletState,
+ depositGroup: DepositGroupRecord,
+ cancellationToken?: CancellationToken,
+): Promise<OperationAttemptResult> {
+ const { depositGroupId } = depositGroup;
+ for (let i = 0; i < depositGroup.depositedPerCoin.length; i++) {
+ const coinPub = depositGroup.payCoinSelection.coinPubs[i];
+ // FIXME: Make the URL part of the coin selection?
+ const exchangeBaseUrl = await ws.db
+ .mktx((x) => [x.coins])
+ .runReadWrite(async (tx) => {
+ const coinRecord = await tx.coins.get(coinPub);
+ checkDbInvariant(!!coinRecord);
+ return coinRecord.exchangeBaseUrl;
+ });
- const payto = parsePaytoUri(depositGroup.wire.payto_uri);
- if (!payto) {
- throw Error(`unparsable payto: ${depositGroup.wire.payto_uri}`);
- }
+ let updatedTxStatus: DepositElementStatus | undefined = undefined;
+ let newWiredCoin:
+ | {
+ id: string;
+ value: DepositTrackingInfo;
+ }
+ | undefined;
+
+ if (depositGroup.transactionPerCoin[i] !== DepositElementStatus.Wired) {
+ const track = await trackDeposit(
+ ws,
+ depositGroup,
+ coinPub,
+ exchangeBaseUrl,
+ );
- const fee = await getExchangeWireFee(
- ws,
- payto.targetType,
- perm.exchange_url,
- track.execution_time,
+ if (track.type === "accepted") {
+ if (!track.kyc_ok && track.requirement_row !== undefined) {
+ const paytoHash = encodeCrock(
+ hashTruncate32(stringToBytes(depositGroup.wire.payto_uri + "\0")),
);
- const raw = Amounts.parseOrThrow(track.coin_contribution);
- const wireFee = Amounts.parseOrThrow(fee.wireFee);
-
- newWiredCoin = {
- value: {
- amountRaw: Amounts.stringify(raw),
- wireFee: Amounts.stringify(wireFee),
- exchangePub: track.exchange_pub,
- timestampExecuted: track.execution_time,
- wireTransferId: track.wtid,
- },
- id: track.exchange_sig,
+ const { requirement_row: requirementRow } = track;
+ const kycInfo: KycPendingInfo = {
+ paytoHash,
+ requirementRow,
};
+ return transitionToKycRequired(
+ ws,
+ depositGroup,
+ kycInfo,
+ exchangeBaseUrl,
+ );
} else {
- updatedTxStatus = DepositElementStatus.Unknown;
+ updatedTxStatus = DepositElementStatus.Accepted;
}
+ } else if (track.type === "wired") {
+ updatedTxStatus = DepositElementStatus.Wired;
+
+ const payto = parsePaytoUri(depositGroup.wire.payto_uri);
+ if (!payto) {
+ throw Error(`unparsable payto: ${depositGroup.wire.payto_uri}`);
+ }
+
+ const fee = await getExchangeWireFee(
+ ws,
+ payto.targetType,
+ exchangeBaseUrl,
+ track.execution_time,
+ );
+ const raw = Amounts.parseOrThrow(track.coin_contribution);
+ const wireFee = Amounts.parseOrThrow(fee.wireFee);
+
+ newWiredCoin = {
+ value: {
+ amountRaw: Amounts.stringify(raw),
+ wireFee: Amounts.stringify(wireFee),
+ exchangePub: track.exchange_pub,
+ timestampExecuted: track.execution_time,
+ wireTransferId: track.wtid,
+ },
+ id: track.exchange_sig,
+ };
+ } else {
+ updatedTxStatus = DepositElementStatus.Unknown;
}
+ }
- if (updatedTxStatus !== undefined || didDeposit) {
- await ws.db
- .mktx((x) => [x.depositGroups])
- .runReadWrite(async (tx) => {
- const dg = await tx.depositGroups.get(depositGroupId);
- if (!dg) {
- return;
- }
- if (didDeposit) {
- dg.depositedPerCoin[i] = didDeposit;
- }
- if (updatedTxStatus !== undefined) {
- dg.transactionPerCoin[i] = updatedTxStatus;
- }
- if (newWiredCoin) {
- /**
- * FIXME: if there is a new wire information from the exchange
- * it should add up to the previous tracking states.
- *
- * This may loose information by overriding prev state.
- *
- * And: add checks to integration tests
- */
- if (!dg.trackingState) {
- dg.trackingState = {};
- }
-
- dg.trackingState[newWiredCoin.id] = newWiredCoin.value;
+ if (updatedTxStatus !== undefined) {
+ await ws.db
+ .mktx((x) => [x.depositGroups])
+ .runReadWrite(async (tx) => {
+ const dg = await tx.depositGroups.get(depositGroupId);
+ if (!dg) {
+ return;
+ }
+ if (updatedTxStatus !== undefined) {
+ dg.transactionPerCoin[i] = updatedTxStatus;
+ }
+ if (newWiredCoin) {
+ /**
+ * FIXME: if there is a new wire information from the exchange
+ * it should add up to the previous tracking states.
+ *
+ * This may loose information by overriding prev state.
+ *
+ * And: add checks to integration tests
+ */
+ if (!dg.trackingState) {
+ dg.trackingState = {};
}
- await tx.depositGroups.put(dg);
- });
+
+ dg.trackingState[newWiredCoin.id] = newWiredCoin.value;
+ }
+ await tx.depositGroups.put(dg);
+ });
+ }
+ }
+
+ let allWired = true;
+
+ const transitionInfo = await ws.db
+ .mktx((x) => [x.depositGroups])
+ .runReadWrite(async (tx) => {
+ const dg = await tx.depositGroups.get(depositGroupId);
+ if (!dg) {
+ return undefined;
+ }
+ const oldTxState = computeDepositTransactionStatus(dg);
+ for (let i = 0; i < depositGroup.depositedPerCoin.length; i++) {
+ if (depositGroup.transactionPerCoin[i] !== DepositElementStatus.Wired) {
+ allWired = false;
+ break;
+ }
+ }
+ if (allWired) {
+ dg.timestampFinished = TalerPreciseTimestamp.now();
+ dg.operationStatus = DepositOperationStatus.Finished;
+ await tx.depositGroups.put(dg);
}
+ const newTxState = computeDepositTransactionStatus(dg);
+ return { oldTxState, newTxState };
+ });
+ const transactionId = constructTransactionIdentifier({
+ tag: TransactionType.Deposit,
+ depositGroupId,
+ });
+ notifyTransition(ws, transactionId, transitionInfo);
+ if (allWired) {
+ return OperationAttemptResult.finishedEmpty();
+ } else {
+ // FIXME: Use long-polling.
+ return OperationAttemptResult.pendingEmpty();
+ }
+}
+
+async function processDepositGroupPendingDeposit(
+ ws: InternalWalletState,
+ depositGroup: DepositGroupRecord,
+ cancellationToken?: CancellationToken,
+): Promise<OperationAttemptResult> {
+ logger.info("processing deposit group in pending(deposit)");
+ const depositGroupId = depositGroup.depositGroupId;
+ const contractData = extractContractData(
+ depositGroup.contractTermsRaw,
+ depositGroup.contractTermsHash,
+ "",
+ );
+
+ const transactionId = constructTransactionIdentifier({
+ tag: TransactionType.Deposit,
+ depositGroupId,
+ });
+
+ // Check for cancellation before expensive operations.
+ cancellationToken?.throwIfCancelled();
+
+ // FIXME: Cache these!
+ const depositPermissions = await generateDepositPermissions(
+ ws,
+ depositGroup.payCoinSelection,
+ contractData,
+ );
+
+ for (let i = 0; i < depositPermissions.length; i++) {
+ const perm = depositPermissions[i];
+
+ if (depositGroup.depositedPerCoin[i]) {
+ continue;
}
- const txStatusNew = await ws.db
+ const requestBody: ExchangeDepositRequest = {
+ contribution: Amounts.stringify(perm.contribution),
+ merchant_payto_uri: depositGroup.wire.payto_uri,
+ wire_salt: depositGroup.wire.salt,
+ 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,
+ h_age_commitment: perm.h_age_commitment,
+ };
+ // Check for cancellation before making network request.
+ cancellationToken?.throwIfCancelled();
+ const url = new URL(`coins/${perm.coin_pub}/deposit`, perm.exchange_url);
+ logger.info(`depositing to ${url}`);
+ const httpResp = await ws.http.fetch(url.href, {
+ method: "POST",
+ body: requestBody,
+ cancellationToken: cancellationToken,
+ });
+ await readSuccessResponseJsonOrThrow(httpResp, codecForDepositSuccess());
+
+ await ws.db
.mktx((x) => [x.depositGroups])
.runReadWrite(async (tx) => {
const dg = await tx.depositGroups.get(depositGroupId);
if (!dg) {
- return undefined;
- }
- let allDepositedAndWired = true;
- for (let i = 0; i < depositGroup.depositedPerCoin.length; i++) {
- if (
- !depositGroup.depositedPerCoin[i] ||
- depositGroup.transactionPerCoin[i] !== DepositElementStatus.Wired
- ) {
- allDepositedAndWired = false;
- break;
- }
- }
- if (allDepositedAndWired) {
- dg.timestampFinished = TalerPreciseTimestamp.now();
- dg.operationStatus = DepositOperationStatus.Finished;
- await tx.depositGroups.put(dg);
+ return;
}
- return computeDepositTransactionStatus(dg);
+ dg.depositedPerCoin[i] = true;
+ await tx.depositGroups.put(dg);
});
+ }
- if (!txStatusNew) {
- // Doesn't exist anymore!
- return OperationAttemptResult.finishedEmpty();
- }
+ const transitionInfo = await ws.db
+ .mktx((x) => [x.depositGroups])
+ .runReadWrite(async (tx) => {
+ const dg = await tx.depositGroups.get(depositGroupId);
+ if (!dg) {
+ return undefined;
+ }
+ const oldTxState = computeDepositTransactionStatus(dg);
+ dg.operationStatus = DepositOperationStatus.PendingTrack;
+ await tx.depositGroups.put(dg);
+ const newTxState = computeDepositTransactionStatus(dg);
+ return { oldTxState, newTxState };
+ });
- // Notify if state transitioned
- if (
- txStateOld.major !== txStatusNew.major ||
- txStateOld.minor !== txStatusNew.minor
- ) {
- ws.notify({
- type: NotificationType.TransactionStateTransition,
- transactionId,
- oldTxState: txStateOld,
- newTxState: txStatusNew,
- });
- }
+ notifyTransition(ws, transactionId, transitionInfo);
+ return OperationAttemptResult.finishedEmpty();
+}
- // FIXME: consider other cases like aborting, suspend, ...
- if (
- txStatusNew.major === TransactionMajorState.Pending ||
- txStatusNew.major === TransactionMajorState.Aborting
- ) {
- return OperationAttemptResult.pendingEmpty();
- } else {
- return OperationAttemptResult.finishedEmpty();
- }
+/**
+ * Process a deposit group that is not in its final state yet.
+ */
+export async function processDepositGroup(
+ ws: InternalWalletState,
+ depositGroupId: string,
+ options: {
+ cancellationToken?: CancellationToken;
+ } = {},
+): Promise<OperationAttemptResult> {
+ const depositGroup = await ws.db
+ .mktx((x) => [x.depositGroups])
+ .runReadOnly(async (tx) => {
+ return tx.depositGroups.get(depositGroupId);
+ });
+ if (!depositGroup) {
+ logger.warn(`deposit group ${depositGroupId} not found`);
+ return OperationAttemptResult.finishedEmpty();
}
- if (depositGroup.operationStatus === DepositOperationStatus.Aborting) {
- logger.info("processing deposit tx in 'aborting'");
- const abortRefreshGroupId = depositGroup.abortRefreshGroupId;
- if (!abortRefreshGroupId) {
- logger.info("refunding deposit group");
- return refundDepositGroup(ws, depositGroup);
- }
- logger.info("waiting for refresh");
- return waitForRefreshOnDepositGroup(ws, depositGroup);
+ switch (depositGroup.operationStatus) {
+ case DepositOperationStatus.PendingTrack:
+ return processDepositGroupPendingTrack(
+ ws,
+ depositGroup,
+ options.cancellationToken,
+ );
+ case DepositOperationStatus.PendingKyc:
+ return processDepositGroupPendingKyc(ws, depositGroup);
+ case DepositOperationStatus.PendingDeposit:
+ return processDepositGroupPendingDeposit(
+ ws,
+ depositGroup,
+ options.cancellationToken,
+ );
+ case DepositOperationStatus.Aborting:
+ return processDepositGroupAborting(ws, depositGroup);
}
+
return OperationAttemptResult.finishedEmpty();
}
@@ -928,16 +1075,17 @@ async function getExchangeWireFee(
async function trackDeposit(
ws: InternalWalletState,
depositGroup: DepositGroupRecord,
- dp: CoinDepositPermission,
+ coinPub: string,
+ exchangeUrl: string,
): Promise<TrackTransaction> {
const wireHash = depositGroup.contractTermsRaw.h_wire;
const url = new URL(
- `deposits/${wireHash}/${depositGroup.merchantPub}/${depositGroup.contractTermsHash}/${dp.coin_pub}`,
- dp.exchange_url,
+ `deposits/${wireHash}/${depositGroup.merchantPub}/${depositGroup.contractTermsHash}/${coinPub}`,
+ exchangeUrl,
);
const sigResp = await ws.cryptoApi.signTrackTransaction({
- coinPub: dp.coin_pub,
+ coinPub,
contractTermsHash: depositGroup.contractTermsHash,
merchantPriv: depositGroup.merchantPriv,
merchantPub: depositGroup.merchantPub,
@@ -1224,7 +1372,7 @@ export async function createDepositGroup(
payto_uri: req.depositPaytoUri,
salt: wireSalt,
},
- operationStatus: DepositOperationStatus.Pending,
+ operationStatus: DepositOperationStatus.PendingDeposit,
};
const transactionId = constructTransactionIdentifier({
@@ -1263,6 +1411,10 @@ export async function createDepositGroup(
newTxState,
});
+ ws.notify({
+ type: NotificationType.BalanceChange,
+ });
+
return {
depositGroupId,
transactionId,
@@ -1332,7 +1484,7 @@ export async function getCounterpartyEffectiveDepositAmount(
* Get the fee amount that will be charged when trying to deposit the
* specified amount using the selected coins and the wire method.
*/
-export async function getTotalFeesForDepositAmount(
+async function getTotalFeesForDepositAmount(
ws: InternalWalletState,
wireType: string,
total: AmountJson,
diff --git a/packages/taler-wallet-core/src/operations/pay-peer-push-credit.ts b/packages/taler-wallet-core/src/operations/pay-peer-push-credit.ts
index b2f641b80..58a1c2b3c 100644
--- a/packages/taler-wallet-core/src/operations/pay-peer-push-credit.ts
+++ b/packages/taler-wallet-core/src/operations/pay-peer-push-credit.ts
@@ -427,7 +427,7 @@ async function handlePendingMerge(
const respJson = await mergeHttpResp.json();
const kycPending = codecForWalletKycUuid().decode(respJson);
logger.info(`kyc uuid response: ${j2s(kycPending)}`);
- processPeerPushCreditKycRequired(ws, peerInc, kycPending);
+ return processPeerPushCreditKycRequired(ws, peerInc, kycPending);
}
logger.trace(`merge request: ${j2s(mergeReq)}`);
diff --git a/packages/taler-wallet-core/src/operations/pending.ts b/packages/taler-wallet-core/src/operations/pending.ts
index e7e7ffcfc..e3f7d0fab 100644
--- a/packages/taler-wallet-core/src/operations/pending.ts
+++ b/packages/taler-wallet-core/src/operations/pending.ts
@@ -32,8 +32,8 @@ import {
PeerPushPaymentIncomingStatus,
PeerPullPaymentInitiationStatus,
WithdrawalGroupStatus,
- DepositGroupOperationStatus,
TipRecordStatus,
+ DepositOperationStatus,
} from "../db.js";
import {
PendingOperationsResponse,
@@ -198,8 +198,8 @@ async function gatherDepositPending(
): Promise<void> {
const dgs = await tx.depositGroups.indexes.byStatus.getAll(
GlobalIDB.KeyRange.bound(
- DepositGroupOperationStatus.Pending,
- DepositGroupOperationStatus.AbortingWithRefresh,
+ DepositOperationStatus.PendingDeposit,
+ DepositOperationStatus.PendingKyc,
),
);
for (const dg of dgs) {
diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts b/packages/taler-wallet-core/src/operations/withdraw.ts
index 28f4eeebb..8eb7f6457 100644
--- a/packages/taler-wallet-core/src/operations/withdraw.ts
+++ b/packages/taler-wallet-core/src/operations/withdraw.ts
@@ -1108,14 +1108,6 @@ async function processPlanchetVerifyAndStoreCoin(
wgContext.planchetsFinished.add(planchet.coinPub);
- // We create the notification here, as the async transaction below
- // allows other planchet withdrawals to change wgContext.planchetsFinished
- const notification: WalletNotification = {
- type: NotificationType.CoinWithdrawn,
- numTotal: wgContext.numPlanchets,
- numWithdrawn: wgContext.planchetsFinished.size,
- };
-
// Check if this is the first time that the whole
// withdrawal succeeded. If so, mark the withdrawal
// group as finished.
@@ -1138,9 +1130,7 @@ async function processPlanchetVerifyAndStoreCoin(
return true;
});
- if (firstSuccess) {
- ws.notify(notification);
- }
+ ws.notify({ type: NotificationType.BalanceChange });
}
/**