aboutsummaryrefslogtreecommitdiff
path: root/packages/taler-wallet-core/src/operations/deposits.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/taler-wallet-core/src/operations/deposits.ts')
-rw-r--r--packages/taler-wallet-core/src/operations/deposits.ts513
1 files changed, 340 insertions, 173 deletions
diff --git a/packages/taler-wallet-core/src/operations/deposits.ts b/packages/taler-wallet-core/src/operations/deposits.ts
index 6e56b0897..051cbc176 100644
--- a/packages/taler-wallet-core/src/operations/deposits.ts
+++ b/packages/taler-wallet-core/src/operations/deposits.ts
@@ -64,13 +64,16 @@ import {
DepositElementStatus,
} from "../db.js";
import { TalerError } from "@gnu-taler/taler-util";
-import { getTotalRefreshCost, KycPendingInfo, KycUserType } from "../index.js";
+import {
+ DepositOperationStatus,
+ getTotalRefreshCost,
+ KycPendingInfo,
+ KycUserType,
+ PendingTaskType,
+} from "../index.js";
import { InternalWalletState } from "../internal-wallet-state.js";
import { readSuccessResponseJsonOrThrow } from "@gnu-taler/taler-util/http";
-import {
- OperationAttemptResult,
- OperationAttemptResultType,
-} from "../util/retries.js";
+import { OperationAttemptResult } from "../util/retries.js";
import { spendCoins } from "./common.js";
import { getExchangeDetails } from "./exchanges.js";
import {
@@ -82,7 +85,9 @@ import { selectPayCoinsNew } from "../util/coinSelection.js";
import {
constructTransactionIdentifier,
parseTransactionIdentifier,
+ stopLongpolling,
} from "./transactions.js";
+import { constructTaskIdentifier } from "../util/retries.js";
/**
* Logger.
@@ -97,12 +102,12 @@ export function computeDepositTransactionStatus(
dg: DepositGroupRecord,
): TransactionState {
switch (dg.operationStatus) {
- case OperationStatus.Finished: {
+ case DepositOperationStatus.Finished: {
return {
major: TransactionMajorState.Done,
};
}
- case OperationStatus.Pending: {
+ case DepositOperationStatus.Pending: {
const numTotal = dg.payCoinSelection.coinPubs.length;
let numDeposited = 0;
let numKycRequired = 0;
@@ -140,6 +145,10 @@ export function computeDepositTransactionStatus(
minor: TransactionMinorState.Deposit,
};
}
+ case DepositOperationStatus.Suspended:
+ return {
+ major: TransactionMajorState.Suspended,
+ };
default:
throw Error("unexpected deposit group state");
}
@@ -149,13 +158,156 @@ export async function suspendDepositGroup(
ws: InternalWalletState,
depositGroupId: string,
): Promise<void> {
- throw Error("not implemented");
+ const transactionId = constructTransactionIdentifier({
+ tag: TransactionType.Deposit,
+ depositGroupId,
+ });
+ const retryTag = constructTaskIdentifier({
+ tag: PendingTaskType.Deposit,
+ depositGroupId,
+ });
+ let res = await ws.db
+ .mktx((x) => [x.depositGroups])
+ .runReadWrite(async (tx) => {
+ const dg = await tx.depositGroups.get(depositGroupId);
+ if (!dg) {
+ logger.warn(
+ `can't suspend deposit group, depositGroupId=${depositGroupId} not found`,
+ );
+ return undefined;
+ }
+ const oldState = computeDepositTransactionStatus(dg);
+ switch (dg.operationStatus) {
+ case DepositOperationStatus.Finished:
+ return undefined;
+ case DepositOperationStatus.Pending: {
+ dg.operationStatus = DepositOperationStatus.Suspended;
+ await tx.depositGroups.put(dg);
+ return {
+ oldTxState: oldState,
+ newTxState: computeDepositTransactionStatus(dg),
+ };
+ }
+ case DepositOperationStatus.Suspended:
+ return undefined;
+ }
+ return undefined;
+ });
+ stopLongpolling(ws, retryTag);
+ if (res) {
+ ws.notify({
+ type: NotificationType.TransactionStateTransition,
+ transactionId,
+ oldTxState: res.oldTxState,
+ newTxState: res.newTxState,
+ });
+ }
+}
+
+export async function resumeDepositGroup(
+ ws: InternalWalletState,
+ depositGroupId: string,
+): Promise<void> {
+ const transactionId = constructTransactionIdentifier({
+ tag: TransactionType.Deposit,
+ depositGroupId,
+ });
+ let res = await ws.db
+ .mktx((x) => [x.depositGroups])
+ .runReadWrite(async (tx) => {
+ const dg = await tx.depositGroups.get(depositGroupId);
+ if (!dg) {
+ logger.warn(
+ `can't resume deposit group, depositGroupId=${depositGroupId} not found`,
+ );
+ return;
+ }
+ const oldState = computeDepositTransactionStatus(dg);
+ switch (dg.operationStatus) {
+ case DepositOperationStatus.Finished:
+ return;
+ case DepositOperationStatus.Pending: {
+ return;
+ }
+ case DepositOperationStatus.Suspended:
+ dg.operationStatus = DepositOperationStatus.Pending;
+ await tx.depositGroups.put(dg);
+ return {
+ oldTxState: oldState,
+ newTxState: computeDepositTransactionStatus(dg),
+ };
+ }
+ return undefined;
+ });
+ ws.latch.trigger();
+ if (res) {
+ ws.notify({
+ type: NotificationType.TransactionStateTransition,
+ transactionId,
+ oldTxState: res.oldTxState,
+ newTxState: res.newTxState,
+ });
+ }
}
export async function abortDepositGroup(
ws: InternalWalletState,
depositGroupId: string,
): Promise<void> {
+ const transactionId = constructTransactionIdentifier({
+ tag: TransactionType.Deposit,
+ depositGroupId,
+ });
+ const retryTag = constructTaskIdentifier({
+ tag: PendingTaskType.Deposit,
+ depositGroupId,
+ });
+ let res = await ws.db
+ .mktx((x) => [x.depositGroups])
+ .runReadWrite(async (tx) => {
+ const dg = await tx.depositGroups.get(depositGroupId);
+ if (!dg) {
+ logger.warn(
+ `can't suspend deposit group, depositGroupId=${depositGroupId} not found`,
+ );
+ return undefined;
+ }
+ const oldState = computeDepositTransactionStatus(dg);
+ switch (dg.operationStatus) {
+ case DepositOperationStatus.Finished:
+ return undefined;
+ case DepositOperationStatus.Pending: {
+ dg.operationStatus = DepositOperationStatus.Aborting;
+ await tx.depositGroups.put(dg);
+ return {
+ oldTxState: oldState,
+ newTxState: computeDepositTransactionStatus(dg),
+ };
+ }
+ case DepositOperationStatus.Suspended:
+ // FIXME: Can we abort a suspended transaction?!
+ return undefined;
+ }
+ return undefined;
+ });
+ stopLongpolling(ws, retryTag);
+ // Need to process the operation again.
+ ws.latch.trigger();
+ if (res) {
+ ws.notify({
+ type: NotificationType.TransactionStateTransition,
+ transactionId,
+ oldTxState: res.oldTxState,
+ newTxState: res.newTxState,
+ });
+ }
+}
+
+export async function deleteDepositGroup(
+ ws: InternalWalletState,
+ depositGroupId: boolean,
+ opts: { forced?: boolean } = {},
+) {
throw Error("not implemented");
}
@@ -230,195 +382,210 @@ export async function processDepositGroup(
const txStateOld = computeDepositTransactionStatus(depositGroup);
- const contractData = extractContractData(
- depositGroup.contractTermsRaw,
- depositGroup.contractTermsHash,
- "",
- );
+ if (depositGroup.operationStatus === DepositOperationStatus.Pending) {
+ const contractData = extractContractData(
+ depositGroup.contractTermsRaw,
+ depositGroup.contractTermsHash,
+ "",
+ );
- // Check for cancellation before expensive operations.
- options.cancellationToken?.throwIfCancelled();
- // FIXME: Cache these!
- const depositPermissions = await generateDepositPermissions(
- ws,
- depositGroup.payCoinSelection,
- contractData,
- );
+ // Check for cancellation before expensive operations.
+ options.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];
-
- let updatedDeposit: 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,
- });
- await readSuccessResponseJsonOrThrow(httpResp, codecForDepositSuccess());
- updatedDeposit = true;
- }
+ for (let i = 0; i < depositPermissions.length; i++) {
+ const perm = depositPermissions[i];
+
+ let updatedDeposit: 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,
+ });
+ await readSuccessResponseJsonOrThrow(
+ httpResp,
+ codecForDepositSuccess(),
+ );
+ updatedDeposit = true;
+ }
- let updatedTxStatus: DepositElementStatus | undefined = undefined;
- type ValueOf<T> = T[keyof T];
+ let updatedTxStatus: DepositElementStatus | undefined = undefined;
+ type ValueOf<T> = T[keyof T];
- let newWiredTransaction:
- | {
- id: string;
- value: ValueOf<NonNullable<DepositGroupRecord["trackingState"]>>;
- }
- | undefined;
+ let newWiredTransaction:
+ | {
+ id: string;
+ value: ValueOf<NonNullable<DepositGroupRecord["trackingState"]>>;
+ }
+ | undefined;
- if (depositGroup.transactionPerCoin[i] !== DepositElementStatus.Wired) {
- const track = await trackDeposit(ws, depositGroup, perm);
+ if (depositGroup.transactionPerCoin[i] !== DepositElementStatus.Wired) {
+ const track = await trackDeposit(ws, depositGroup, perm);
- 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(
+ 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;
+
+ const payto = parsePaytoUri(depositGroup.wire.payto_uri);
+ if (!payto) {
+ throw Error(`unparsable payto: ${depositGroup.wire.payto_uri}`);
+ }
+
+ const fee = await getExchangeWireFee(
ws,
+ payto.targetType,
perm.exchange_url,
- { paytoHash, requirementRow },
- "individual",
+ track.execution_time,
);
+ const raw = Amounts.parseOrThrow(track.coin_contribution);
+ const wireFee = Amounts.parseOrThrow(fee.wireFee);
+
+ newWiredTransaction = {
+ 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.Accepted;
+ updatedTxStatus = DepositElementStatus.Unknown;
}
- } 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}`);
- }
+ if (updatedTxStatus !== undefined || updatedDeposit) {
+ await ws.db
+ .mktx((x) => [x.depositGroups])
+ .runReadWrite(async (tx) => {
+ const dg = await tx.depositGroups.get(depositGroupId);
+ if (!dg) {
+ return;
+ }
+ if (updatedDeposit !== undefined) {
+ dg.depositedPerCoin[i] = updatedDeposit;
+ }
+ if (updatedTxStatus !== undefined) {
+ dg.transactionPerCoin[i] = updatedTxStatus;
+ }
+ if (newWiredTransaction) {
+ if (!dg.trackingState) {
+ dg.trackingState = {};
+ }
- const fee = await getExchangeWireFee(
- ws,
- payto.targetType,
- perm.exchange_url,
- track.execution_time,
- );
- const raw = Amounts.parseOrThrow(track.coin_contribution);
- const wireFee = Amounts.parseOrThrow(fee.wireFee);
-
- newWiredTransaction = {
- 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;
+ dg.trackingState[newWiredTransaction.id] =
+ newWiredTransaction.value;
+ }
+ await tx.depositGroups.put(dg);
+ });
}
}
- if (updatedTxStatus !== undefined || updatedDeposit) {
- await ws.db
- .mktx((x) => [x.depositGroups])
- .runReadWrite(async (tx) => {
- const dg = await tx.depositGroups.get(depositGroupId);
- if (!dg) {
- return;
- }
- if (updatedDeposit !== undefined) {
- dg.depositedPerCoin[i] = updatedDeposit;
- }
- if (updatedTxStatus !== undefined) {
- dg.transactionPerCoin[i] = updatedTxStatus;
- }
- if (newWiredTransaction) {
- if (!dg.trackingState) {
- dg.trackingState = {};
- }
-
- dg.trackingState[newWiredTransaction.id] =
- newWiredTransaction.value;
+ const txStatusNew = 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 = TalerProtocolTimestamp.now();
+ dg.operationStatus = DepositOperationStatus.Finished;
await tx.depositGroups.put(dg);
- });
- }
- }
-
- const txStatusNew = 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 = TalerProtocolTimestamp.now();
- dg.operationStatus = OperationStatus.Finished;
- await tx.depositGroups.put(dg);
- }
- return computeDepositTransactionStatus(dg);
- });
+ return computeDepositTransactionStatus(dg);
+ });
- if (!txStatusNew) {
- // Doesn't exist anymore!
- return OperationAttemptResult.finishedEmpty();
- }
+ if (!txStatusNew) {
+ // Doesn't exist anymore!
+ return OperationAttemptResult.finishedEmpty();
+ }
- // Notify if state transitioned
- if (
- txStateOld.major !== txStatusNew.major ||
- txStateOld.minor !== txStatusNew.minor
- ) {
- ws.notify({
- type: NotificationType.TransactionStateTransition,
- transactionId,
- oldTxState: txStateOld,
- newTxState: txStatusNew,
- });
+ // Notify if state transitioned
+ if (
+ txStateOld.major !== txStatusNew.major ||
+ txStateOld.minor !== txStatusNew.minor
+ ) {
+ ws.notify({
+ type: NotificationType.TransactionStateTransition,
+ transactionId,
+ oldTxState: txStateOld,
+ newTxState: txStatusNew,
+ });
+ }
+
+ // FIXME: consider other cases like aborting, suspend, ...
+ if (
+ txStatusNew.major === TransactionMajorState.Pending ||
+ txStatusNew.major === TransactionMajorState.Aborting
+ ) {
+ return OperationAttemptResult.pendingEmpty();
+ } else {
+ return OperationAttemptResult.finishedEmpty();
+ }
}
- // FIXME: consider other cases like aborting, suspend, ...
- if (
- txStatusNew.major === TransactionMajorState.Pending ||
- txStatusNew.major === TransactionMajorState.Aborting
- ) {
+ if (depositGroup.operationStatus === DepositOperationStatus.Aborting) {
+ // FIXME: Implement!
return OperationAttemptResult.pendingEmpty();
- } else {
- return OperationAttemptResult.finishedEmpty();
}
+
+ return OperationAttemptResult.finishedEmpty();
}
async function getExchangeWireFee(
@@ -763,7 +930,7 @@ export async function createDepositGroup(
payto_uri: req.depositPaytoUri,
salt: wireSalt,
},
- operationStatus: OperationStatus.Pending,
+ operationStatus: DepositOperationStatus.Pending,
};
const transactionId = constructTransactionIdentifier({