From efbde0e16033542ae104f5365be5cee6e65ef7b0 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Wed, 29 Mar 2023 00:06:24 -0300 Subject: handle kyc error on invoice and transfer --- packages/taler-util/src/notifications.ts | 8 +- packages/taler-wallet-core/src/db.ts | 6 ++ .../taler-wallet-core/src/operations/pay-peer.ts | 103 ++++++++++++++++++++- .../taler-wallet-core/src/operations/withdraw.ts | 41 ++++---- .../src/wallet/DeveloperPage.tsx | 21 +++-- .../src/wallet/Transaction.tsx | 15 +-- 6 files changed, 155 insertions(+), 39 deletions(-) (limited to 'packages') diff --git a/packages/taler-util/src/notifications.ts b/packages/taler-util/src/notifications.ts index 51d573a98..0d85c85e9 100644 --- a/packages/taler-util/src/notifications.ts +++ b/packages/taler-util/src/notifications.ts @@ -62,7 +62,7 @@ export enum NotificationType { PendingOperationProcessed = "pending-operation-processed", ProposalRefused = "proposal-refused", ReserveRegisteredWithBank = "reserve-registered-with-bank", - WithdrawalGroupKycRequested = "withdrawal-group-kyc-requested", + KycRequested = "kyc-requested", WithdrawalGroupBankConfirmed = "withdrawal-group-bank-confirmed", WithdrawalGroupReserveReady = "withdrawal-group-reserve-ready", PeerPullCreditReady = "peer-pull-credit-ready", @@ -125,8 +125,8 @@ export interface RefreshMeltedNotification { type: NotificationType.RefreshMelted; } -export interface WithdrawalGroupKycRequested { - type: NotificationType.WithdrawalGroupKycRequested; +export interface KycRequestedNotification { + type: NotificationType.KycRequested; transactionId: string; kycUrl: string; } @@ -324,7 +324,7 @@ export type WalletNotification = | ReserveRegisteredWithBankNotification | ReserveNotYetFoundNotification | PayOperationSuccessNotification - | WithdrawalGroupKycRequested + | KycRequestedNotification | WithdrawalGroupBankConfirmed | WithdrawalGroupReserveReadyNotification | PeerPullCreditReadyNotification; diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts index a7bdda3ec..cd676b7ca 100644 --- a/packages/taler-wallet-core/src/db.ts +++ b/packages/taler-wallet-core/src/db.ts @@ -1781,6 +1781,7 @@ export enum PeerPullPaymentInitiationStatus { * invoice and deposit money into it. */ PurseCreated = 11 /* ACTIVE_START + 1 */, + KycRequired = 12 /* ACTIVE_START + 2 */, PurseDeposited = 50 /* DORMANT_START */, } @@ -1831,12 +1832,15 @@ export interface PeerPullPaymentInitiationRecord { */ status: PeerPullPaymentInitiationStatus; + kycInfo?: KycPendingInfo; + withdrawalGroupId: string | undefined; } export enum PeerPushPaymentIncomingStatus { Proposed = 30 /* USER_ATTENTION_START */, Accepted = 10 /* ACTIVE_START */, + KycRequired = 11 /* ACTIVE_START + 1 */, /** * Merge was successful and withdrawal group has been created, now * everything is in the hand of the withdrawal group. @@ -1887,6 +1891,8 @@ export interface PeerPushPaymentIncomingRecord { * with older (ver_minor<4) DB versions. */ currency: string | undefined; + + kycInfo?: KycPendingInfo; } export enum PeerPullPaymentIncomingStatus { diff --git a/packages/taler-wallet-core/src/operations/pay-peer.ts b/packages/taler-wallet-core/src/operations/pay-peer.ts index 73dd3bf2c..ff0e15c00 100644 --- a/packages/taler-wallet-core/src/operations/pay-peer.ts +++ b/packages/taler-wallet-core/src/operations/pay-peer.ts @@ -73,10 +73,15 @@ import { codecForTimestamp, CancellationToken, NotificationType, + HttpStatusCode, + codecForWalletKycUuid, + WalletKycUuid, } from "@gnu-taler/taler-util"; import { SpendCoinDetails } from "../crypto/cryptoImplementation.js"; import { DenominationRecord, + KycPendingInfo, + KycUserType, OperationStatus, PeerPullPaymentIncomingStatus, PeerPullPaymentInitiationRecord, @@ -115,6 +120,7 @@ import { getPeerPaymentBalanceDetailsInTx } from "./balance.js"; import { updateExchangeFromUrl } from "./exchanges.js"; import { getTotalRefreshCost } from "./refresh.js"; import { + checkWithdrawalKycStatus, getExchangeWithdrawalInfo, internalCreateWithdrawalGroup, processWithdrawalGroup, @@ -866,6 +872,23 @@ export async function processPeerPushCredit( const amount = Amounts.parseOrThrow(contractTerms.amount); + if ( + peerInc.status === PeerPushPaymentIncomingStatus.KycRequired && + peerInc.kycInfo + ) { + const txId = makeTransactionId( + TransactionType.PeerPushCredit, + peerInc.peerPushPaymentIncomingId, + ); + await checkWithdrawalKycStatus( + ws, + peerInc.exchangeBaseUrl, + txId, + peerInc.kycInfo, + "individual", + ); + } + const mergeReserveInfo = await getMergeReserveInfo(ws, { exchangeBaseUrl: peerInc.exchangeBaseUrl, }); @@ -902,10 +925,40 @@ export async function processPeerPushCredit( reserve_sig: sigRes.accountSig, }; - const mergeHttpReq = await ws.http.postJson(mergePurseUrl.href, mergeReq); + const mergeHttpResp = await ws.http.postJson(mergePurseUrl.href, mergeReq); + + if (mergeHttpResp.status === HttpStatusCode.UnavailableForLegalReasons) { + const respJson = await mergeHttpResp.json(); + const kycPending = codecForWalletKycUuid().decode(respJson); + logger.info(`kyc uuid response: ${j2s(kycPending)}`); + + await ws.db + .mktx((x) => [x.peerPushPaymentIncoming]) + .runReadWrite(async (tx) => { + const peerInc = await tx.peerPushPaymentIncoming.get( + peerPushPaymentIncomingId, + ); + if (!peerInc) { + return; + } + peerInc.kycInfo = { + paytoHash: kycPending.h_payto, + requirementRow: kycPending.requirement_row, + }; + peerInc.status = PeerPushPaymentIncomingStatus.KycRequired; + await tx.peerPushPaymentIncoming.put(peerInc); + }); + return { + type: OperationAttemptResultType.Pending, + result: undefined, + }; + } logger.trace(`merge request: ${j2s(mergeReq)}`); - const res = await readSuccessResponseJsonOrThrow(mergeHttpReq, codecForAny()); + const res = await readSuccessResponseJsonOrThrow( + mergeHttpResp, + codecForAny(), + ); logger.trace(`merge response: ${j2s(res)}`); await internalCreateWithdrawalGroup(ws, { @@ -932,7 +985,10 @@ export async function processPeerPushCredit( if (!peerInc) { return; } - if (peerInc.status === PeerPushPaymentIncomingStatus.Accepted) { + if ( + peerInc.status === PeerPushPaymentIncomingStatus.Accepted || + peerInc.status === PeerPushPaymentIncomingStatus.KycRequired + ) { peerInc.status = PeerPushPaymentIncomingStatus.WithdrawalCreated; } await tx.peerPushPaymentIncoming.put(peerInc); @@ -1423,6 +1479,22 @@ export async function processPeerPullCredit( return { type: OperationAttemptResultType.Longpoll, }; + case PeerPullPaymentInitiationStatus.KycRequired: { + if (pullIni.kycInfo) { + const txId = makeTransactionId( + TransactionType.PeerPullCredit, + pullIni.pursePub, + ); + await checkWithdrawalKycStatus( + ws, + pullIni.exchangeBaseUrl, + txId, + pullIni.kycInfo, + "individual", + ); + } + break; + } case PeerPullPaymentInitiationStatus.Initial: break; default: @@ -1496,6 +1568,31 @@ export async function processPeerPullCredit( reservePurseReqBody, ); + if (httpResp.status === HttpStatusCode.UnavailableForLegalReasons) { + const respJson = await httpResp.json(); + const kycPending = codecForWalletKycUuid().decode(respJson); + logger.info(`kyc uuid response: ${j2s(kycPending)}`); + + await ws.db + .mktx((x) => [x.peerPullPaymentInitiations]) + .runReadWrite(async (tx) => { + const peerIni = await tx.peerPullPaymentInitiations.get(pursePub); + if (!peerIni) { + return; + } + peerIni.kycInfo = { + paytoHash: kycPending.h_payto, + requirementRow: kycPending.requirement_row, + }; + peerIni.status = PeerPullPaymentInitiationStatus.KycRequired; + await tx.peerPullPaymentInitiations.put(peerIni); + }); + return { + type: OperationAttemptResultType.Pending, + result: undefined, + }; + } + const resp = await readSuccessResponseJsonOrThrow(httpResp, codecForAny()); logger.info(`reserve merge response: ${j2s(resp)}`); diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts b/packages/taler-wallet-core/src/operations/withdraw.ts index 3c3878792..2c91d4184 100644 --- a/packages/taler-wallet-core/src/operations/withdraw.ts +++ b/packages/taler-wallet-core/src/operations/withdraw.ts @@ -116,9 +116,7 @@ import { WALLET_BANK_INTEGRATION_PROTOCOL_VERSION, WALLET_EXCHANGE_PROTOCOL_VERSION, } from "../versions.js"; -import { - makeTransactionId, -} from "./common.js"; +import { makeTransactionId } from "./common.js"; import { getExchangeDetails, getExchangePaytoUri, @@ -1226,9 +1224,14 @@ export async function processWithdrawalGroup( if (numKycRequired > 0) { if (kycInfo) { + const txId = makeTransactionId( + TransactionType.Withdrawal, + withdrawalGroup.withdrawalGroupId, + ); await checkWithdrawalKycStatus( ws, - withdrawalGroup, + withdrawalGroup.exchangeBaseUrl, + txId, kycInfo, "individual", ); @@ -1271,42 +1274,44 @@ export async function processWithdrawalGroup( export async function checkWithdrawalKycStatus( ws: InternalWalletState, - wg: WithdrawalGroupRecord, + exchangeUrl: string, + txId: string, kycInfo: KycPendingInfo, userType: KycUserType, ): Promise { - const exchangeUrl = wg.exchangeBaseUrl; const url = new URL( `kyc-check/${kycInfo.requirementRow}/${kycInfo.paytoHash}/${userType}`, exchangeUrl, ); logger.info(`kyc url ${url.href}`); - const kycStatusReq = await ws.http.fetch(url.href, { + const kycStatusRes = await ws.http.fetch(url.href, { method: "GET", }); - if (kycStatusReq.status === HttpStatusCode.Ok) { + 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 (kycStatusReq.status === HttpStatusCode.Accepted) { - const kycStatus = await kycStatusReq.json(); + } else if (kycStatusRes.status === HttpStatusCode.Accepted) { + const kycStatus = await kycStatusRes.json(); logger.info(`kyc status: ${j2s(kycStatus)}`); ws.notify({ - type: NotificationType.WithdrawalGroupKycRequested, + type: NotificationType.KycRequested, kycUrl: kycStatus.kyc_url, - transactionId: makeTransactionId( - TransactionType.Withdrawal, - wg.withdrawalGroupId, - ), + transactionId: txId, }); throw TalerError.fromDetail( - TalerErrorCode.WALLET_WITHDRAWAL_KYC_REQUIRED, + TalerErrorCode.WALLET_WITHDRAWAL_KYC_REQUIRED, //FIXME: another error code or rename for merge { kycUrl: kycStatus.kyc_url, }, - `KYC check required for withdrawal`, + `KYC check required for transfer`, ); } else { - throw Error(`unexpected response from kyc-check (${kycStatusReq.status})`); + throw Error(`unexpected response from kyc-check (${kycStatusRes.status})`); } } diff --git a/packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx b/packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx index 2db2041d4..339e69213 100644 --- a/packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx +++ b/packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx @@ -140,12 +140,19 @@ export function View({ }); } const api = useBackendContext(); + const fileRef = useRef(null); async function onImportDatabase(str: string): Promise { return api.wallet.call(WalletApiOperation.ImportDb, { dump: JSON.parse(str), }); } + + const hook = useAsyncAsHook(() => + api.wallet.call(WalletApiOperation.ListExchanges, {}), + ); + const exchangeList = hook && !hook.hasError ? hook.response.exchanges : []; + const currencies: { [ex: string]: string } = {}; const money_by_exchange = coins.reduce( (prev, cur) => { @@ -171,7 +178,6 @@ export function View({ [exchange_name: string]: CalculatedCoinfInfo[]; }, ); - const exchanges = Object.keys(money_by_exchange); const [tagName, setTagName] = useState(""); const [logLevel, setLogLevel] = useState("info"); @@ -324,27 +330,28 @@ export function View({ variant="contained" onClick={async () => { const result = await Promise.all( - exchanges.map(async (ex) => { + exchangeList.map(async (exchange) => { + const url = exchange.exchangeBaseUrl; const oldKeys = JSON.stringify( - await (await fetch(`${ex}keys`)).json(), + await (await fetch(`${url}keys`)).json(), ); const oldWire = JSON.stringify( - await (await fetch(`${ex}wire`)).json(), + await (await fetch(`${url}wire`)).json(), ); const newKeys = JSON.stringify( await ( - await fetch(`${ex}keys`, { cache: "no-cache" }) + await fetch(`${url}keys`, { cache: "no-cache" }) ).json(), ); const newWire = JSON.stringify( await ( - await fetch(`${ex}wire`, { cache: "no-cache" }) + await fetch(`${url}wire`, { cache: "no-cache" }) ).json(), ); return oldKeys !== newKeys || newWire !== oldWire; }), ); - const ex = exchanges.filter((e, i) => result[i]); + const ex = exchangeList.filter((e, i) => result[i]); if (!ex.length) { alert("no exchange was outdated"); } else { diff --git a/packages/taler-wallet-webextension/src/wallet/Transaction.tsx b/packages/taler-wallet-webextension/src/wallet/Transaction.tsx index b9b1aa198..217a77575 100644 --- a/packages/taler-wallet-webextension/src/wallet/Transaction.tsx +++ b/packages/taler-wallet-webextension/src/wallet/Transaction.tsx @@ -879,13 +879,14 @@ export function TransactionView({ kind="neutral" /> {transaction.extendedStatus === - ExtendedStatus.Pending /** pending is not-pay */ && ( - } - kind="neutral" - /> - )} + ExtendedStatus.Pending /** pending is not-pay */ && + !transaction.error && ( + } + kind="neutral" + /> + )}