wallet-core: fix deposit tx states, long-poll on kyc

This commit is contained in:
Florian Dold 2023-06-26 12:48:20 +02:00
parent 66432cdd05
commit fca893038d
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
6 changed files with 483 additions and 336 deletions

View File

@ -910,14 +910,6 @@ export enum RefreshOperationStatus {
Failed = 51 /* DORMANT_START + 1 */, 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. * Status of a single element of a deposit group.
*/ */
@ -1653,11 +1645,15 @@ export interface BackupProviderRecord {
} }
export enum DepositOperationStatus { export enum DepositOperationStatus {
Pending = 10, PendingDeposit = 10,
Aborting = 11, Aborting = 11,
PendingTrack = 12,
PendingKyc = 13,
Suspended = 20, SuspendedDeposit = 20,
SuspendedAborting = 21, SuspendedAborting = 21,
SuspendedTrack = 22,
SuspendedKyc = 23,
Finished = 50, Finished = 50,
Failed = 51, Failed = 51,
@ -1737,12 +1733,22 @@ export interface DepositGroupRecord {
*/ */
abortRefreshGroupId?: string; abortRefreshGroupId?: string;
kycInfo?: DepositKycInfo;
// FIXME: Do we need this and should it be in this object store? // FIXME: Do we need this and should it be in this object store?
trackingState?: { trackingState?: {
[signature: string]: DepositTrackingInfo; [signature: string]: DepositTrackingInfo;
}; };
} }
export interface DepositKycInfo {
kycUrl: string;
requirementRow: number;
paytoHash: string;
exchangeBaseUrl: string;
}
/** /**
* Record for a deposits that the wallet observed * Record for a deposits that the wallet observed
* as a result of double spending, but which is not * as a result of double spending, but which is not

View File

@ -108,7 +108,6 @@ function computeRefreshGroupAvailableAmount(r: RefreshGroupRecord): AmountJson {
export async function getBalancesInsideTransaction( export async function getBalancesInsideTransaction(
ws: InternalWalletState, ws: InternalWalletState,
tx: GetReadOnlyAccess<{ tx: GetReadOnlyAccess<{
coins: typeof WalletStoresV1.coins;
coinAvailability: typeof WalletStoresV1.coinAvailability; coinAvailability: typeof WalletStoresV1.coinAvailability;
refreshGroups: typeof WalletStoresV1.refreshGroups; refreshGroups: typeof WalletStoresV1.refreshGroups;
withdrawalGroups: typeof WalletStoresV1.withdrawalGroups; withdrawalGroups: typeof WalletStoresV1.withdrawalGroups;

View File

@ -1,6 +1,6 @@
/* /*
This file is part of GNU Taler 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 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 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 { import {
constructTaskIdentifier, constructTaskIdentifier,
OperationAttemptResult, OperationAttemptResult,
runLongpollAsync,
spendCoins, spendCoins,
TombstoneTag, TombstoneTag,
} from "./common.js"; } from "./common.js";
@ -100,6 +101,7 @@ import {
stopLongpolling, stopLongpolling,
} from "./transactions.js"; } from "./transactions.js";
import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js"; import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js";
import { assertUnreachable } from "../util/assertUnreachable.js";
/** /**
* Logger. * Logger.
@ -114,51 +116,36 @@ export function computeDepositTransactionStatus(
dg: DepositGroupRecord, dg: DepositGroupRecord,
): TransactionState { ): TransactionState {
switch (dg.operationStatus) { switch (dg.operationStatus) {
case DepositOperationStatus.Finished: { case DepositOperationStatus.Finished:
return { return {
major: TransactionMajorState.Done, major: TransactionMajorState.Done,
}; };
} case DepositOperationStatus.PendingDeposit:
// 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,
};
}
return { return {
major: TransactionMajorState.Pending, major: TransactionMajorState.Pending,
minor: TransactionMinorState.Deposit, minor: TransactionMinorState.Deposit,
}; };
} case DepositOperationStatus.PendingKyc:
case DepositOperationStatus.Suspended: 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 { return {
major: TransactionMajorState.Suspended, major: TransactionMajorState.Suspended,
}; };
@ -179,7 +166,7 @@ export function computeDepositTransactionStatus(
major: TransactionMajorState.SuspendedAborting, major: TransactionMajorState.SuspendedAborting,
}; };
default: default:
throw Error(`unexpected deposit group state (${dg.operationStatus})`); assertUnreachable(dg.operationStatus);
} }
} }
@ -187,39 +174,11 @@ export function computeDepositTransactionActions(
dg: DepositGroupRecord, dg: DepositGroupRecord,
): TransactionAction[] { ): TransactionAction[] {
switch (dg.operationStatus) { switch (dg.operationStatus) {
case DepositOperationStatus.Finished: { case DepositOperationStatus.Finished:
return [TransactionAction.Delete]; return [TransactionAction.Delete];
} case DepositOperationStatus.PendingDeposit:
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];
}
return [TransactionAction.Suspend, TransactionAction.Abort]; return [TransactionAction.Suspend, TransactionAction.Abort];
} case DepositOperationStatus.SuspendedDeposit:
case DepositOperationStatus.Suspended:
return [TransactionAction.Resume]; return [TransactionAction.Resume];
case DepositOperationStatus.Aborting: case DepositOperationStatus.Aborting:
return [TransactionAction.Fail, TransactionAction.Suspend]; return [TransactionAction.Fail, TransactionAction.Suspend];
@ -229,8 +188,16 @@ export function computeDepositTransactionActions(
return [TransactionAction.Delete]; return [TransactionAction.Delete];
case DepositOperationStatus.SuspendedAborting: case DepositOperationStatus.SuspendedAborting:
return [TransactionAction.Resume, TransactionAction.Fail]; 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: default:
throw Error(`unexpected deposit group state (${dg.operationStatus})`); assertUnreachable(dg.operationStatus);
} }
} }
@ -260,15 +227,15 @@ export async function suspendDepositGroup(
switch (dg.operationStatus) { switch (dg.operationStatus) {
case DepositOperationStatus.Finished: case DepositOperationStatus.Finished:
return undefined; return undefined;
case DepositOperationStatus.Pending: { case DepositOperationStatus.PendingDeposit: {
dg.operationStatus = DepositOperationStatus.Suspended; dg.operationStatus = DepositOperationStatus.SuspendedDeposit;
await tx.depositGroups.put(dg); await tx.depositGroups.put(dg);
return { return {
oldTxState: oldState, oldTxState: oldState,
newTxState: computeDepositTransactionStatus(dg), newTxState: computeDepositTransactionStatus(dg),
}; };
} }
case DepositOperationStatus.Suspended: case DepositOperationStatus.SuspendedDeposit:
return undefined; return undefined;
} }
return undefined; return undefined;
@ -299,11 +266,11 @@ export async function resumeDepositGroup(
switch (dg.operationStatus) { switch (dg.operationStatus) {
case DepositOperationStatus.Finished: case DepositOperationStatus.Finished:
return; return;
case DepositOperationStatus.Pending: { case DepositOperationStatus.PendingDeposit: {
return; return;
} }
case DepositOperationStatus.Suspended: case DepositOperationStatus.SuspendedDeposit:
dg.operationStatus = DepositOperationStatus.Pending; dg.operationStatus = DepositOperationStatus.PendingDeposit;
await tx.depositGroups.put(dg); await tx.depositGroups.put(dg);
return { return {
oldTxState: oldState, oldTxState: oldState,
@ -342,7 +309,7 @@ export async function abortDepositGroup(
switch (dg.operationStatus) { switch (dg.operationStatus) {
case DepositOperationStatus.Finished: case DepositOperationStatus.Finished:
return undefined; return undefined;
case DepositOperationStatus.Pending: { case DepositOperationStatus.PendingDeposit: {
dg.operationStatus = DepositOperationStatus.Aborting; dg.operationStatus = DepositOperationStatus.Aborting;
await tx.depositGroups.put(dg); await tx.depositGroups.put(dg);
return { return {
@ -350,7 +317,7 @@ export async function abortDepositGroup(
newTxState: computeDepositTransactionStatus(dg), newTxState: computeDepositTransactionStatus(dg),
}; };
} }
case DepositOperationStatus.Suspended: case DepositOperationStatus.SuspendedDeposit:
// FIXME: Can we abort a suspended transaction?! // FIXME: Can we abort a suspended transaction?!
return undefined; return undefined;
} }
@ -633,95 +600,167 @@ async function refundDepositGroup(
return OperationAttemptResult.pendingEmpty(); return OperationAttemptResult.pendingEmpty();
} }
/** async function processDepositGroupAborting(
* Process a deposit group that is not in its final state yet.
*/
export async function processDepositGroup(
ws: InternalWalletState, ws: InternalWalletState,
depositGroupId: string, depositGroup: DepositGroupRecord,
options: {
cancellationToken?: CancellationToken;
} = {},
): Promise<OperationAttemptResult> { ): Promise<OperationAttemptResult> {
const depositGroup = await ws.db logger.info("processing deposit tx in 'aborting'");
.mktx((x) => [x.depositGroups]) const abortRefreshGroupId = depositGroup.abortRefreshGroupId;
.runReadOnly(async (tx) => { if (!abortRefreshGroupId) {
return tx.depositGroups.get(depositGroupId); 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,
}); });
if (!depositGroup) { const retryTag = constructTaskIdentifier({
logger.warn(`deposit group ${depositGroupId} not found`); tag: PendingTaskType.Deposit,
return OperationAttemptResult.finishedEmpty(); depositGroupId,
});
const kycInfo = depositGroup.kycInfo;
const userType = "individual";
if (!kycInfo) {
throw Error("invalid DB state, in pending(kyc), but no kycInfo present");
} }
if (depositGroup.timestampFinished) {
logger.trace(`deposit group ${depositGroupId} already finished`); runLongpollAsync(ws, retryTag, async (ct) => {
return OperationAttemptResult.finishedEmpty(); const url = new URL(
`kyc-check/${kycInfo.requirementRow}/${kycInfo.paytoHash}/${userType}`,
kycInfo.exchangeBaseUrl,
);
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 };
});
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();
}
/**
* 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";
const transactionId = constructTransactionIdentifier({ const transactionId = constructTransactionIdentifier({
tag: TransactionType.Deposit, tag: TransactionType.Deposit,
depositGroupId, depositGroupId,
}); });
const txStateOld = computeDepositTransactionStatus(depositGroup);
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,
);
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( const url = new URL(
`coins/${perm.coin_pub}/deposit`, `kyc-check/${kycInfo.requirementRow}/${kycInfo.paytoHash}/${userType}`,
perm.exchange_url, exchangeUrl,
); );
logger.info(`depositing to ${url}`); logger.info(`kyc url ${url.href}`);
const httpResp = await ws.http.fetch(url.href, { const kycStatusReq = await ws.http.fetch(url.href, {
method: "POST", method: "GET",
body: requestBody,
cancellationToken: options.cancellationToken,
}); });
await readSuccessResponseJsonOrThrow( if (kycStatusReq.status === HttpStatusCode.Ok) {
httpResp, logger.warn("kyc requested, but already fulfilled");
codecForDepositSuccess(), return OperationAttemptResult.finishedEmpty();
); } else if (kycStatusReq.status === HttpStatusCode.Accepted) {
didDeposit = true; 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})`);
}
} }
let updatedTxStatus: DepositElementStatus | undefined = undefined; 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;
});
let updatedTxStatus: DepositElementStatus | undefined = undefined;
let newWiredCoin: let newWiredCoin:
| { | {
id: string; id: string;
@ -730,20 +769,28 @@ export async function processDepositGroup(
| undefined; | undefined;
if (depositGroup.transactionPerCoin[i] !== DepositElementStatus.Wired) { if (depositGroup.transactionPerCoin[i] !== DepositElementStatus.Wired) {
const track = await trackDeposit(ws, depositGroup, perm); const track = await trackDeposit(
ws,
depositGroup,
coinPub,
exchangeBaseUrl,
);
if (track.type === "accepted") { if (track.type === "accepted") {
if (!track.kyc_ok && track.requirement_row !== undefined) { if (!track.kyc_ok && track.requirement_row !== undefined) {
updatedTxStatus = DepositElementStatus.KycRequired;
const { requirement_row: requirementRow } = track;
const paytoHash = encodeCrock( const paytoHash = encodeCrock(
hashTruncate32(stringToBytes(depositGroup.wire.payto_uri + "\0")), hashTruncate32(stringToBytes(depositGroup.wire.payto_uri + "\0")),
); );
await checkDepositKycStatus( const { requirement_row: requirementRow } = track;
const kycInfo: KycPendingInfo = {
paytoHash,
requirementRow,
};
return transitionToKycRequired(
ws, ws,
perm.exchange_url, depositGroup,
{ paytoHash, requirementRow }, kycInfo,
"individual", exchangeBaseUrl,
); );
} else { } else {
updatedTxStatus = DepositElementStatus.Accepted; updatedTxStatus = DepositElementStatus.Accepted;
@ -759,7 +806,7 @@ export async function processDepositGroup(
const fee = await getExchangeWireFee( const fee = await getExchangeWireFee(
ws, ws,
payto.targetType, payto.targetType,
perm.exchange_url, exchangeBaseUrl,
track.execution_time, track.execution_time,
); );
const raw = Amounts.parseOrThrow(track.coin_contribution); const raw = Amounts.parseOrThrow(track.coin_contribution);
@ -780,7 +827,7 @@ export async function processDepositGroup(
} }
} }
if (updatedTxStatus !== undefined || didDeposit) { if (updatedTxStatus !== undefined) {
await ws.db await ws.db
.mktx((x) => [x.depositGroups]) .mktx((x) => [x.depositGroups])
.runReadWrite(async (tx) => { .runReadWrite(async (tx) => {
@ -788,9 +835,6 @@ export async function processDepositGroup(
if (!dg) { if (!dg) {
return; return;
} }
if (didDeposit) {
dg.depositedPerCoin[i] = didDeposit;
}
if (updatedTxStatus !== undefined) { if (updatedTxStatus !== undefined) {
dg.transactionPerCoin[i] = updatedTxStatus; dg.transactionPerCoin[i] = updatedTxStatus;
} }
@ -814,70 +858,173 @@ export async function processDepositGroup(
} }
} }
const txStatusNew = await ws.db let allWired = true;
const transitionInfo = await ws.db
.mktx((x) => [x.depositGroups]) .mktx((x) => [x.depositGroups])
.runReadWrite(async (tx) => { .runReadWrite(async (tx) => {
const dg = await tx.depositGroups.get(depositGroupId); const dg = await tx.depositGroups.get(depositGroupId);
if (!dg) { if (!dg) {
return undefined; return undefined;
} }
let allDepositedAndWired = true; const oldTxState = computeDepositTransactionStatus(dg);
for (let i = 0; i < depositGroup.depositedPerCoin.length; i++) { for (let i = 0; i < depositGroup.depositedPerCoin.length; i++) {
if ( if (depositGroup.transactionPerCoin[i] !== DepositElementStatus.Wired) {
!depositGroup.depositedPerCoin[i] || allWired = false;
depositGroup.transactionPerCoin[i] !== DepositElementStatus.Wired
) {
allDepositedAndWired = false;
break; break;
} }
} }
if (allDepositedAndWired) { if (allWired) {
dg.timestampFinished = TalerPreciseTimestamp.now(); dg.timestampFinished = TalerPreciseTimestamp.now();
dg.operationStatus = DepositOperationStatus.Finished; dg.operationStatus = DepositOperationStatus.Finished;
await tx.depositGroups.put(dg); await tx.depositGroups.put(dg);
} }
return computeDepositTransactionStatus(dg); const newTxState = computeDepositTransactionStatus(dg);
return { oldTxState, newTxState };
}); });
const transactionId = constructTransactionIdentifier({
if (!txStatusNew) { tag: TransactionType.Deposit,
// Doesn't exist anymore! depositGroupId,
});
notifyTransition(ws, transactionId, transitionInfo);
if (allWired) {
return OperationAttemptResult.finishedEmpty(); 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,
});
}
// FIXME: consider other cases like aborting, suspend, ...
if (
txStatusNew.major === TransactionMajorState.Pending ||
txStatusNew.major === TransactionMajorState.Aborting
) {
return OperationAttemptResult.pendingEmpty();
} else { } else {
return OperationAttemptResult.finishedEmpty(); // FIXME: Use long-polling.
return OperationAttemptResult.pendingEmpty();
} }
} }
if (depositGroup.operationStatus === DepositOperationStatus.Aborting) { async function processDepositGroupPendingDeposit(
logger.info("processing deposit tx in 'aborting'"); ws: InternalWalletState,
const abortRefreshGroupId = depositGroup.abortRefreshGroupId; depositGroup: DepositGroupRecord,
if (!abortRefreshGroupId) { cancellationToken?: CancellationToken,
logger.info("refunding deposit group"); ): Promise<OperationAttemptResult> {
return refundDepositGroup(ws, depositGroup); 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;
} }
logger.info("waiting for refresh");
return waitForRefreshOnDepositGroup(ws, depositGroup); 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;
} }
dg.depositedPerCoin[i] = true;
await tx.depositGroups.put(dg);
});
}
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 };
});
notifyTransition(ws, transactionId, transitionInfo);
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();
}
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(); return OperationAttemptResult.finishedEmpty();
} }
@ -928,16 +1075,17 @@ async function getExchangeWireFee(
async function trackDeposit( async function trackDeposit(
ws: InternalWalletState, ws: InternalWalletState,
depositGroup: DepositGroupRecord, depositGroup: DepositGroupRecord,
dp: CoinDepositPermission, coinPub: string,
exchangeUrl: string,
): Promise<TrackTransaction> { ): Promise<TrackTransaction> {
const wireHash = depositGroup.contractTermsRaw.h_wire; const wireHash = depositGroup.contractTermsRaw.h_wire;
const url = new URL( const url = new URL(
`deposits/${wireHash}/${depositGroup.merchantPub}/${depositGroup.contractTermsHash}/${dp.coin_pub}`, `deposits/${wireHash}/${depositGroup.merchantPub}/${depositGroup.contractTermsHash}/${coinPub}`,
dp.exchange_url, exchangeUrl,
); );
const sigResp = await ws.cryptoApi.signTrackTransaction({ const sigResp = await ws.cryptoApi.signTrackTransaction({
coinPub: dp.coin_pub, coinPub,
contractTermsHash: depositGroup.contractTermsHash, contractTermsHash: depositGroup.contractTermsHash,
merchantPriv: depositGroup.merchantPriv, merchantPriv: depositGroup.merchantPriv,
merchantPub: depositGroup.merchantPub, merchantPub: depositGroup.merchantPub,
@ -1224,7 +1372,7 @@ export async function createDepositGroup(
payto_uri: req.depositPaytoUri, payto_uri: req.depositPaytoUri,
salt: wireSalt, salt: wireSalt,
}, },
operationStatus: DepositOperationStatus.Pending, operationStatus: DepositOperationStatus.PendingDeposit,
}; };
const transactionId = constructTransactionIdentifier({ const transactionId = constructTransactionIdentifier({
@ -1263,6 +1411,10 @@ export async function createDepositGroup(
newTxState, newTxState,
}); });
ws.notify({
type: NotificationType.BalanceChange,
});
return { return {
depositGroupId, depositGroupId,
transactionId, transactionId,
@ -1332,7 +1484,7 @@ export async function getCounterpartyEffectiveDepositAmount(
* Get the fee amount that will be charged when trying to deposit the * Get the fee amount that will be charged when trying to deposit the
* specified amount using the selected coins and the wire method. * specified amount using the selected coins and the wire method.
*/ */
export async function getTotalFeesForDepositAmount( async function getTotalFeesForDepositAmount(
ws: InternalWalletState, ws: InternalWalletState,
wireType: string, wireType: string,
total: AmountJson, total: AmountJson,

View File

@ -427,7 +427,7 @@ async function handlePendingMerge(
const respJson = await mergeHttpResp.json(); const respJson = await mergeHttpResp.json();
const kycPending = codecForWalletKycUuid().decode(respJson); const kycPending = codecForWalletKycUuid().decode(respJson);
logger.info(`kyc uuid response: ${j2s(kycPending)}`); logger.info(`kyc uuid response: ${j2s(kycPending)}`);
processPeerPushCreditKycRequired(ws, peerInc, kycPending); return processPeerPushCreditKycRequired(ws, peerInc, kycPending);
} }
logger.trace(`merge request: ${j2s(mergeReq)}`); logger.trace(`merge request: ${j2s(mergeReq)}`);

View File

@ -32,8 +32,8 @@ import {
PeerPushPaymentIncomingStatus, PeerPushPaymentIncomingStatus,
PeerPullPaymentInitiationStatus, PeerPullPaymentInitiationStatus,
WithdrawalGroupStatus, WithdrawalGroupStatus,
DepositGroupOperationStatus,
TipRecordStatus, TipRecordStatus,
DepositOperationStatus,
} from "../db.js"; } from "../db.js";
import { import {
PendingOperationsResponse, PendingOperationsResponse,
@ -198,8 +198,8 @@ async function gatherDepositPending(
): Promise<void> { ): Promise<void> {
const dgs = await tx.depositGroups.indexes.byStatus.getAll( const dgs = await tx.depositGroups.indexes.byStatus.getAll(
GlobalIDB.KeyRange.bound( GlobalIDB.KeyRange.bound(
DepositGroupOperationStatus.Pending, DepositOperationStatus.PendingDeposit,
DepositGroupOperationStatus.AbortingWithRefresh, DepositOperationStatus.PendingKyc,
), ),
); );
for (const dg of dgs) { for (const dg of dgs) {

View File

@ -1108,14 +1108,6 @@ async function processPlanchetVerifyAndStoreCoin(
wgContext.planchetsFinished.add(planchet.coinPub); 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 // Check if this is the first time that the whole
// withdrawal succeeded. If so, mark the withdrawal // withdrawal succeeded. If so, mark the withdrawal
// group as finished. // group as finished.
@ -1138,9 +1130,7 @@ async function processPlanchetVerifyAndStoreCoin(
return true; return true;
}); });
if (firstSuccess) { ws.notify({ type: NotificationType.BalanceChange });
ws.notify(notification);
}
} }
/** /**