diff options
Diffstat (limited to 'packages/taler-wallet-core/src/operations')
5 files changed, 385 insertions, 73 deletions
diff --git a/packages/taler-wallet-core/src/operations/common.ts b/packages/taler-wallet-core/src/operations/common.ts index 203bf6788..ef5c54002 100644 --- a/packages/taler-wallet-core/src/operations/common.ts +++ b/packages/taler-wallet-core/src/operations/common.ts @@ -36,7 +36,6 @@ import { TalerErrorDetail, TombstoneIdStr, TransactionIdStr, - TransactionType, } from "@gnu-taler/taler-util"; import { WalletStoresV1, diff --git a/packages/taler-wallet-core/src/operations/pay-peer-common.ts b/packages/taler-wallet-core/src/operations/pay-peer-common.ts index 4b1dd31a5..717b25f49 100644 --- a/packages/taler-wallet-core/src/operations/pay-peer-common.ts +++ b/packages/taler-wallet-core/src/operations/pay-peer-common.ts @@ -25,19 +25,26 @@ import { Codec, CoinPublicKeyString, CoinStatus, + HttpStatusCode, Logger, + NotificationType, PayPeerInsufficientBalanceDetails, + TalerError, + TalerErrorCode, TalerProtocolTimestamp, UnblindedSignature, buildCodecForObject, codecForAmountString, codecForTimestamp, codecOptional, + j2s, strcmp, } from "@gnu-taler/taler-util"; import { SpendCoinDetails } from "../crypto/cryptoImplementation.js"; import { DenominationRecord, + KycPendingInfo, + KycUserType, PeerPushPaymentCoinSelection, ReserveRecord, } from "../db.js"; @@ -45,6 +52,7 @@ import { InternalWalletState } from "../internal-wallet-state.js"; import { checkDbInvariant } from "../util/invariants.js"; import { getPeerPaymentBalanceDetailsInTx } from "./balance.js"; import { getTotalRefreshCost } from "./refresh.js"; +import { OperationAttemptLongpollResult, OperationAttemptResult, OperationAttemptResultType } from "../util/retries.js"; const logger = new Logger("operations/peer-to-peer.ts"); diff --git a/packages/taler-wallet-core/src/operations/pay-peer-pull-credit.ts b/packages/taler-wallet-core/src/operations/pay-peer-pull-credit.ts index b9c9728a1..333202a69 100644 --- a/packages/taler-wallet-core/src/operations/pay-peer-pull-credit.ts +++ b/packages/taler-wallet-core/src/operations/pay-peer-pull-credit.ts @@ -26,6 +26,7 @@ import { InitiatePeerPullCreditRequest, InitiatePeerPullCreditResponse, Logger, + TalerErrorCode, TalerPreciseTimestamp, TransactionAction, TransactionMajorState, @@ -33,12 +34,14 @@ import { TransactionState, TransactionType, WalletAccountMergeFlags, + WalletKycUuid, codecForAny, codecForWalletKycUuid, constructPayPullUri, encodeCrock, getRandomBytes, j2s, + makeErrorDetail, } from "@gnu-taler/taler-util"; import { readSuccessResponseJsonOrErrorCode, @@ -46,6 +49,8 @@ import { throwUnexpectedRequestError, } from "@gnu-taler/taler-util/http"; import { + KycPendingInfo, + KycUserType, PeerPullPaymentInitiationRecord, PeerPullPaymentInitiationStatus, WithdrawalGroupStatus, @@ -167,6 +172,75 @@ export async function queryPurseForPeerPullCredit( }; } +async function longpollKycStatus( + ws: InternalWalletState, + pursePub: string, + exchangeUrl: string, + kycInfo: KycPendingInfo, + userType: KycUserType, +): Promise<OperationAttemptResult> { + const transactionId = constructTransactionIdentifier({ + tag: TransactionType.PeerPullCredit, + pursePub, + }); + const retryTag = constructTaskIdentifier({ + tag: PendingTaskType.PeerPullCredit, + pursePub, + }); + + runLongpollAsync(ws, retryTag, async (ct) => { + const url = new URL( + `kyc-check/${kycInfo.requirementRow}/${kycInfo.paytoHash}/${userType}`, + exchangeUrl, + ); + url.searchParams.set("timeout_ms", "10000"); + logger.info(`kyc url ${url.href}`); + const kycStatusRes = await ws.http.fetch(url.href, { + method: "GET", + cancellationToken: ct, + }); + 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 + ) { + const transitionInfo = await ws.db + .mktx((x) => [x.peerPullPaymentInitiations]) + .runReadWrite(async (tx) => { + const peerIni = await tx.peerPullPaymentInitiations.get( + pursePub, + ); + if (!peerIni) { + return; + } + if ( + peerIni.status !== PeerPullPaymentInitiationStatus.PendingMergeKycRequired + ) { + return; + } + const oldTxState = computePeerPullCreditTransactionState(peerIni); + peerIni.status = PeerPullPaymentInitiationStatus.PendingCreatePurse; + const newTxState = computePeerPullCreditTransactionState(peerIni); + await tx.peerPullPaymentInitiations.put(peerIni); + return { oldTxState, newTxState }; + }); + notifyTransition(ws, transactionId, transitionInfo); + return { ready: true }; + } else if (kycStatusRes.status === HttpStatusCode.Accepted) { + // FIXME: Do we have to update the URL here? + return { ready: false }; + } else { + throw Error( + `unexpected response from kyc-check (${kycStatusRes.status})`, + ); + } + }); + return { + type: OperationAttemptResultType.Longpoll, + }; +} + export async function processPeerPullCredit( ws: InternalWalletState, pursePub: string, @@ -233,20 +307,16 @@ export async function processPeerPullCredit( type: OperationAttemptResultType.Longpoll, }; case PeerPullPaymentInitiationStatus.PendingMergeKycRequired: { - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.PeerPullCredit, - pursePub: pullIni.pursePub, - }); - if (pullIni.kycInfo) { - await checkWithdrawalKycStatus( - ws, - pullIni.exchangeBaseUrl, - transactionId, - pullIni.kycInfo, - "individual", - ); + if (!pullIni.kycInfo) { + throw Error("invalid state, kycInfo required"); } - break; + return await longpollKycStatus( + ws, + pursePub, + pullIni.exchangeBaseUrl, + pullIni.kycInfo, + "individual", + ); } case PeerPullPaymentInitiationStatus.PendingCreatePurse: break; @@ -325,26 +395,7 @@ export async function processPeerPullCredit( 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.PendingMergeKycRequired; - await tx.peerPullPaymentInitiations.put(peerIni); - }); - return { - type: OperationAttemptResultType.Pending, - result: undefined, - }; + return processPeerPullCreditKycRequired(ws, pullIni, kycPending); } const resp = await readSuccessResponseJsonOrThrow(httpResp, codecForAny()); @@ -368,6 +419,89 @@ export async function processPeerPullCredit( }; } +async function processPeerPullCreditKycRequired( + ws: InternalWalletState, + peerIni: PeerPullPaymentInitiationRecord, + kycPending: WalletKycUuid, +): Promise<OperationAttemptResult> { + const transactionId = constructTransactionIdentifier({ + tag: TransactionType.PeerPullCredit, + pursePub: peerIni.pursePub, + }); + const { pursePub } = peerIni; + + const userType = "individual"; + const url = new URL( + `kyc-check/${kycPending.requirement_row}/${kycPending.h_payto}/${userType}`, + peerIni.exchangeBaseUrl, + ); + + 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 { + type: OperationAttemptResultType.Finished, + result: undefined, + }; + } else if (kycStatusRes.status === HttpStatusCode.Accepted) { + const kycStatus = await kycStatusRes.json(); + logger.info(`kyc status: ${j2s(kycStatus)}`); + const { transitionInfo, result } = await ws.db + .mktx((x) => [x.peerPullPaymentInitiations]) + .runReadWrite(async (tx) => { + const peerInc = await tx.peerPullPaymentInitiations.get( + pursePub, + ); + if (!peerInc) { + return { + transitionInfo: undefined, + result: OperationAttemptResult.finishedEmpty(), + }; + } + const oldTxState = computePeerPullCreditTransactionState(peerInc); + peerInc.kycInfo = { + paytoHash: kycPending.h_payto, + requirementRow: kycPending.requirement_row, + }; + peerInc.kycUrl = kycStatus.kyc_url; + peerInc.status = PeerPullPaymentInitiationStatus.PendingMergeKycRequired; + const newTxState = computePeerPullCreditTransactionState(peerInc); + await tx.peerPullPaymentInitiations.put(peerInc); + // We'll remove this eventually! New clients should rely on the + // kycUrl field of the transaction, not the error code. + const res: OperationAttemptResult = { + type: OperationAttemptResultType.Error, + errorDetail: makeErrorDetail( + TalerErrorCode.WALLET_WITHDRAWAL_KYC_REQUIRED, + { + kycUrl: kycStatus.kyc_url, + }, + ), + }; + return { + transitionInfo: { oldTxState, newTxState }, + result: res, + }; + }); + notifyTransition(ws, transactionId, transitionInfo); + return { + type: OperationAttemptResultType.Pending, + result: undefined, + }; + } else { + throw Error(`unexpected response from kyc-check (${kycStatusRes.status})`); + } +} + /** * Check fees and available exchanges for a peer push payment initiation. */ diff --git a/packages/taler-wallet-core/src/operations/pay-peer-push-credit.ts b/packages/taler-wallet-core/src/operations/pay-peer-push-credit.ts index 69e0f3c27..91b0b6022 100644 --- a/packages/taler-wallet-core/src/operations/pay-peer-push-credit.ts +++ b/packages/taler-wallet-core/src/operations/pay-peer-push-credit.ts @@ -44,10 +44,16 @@ import { TransactionMajorState, TransactionMinorState, TransactionState, + TalerError, + TalerErrorCode, + WalletKycUuid, + makeErrorDetail, } from "@gnu-taler/taler-util"; import { readSuccessResponseJsonOrThrow } from "@gnu-taler/taler-util/http"; import { InternalWalletState, + KycPendingInfo, + KycUserType, PeerPullDebitRecordStatus, PeerPushPaymentIncomingRecord, PeerPushPaymentIncomingStatus, @@ -62,9 +68,12 @@ import { queryCoinInfosForSelection, talerPaytoFromExchangeReserve, } from "./pay-peer-common.js"; -import { constructTransactionIdentifier, notifyTransition, stopLongpolling } from "./transactions.js"; import { - checkWithdrawalKycStatus, + constructTransactionIdentifier, + notifyTransition, + stopLongpolling, +} from "./transactions.js"; +import { getExchangeWithdrawalInfo, internalCreateWithdrawalGroup, } from "./withdraw.js"; @@ -75,6 +84,7 @@ import { constructTaskIdentifier, } from "../util/retries.js"; import { assertUnreachable } from "../util/assertUnreachable.js"; +import { runLongpollAsync } from "./common.js"; const logger = new Logger("pay-peer-push-credit.ts"); @@ -215,6 +225,156 @@ export async function preparePeerPushCredit( }; } +async function longpollKycStatus( + ws: InternalWalletState, + peerPushPaymentIncomingId: string, + exchangeUrl: string, + kycInfo: KycPendingInfo, + userType: KycUserType, +): Promise<OperationAttemptResult> { + const transactionId = constructTransactionIdentifier({ + tag: TransactionType.PeerPushCredit, + peerPushPaymentIncomingId, + }); + const retryTag = constructTaskIdentifier({ + tag: PendingTaskType.PeerPushCredit, + peerPushPaymentIncomingId, + }); + + runLongpollAsync(ws, retryTag, async (ct) => { + const url = new URL( + `kyc-check/${kycInfo.requirementRow}/${kycInfo.paytoHash}/${userType}`, + exchangeUrl, + ); + url.searchParams.set("timeout_ms", "10000"); + logger.info(`kyc url ${url.href}`); + const kycStatusRes = await ws.http.fetch(url.href, { + method: "GET", + cancellationToken: ct, + }); + 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 + ) { + const transitionInfo = await ws.db + .mktx((x) => [x.peerPushPaymentIncoming]) + .runReadWrite(async (tx) => { + const peerInc = await tx.peerPushPaymentIncoming.get( + peerPushPaymentIncomingId, + ); + if (!peerInc) { + return; + } + if ( + peerInc.status !== + PeerPushPaymentIncomingStatus.PendingMergeKycRequired + ) { + return; + } + const oldTxState = computePeerPushCreditTransactionState(peerInc); + peerInc.status = PeerPushPaymentIncomingStatus.PendingMerge; + const newTxState = computePeerPushCreditTransactionState(peerInc); + await tx.peerPushPaymentIncoming.put(peerInc); + return { oldTxState, newTxState }; + }); + notifyTransition(ws, transactionId, transitionInfo); + return { ready: true }; + } else if (kycStatusRes.status === HttpStatusCode.Accepted) { + // FIXME: Do we have to update the URL here? + return { ready: false }; + } else { + throw Error( + `unexpected response from kyc-check (${kycStatusRes.status})`, + ); + } + }); + return { + type: OperationAttemptResultType.Longpoll, + }; +} + +async function processPeerPushCreditKycRequired( + ws: InternalWalletState, + peerInc: PeerPushPaymentIncomingRecord, + kycPending: WalletKycUuid, +): Promise<OperationAttemptResult> { + const transactionId = constructTransactionIdentifier({ + tag: TransactionType.PeerPushCredit, + peerPushPaymentIncomingId: peerInc.peerPushPaymentIncomingId, + }); + const { peerPushPaymentIncomingId } = peerInc; + + const userType = "individual"; + const url = new URL( + `kyc-check/${kycPending.requirement_row}/${kycPending.h_payto}/${userType}`, + peerInc.exchangeBaseUrl, + ); + + 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 { + type: OperationAttemptResultType.Finished, + result: undefined, + }; + } else if (kycStatusRes.status === HttpStatusCode.Accepted) { + const kycStatus = await kycStatusRes.json(); + logger.info(`kyc status: ${j2s(kycStatus)}`); + const { transitionInfo, result } = await ws.db + .mktx((x) => [x.peerPushPaymentIncoming]) + .runReadWrite(async (tx) => { + const peerInc = await tx.peerPushPaymentIncoming.get( + peerPushPaymentIncomingId, + ); + if (!peerInc) { + return { + transitionInfo: undefined, + result: OperationAttemptResult.finishedEmpty(), + }; + } + const oldTxState = computePeerPushCreditTransactionState(peerInc); + peerInc.kycInfo = { + paytoHash: kycPending.h_payto, + requirementRow: kycPending.requirement_row, + }; + peerInc.kycUrl = kycStatus.kyc_url; + peerInc.status = PeerPushPaymentIncomingStatus.PendingMergeKycRequired; + const newTxState = computePeerPushCreditTransactionState(peerInc); + await tx.peerPushPaymentIncoming.put(peerInc); + // We'll remove this eventually! New clients should rely on the + // kycUrl field of the transaction, not the error code. + const res: OperationAttemptResult = { + type: OperationAttemptResultType.Error, + errorDetail: makeErrorDetail( + TalerErrorCode.WALLET_WITHDRAWAL_KYC_REQUIRED, + { + kycUrl: kycStatus.kyc_url, + }, + ), + }; + return { + transitionInfo: { oldTxState, newTxState }, + result: res, + }; + }); + notifyTransition(ws, transactionId, transitionInfo); + return result; + } else { + throw Error(`unexpected response from kyc-check (${kycStatusRes.status})`); + } +} + export async function processPeerPushCredit( ws: InternalWalletState, peerPushPaymentIncomingId: string, @@ -246,17 +406,15 @@ export async function processPeerPushCredit( const amount = Amounts.parseOrThrow(contractTerms.amount); if ( - peerInc.status === PeerPushPaymentIncomingStatus.PendingMergeKycRequired && - peerInc.kycInfo + peerInc.status === PeerPushPaymentIncomingStatus.PendingMergeKycRequired ) { - const txId = constructTransactionIdentifier({ - tag: TransactionType.PeerPushCredit, - peerPushPaymentIncomingId: peerInc.peerPushPaymentIncomingId, - }); - await checkWithdrawalKycStatus( + if (!peerInc.kycInfo) { + throw Error("invalid state, kycInfo required"); + } + return await longpollKycStatus( ws, + peerPushPaymentIncomingId, peerInc.exchangeBaseUrl, - txId, peerInc.kycInfo, "individual", ); @@ -298,33 +456,16 @@ export async function processPeerPushCredit( reserve_sig: sigRes.accountSig, }; - const mergeHttpResp = await ws.http.postJson(mergePurseUrl.href, mergeReq); + const mergeHttpResp = await ws.http.fetch(mergePurseUrl.href, { + method: "POST", + body: 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.PendingMergeKycRequired; - await tx.peerPushPaymentIncoming.put(peerInc); - }); - return { - type: OperationAttemptResultType.Pending, - result: undefined, - }; + processPeerPushCreditKycRequired(ws, peerInc, kycPending); } logger.trace(`merge request: ${j2s(mergeReq)}`); @@ -412,7 +553,6 @@ export async function confirmPeerPushCredit( }; } - export async function processPeerPullDebit( ws: InternalWalletState, peerPullPaymentIncomingId: string, @@ -483,7 +623,6 @@ export async function processPeerPullDebit( }; } - export async function suspendPeerPushCreditTransaction( ws: InternalWalletState, peerPushPaymentIncomingId: string, @@ -767,4 +906,4 @@ export function computePeerPushCreditTransactionActions( default: assertUnreachable(pushCreditRecord.status); } -}
\ No newline at end of file +} diff --git a/packages/taler-wallet-core/src/operations/transactions.ts b/packages/taler-wallet-core/src/operations/transactions.ts index 1bd024d28..6fe0f1390 100644 --- a/packages/taler-wallet-core/src/operations/transactions.ts +++ b/packages/taler-wallet-core/src/operations/transactions.ts @@ -117,10 +117,38 @@ import { suspendWithdrawalTransaction, computeWithdrawalTransactionActions, } from "./withdraw.js"; -import { computePeerPullCreditTransactionState, computePeerPullCreditTransactionActions, suspendPeerPullCreditTransaction, failPeerPullCreditTransaction, resumePeerPullCreditTransaction, abortPeerPullCreditTransaction } from "./pay-peer-pull-credit.js"; -import { computePeerPullDebitTransactionState, computePeerPullDebitTransactionActions, suspendPeerPullDebitTransaction, failPeerPullDebitTransaction, resumePeerPullDebitTransaction, abortPeerPullDebitTransaction } from "./pay-peer-pull-debit.js"; -import { computePeerPushCreditTransactionState, computePeerPushCreditTransactionActions, suspendPeerPushCreditTransaction, failPeerPushCreditTransaction, resumePeerPushCreditTransaction, abortPeerPushCreditTransaction } from "./pay-peer-push-credit.js"; -import { computePeerPushDebitTransactionState, computePeerPushDebitTransactionActions, suspendPeerPushDebitTransaction, failPeerPushDebitTransaction, resumePeerPushDebitTransaction, abortPeerPushDebitTransaction } from "./pay-peer-push-debit.js"; +import { + computePeerPullCreditTransactionState, + computePeerPullCreditTransactionActions, + suspendPeerPullCreditTransaction, + failPeerPullCreditTransaction, + resumePeerPullCreditTransaction, + abortPeerPullCreditTransaction, +} from "./pay-peer-pull-credit.js"; +import { + computePeerPullDebitTransactionState, + computePeerPullDebitTransactionActions, + suspendPeerPullDebitTransaction, + failPeerPullDebitTransaction, + resumePeerPullDebitTransaction, + abortPeerPullDebitTransaction, +} from "./pay-peer-pull-debit.js"; +import { + computePeerPushCreditTransactionState, + computePeerPushCreditTransactionActions, + suspendPeerPushCreditTransaction, + failPeerPushCreditTransaction, + resumePeerPushCreditTransaction, + abortPeerPushCreditTransaction, +} from "./pay-peer-push-credit.js"; +import { + computePeerPushDebitTransactionState, + computePeerPushDebitTransactionActions, + suspendPeerPushDebitTransaction, + failPeerPushDebitTransaction, + resumePeerPushDebitTransaction, + abortPeerPushDebitTransaction, +} from "./pay-peer-push-debit.js"; const logger = new Logger("taler-wallet-core:transactions.ts"); @@ -510,6 +538,7 @@ function buildTransactionForPeerPullCredit( tag: TransactionType.PeerPullCredit, pursePub: pullCredit.pursePub, }), + kycUrl: pullCredit.kycUrl, ...(wsrOrt?.lastError ? { error: silentWithdrawalErrorForInvoice @@ -541,6 +570,7 @@ function buildTransactionForPeerPullCredit( tag: TransactionType.PeerPullCredit, pursePub: pullCredit.pursePub, }), + kycUrl: pullCredit.kycUrl, ...(pullCreditOrt?.lastError ? { error: pullCreditOrt.lastError } : {}), }; } @@ -573,6 +603,7 @@ function buildTransactionForPeerPushCredit( tag: TransactionType.PeerPushCredit, peerPushPaymentIncomingId: pushInc.peerPushPaymentIncomingId, }), + kycUrl: pushInc.kycUrl, ...(wsrOrt?.lastError ? { error: wsrOrt.lastError } : {}), }; } @@ -589,6 +620,7 @@ function buildTransactionForPeerPushCredit( expiration: peerContractTerms.purse_expiration, summary: peerContractTerms.summary, }, + kycUrl: pushInc.kycUrl, timestamp: pushInc.timestamp, transactionId: constructTransactionIdentifier({ tag: TransactionType.PeerPushCredit, |