wallet-core: fix withdrawal KYC transitions and use long-polling
This commit is contained in:
parent
30fb003ee3
commit
5eb339b836
@ -1416,6 +1416,8 @@ export interface WithdrawalGroupRecord {
|
|||||||
|
|
||||||
kycPending?: KycPendingInfo;
|
kycPending?: KycPendingInfo;
|
||||||
|
|
||||||
|
kycUrl?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Secret seed used to derive planchets.
|
* Secret seed used to derive planchets.
|
||||||
* Stored since planchets are created lazily.
|
* Stored since planchets are created lazily.
|
||||||
|
@ -655,6 +655,7 @@ function buildTransactionForBankIntegratedWithdraw(
|
|||||||
wgRecord.status === WithdrawalGroupStatus.Finished ||
|
wgRecord.status === WithdrawalGroupStatus.Finished ||
|
||||||
wgRecord.status === WithdrawalGroupStatus.PendingReady,
|
wgRecord.status === WithdrawalGroupStatus.PendingReady,
|
||||||
},
|
},
|
||||||
|
kycUrl: wgRecord.kycUrl,
|
||||||
exchangeBaseUrl: wgRecord.exchangeBaseUrl,
|
exchangeBaseUrl: wgRecord.exchangeBaseUrl,
|
||||||
timestamp: wgRecord.timestampStart,
|
timestamp: wgRecord.timestampStart,
|
||||||
transactionId: constructTransactionIdentifier({
|
transactionId: constructTransactionIdentifier({
|
||||||
|
@ -731,6 +731,96 @@ interface WithdrawalBatchResult {
|
|||||||
batchResp: ExchangeWithdrawBatchResponse;
|
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.
|
* 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> {
|
async function storeCoinError(e: any, coinIdx: number): Promise<void> {
|
||||||
const errDetail = getErrorDetailFromException(e);
|
const errDetail = getErrorDetailFromException(e);
|
||||||
logger.trace("withdrawal request failed", e);
|
logger.trace("withdrawal request failed", e);
|
||||||
@ -872,7 +925,7 @@ async function processPlanchetExchangeBatchRequest(
|
|||||||
try {
|
try {
|
||||||
const resp = await ws.http.postJson(reqUrl, batchReq);
|
const resp = await ws.http.postJson(reqUrl, batchReq);
|
||||||
if (resp.status === HttpStatusCode.UnavailableForLegalReasons) {
|
if (resp.status === HttpStatusCode.UnavailableForLegalReasons) {
|
||||||
await handleKycRequired(resp, 0);
|
await handleKycRequired(ws, withdrawalGroup, resp, 0, requestCoinIdxs);
|
||||||
}
|
}
|
||||||
const r = await readSuccessResponseJsonOrThrow(
|
const r = await readSuccessResponseJsonOrThrow(
|
||||||
resp,
|
resp,
|
||||||
@ -902,9 +955,15 @@ async function processPlanchetExchangeBatchRequest(
|
|||||||
`reserves/${withdrawalGroup.reservePub}/withdraw`,
|
`reserves/${withdrawalGroup.reservePub}/withdraw`,
|
||||||
withdrawalGroup.exchangeBaseUrl,
|
withdrawalGroup.exchangeBaseUrl,
|
||||||
).href;
|
).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) {
|
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.
|
// We still return blinded coins that we could actually withdraw.
|
||||||
return {
|
return {
|
||||||
coinIdxs: responseCoinIdxs,
|
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(
|
async function processWithdrawalGroupPendingReady(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
withdrawalGroup: WithdrawalGroupRecord,
|
withdrawalGroup: WithdrawalGroupRecord,
|
||||||
@ -1419,8 +1568,6 @@ async function processWithdrawalGroupPendingReady(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let numFinished = 0;
|
let numFinished = 0;
|
||||||
let numKycRequired = 0;
|
|
||||||
let finishedForFirstTime = false;
|
|
||||||
const errorsPerCoin: Record<number, TalerErrorDetail> = {};
|
const errorsPerCoin: Record<number, TalerErrorDetail> = {};
|
||||||
let numPlanchetErrors = 0;
|
let numPlanchetErrors = 0;
|
||||||
const maxReportedErrors = 5;
|
const maxReportedErrors = 5;
|
||||||
@ -1439,9 +1586,6 @@ async function processWithdrawalGroupPendingReady(
|
|||||||
if (x.planchetStatus === PlanchetStatus.WithdrawalDone) {
|
if (x.planchetStatus === PlanchetStatus.WithdrawalDone) {
|
||||||
numFinished++;
|
numFinished++;
|
||||||
}
|
}
|
||||||
if (x.planchetStatus === PlanchetStatus.KycRequired) {
|
|
||||||
numKycRequired++;
|
|
||||||
}
|
|
||||||
if (x.lastError) {
|
if (x.lastError) {
|
||||||
numPlanchetErrors++;
|
numPlanchetErrors++;
|
||||||
if (numPlanchetErrors < maxReportedErrors) {
|
if (numPlanchetErrors < maxReportedErrors) {
|
||||||
@ -1452,7 +1596,6 @@ async function processWithdrawalGroupPendingReady(
|
|||||||
const oldTxState = computeWithdrawalTransactionStatus(wg);
|
const oldTxState = computeWithdrawalTransactionStatus(wg);
|
||||||
logger.info(`now withdrawn ${numFinished} of ${numTotalCoins} coins`);
|
logger.info(`now withdrawn ${numFinished} of ${numTotalCoins} coins`);
|
||||||
if (wg.timestampFinish === undefined && numFinished === numTotalCoins) {
|
if (wg.timestampFinish === undefined && numFinished === numTotalCoins) {
|
||||||
finishedForFirstTime = true;
|
|
||||||
wg.timestampFinish = TalerPreciseTimestamp.now();
|
wg.timestampFinish = TalerPreciseTimestamp.now();
|
||||||
wg.status = WithdrawalGroupStatus.Finished;
|
wg.status = WithdrawalGroupStatus.Finished;
|
||||||
}
|
}
|
||||||
@ -1475,46 +1618,6 @@ async function processWithdrawalGroupPendingReady(
|
|||||||
|
|
||||||
notifyTransition(ws, transactionId, res.transitionInfo);
|
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 {
|
return {
|
||||||
type: OperationAttemptResultType.Finished,
|
type: OperationAttemptResultType.Finished,
|
||||||
result: undefined,
|
result: undefined,
|
||||||
@ -1588,53 +1691,30 @@ export async function processWithdrawalGroup(
|
|||||||
result: undefined,
|
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:
|
case WithdrawalGroupStatus.PendingReady:
|
||||||
// Continue with the actual withdrawal!
|
// Continue with the actual withdrawal!
|
||||||
return await processWithdrawalGroupPendingReady(ws, withdrawalGroup);
|
return await processWithdrawalGroupPendingReady(ws, withdrawalGroup);
|
||||||
case WithdrawalGroupStatus.AbortingBank:
|
case WithdrawalGroupStatus.AbortingBank:
|
||||||
return await processWithdrawalGroupAbortingBank(ws, withdrawalGroup);
|
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:
|
default:
|
||||||
throw new InvariantViolatedError(
|
assertUnreachable(withdrawalGroup.status);
|
||||||
`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})`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user