aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/taler-util/src/types-taler-wallet-transactions.ts1
-rw-r--r--packages/taler-wallet-core/src/balance.ts2
-rw-r--r--packages/taler-wallet-core/src/common.ts52
-rw-r--r--packages/taler-wallet-core/src/db.ts49
-rw-r--r--packages/taler-wallet-core/src/exchanges.ts89
-rw-r--r--packages/taler-wallet-core/src/pay-peer-pull-credit.ts126
-rw-r--r--packages/taler-wallet-core/src/pay-peer-push-credit.ts217
-rw-r--r--packages/taler-wallet-core/src/withdraw.ts116
8 files changed, 472 insertions, 180 deletions
diff --git a/packages/taler-util/src/types-taler-wallet-transactions.ts b/packages/taler-util/src/types-taler-wallet-transactions.ts
index 018c3a041..675878e0c 100644
--- a/packages/taler-util/src/types-taler-wallet-transactions.ts
+++ b/packages/taler-util/src/types-taler-wallet-transactions.ts
@@ -129,6 +129,7 @@ export enum TransactionMinorState {
KycRequired = "kyc",
MergeKycRequired = "merge-kyc",
BalanceKycRequired = "balance-kyc",
+ BalanceKycInit = "balance-kyc-init",
Track = "track",
SubmitPayment = "submit-payment",
RebindSession = "rebind-session",
diff --git a/packages/taler-wallet-core/src/balance.ts b/packages/taler-wallet-core/src/balance.ts
index 3b61ee4de..396efda1f 100644
--- a/packages/taler-wallet-core/src/balance.ts
+++ b/packages/taler-wallet-core/src/balance.ts
@@ -363,6 +363,8 @@ export async function getBalancesInsideTransaction(
case WithdrawalGroupStatus.PendingBalanceKyc:
case WithdrawalGroupStatus.SuspendedBalanceKyc:
case WithdrawalGroupStatus.SuspendedKyc:
+ case WithdrawalGroupStatus.PendingBalanceKycInit:
+ case WithdrawalGroupStatus.SuspendedBalanceKycInit:
case WithdrawalGroupStatus.PendingKyc: {
checkDbInvariant(
wg.denomsSel !== undefined,
diff --git a/packages/taler-wallet-core/src/common.ts b/packages/taler-wallet-core/src/common.ts
index 668ebe5c1..02dca20e8 100644
--- a/packages/taler-wallet-core/src/common.ts
+++ b/packages/taler-wallet-core/src/common.ts
@@ -863,6 +863,58 @@ export async function genericWaitForState(
}
}
+/**
+ * Wait until the wallet is in a particular state.
+ *
+ * Two functions must be provided:
+ * 1. checkState, which checks if the wallet is in the
+ * desired state.
+ * 2. filterNotification, which checks whether a notification
+ * might have lead to a state change.
+ */
+export async function genericWaitForStateVal<T>(
+ wex: WalletExecutionContext,
+ args: {
+ checkState: () => Promise<T | undefined>;
+ filterNotification: (notif: WalletNotification) => boolean;
+ },
+): Promise<T> {
+ await wex.taskScheduler.ensureRunning();
+
+ // FIXME: Clean up using the new JS "using" / Symbol.dispose syntax.
+ const flag = new AsyncFlag();
+ // Raise purchaseNotifFlag whenever we get a notification
+ // about our refresh.
+ const cancelNotif = wex.ws.addNotificationListener((notif) => {
+ if (args.filterNotification(notif)) {
+ flag.raise();
+ }
+ });
+ const unregisterOnCancelled = wex.cancellationToken.onCancelled((reason) => {
+ cancelNotif();
+ flag.raise();
+ });
+
+ try {
+ while (true) {
+ if (wex.cancellationToken.isCancelled) {
+ throw Error("cancelled");
+ }
+ const val = await args.checkState();
+ if (val != null) {
+ return val;
+ }
+ // Wait for the next transition
+ await flag.wait();
+ flag.reset();
+ }
+ } catch (e) {
+ unregisterOnCancelled();
+ cancelNotif();
+ throw e;
+ }
+}
+
export function requireExchangeTosAcceptedOrThrow(
exchange: ReadyExchangeSummary,
): void {
diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts
index 2cee19f8e..c48a43f11 100644
--- a/packages/taler-wallet-core/src/db.ts
+++ b/packages/taler-wallet-core/src/db.ts
@@ -311,11 +311,20 @@ export enum WithdrawalGroupStatus {
/**
* Exchange wants KYC info from the user.
+ * KYC link is ready.
*/
PendingBalanceKyc = 0x0100_0006,
SuspendedBalanceKyc = 0x0110_006,
/**
+ * Exchange wants KYC info from the user.
+ *
+ * KYC link is not ready yet, the KYC process is still initializing.
+ */
+ PendingBalanceKycInit = 0x0100_0007,
+ SuspendedBalanceKycInit = 0x0110_007,
+
+ /**
* Proposed to the user, has can choose to accept/refuse.
*/
DialogProposed = 0x0101_0000,
@@ -1461,6 +1470,7 @@ export interface KycPendingInfo {
paytoHash: string;
requirementRow: number;
}
+
/**
* Group of withdrawal operations that need to be executed.
* (Either for a normal withdrawal or from a reward.)
@@ -1936,24 +1946,33 @@ export interface PeerPushDebitRecord {
}
export enum PeerPullPaymentCreditStatus {
+ /**
+ * Typically the initial state of the peer-pull-credit transaction,
+ * purse will be created.
+ */
PendingCreatePurse = 0x0100_0000,
+ SuspendedCreatePurse = 0x0110_0000,
+
/**
* Purse created, waiting for the other party to accept the
* invoice and deposit money into it.
*/
PendingReady = 0x0100_0001,
- PendingMergeKycRequired = 0x0100_0002,
- PendingWithdrawing = 0x0100_0003,
- PendingBalanceKycRequired = 0x0100_0004,
-
- AbortingDeletePurse = 0x0103_0000,
-
- SuspendedCreatePurse = 0x0110_0000,
SuspendedReady = 0x0110_0001,
+
+ PendingMergeKycRequired = 0x0100_0002,
SuspendedMergeKycRequired = 0x0110_0002,
+
+ PendingWithdrawing = 0x0100_0003,
SuspendedWithdrawing = 0x0110_0003,
+
+ PendingBalanceKycRequired = 0x0100_0004,
SuspendedBalanceKycRequired = 0x0110_0004,
+ PendingBalanceKycInit = 0x0100_0005,
+ SuspendedBalanceKycInit = 0x0110_0005,
+
+ AbortingDeletePurse = 0x0103_0000,
SuspendedAbortingDeletePurse = 0x0113_0000,
Done = 0x0500_0000,
@@ -2014,25 +2033,31 @@ export interface PeerPullCreditRecord {
kycUrl?: string;
+ kycAccessToken?: string;
+
withdrawalGroupId: string | undefined;
}
export enum PeerPushCreditStatus {
PendingMerge = 0x0100_0000,
+ SuspendedMerge = 0x0110_0000,
+
PendingMergeKycRequired = 0x0100_0001,
+ SuspendedMergeKycRequired = 0x0110_0001,
+
/**
* Merge was successful and withdrawal group has been created, now
* everything is in the hand of the withdrawal group.
*/
PendingWithdrawing = 0x0100_0002,
+ SuspendedWithdrawing = 0x0110_0002,
PendingBalanceKycRequired = 0x0100_0003,
-
- SuspendedMerge = 0x0110_0000,
- SuspendedMergeKycRequired = 0x0110_0001,
- SuspendedWithdrawing = 0x0110_0002,
SuspendedBalanceKycRequired = 0x0110_0003,
+ PendingBalanceKycInit = 0x0100_0004,
+ SuspendedBalanceKycInit = 0x0110_0004,
+
DialogProposed = 0x0101_0000,
Done = 0x0500_0000,
@@ -2087,6 +2112,8 @@ export interface PeerPushPaymentIncomingRecord {
kycInfo?: KycPendingInfo;
kycUrl?: string;
+
+ kycAccessToken?: string;
}
export enum PeerPullDebitRecordStatus {
diff --git a/packages/taler-wallet-core/src/exchanges.ts b/packages/taler-wallet-core/src/exchanges.ts
index fca2d4012..1820751f8 100644
--- a/packages/taler-wallet-core/src/exchanges.ts
+++ b/packages/taler-wallet-core/src/exchanges.ts
@@ -404,6 +404,23 @@ async function internalGetExchangeScopeInfo(
};
}
+function getKycStatusFromReserveStatus(
+ status: ReserveRecordStatus,
+): ExchangeWalletKycStatus {
+ switch (status) {
+ case ReserveRecordStatus.Done:
+ return ExchangeWalletKycStatus.Done;
+ // FIXME: Do we handle the suspended state?
+ case ReserveRecordStatus.SuspendedLegiInit:
+ case ReserveRecordStatus.PendingLegiInit:
+ return ExchangeWalletKycStatus.LegiInit;
+ // FIXME: Do we handle the suspended state?
+ case ReserveRecordStatus.SuspendedLegi:
+ case ReserveRecordStatus.PendingLegi:
+ return ExchangeWalletKycStatus.Legi;
+ }
+}
+
async function makeExchangeListItem(
tx: WalletDbReadOnlyTransaction<
["globalCurrencyExchanges", "globalCurrencyAuditors"]
@@ -425,24 +442,10 @@ async function makeExchangeListItem(
scopeInfo = await internalGetExchangeScopeInfo(tx, exchangeDetails);
}
- let walletKycStatus: ExchangeWalletKycStatus | undefined = undefined;
- if (reserveRec) {
- switch (reserveRec.status) {
- case ReserveRecordStatus.Done:
- walletKycStatus = ExchangeWalletKycStatus.Done;
- break;
- // FIXME: Do we handle the suspended state?
- case ReserveRecordStatus.SuspendedLegiInit:
- case ReserveRecordStatus.PendingLegiInit:
- walletKycStatus = ExchangeWalletKycStatus.LegiInit;
- break;
- // FIXME: Do we handle the suspended state?
- case ReserveRecordStatus.SuspendedLegi:
- case ReserveRecordStatus.PendingLegi:
- walletKycStatus = ExchangeWalletKycStatus.Legi;
- break;
- }
- }
+ let walletKycStatus: ExchangeWalletKycStatus | undefined =
+ reserveRec && reserveRec.status
+ ? getKycStatusFromReserveStatus(reserveRec.status)
+ : undefined;
const listItem: ExchangeListItem = {
exchangeBaseUrl: r.baseUrl,
@@ -2970,6 +2973,8 @@ export type BalanceThresholdCheckResult =
| {
result: "violation";
nextThreshold: AmountString;
+ walletKycStatus: ExchangeWalletKycStatus | undefined;
+ walletKycAccessToken: string | undefined;
};
export async function checkIncomingAmountLegalUnderKycBalanceThreshold(
@@ -3013,8 +3018,9 @@ export async function checkIncomingAmountLegalUnderKycBalanceThreshold(
// Check if we already have KYC for a sufficient threshold.
const reserveId = exchangeRec.currentMergeReserveRowId;
+ let reserveRec: ReserveRecord | undefined;
if (reserveId) {
- const reserveRec = await tx.reserves.get(reserveId);
+ reserveRec = await tx.reserves.get(reserveId);
checkDbInvariant(!!reserveRec, "reserve");
// FIXME: also consider KYC expiration!
if (reserveRec.thresholdNext) {
@@ -3066,6 +3072,10 @@ export async function checkIncomingAmountLegalUnderKycBalanceThreshold(
return {
result: "violation",
nextThreshold: limNext ?? limViolated,
+ walletKycStatus: reserveRec?.status
+ ? getKycStatusFromReserveStatus(reserveRec.status)
+ : undefined,
+ walletKycAccessToken: reserveRec?.kycAccessToken,
};
}
},
@@ -3073,47 +3083,6 @@ export async function checkIncomingAmountLegalUnderKycBalanceThreshold(
}
/**
- * Wait until it is allowed for the user to add the given amount
- * to the wallet balance, either because the balance is low enough
- * or KYC was completed.
- */
-export async function waitIncomingAmountLegalUnderKycBalanceThreshold(
- wex: WalletExecutionContext,
- exchangeBaseUrl: string,
- amountExpected: AmountLike,
-): Promise<void> {
- await genericWaitForState(wex, {
- async checkState(): Promise<boolean> {
- const checkRes = await checkIncomingAmountLegalUnderKycBalanceThreshold(
- wex,
- exchangeBaseUrl,
- amountExpected,
- );
- logger.info(
- `balance check result for ${exchangeBaseUrl} +${amountExpected}: ${j2s(
- checkRes,
- )}`,
- );
- if (checkRes.result === "ok") {
- return true;
- }
- await handleStartExchangeWalletKyc(wex, {
- amount: checkRes.nextThreshold,
- exchangeBaseUrl,
- });
- return false;
- },
- filterNotification(notif) {
- return (
- (notif.type === NotificationType.ExchangeStateTransition &&
- notif.exchangeBaseUrl === exchangeBaseUrl) ||
- notif.type === NotificationType.BalanceChange
- );
- },
- });
-}
-
-/**
* Wait until kyc has passed for the wallet.
*
* If passed==false, already return when legitimization
diff --git a/packages/taler-wallet-core/src/pay-peer-pull-credit.ts b/packages/taler-wallet-core/src/pay-peer-pull-credit.ts
index ac50f2e0e..e9a3970c6 100644
--- a/packages/taler-wallet-core/src/pay-peer-pull-credit.ts
+++ b/packages/taler-wallet-core/src/pay-peer-pull-credit.ts
@@ -24,6 +24,7 @@ import {
CheckPeerPullCreditResponse,
ContractTermsUtil,
ExchangeReservePurseRequest,
+ ExchangeWalletKycStatus,
HttpStatusCode,
InitiatePeerPullCreditRequest,
InitiatePeerPullCreditResponse,
@@ -65,6 +66,7 @@ import {
TransitionResult,
TransitionResultType,
constructTaskIdentifier,
+ genericWaitForStateVal,
requireExchangeTosAcceptedOrThrow,
runWithClientCancellation,
} from "./common.js";
@@ -84,11 +86,11 @@ import {
timestampPreciseToDb,
} from "./db.js";
import {
+ BalanceThresholdCheckResult,
checkIncomingAmountLegalUnderKycBalanceThreshold,
fetchFreshExchange,
getScopeForAllExchanges,
handleStartExchangeWalletKyc,
- waitIncomingAmountLegalUnderKycBalanceThreshold,
} from "./exchanges.js";
import {
codecForExchangePurseStatus,
@@ -415,16 +417,20 @@ export class PeerPullCreditTransactionContext implements TransactionContext {
case PeerPullPaymentCreditStatus.PendingBalanceKycRequired:
newStatus = PeerPullPaymentCreditStatus.SuspendedBalanceKycRequired;
break;
- case PeerPullPaymentCreditStatus.Done:
+ case PeerPullPaymentCreditStatus.PendingBalanceKycInit:
+ newStatus = PeerPullPaymentCreditStatus.SuspendedBalanceKycInit;
+ break;
case PeerPullPaymentCreditStatus.SuspendedBalanceKycRequired:
case PeerPullPaymentCreditStatus.SuspendedCreatePurse:
case PeerPullPaymentCreditStatus.SuspendedMergeKycRequired:
case PeerPullPaymentCreditStatus.SuspendedReady:
case PeerPullPaymentCreditStatus.SuspendedWithdrawing:
+ case PeerPullPaymentCreditStatus.SuspendedBalanceKycInit:
+ case PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse:
+ case PeerPullPaymentCreditStatus.Done:
case PeerPullPaymentCreditStatus.Aborted:
case PeerPullPaymentCreditStatus.Failed:
case PeerPullPaymentCreditStatus.Expired:
- case PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse:
break;
default:
assertUnreachable(pullCreditRec.status);
@@ -475,6 +481,8 @@ export class PeerPullCreditTransactionContext implements TransactionContext {
case PeerPullPaymentCreditStatus.Expired:
case PeerPullPaymentCreditStatus.PendingBalanceKycRequired:
case PeerPullPaymentCreditStatus.SuspendedBalanceKycRequired:
+ case PeerPullPaymentCreditStatus.PendingBalanceKycInit:
+ case PeerPullPaymentCreditStatus.SuspendedBalanceKycInit:
break;
case PeerPullPaymentCreditStatus.AbortingDeletePurse:
case PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse:
@@ -520,12 +528,16 @@ export class PeerPullCreditTransactionContext implements TransactionContext {
case PeerPullPaymentCreditStatus.PendingWithdrawing:
case PeerPullPaymentCreditStatus.PendingReady:
case PeerPullPaymentCreditStatus.PendingBalanceKycRequired:
+ case PeerPullPaymentCreditStatus.PendingBalanceKycInit:
case PeerPullPaymentCreditStatus.AbortingDeletePurse:
case PeerPullPaymentCreditStatus.Done:
case PeerPullPaymentCreditStatus.Failed:
case PeerPullPaymentCreditStatus.Expired:
case PeerPullPaymentCreditStatus.Aborted:
break;
+ case PeerPullPaymentCreditStatus.SuspendedBalanceKycInit:
+ newStatus = PeerPullPaymentCreditStatus.PendingBalanceKycInit;
+ break;
case PeerPullPaymentCreditStatus.SuspendedCreatePurse:
newStatus = PeerPullPaymentCreditStatus.PendingCreatePurse;
break;
@@ -581,6 +593,8 @@ export class PeerPullCreditTransactionContext implements TransactionContext {
switch (pullCreditRec.status) {
case PeerPullPaymentCreditStatus.PendingBalanceKycRequired:
case PeerPullPaymentCreditStatus.SuspendedBalanceKycRequired:
+ case PeerPullPaymentCreditStatus.PendingBalanceKycInit:
+ case PeerPullPaymentCreditStatus.SuspendedBalanceKycInit:
case PeerPullPaymentCreditStatus.PendingCreatePurse:
case PeerPullPaymentCreditStatus.PendingMergeKycRequired:
newStatus = PeerPullPaymentCreditStatus.AbortingDeletePurse;
@@ -942,7 +956,7 @@ async function handlePeerPullCreditCreatePurse(
if (rec.status !== PeerPullPaymentCreditStatus.PendingCreatePurse) {
return TransitionResult.stay();
}
- rec.status = PeerPullPaymentCreditStatus.PendingBalanceKycRequired;
+ rec.status = PeerPullPaymentCreditStatus.PendingBalanceKycInit;
return TransitionResult.transition(rec);
});
return TaskRunResult.progress();
@@ -1114,6 +1128,7 @@ export async function processPeerPullCredit(
case PeerPullPaymentCreditStatus.PendingWithdrawing:
return handlePeerPullCreditWithdrawing(wex, pullIni);
case PeerPullPaymentCreditStatus.PendingBalanceKycRequired:
+ case PeerPullPaymentCreditStatus.PendingBalanceKycInit:
return processPeerPullCreditBalanceKyc(ctx, pullIni);
case PeerPullPaymentCreditStatus.Aborted:
case PeerPullPaymentCreditStatus.Failed:
@@ -1124,6 +1139,7 @@ export async function processPeerPullCredit(
case PeerPullPaymentCreditStatus.SuspendedMergeKycRequired:
case PeerPullPaymentCreditStatus.SuspendedReady:
case PeerPullPaymentCreditStatus.SuspendedWithdrawing:
+ case PeerPullPaymentCreditStatus.SuspendedBalanceKycInit:
break;
default:
assertUnreachable(pullIni.status);
@@ -1139,22 +1155,80 @@ async function processPeerPullCreditBalanceKyc(
const exchangeBaseUrl = peerInc.exchangeBaseUrl;
const amount = peerInc.estimatedAmountEffective;
- await waitIncomingAmountLegalUnderKycBalanceThreshold(
- ctx.wex,
- exchangeBaseUrl,
- amount,
- );
- await ctx.transition({}, async (rec) => {
- if (!rec) {
- return TransitionResult.stay();
- }
- if (rec.status !== PeerPullPaymentCreditStatus.PendingBalanceKycRequired) {
- return TransitionResult.stay();
- }
- rec.status = PeerPullPaymentCreditStatus.PendingCreatePurse;
- return TransitionResult.transition(rec);
+ const ret = await genericWaitForStateVal(ctx.wex, {
+ async checkState(): Promise<BalanceThresholdCheckResult | undefined> {
+ const checkRes = await checkIncomingAmountLegalUnderKycBalanceThreshold(
+ ctx.wex,
+ exchangeBaseUrl,
+ amount,
+ );
+ logger.info(
+ `balance check result for ${exchangeBaseUrl} +${amount}: ${j2s(
+ checkRes,
+ )}`,
+ );
+ if (checkRes.result === "ok") {
+ return checkRes;
+ }
+ if (
+ peerInc.status === PeerPullPaymentCreditStatus.PendingBalanceKycInit &&
+ checkRes.walletKycStatus === ExchangeWalletKycStatus.Legi
+ ) {
+ return checkRes;
+ }
+ await handleStartExchangeWalletKyc(ctx.wex, {
+ amount: checkRes.nextThreshold,
+ exchangeBaseUrl,
+ });
+ return undefined;
+ },
+ filterNotification(notif) {
+ return (
+ (notif.type === NotificationType.ExchangeStateTransition &&
+ notif.exchangeBaseUrl === exchangeBaseUrl) ||
+ notif.type === NotificationType.BalanceChange
+ );
+ },
});
- return TaskRunResult.progress();
+
+ if (ret.result === "ok") {
+ await ctx.transition({}, async (rec) => {
+ if (!rec) {
+ return TransitionResult.stay();
+ }
+ if (
+ rec.status !== PeerPullPaymentCreditStatus.PendingBalanceKycRequired
+ ) {
+ return TransitionResult.stay();
+ }
+ rec.status = PeerPullPaymentCreditStatus.PendingCreatePurse;
+ return TransitionResult.transition(rec);
+ });
+ return TaskRunResult.progress();
+ } else if (
+ peerInc.status === PeerPullPaymentCreditStatus.PendingBalanceKycInit &&
+ ret.walletKycStatus === ExchangeWalletKycStatus.Legi
+ ) {
+ await ctx.transition({}, async (rec) => {
+ if (!rec) {
+ return TransitionResult.stay();
+ }
+ if (rec.status !== PeerPullPaymentCreditStatus.PendingBalanceKycInit) {
+ return TransitionResult.stay();
+ }
+ rec.status = PeerPullPaymentCreditStatus.PendingBalanceKycRequired;
+ delete rec.kycInfo;
+ rec.kycAccessToken = ret.walletKycAccessToken;
+ rec.kycUrl = new URL(
+ `kyc-spa/${ret.walletKycAccessToken}`,
+ exchangeBaseUrl,
+ ).href;
+ return TransitionResult.transition(rec);
+ });
+ return TaskRunResult.progress();
+ } else {
+ throw Error("not reached");
+ }
}
async function processPeerPullCreditKycRequired(
@@ -1547,6 +1621,16 @@ export function computePeerPullCreditTransactionState(
major: TransactionMajorState.Suspended,
minor: TransactionMinorState.BalanceKycRequired,
};
+ case PeerPullPaymentCreditStatus.PendingBalanceKycInit:
+ return {
+ major: TransactionMajorState.Pending,
+ minor: TransactionMinorState.BalanceKycInit,
+ };
+ case PeerPullPaymentCreditStatus.SuspendedBalanceKycInit:
+ return {
+ major: TransactionMajorState.Suspended,
+ minor: TransactionMinorState.BalanceKycInit,
+ };
}
}
@@ -1606,5 +1690,9 @@ export function computePeerPullCreditTransactionActions(
return [TransactionAction.Suspend, TransactionAction.Abort];
case PeerPullPaymentCreditStatus.SuspendedBalanceKycRequired:
return [TransactionAction.Resume, TransactionAction.Abort];
+ case PeerPullPaymentCreditStatus.PendingBalanceKycInit:
+ return [TransactionAction.Suspend, TransactionAction.Abort];
+ case PeerPullPaymentCreditStatus.SuspendedBalanceKycInit:
+ return [TransactionAction.Resume, TransactionAction.Abort];
}
}
diff --git a/packages/taler-wallet-core/src/pay-peer-push-credit.ts b/packages/taler-wallet-core/src/pay-peer-push-credit.ts
index 6f2b15d20..ed59f0d4d 100644
--- a/packages/taler-wallet-core/src/pay-peer-push-credit.ts
+++ b/packages/taler-wallet-core/src/pay-peer-push-credit.ts
@@ -21,6 +21,7 @@ import {
ConfirmPeerPushCreditRequest,
ContractTermsUtil,
ExchangePurseMergeRequest,
+ ExchangeWalletKycStatus,
HttpStatusCode,
Logger,
NotificationType,
@@ -62,6 +63,7 @@ import {
TransitionResult,
TransitionResultType,
constructTaskIdentifier,
+ genericWaitForStateVal,
requireExchangeTosAcceptedOrThrow,
} from "./common.js";
import {
@@ -79,11 +81,11 @@ import {
timestampPreciseToDb,
} from "./db.js";
import {
+ BalanceThresholdCheckResult,
checkIncomingAmountLegalUnderKycBalanceThreshold,
fetchFreshExchange,
getScopeForAllExchanges,
handleStartExchangeWalletKyc,
- waitIncomingAmountLegalUnderKycBalanceThreshold,
} from "./exchanges.js";
import {
codecForExchangePurseStatus,
@@ -362,60 +364,48 @@ export class PeerPushCreditTransactionContext implements TransactionContext {
}
async suspendTransaction(): Promise<void> {
- const { wex, peerPushCreditId, taskId: retryTag, transactionId } = this;
- const transitionInfo = await wex.db.runReadWriteTx(
- { storeNames: ["peerPushCredit", "transactionsMeta"] },
- async (tx) => {
- const pushCreditRec = await tx.peerPushCredit.get(peerPushCreditId);
- if (!pushCreditRec) {
- logger.warn(`peer push credit ${peerPushCreditId} not found`);
- return;
- }
- let newStatus: PeerPushCreditStatus | undefined = undefined;
- switch (pushCreditRec.status) {
- case PeerPushCreditStatus.DialogProposed:
- case PeerPushCreditStatus.Done:
- case PeerPushCreditStatus.SuspendedMerge:
- case PeerPushCreditStatus.SuspendedMergeKycRequired:
- case PeerPushCreditStatus.SuspendedWithdrawing:
- case PeerPushCreditStatus.SuspendedBalanceKycRequired:
- case PeerPushCreditStatus.Failed:
- case PeerPushCreditStatus.Aborted:
- break;
- case PeerPushCreditStatus.PendingBalanceKycRequired:
- newStatus = PeerPushCreditStatus.SuspendedBalanceKycRequired;
- break;
- case PeerPushCreditStatus.PendingMergeKycRequired:
- newStatus = PeerPushCreditStatus.SuspendedMergeKycRequired;
- break;
- case PeerPushCreditStatus.PendingMerge:
- newStatus = PeerPushCreditStatus.SuspendedMerge;
- break;
- case PeerPushCreditStatus.PendingWithdrawing:
- // FIXME: Suspend internal withdrawal transaction!
- newStatus = PeerPushCreditStatus.SuspendedWithdrawing;
- break;
- default:
- assertUnreachable(pushCreditRec.status);
- }
- if (newStatus != null) {
- const oldTxState =
- computePeerPushCreditTransactionState(pushCreditRec);
- pushCreditRec.status = newStatus;
- const newTxState =
- computePeerPushCreditTransactionState(pushCreditRec);
- await tx.peerPushCredit.put(pushCreditRec);
- await this.updateTransactionMeta(tx);
- return {
- oldTxState,
- newTxState,
- };
- }
- return undefined;
- },
- );
- notifyTransition(wex, transactionId, transitionInfo);
- wex.taskScheduler.stopShepherdTask(retryTag);
+ await this.transition({}, async (pushCreditRec) => {
+ if (!pushCreditRec) {
+ return TransitionResult.stay();
+ }
+ let newStatus: PeerPushCreditStatus | undefined = undefined;
+ switch (pushCreditRec.status) {
+ case PeerPushCreditStatus.DialogProposed:
+ case PeerPushCreditStatus.Done:
+ case PeerPushCreditStatus.SuspendedMerge:
+ case PeerPushCreditStatus.SuspendedMergeKycRequired:
+ case PeerPushCreditStatus.SuspendedWithdrawing:
+ case PeerPushCreditStatus.SuspendedBalanceKycRequired:
+ case PeerPushCreditStatus.SuspendedBalanceKycInit:
+ case PeerPushCreditStatus.Failed:
+ case PeerPushCreditStatus.Aborted:
+ break;
+ case PeerPushCreditStatus.PendingBalanceKycRequired:
+ newStatus = PeerPushCreditStatus.SuspendedBalanceKycRequired;
+ break;
+ case PeerPushCreditStatus.PendingBalanceKycInit:
+ newStatus = PeerPushCreditStatus.SuspendedBalanceKycInit;
+ break;
+ case PeerPushCreditStatus.PendingMergeKycRequired:
+ newStatus = PeerPushCreditStatus.SuspendedMergeKycRequired;
+ break;
+ case PeerPushCreditStatus.PendingMerge:
+ newStatus = PeerPushCreditStatus.SuspendedMerge;
+ break;
+ case PeerPushCreditStatus.PendingWithdrawing:
+ // FIXME: Suspend internal withdrawal transaction!
+ newStatus = PeerPushCreditStatus.SuspendedWithdrawing;
+ break;
+ default:
+ assertUnreachable(pushCreditRec.status);
+ }
+ if (newStatus != null) {
+ pushCreditRec.status = newStatus;
+ return TransitionResult.transition(pushCreditRec);
+ } else {
+ return TransitionResult.stay();
+ }
+ });
}
async abortTransaction(): Promise<void> {
@@ -443,6 +433,8 @@ export class PeerPushCreditTransactionContext implements TransactionContext {
case PeerPushCreditStatus.PendingWithdrawing:
case PeerPushCreditStatus.PendingMergeKycRequired:
case PeerPushCreditStatus.PendingMerge:
+ case PeerPushCreditStatus.PendingBalanceKycInit:
+ case PeerPushCreditStatus.SuspendedBalanceKycInit:
newStatus = PeerPushCreditStatus.Aborted;
break;
default:
@@ -481,13 +473,14 @@ export class PeerPushCreditTransactionContext implements TransactionContext {
let newStatus: PeerPushCreditStatus | undefined = undefined;
switch (pushCreditRec.status) {
case PeerPushCreditStatus.DialogProposed:
- case PeerPushCreditStatus.Done:
case PeerPushCreditStatus.PendingMergeKycRequired:
case PeerPushCreditStatus.PendingMerge:
case PeerPushCreditStatus.PendingWithdrawing:
+ case PeerPushCreditStatus.PendingBalanceKycRequired:
+ case PeerPushCreditStatus.PendingBalanceKycInit:
+ case PeerPushCreditStatus.Done:
case PeerPushCreditStatus.Aborted:
case PeerPushCreditStatus.Failed:
- case PeerPushCreditStatus.PendingBalanceKycRequired:
break;
case PeerPushCreditStatus.SuspendedMerge:
newStatus = PeerPushCreditStatus.PendingMerge;
@@ -502,6 +495,9 @@ export class PeerPushCreditTransactionContext implements TransactionContext {
case PeerPushCreditStatus.SuspendedBalanceKycRequired:
newStatus = PeerPushCreditStatus.PendingBalanceKycRequired;
break;
+ case PeerPushCreditStatus.SuspendedBalanceKycInit:
+ newStatus = PeerPushCreditStatus.PendingBalanceKycInit;
+ break;
default:
assertUnreachable(pushCreditRec.status);
}
@@ -551,6 +547,8 @@ export class PeerPushCreditTransactionContext implements TransactionContext {
case PeerPushCreditStatus.SuspendedWithdrawing:
case PeerPushCreditStatus.PendingBalanceKycRequired:
case PeerPushCreditStatus.SuspendedBalanceKycRequired:
+ case PeerPushCreditStatus.PendingBalanceKycInit:
+ case PeerPushCreditStatus.SuspendedBalanceKycInit:
newStatus = PeerPushCreditStatus.Failed;
break;
default:
@@ -925,7 +923,7 @@ async function handlePendingMerge(
if (rec.status !== PeerPushCreditStatus.PendingMerge) {
return TransitionResult.stay();
}
- rec.status = PeerPushCreditStatus.PendingBalanceKycRequired;
+ rec.status = PeerPushCreditStatus.PendingBalanceKycInit;
return TransitionResult.transition(rec);
});
return TaskRunResult.progress();
@@ -1181,6 +1179,7 @@ export async function processPeerPushCredit(
case PeerPushCreditStatus.PendingWithdrawing: {
return handlePendingWithdrawing(wex, peerInc);
}
+ case PeerPushCreditStatus.PendingBalanceKycInit:
case PeerPushCreditStatus.PendingBalanceKycRequired: {
return await processPeerPushCreditBalanceKyc(ctx, peerInc);
}
@@ -1196,22 +1195,80 @@ async function processPeerPushCreditBalanceKyc(
const exchangeBaseUrl = peerInc.exchangeBaseUrl;
const amount = peerInc.estimatedAmountEffective;
- await waitIncomingAmountLegalUnderKycBalanceThreshold(
- ctx.wex,
- exchangeBaseUrl,
- amount,
- );
- await ctx.transition({}, async (rec) => {
- if (!rec) {
- return TransitionResult.stay();
- }
- if (rec.status !== PeerPushCreditStatus.PendingBalanceKycRequired) {
- return TransitionResult.stay();
- }
- rec.status = PeerPushCreditStatus.PendingMerge;
- return TransitionResult.transition(rec);
+ const ret = await genericWaitForStateVal(ctx.wex, {
+ async checkState(): Promise<BalanceThresholdCheckResult | undefined> {
+ const checkRes = await checkIncomingAmountLegalUnderKycBalanceThreshold(
+ ctx.wex,
+ exchangeBaseUrl,
+ amount,
+ );
+ logger.info(
+ `balance check result for ${exchangeBaseUrl} +${amount}: ${j2s(
+ checkRes,
+ )}`,
+ );
+ if (checkRes.result === "ok") {
+ return checkRes;
+ }
+ if (
+ peerInc.status === PeerPushCreditStatus.PendingBalanceKycInit &&
+ checkRes.walletKycStatus === ExchangeWalletKycStatus.Legi
+ ) {
+ return checkRes;
+ }
+ await handleStartExchangeWalletKyc(ctx.wex, {
+ amount: checkRes.nextThreshold,
+ exchangeBaseUrl,
+ });
+ return undefined;
+ },
+ filterNotification(notif) {
+ return (
+ (notif.type === NotificationType.ExchangeStateTransition &&
+ notif.exchangeBaseUrl === exchangeBaseUrl) ||
+ notif.type === NotificationType.BalanceChange
+ );
+ },
});
- return TaskRunResult.progress();
+
+ if (ret.result === "ok") {
+ await ctx.transition({}, async (rec) => {
+ if (!rec) {
+ return TransitionResult.stay();
+ }
+ if (
+ rec.status !== PeerPushCreditStatus.PendingBalanceKycRequired
+ ) {
+ return TransitionResult.stay();
+ }
+ rec.status = PeerPushCreditStatus.PendingMerge;
+ return TransitionResult.transition(rec);
+ });
+ return TaskRunResult.progress();
+ } else if (
+ peerInc.status === PeerPushCreditStatus.PendingBalanceKycInit &&
+ ret.walletKycStatus === ExchangeWalletKycStatus.Legi
+ ) {
+ await ctx.transition({}, async (rec) => {
+ if (!rec) {
+ return TransitionResult.stay();
+ }
+ if (rec.status !== PeerPushCreditStatus.PendingBalanceKycInit) {
+ return TransitionResult.stay();
+ }
+ rec.status = PeerPushCreditStatus.PendingBalanceKycRequired;
+ delete rec.kycInfo;
+ rec.kycAccessToken = ret.walletKycAccessToken;
+ rec.kycUrl = new URL(
+ `kyc-spa/${ret.walletKycAccessToken}`,
+ exchangeBaseUrl,
+ ).href;
+ return TransitionResult.transition(rec);
+ });
+ return TaskRunResult.progress();
+ } else {
+ throw Error("not reached");
+ }
}
export async function confirmPeerPushCredit(
@@ -1325,6 +1382,16 @@ export function computePeerPushCreditTransactionState(
major: TransactionMajorState.Suspended,
minor: TransactionMinorState.BalanceKycRequired,
};
+ case PeerPushCreditStatus.PendingBalanceKycInit:
+ return {
+ major: TransactionMajorState.Pending,
+ minor: TransactionMinorState.BalanceKycInit,
+ };
+ case PeerPushCreditStatus.SuspendedBalanceKycInit:
+ return {
+ major: TransactionMajorState.Suspended,
+ minor: TransactionMinorState.BalanceKycInit,
+ };
default:
assertUnreachable(pushCreditRecord.status);
}
@@ -1366,6 +1433,10 @@ export function computePeerPushCreditTransactionActions(
return [TransactionAction.Suspend, TransactionAction.Abort];
case PeerPushCreditStatus.SuspendedBalanceKycRequired:
return [TransactionAction.Resume, TransactionAction.Abort];
+ case PeerPushCreditStatus.PendingBalanceKycInit:
+ return [TransactionAction.Suspend, TransactionAction.Abort];
+ case PeerPushCreditStatus.SuspendedBalanceKycInit:
+ return [TransactionAction.Resume, TransactionAction.Abort];
case PeerPushCreditStatus.Aborted:
return [TransactionAction.Delete];
case PeerPushCreditStatus.Failed:
diff --git a/packages/taler-wallet-core/src/withdraw.ts b/packages/taler-wallet-core/src/withdraw.ts
index d4038f008..8c56adb49 100644
--- a/packages/taler-wallet-core/src/withdraw.ts
+++ b/packages/taler-wallet-core/src/withdraw.ts
@@ -47,6 +47,7 @@ import {
ExchangeBatchWithdrawRequest,
ExchangeListItem,
ExchangeUpdateStatus,
+ ExchangeWalletKycStatus,
ExchangeWireAccount,
ExchangeWithdrawBatchResponse,
ExchangeWithdrawRequest,
@@ -122,6 +123,7 @@ import {
TransitionResultType,
constructTaskIdentifier,
genericWaitForState,
+ genericWaitForStateVal,
makeCoinAvailable,
makeCoinsVisible,
requireExchangeTosAcceptedOrThrow,
@@ -157,6 +159,7 @@ import {
} from "./denomSelection.js";
import { isWithdrawableDenom } from "./denominations.js";
import {
+ BalanceThresholdCheckResult,
ExchangeWireDetails,
ReadyExchangeSummary,
checkIncomingAmountLegalUnderKycBalanceThreshold,
@@ -168,7 +171,6 @@ import {
listExchanges,
lookupExchangeByUri,
markExchangeUsed,
- waitIncomingAmountLegalUnderKycBalanceThreshold,
} from "./exchanges.js";
import { DbAccess } from "./query.js";
import {
@@ -629,6 +631,8 @@ export class WithdrawTransactionContext implements TransactionContext {
case WithdrawalGroupStatus.PendingQueryingStatus:
case WithdrawalGroupStatus.PendingBalanceKyc:
case WithdrawalGroupStatus.SuspendedBalanceKyc:
+ case WithdrawalGroupStatus.PendingBalanceKycInit:
+ case WithdrawalGroupStatus.SuspendedBalanceKycInit:
newStatus = WithdrawalGroupStatus.AbortedExchange;
break;
case WithdrawalGroupStatus.PendingReady:
@@ -845,6 +849,16 @@ export function computeWithdrawalTransactionStatus(
major: TransactionMajorState.Suspended,
minor: TransactionMinorState.BalanceKycRequired,
};
+ case WithdrawalGroupStatus.PendingBalanceKycInit:
+ return {
+ major: TransactionMajorState.Pending,
+ minor: TransactionMinorState.BalanceKycInit,
+ };
+ case WithdrawalGroupStatus.SuspendedBalanceKycInit:
+ return {
+ major: TransactionMajorState.Suspended,
+ minor: TransactionMinorState.BalanceKycInit,
+ };
}
}
@@ -924,6 +938,14 @@ export function computeWithdrawalTransactionActions(
];
case WithdrawalGroupStatus.SuspendedBalanceKyc:
return [TransactionAction.Resume, TransactionAction.Abort];
+ case WithdrawalGroupStatus.PendingBalanceKycInit:
+ return [
+ TransactionAction.Suspend,
+ TransactionAction.Retry,
+ TransactionAction.Abort,
+ ];
+ case WithdrawalGroupStatus.SuspendedBalanceKycInit:
+ return [TransactionAction.Resume, TransactionAction.Abort];
}
}
@@ -943,22 +965,80 @@ async function processWithdrawalGroupBalanceKyc(
"invalid state (expected withdrawal group to have effective withdrawal amount)",
);
}
- await waitIncomingAmountLegalUnderKycBalanceThreshold(
- ctx.wex,
- exchangeBaseUrl,
- amount,
- );
- await ctx.transition({}, async (wg) => {
- if (!wg) {
- return TransitionResult.stay();
- }
- if (wg.status !== WithdrawalGroupStatus.PendingBalanceKyc) {
- return TransitionResult.stay();
- }
- wg.status = WithdrawalGroupStatus.PendingReady;
- return TransitionResult.transition(wg);
+
+ const ret = await genericWaitForStateVal(ctx.wex, {
+ async checkState(): Promise<BalanceThresholdCheckResult | undefined> {
+ const checkRes = await checkIncomingAmountLegalUnderKycBalanceThreshold(
+ ctx.wex,
+ exchangeBaseUrl,
+ amount,
+ );
+ logger.info(
+ `balance check result for ${exchangeBaseUrl} +${amount}: ${j2s(
+ checkRes,
+ )}`,
+ );
+ if (checkRes.result === "ok") {
+ return checkRes;
+ }
+ if (
+ withdrawalGroup.status ===
+ WithdrawalGroupStatus.PendingBalanceKycInit &&
+ checkRes.walletKycStatus === ExchangeWalletKycStatus.Legi
+ ) {
+ return checkRes;
+ }
+ await handleStartExchangeWalletKyc(ctx.wex, {
+ amount: checkRes.nextThreshold,
+ exchangeBaseUrl,
+ });
+ return undefined;
+ },
+ filterNotification(notif) {
+ return (
+ (notif.type === NotificationType.ExchangeStateTransition &&
+ notif.exchangeBaseUrl === exchangeBaseUrl) ||
+ notif.type === NotificationType.BalanceChange
+ );
+ },
});
- return TaskRunResult.progress();
+
+ if (ret.result === "ok") {
+ await ctx.transition({}, async (wg) => {
+ if (!wg) {
+ return TransitionResult.stay();
+ }
+ if (wg.status !== WithdrawalGroupStatus.PendingBalanceKyc) {
+ return TransitionResult.stay();
+ }
+ wg.status = WithdrawalGroupStatus.PendingReady;
+ return TransitionResult.transition(wg);
+ });
+ return TaskRunResult.progress();
+ } else if (
+ withdrawalGroup.status === WithdrawalGroupStatus.PendingBalanceKycInit &&
+ ret.walletKycStatus === ExchangeWalletKycStatus.Legi
+ ) {
+ await ctx.transition({}, async (wg) => {
+ if (!wg) {
+ return TransitionResult.stay();
+ }
+ if (wg.status !== WithdrawalGroupStatus.PendingBalanceKycInit) {
+ return TransitionResult.stay();
+ }
+ wg.status = WithdrawalGroupStatus.PendingBalanceKyc;
+ wg.kycPending = undefined;
+ wg.kycAccessToken = ret.walletKycAccessToken;
+ wg.kycUrl = new URL(
+ `kyc-spa/${ret.walletKycAccessToken}`,
+ exchangeBaseUrl,
+ ).href;
+ return TransitionResult.transition(wg);
+ });
+ return TaskRunResult.progress();
+ } else {
+ throw Error("not reached");
+ }
}
async function processWithdrawalGroupDialogProposed(
@@ -2194,7 +2274,7 @@ async function processWithdrawalGroupPendingReady(
if (!wg) {
return TransitionResult.stay();
}
- wg.status = WithdrawalGroupStatus.PendingBalanceKyc;
+ wg.status = WithdrawalGroupStatus.PendingBalanceKycInit;
return TransitionResult.transition(wg);
});
return TaskRunResult.progress();
@@ -2389,6 +2469,7 @@ export async function processWithdrawalGroup(
case WithdrawalGroupStatus.DialogProposed:
return await processWithdrawalGroupDialogProposed(ctx, withdrawalGroup);
case WithdrawalGroupStatus.PendingBalanceKyc:
+ case WithdrawalGroupStatus.PendingBalanceKycInit:
return await processWithdrawalGroupBalanceKyc(ctx, withdrawalGroup);
case WithdrawalGroupStatus.AbortedBank:
case WithdrawalGroupStatus.AbortedExchange:
@@ -2400,6 +2481,7 @@ export async function processWithdrawalGroup(
case WithdrawalGroupStatus.SuspendedRegisteringBank:
case WithdrawalGroupStatus.SuspendedWaitConfirmBank:
case WithdrawalGroupStatus.SuspendedBalanceKyc:
+ case WithdrawalGroupStatus.SuspendedBalanceKycInit:
case WithdrawalGroupStatus.Done:
case WithdrawalGroupStatus.FailedBankAborted:
case WithdrawalGroupStatus.AbortedUserRefused: