handle kyc error on invoice and transfer

This commit is contained in:
Sebastian 2023-03-29 00:06:24 -03:00
parent f414ca39e4
commit efbde0e160
No known key found for this signature in database
GPG Key ID: 173909D1A5F66069
6 changed files with 155 additions and 39 deletions

View File

@ -62,7 +62,7 @@ export enum NotificationType {
PendingOperationProcessed = "pending-operation-processed", PendingOperationProcessed = "pending-operation-processed",
ProposalRefused = "proposal-refused", ProposalRefused = "proposal-refused",
ReserveRegisteredWithBank = "reserve-registered-with-bank", ReserveRegisteredWithBank = "reserve-registered-with-bank",
WithdrawalGroupKycRequested = "withdrawal-group-kyc-requested", KycRequested = "kyc-requested",
WithdrawalGroupBankConfirmed = "withdrawal-group-bank-confirmed", WithdrawalGroupBankConfirmed = "withdrawal-group-bank-confirmed",
WithdrawalGroupReserveReady = "withdrawal-group-reserve-ready", WithdrawalGroupReserveReady = "withdrawal-group-reserve-ready",
PeerPullCreditReady = "peer-pull-credit-ready", PeerPullCreditReady = "peer-pull-credit-ready",
@ -125,8 +125,8 @@ export interface RefreshMeltedNotification {
type: NotificationType.RefreshMelted; type: NotificationType.RefreshMelted;
} }
export interface WithdrawalGroupKycRequested { export interface KycRequestedNotification {
type: NotificationType.WithdrawalGroupKycRequested; type: NotificationType.KycRequested;
transactionId: string; transactionId: string;
kycUrl: string; kycUrl: string;
} }
@ -324,7 +324,7 @@ export type WalletNotification =
| ReserveRegisteredWithBankNotification | ReserveRegisteredWithBankNotification
| ReserveNotYetFoundNotification | ReserveNotYetFoundNotification
| PayOperationSuccessNotification | PayOperationSuccessNotification
| WithdrawalGroupKycRequested | KycRequestedNotification
| WithdrawalGroupBankConfirmed | WithdrawalGroupBankConfirmed
| WithdrawalGroupReserveReadyNotification | WithdrawalGroupReserveReadyNotification
| PeerPullCreditReadyNotification; | PeerPullCreditReadyNotification;

View File

@ -1781,6 +1781,7 @@ export enum PeerPullPaymentInitiationStatus {
* invoice and deposit money into it. * invoice and deposit money into it.
*/ */
PurseCreated = 11 /* ACTIVE_START + 1 */, PurseCreated = 11 /* ACTIVE_START + 1 */,
KycRequired = 12 /* ACTIVE_START + 2 */,
PurseDeposited = 50 /* DORMANT_START */, PurseDeposited = 50 /* DORMANT_START */,
} }
@ -1831,12 +1832,15 @@ export interface PeerPullPaymentInitiationRecord {
*/ */
status: PeerPullPaymentInitiationStatus; status: PeerPullPaymentInitiationStatus;
kycInfo?: KycPendingInfo;
withdrawalGroupId: string | undefined; withdrawalGroupId: string | undefined;
} }
export enum PeerPushPaymentIncomingStatus { export enum PeerPushPaymentIncomingStatus {
Proposed = 30 /* USER_ATTENTION_START */, Proposed = 30 /* USER_ATTENTION_START */,
Accepted = 10 /* ACTIVE_START */, Accepted = 10 /* ACTIVE_START */,
KycRequired = 11 /* ACTIVE_START + 1 */,
/** /**
* Merge was successful and withdrawal group has been created, now * Merge was successful and withdrawal group has been created, now
* everything is in the hand of the withdrawal group. * everything is in the hand of the withdrawal group.
@ -1887,6 +1891,8 @@ export interface PeerPushPaymentIncomingRecord {
* with older (ver_minor<4) DB versions. * with older (ver_minor<4) DB versions.
*/ */
currency: string | undefined; currency: string | undefined;
kycInfo?: KycPendingInfo;
} }
export enum PeerPullPaymentIncomingStatus { export enum PeerPullPaymentIncomingStatus {

View File

@ -73,10 +73,15 @@ import {
codecForTimestamp, codecForTimestamp,
CancellationToken, CancellationToken,
NotificationType, NotificationType,
HttpStatusCode,
codecForWalletKycUuid,
WalletKycUuid,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { SpendCoinDetails } from "../crypto/cryptoImplementation.js"; import { SpendCoinDetails } from "../crypto/cryptoImplementation.js";
import { import {
DenominationRecord, DenominationRecord,
KycPendingInfo,
KycUserType,
OperationStatus, OperationStatus,
PeerPullPaymentIncomingStatus, PeerPullPaymentIncomingStatus,
PeerPullPaymentInitiationRecord, PeerPullPaymentInitiationRecord,
@ -115,6 +120,7 @@ import { getPeerPaymentBalanceDetailsInTx } from "./balance.js";
import { updateExchangeFromUrl } from "./exchanges.js"; import { updateExchangeFromUrl } from "./exchanges.js";
import { getTotalRefreshCost } from "./refresh.js"; import { getTotalRefreshCost } from "./refresh.js";
import { import {
checkWithdrawalKycStatus,
getExchangeWithdrawalInfo, getExchangeWithdrawalInfo,
internalCreateWithdrawalGroup, internalCreateWithdrawalGroup,
processWithdrawalGroup, processWithdrawalGroup,
@ -866,6 +872,23 @@ export async function processPeerPushCredit(
const amount = Amounts.parseOrThrow(contractTerms.amount); 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, { const mergeReserveInfo = await getMergeReserveInfo(ws, {
exchangeBaseUrl: peerInc.exchangeBaseUrl, exchangeBaseUrl: peerInc.exchangeBaseUrl,
}); });
@ -902,10 +925,40 @@ export async function processPeerPushCredit(
reserve_sig: sigRes.accountSig, 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)}`); logger.trace(`merge request: ${j2s(mergeReq)}`);
const res = await readSuccessResponseJsonOrThrow(mergeHttpReq, codecForAny()); const res = await readSuccessResponseJsonOrThrow(
mergeHttpResp,
codecForAny(),
);
logger.trace(`merge response: ${j2s(res)}`); logger.trace(`merge response: ${j2s(res)}`);
await internalCreateWithdrawalGroup(ws, { await internalCreateWithdrawalGroup(ws, {
@ -932,7 +985,10 @@ export async function processPeerPushCredit(
if (!peerInc) { if (!peerInc) {
return; return;
} }
if (peerInc.status === PeerPushPaymentIncomingStatus.Accepted) { if (
peerInc.status === PeerPushPaymentIncomingStatus.Accepted ||
peerInc.status === PeerPushPaymentIncomingStatus.KycRequired
) {
peerInc.status = PeerPushPaymentIncomingStatus.WithdrawalCreated; peerInc.status = PeerPushPaymentIncomingStatus.WithdrawalCreated;
} }
await tx.peerPushPaymentIncoming.put(peerInc); await tx.peerPushPaymentIncoming.put(peerInc);
@ -1423,6 +1479,22 @@ export async function processPeerPullCredit(
return { return {
type: OperationAttemptResultType.Longpoll, 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: case PeerPullPaymentInitiationStatus.Initial:
break; break;
default: default:
@ -1496,6 +1568,31 @@ export async function processPeerPullCredit(
reservePurseReqBody, 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()); const resp = await readSuccessResponseJsonOrThrow(httpResp, codecForAny());
logger.info(`reserve merge response: ${j2s(resp)}`); logger.info(`reserve merge response: ${j2s(resp)}`);

View File

@ -116,9 +116,7 @@ import {
WALLET_BANK_INTEGRATION_PROTOCOL_VERSION, WALLET_BANK_INTEGRATION_PROTOCOL_VERSION,
WALLET_EXCHANGE_PROTOCOL_VERSION, WALLET_EXCHANGE_PROTOCOL_VERSION,
} from "../versions.js"; } from "../versions.js";
import { import { makeTransactionId } from "./common.js";
makeTransactionId,
} from "./common.js";
import { import {
getExchangeDetails, getExchangeDetails,
getExchangePaytoUri, getExchangePaytoUri,
@ -1226,9 +1224,14 @@ export async function processWithdrawalGroup(
if (numKycRequired > 0) { if (numKycRequired > 0) {
if (kycInfo) { if (kycInfo) {
const txId = makeTransactionId(
TransactionType.Withdrawal,
withdrawalGroup.withdrawalGroupId,
);
await checkWithdrawalKycStatus( await checkWithdrawalKycStatus(
ws, ws,
withdrawalGroup, withdrawalGroup.exchangeBaseUrl,
txId,
kycInfo, kycInfo,
"individual", "individual",
); );
@ -1271,42 +1274,44 @@ export async function processWithdrawalGroup(
export async function checkWithdrawalKycStatus( export async function checkWithdrawalKycStatus(
ws: InternalWalletState, ws: InternalWalletState,
wg: WithdrawalGroupRecord, exchangeUrl: string,
txId: string,
kycInfo: KycPendingInfo, kycInfo: KycPendingInfo,
userType: KycUserType, userType: KycUserType,
): Promise<void> { ): Promise<void> {
const exchangeUrl = wg.exchangeBaseUrl;
const url = new URL( const url = new URL(
`kyc-check/${kycInfo.requirementRow}/${kycInfo.paytoHash}/${userType}`, `kyc-check/${kycInfo.requirementRow}/${kycInfo.paytoHash}/${userType}`,
exchangeUrl, exchangeUrl,
); );
logger.info(`kyc url ${url.href}`); logger.info(`kyc url ${url.href}`);
const kycStatusReq = await ws.http.fetch(url.href, { const kycStatusRes = await ws.http.fetch(url.href, {
method: "GET", 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"); logger.warn("kyc requested, but already fulfilled");
return; return;
} else if (kycStatusReq.status === HttpStatusCode.Accepted) { } else if (kycStatusRes.status === HttpStatusCode.Accepted) {
const kycStatus = await kycStatusReq.json(); const kycStatus = await kycStatusRes.json();
logger.info(`kyc status: ${j2s(kycStatus)}`); logger.info(`kyc status: ${j2s(kycStatus)}`);
ws.notify({ ws.notify({
type: NotificationType.WithdrawalGroupKycRequested, type: NotificationType.KycRequested,
kycUrl: kycStatus.kyc_url, kycUrl: kycStatus.kyc_url,
transactionId: makeTransactionId( transactionId: txId,
TransactionType.Withdrawal,
wg.withdrawalGroupId,
),
}); });
throw TalerError.fromDetail( 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, kycUrl: kycStatus.kyc_url,
}, },
`KYC check required for withdrawal`, `KYC check required for transfer`,
); );
} else { } else {
throw Error(`unexpected response from kyc-check (${kycStatusReq.status})`); throw Error(`unexpected response from kyc-check (${kycStatusRes.status})`);
} }
} }

View File

@ -140,12 +140,19 @@ export function View({
}); });
} }
const api = useBackendContext(); const api = useBackendContext();
const fileRef = useRef<HTMLInputElement>(null); const fileRef = useRef<HTMLInputElement>(null);
async function onImportDatabase(str: string): Promise<void> { async function onImportDatabase(str: string): Promise<void> {
return api.wallet.call(WalletApiOperation.ImportDb, { return api.wallet.call(WalletApiOperation.ImportDb, {
dump: JSON.parse(str), 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 currencies: { [ex: string]: string } = {};
const money_by_exchange = coins.reduce( const money_by_exchange = coins.reduce(
(prev, cur) => { (prev, cur) => {
@ -171,7 +178,6 @@ export function View({
[exchange_name: string]: CalculatedCoinfInfo[]; [exchange_name: string]: CalculatedCoinfInfo[];
}, },
); );
const exchanges = Object.keys(money_by_exchange);
const [tagName, setTagName] = useState(""); const [tagName, setTagName] = useState("");
const [logLevel, setLogLevel] = useState("info"); const [logLevel, setLogLevel] = useState("info");
@ -324,27 +330,28 @@ export function View({
variant="contained" variant="contained"
onClick={async () => { onClick={async () => {
const result = await Promise.all( const result = await Promise.all(
exchanges.map(async (ex) => { exchangeList.map(async (exchange) => {
const url = exchange.exchangeBaseUrl;
const oldKeys = JSON.stringify( const oldKeys = JSON.stringify(
await (await fetch(`${ex}keys`)).json(), await (await fetch(`${url}keys`)).json(),
); );
const oldWire = JSON.stringify( const oldWire = JSON.stringify(
await (await fetch(`${ex}wire`)).json(), await (await fetch(`${url}wire`)).json(),
); );
const newKeys = JSON.stringify( const newKeys = JSON.stringify(
await ( await (
await fetch(`${ex}keys`, { cache: "no-cache" }) await fetch(`${url}keys`, { cache: "no-cache" })
).json(), ).json(),
); );
const newWire = JSON.stringify( const newWire = JSON.stringify(
await ( await (
await fetch(`${ex}wire`, { cache: "no-cache" }) await fetch(`${url}wire`, { cache: "no-cache" })
).json(), ).json(),
); );
return oldKeys !== newKeys || newWire !== oldWire; return oldKeys !== newKeys || newWire !== oldWire;
}), }),
); );
const ex = exchanges.filter((e, i) => result[i]); const ex = exchangeList.filter((e, i) => result[i]);
if (!ex.length) { if (!ex.length) {
alert("no exchange was outdated"); alert("no exchange was outdated");
} else { } else {

View File

@ -879,7 +879,8 @@ export function TransactionView({
kind="neutral" kind="neutral"
/> />
{transaction.extendedStatus === {transaction.extendedStatus ===
ExtendedStatus.Pending /** pending is not-pay */ && ( ExtendedStatus.Pending /** pending is not-pay */ &&
!transaction.error && (
<Part <Part
title={i18n.str`URI`} title={i18n.str`URI`}
text={<ShowQrWithCopy text={transaction.talerUri} />} text={<ShowQrWithCopy text={transaction.talerUri} />}