aboutsummaryrefslogtreecommitdiff
path: root/packages
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2023-06-21 12:21:48 +0200
committerFlorian Dold <florian@dold.me>2023-06-21 12:21:48 +0200
commit5eb339b836b250891f00d8287781175b50788eb7 (patch)
tree827c62343e8fcc672213003caf7a14ad544e1b60 /packages
parent30fb003ee371d2c58bfd32e7c7afe0a1daeaf8ec (diff)
wallet-core: fix withdrawal KYC transitions and use long-polling
Diffstat (limited to 'packages')
-rw-r--r--packages/taler-wallet-core/src/db.ts2
-rw-r--r--packages/taler-wallet-core/src/operations/transactions.ts1
-rw-r--r--packages/taler-wallet-core/src/operations/withdraw.ts334
3 files changed, 210 insertions, 127 deletions
diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts
index 3bf28aa94..9d0efbc6a 100644
--- a/packages/taler-wallet-core/src/db.ts
+++ b/packages/taler-wallet-core/src/db.ts
@@ -1416,6 +1416,8 @@ export interface WithdrawalGroupRecord {
kycPending?: KycPendingInfo;
+ kycUrl?: string;
+
/**
* Secret seed used to derive planchets.
* Stored since planchets are created lazily.
diff --git a/packages/taler-wallet-core/src/operations/transactions.ts b/packages/taler-wallet-core/src/operations/transactions.ts
index 82b7cea64..b4791e7c3 100644
--- a/packages/taler-wallet-core/src/operations/transactions.ts
+++ b/packages/taler-wallet-core/src/operations/transactions.ts
@@ -655,6 +655,7 @@ function buildTransactionForBankIntegratedWithdraw(
wgRecord.status === WithdrawalGroupStatus.Finished ||
wgRecord.status === WithdrawalGroupStatus.PendingReady,
},
+ kycUrl: wgRecord.kycUrl,
exchangeBaseUrl: wgRecord.exchangeBaseUrl,
timestamp: wgRecord.timestampStart,
transactionId: constructTransactionIdentifier({
diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts b/packages/taler-wallet-core/src/operations/withdraw.ts
index ed9522c0f..28f4eeebb 100644
--- a/packages/taler-wallet-core/src/operations/withdraw.ts
+++ b/packages/taler-wallet-core/src/operations/withdraw.ts
@@ -731,6 +731,96 @@ interface WithdrawalBatchResult {
batchResp: ExchangeWithdrawBatchResponse;
}
+async function handleKycRequired(
+ ws: InternalWalletState,
+ withdrawalGroup: WithdrawalGroupRecord,
+ resp: HttpResponse,
+ startIdx: number,
+ requestCoinIdxs: number[],
+): Promise<void> {
+ logger.info("withdrawal requires KYC");
+ const respJson = await resp.json();
+ const uuidResp = codecForWalletKycUuid().decode(respJson);
+ const withdrawalGroupId = withdrawalGroup.withdrawalGroupId;
+ const transactionId = constructTransactionIdentifier({
+ tag: TransactionType.Withdrawal,
+ withdrawalGroupId,
+ });
+ logger.info(`kyc uuid response: ${j2s(uuidResp)}`);
+ const exchangeUrl = withdrawalGroup.exchangeBaseUrl;
+ const userType = "individual";
+ const kycInfo: KycPendingInfo = {
+ paytoHash: uuidResp.h_payto,
+ requirementRow: uuidResp.requirement_row,
+ };
+ const url = new URL(
+ `kyc-check/${kycInfo.requirementRow}/${kycInfo.paytoHash}/${userType}`,
+ exchangeUrl,
+ );
+ logger.info(`kyc url ${url.href}`);
+ const kycStatusRes = await ws.http.fetch(url.href, {
+ method: "GET",
+ });
+ let kycUrl: string;
+ 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
+ ) {
+ logger.warn("kyc requested, but already fulfilled");
+ return;
+ } else if (kycStatusRes.status === HttpStatusCode.Accepted) {
+ const kycStatus = await kycStatusRes.json();
+ logger.info(`kyc status: ${j2s(kycStatus)}`);
+ kycUrl = kycStatus.kyc_url;
+ } else {
+ throw Error(`unexpected response from kyc-check (${kycStatusRes.status})`);
+ }
+
+ const transitionInfo = await ws.db
+ .mktx((x) => [x.planchets, x.withdrawalGroups])
+ .runReadWrite(async (tx) => {
+ for (let i = startIdx; i < requestCoinIdxs.length; i++) {
+ let planchet = await tx.planchets.indexes.byGroupAndIndex.get([
+ withdrawalGroup.withdrawalGroupId,
+ requestCoinIdxs[i],
+ ]);
+ if (!planchet) {
+ continue;
+ }
+ planchet.planchetStatus = PlanchetStatus.KycRequired;
+ await tx.planchets.put(planchet);
+ }
+ const wg2 = await tx.withdrawalGroups.get(
+ withdrawalGroup.withdrawalGroupId,
+ );
+ if (!wg2) {
+ return;
+ }
+ const oldTxState = computeWithdrawalTransactionStatus(wg2);
+ switch (wg2.status) {
+ case WithdrawalGroupStatus.PendingReady: {
+ wg2.kycPending = {
+ paytoHash: uuidResp.h_payto,
+ requirementRow: uuidResp.requirement_row,
+ };
+ wg2.kycUrl = kycUrl;
+ wg2.status = WithdrawalGroupStatus.PendingKyc;
+ await tx.withdrawalGroups.put(wg2);
+ const newTxState = computeWithdrawalTransactionStatus(wg2);
+ return {
+ oldTxState,
+ newTxState,
+ };
+ }
+ default:
+ return undefined;
+ }
+ });
+ notifyTransition(ws, transactionId, transitionInfo);
+}
+
/**
* Send the withdrawal request for a generated planchet to the exchange.
*
@@ -805,43 +895,6 @@ async function processPlanchetExchangeBatchRequest(
};
}
- async function handleKycRequired(
- resp: HttpResponse,
- startIdx: number,
- ): Promise<void> {
- logger.info("withdrawal requires KYC");
- const respJson = await resp.json();
- const uuidResp = codecForWalletKycUuid().decode(respJson);
- logger.info(`kyc uuid response: ${j2s(uuidResp)}`);
- await ws.db
- .mktx((x) => [x.planchets, x.withdrawalGroups])
- .runReadWrite(async (tx) => {
- for (let i = startIdx; i < requestCoinIdxs.length; i++) {
- let planchet = await tx.planchets.indexes.byGroupAndIndex.get([
- withdrawalGroup.withdrawalGroupId,
- requestCoinIdxs[i],
- ]);
- if (!planchet) {
- continue;
- }
- planchet.planchetStatus = PlanchetStatus.KycRequired;
- await tx.planchets.put(planchet);
- }
- const wg2 = await tx.withdrawalGroups.get(
- withdrawalGroup.withdrawalGroupId,
- );
- if (!wg2) {
- return;
- }
- wg2.kycPending = {
- paytoHash: uuidResp.h_payto,
- requirementRow: uuidResp.requirement_row,
- };
- await tx.withdrawalGroups.put(wg2);
- });
- return;
- }
-
async function storeCoinError(e: any, coinIdx: number): Promise<void> {
const errDetail = getErrorDetailFromException(e);
logger.trace("withdrawal request failed", e);
@@ -872,7 +925,7 @@ async function processPlanchetExchangeBatchRequest(
try {
const resp = await ws.http.postJson(reqUrl, batchReq);
if (resp.status === HttpStatusCode.UnavailableForLegalReasons) {
- await handleKycRequired(resp, 0);
+ await handleKycRequired(ws, withdrawalGroup, resp, 0, requestCoinIdxs);
}
const r = await readSuccessResponseJsonOrThrow(
resp,
@@ -902,9 +955,15 @@ async function processPlanchetExchangeBatchRequest(
`reserves/${withdrawalGroup.reservePub}/withdraw`,
withdrawalGroup.exchangeBaseUrl,
).href;
- const resp = await ws.http.postJson(reqUrl, p);
+ const resp = await ws.http.fetch(reqUrl, { method: "POST", body: p });
if (resp.status === HttpStatusCode.UnavailableForLegalReasons) {
- await handleKycRequired(resp, i);
+ await handleKycRequired(
+ ws,
+ withdrawalGroup,
+ resp,
+ i,
+ requestCoinIdxs,
+ );
// We still return blinded coins that we could actually withdraw.
return {
coinIdxs: responseCoinIdxs,
@@ -1321,6 +1380,96 @@ async function processWithdrawalGroupAbortingBank(
};
}
+/**
+ * Store in the database that the KYC for a withdrawal is now
+ * satisfied.
+ */
+async function transitionKycSatisfied(
+ ws: InternalWalletState,
+ withdrawalGroup: WithdrawalGroupRecord,
+): Promise<void> {
+ const transactionId = constructTransactionIdentifier({
+ tag: TransactionType.Withdrawal,
+ withdrawalGroupId: withdrawalGroup.withdrawalGroupId,
+ });
+ const transitionInfo = await ws.db
+ .mktx((x) => [x.withdrawalGroups])
+ .runReadWrite(async (tx) => {
+ const wg2 = await tx.withdrawalGroups.get(
+ withdrawalGroup.withdrawalGroupId,
+ );
+ if (!wg2) {
+ return;
+ }
+ const oldTxState = computeWithdrawalTransactionStatus(wg2);
+ switch (wg2.status) {
+ case WithdrawalGroupStatus.PendingKyc: {
+ delete wg2.kycPending;
+ delete wg2.kycUrl;
+ wg2.status = WithdrawalGroupStatus.PendingReady;
+ await tx.withdrawalGroups.put(wg2);
+ const newTxState = computeWithdrawalTransactionStatus(wg2);
+ return {
+ oldTxState,
+ newTxState,
+ };
+ }
+ default:
+ return undefined;
+ }
+ });
+ notifyTransition(ws, transactionId, transitionInfo);
+}
+
+async function processWithdrawalGroupPendingKyc(
+ ws: InternalWalletState,
+ withdrawalGroup: WithdrawalGroupRecord,
+): Promise<OperationAttemptResult> {
+ const userType = "individual";
+ const kycInfo = withdrawalGroup.kycPending;
+ if (!kycInfo) {
+ throw Error("no kyc info available in pending(kyc)");
+ }
+ const exchangeUrl = withdrawalGroup.exchangeBaseUrl;
+ const url = new URL(
+ `kyc-check/${kycInfo.requirementRow}/${kycInfo.paytoHash}/${userType}`,
+ exchangeUrl,
+ );
+ url.searchParams.set("timeout_ms", "30000");
+
+ const retryTag = TaskIdentifiers.forWithdrawal(withdrawalGroup);
+
+ runLongpollAsync(ws, retryTag, async (cancellationToken) => {
+ logger.info(`long-polling for withdrawal KYC status via ${url.href}`);
+ const kycStatusRes = await ws.http.fetch(url.href, {
+ method: "GET",
+ cancellationToken,
+ });
+ logger.info(
+ `kyc long-polling response status: HTTP ${kycStatusRes.status}`,
+ );
+ 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
+ ) {
+ await transitionKycSatisfied(ws, withdrawalGroup);
+ return { ready: true };
+ } else if (kycStatusRes.status === HttpStatusCode.Accepted) {
+ const kycStatus = await kycStatusRes.json();
+ logger.info(`kyc status: ${j2s(kycStatus)}`);
+ // FIXME: do we need to update the KYC url, or does it always stay constant?
+ return { ready: false };
+ } else {
+ throw Error(
+ `unexpected response from kyc-check (${kycStatusRes.status})`,
+ );
+ }
+ });
+ return OperationAttemptResult.longpoll();
+}
+
async function processWithdrawalGroupPendingReady(
ws: InternalWalletState,
withdrawalGroup: WithdrawalGroupRecord,
@@ -1419,8 +1568,6 @@ async function processWithdrawalGroupPendingReady(
}
let numFinished = 0;
- let numKycRequired = 0;
- let finishedForFirstTime = false;
const errorsPerCoin: Record<number, TalerErrorDetail> = {};
let numPlanchetErrors = 0;
const maxReportedErrors = 5;
@@ -1439,9 +1586,6 @@ async function processWithdrawalGroupPendingReady(
if (x.planchetStatus === PlanchetStatus.WithdrawalDone) {
numFinished++;
}
- if (x.planchetStatus === PlanchetStatus.KycRequired) {
- numKycRequired++;
- }
if (x.lastError) {
numPlanchetErrors++;
if (numPlanchetErrors < maxReportedErrors) {
@@ -1452,7 +1596,6 @@ async function processWithdrawalGroupPendingReady(
const oldTxState = computeWithdrawalTransactionStatus(wg);
logger.info(`now withdrawn ${numFinished} of ${numTotalCoins} coins`);
if (wg.timestampFinish === undefined && numFinished === numTotalCoins) {
- finishedForFirstTime = true;
wg.timestampFinish = TalerPreciseTimestamp.now();
wg.status = WithdrawalGroupStatus.Finished;
}
@@ -1475,46 +1618,6 @@ async function processWithdrawalGroupPendingReady(
notifyTransition(ws, transactionId, res.transitionInfo);
- const { kycInfo } = res;
-
- if (numKycRequired > 0) {
- if (kycInfo) {
- const txId = constructTransactionIdentifier({
- tag: TransactionType.Withdrawal,
- withdrawalGroupId: withdrawalGroup.withdrawalGroupId,
- });
- await checkWithdrawalKycStatus(
- ws,
- withdrawalGroup.exchangeBaseUrl,
- txId,
- kycInfo,
- "individual",
- );
- return {
- type: OperationAttemptResultType.Pending,
- result: undefined,
- };
- } else {
- throw TalerError.fromDetail(
- TalerErrorCode.WALLET_WITHDRAWAL_KYC_REQUIRED,
- {
- //FIXME we can't rise KYC error here since we don't have the url
- } as any,
- `KYC check required for withdrawal (not yet implemented in wallet-core)`,
- );
- }
- }
- if (numFinished != numTotalCoins) {
- throw TalerError.fromDetail(
- TalerErrorCode.WALLET_WITHDRAWAL_GROUP_INCOMPLETE,
- {
- numErrors: numPlanchetErrors,
- errorsPerCoin,
- },
- `withdrawal did not finish (${numFinished} / ${numTotalCoins} coins withdrawn)`,
- );
- }
-
return {
type: OperationAttemptResultType.Finished,
result: undefined,
@@ -1588,53 +1691,30 @@ export async function processWithdrawalGroup(
result: undefined,
};
}
+ case WithdrawalGroupStatus.PendingAml:
+ // FIXME: Handle this case, withdrawal doesn't support AML yet.
+ return OperationAttemptResult.pendingEmpty();
+ case WithdrawalGroupStatus.PendingKyc:
+ return processWithdrawalGroupPendingKyc(ws, withdrawalGroup);
case WithdrawalGroupStatus.PendingReady:
// Continue with the actual withdrawal!
return await processWithdrawalGroupPendingReady(ws, withdrawalGroup);
case WithdrawalGroupStatus.AbortingBank:
return await processWithdrawalGroupAbortingBank(ws, withdrawalGroup);
+ case WithdrawalGroupStatus.AbortedBank:
+ case WithdrawalGroupStatus.AbortedExchange:
+ case WithdrawalGroupStatus.FailedAbortingBank:
+ case WithdrawalGroupStatus.SuspendedAbortingBank:
+ case WithdrawalGroupStatus.SuspendedAml:
+ case WithdrawalGroupStatus.SuspendedKyc:
+ case WithdrawalGroupStatus.SuspendedQueryingStatus:
+ case WithdrawalGroupStatus.SuspendedReady:
+ case WithdrawalGroupStatus.SuspendedRegisteringBank:
+ case WithdrawalGroupStatus.SuspendedWaitConfirmBank:
+ // Nothing to do.
+ return OperationAttemptResult.finishedEmpty();
default:
- throw new InvariantViolatedError(
- `unknown withdrawal group status: ${withdrawalGroup.status}`,
- );
- }
-}
-
-export async function checkWithdrawalKycStatus(
- ws: InternalWalletState,
- exchangeUrl: string,
- txId: string,
- kycInfo: KycPendingInfo,
- userType: KycUserType,
-): Promise<void> {
- const url = new URL(
- `kyc-check/${kycInfo.requirementRow}/${kycInfo.paytoHash}/${userType}`,
- exchangeUrl,
- );
- logger.info(`kyc url ${url.href}`);
- const kycStatusRes = await ws.http.fetch(url.href, {
- method: "GET",
- });
- 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
- ) {
- logger.warn("kyc requested, but already fulfilled");
- return;
- } else if (kycStatusRes.status === HttpStatusCode.Accepted) {
- const kycStatus = await kycStatusRes.json();
- logger.info(`kyc status: ${j2s(kycStatus)}`);
- throw TalerError.fromDetail(
- TalerErrorCode.WALLET_WITHDRAWAL_KYC_REQUIRED, //FIXME: another error code or rename for merge
- {
- kycUrl: kycStatus.kyc_url,
- },
- `KYC check required for transfer`,
- );
- } else {
- throw Error(`unexpected response from kyc-check (${kycStatusRes.status})`);
+ assertUnreachable(withdrawalGroup.status);
}
}