diff options
Diffstat (limited to 'packages/taler-wallet-core/src/operations/pay-peer-pull-credit.ts')
-rw-r--r-- | packages/taler-wallet-core/src/operations/pay-peer-pull-credit.ts | 1204 |
1 files changed, 0 insertions, 1204 deletions
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 deleted file mode 100644 index e97466084..000000000 --- a/packages/taler-wallet-core/src/operations/pay-peer-pull-credit.ts +++ /dev/null @@ -1,1204 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2022-2023 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -import { - AbsoluteTime, - Amounts, - CancellationToken, - CheckPeerPullCreditRequest, - CheckPeerPullCreditResponse, - ContractTermsUtil, - ExchangeReservePurseRequest, - HttpStatusCode, - InitiatePeerPullCreditRequest, - InitiatePeerPullCreditResponse, - Logger, - NotificationType, - PeerContractTerms, - TalerErrorCode, - TalerPreciseTimestamp, - TalerProtocolTimestamp, - TalerUriAction, - TransactionAction, - TransactionIdStr, - TransactionMajorState, - TransactionMinorState, - TransactionState, - TransactionType, - WalletAccountMergeFlags, - WalletKycUuid, - codecForAny, - codecForWalletKycUuid, - encodeCrock, - getRandomBytes, - j2s, - makeErrorDetail, - stringifyTalerUri, - talerPaytoFromExchangeReserve, -} from "@gnu-taler/taler-util"; -import { readSuccessResponseJsonOrThrow } from "@gnu-taler/taler-util/http"; -import { - KycPendingInfo, - KycUserType, - PeerPullCreditRecord, - PeerPullPaymentCreditStatus, - WithdrawalGroupStatus, - WithdrawalRecordType, - fetchFreshExchange, - timestampOptionalPreciseFromDb, - timestampPreciseFromDb, - timestampPreciseToDb, -} from "../index.js"; -import { InternalWalletState } from "../internal-wallet-state.js"; -import { PendingTaskType, TaskId } from "../pending-types.js"; -import { assertUnreachable } from "../util/assertUnreachable.js"; -import { checkDbInvariant } from "../util/invariants.js"; -import { - TaskRunResult, - TaskRunResultType, - TombstoneTag, - TransactionContext, - constructTaskIdentifier, -} from "./common.js"; -import { - codecForExchangePurseStatus, - getMergeReserveInfo, -} from "./pay-peer-common.js"; -import { - constructTransactionIdentifier, - notifyTransition, -} from "./transactions.js"; -import { - getExchangeWithdrawalInfo, - internalCreateWithdrawalGroup, -} from "./withdraw.js"; - -const logger = new Logger("pay-peer-pull-credit.ts"); - -export class PeerPullCreditTransactionContext implements TransactionContext { - readonly transactionId: TransactionIdStr; - readonly retryTag: TaskId; - - constructor( - public ws: InternalWalletState, - public pursePub: string, - ) { - this.retryTag = constructTaskIdentifier({ - tag: PendingTaskType.PeerPullCredit, - pursePub, - }); - this.transactionId = constructTransactionIdentifier({ - tag: TransactionType.PeerPullCredit, - pursePub, - }); - } - - async deleteTransaction(): Promise<void> { - const { ws, pursePub } = this; - await ws.db.runReadWriteTx( - ["withdrawalGroups", "peerPullCredit", "tombstones"], - async (tx) => { - const pullIni = await tx.peerPullCredit.get(pursePub); - if (!pullIni) { - return; - } - if (pullIni.withdrawalGroupId) { - const withdrawalGroupId = pullIni.withdrawalGroupId; - const withdrawalGroupRecord = - await tx.withdrawalGroups.get(withdrawalGroupId); - if (withdrawalGroupRecord) { - await tx.withdrawalGroups.delete(withdrawalGroupId); - await tx.tombstones.put({ - id: TombstoneTag.DeleteWithdrawalGroup + ":" + withdrawalGroupId, - }); - } - } - await tx.peerPullCredit.delete(pursePub); - await tx.tombstones.put({ - id: TombstoneTag.DeletePeerPullCredit + ":" + pursePub, - }); - }, - ); - - return; - } - - async suspendTransaction(): Promise<void> { - const { ws, pursePub, retryTag, transactionId } = this; - const transitionInfo = await ws.db.runReadWriteTx( - ["peerPullCredit"], - async (tx) => { - const pullCreditRec = await tx.peerPullCredit.get(pursePub); - if (!pullCreditRec) { - logger.warn(`peer pull credit ${pursePub} not found`); - return; - } - let newStatus: PeerPullPaymentCreditStatus | undefined = undefined; - switch (pullCreditRec.status) { - case PeerPullPaymentCreditStatus.PendingCreatePurse: - newStatus = PeerPullPaymentCreditStatus.SuspendedCreatePurse; - break; - case PeerPullPaymentCreditStatus.PendingMergeKycRequired: - newStatus = PeerPullPaymentCreditStatus.SuspendedMergeKycRequired; - break; - case PeerPullPaymentCreditStatus.PendingWithdrawing: - newStatus = PeerPullPaymentCreditStatus.SuspendedWithdrawing; - break; - case PeerPullPaymentCreditStatus.PendingReady: - newStatus = PeerPullPaymentCreditStatus.SuspendedReady; - break; - case PeerPullPaymentCreditStatus.AbortingDeletePurse: - newStatus = - PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse; - break; - case PeerPullPaymentCreditStatus.Done: - case PeerPullPaymentCreditStatus.SuspendedCreatePurse: - case PeerPullPaymentCreditStatus.SuspendedMergeKycRequired: - case PeerPullPaymentCreditStatus.SuspendedReady: - case PeerPullPaymentCreditStatus.SuspendedWithdrawing: - case PeerPullPaymentCreditStatus.Aborted: - case PeerPullPaymentCreditStatus.Failed: - case PeerPullPaymentCreditStatus.Expired: - case PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse: - break; - default: - assertUnreachable(pullCreditRec.status); - } - if (newStatus != null) { - const oldTxState = - computePeerPullCreditTransactionState(pullCreditRec); - pullCreditRec.status = newStatus; - const newTxState = - computePeerPullCreditTransactionState(pullCreditRec); - await tx.peerPullCredit.put(pullCreditRec); - return { - oldTxState, - newTxState, - }; - } - return undefined; - }, - ); - ws.taskScheduler.stopShepherdTask(retryTag); - notifyTransition(ws, transactionId, transitionInfo); - } - - async failTransaction(): Promise<void> { - const { ws, pursePub, retryTag, transactionId } = this; - const transitionInfo = await ws.db.runReadWriteTx( - ["peerPullCredit"], - async (tx) => { - const pullCreditRec = await tx.peerPullCredit.get(pursePub); - if (!pullCreditRec) { - logger.warn(`peer pull credit ${pursePub} not found`); - return; - } - let newStatus: PeerPullPaymentCreditStatus | undefined = undefined; - switch (pullCreditRec.status) { - case PeerPullPaymentCreditStatus.PendingCreatePurse: - case PeerPullPaymentCreditStatus.PendingMergeKycRequired: - case PeerPullPaymentCreditStatus.PendingWithdrawing: - case PeerPullPaymentCreditStatus.PendingReady: - case PeerPullPaymentCreditStatus.Done: - case PeerPullPaymentCreditStatus.SuspendedCreatePurse: - case PeerPullPaymentCreditStatus.SuspendedMergeKycRequired: - case PeerPullPaymentCreditStatus.SuspendedReady: - case PeerPullPaymentCreditStatus.SuspendedWithdrawing: - case PeerPullPaymentCreditStatus.Aborted: - case PeerPullPaymentCreditStatus.Failed: - case PeerPullPaymentCreditStatus.Expired: - break; - case PeerPullPaymentCreditStatus.AbortingDeletePurse: - case PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse: - newStatus = PeerPullPaymentCreditStatus.Failed; - break; - default: - assertUnreachable(pullCreditRec.status); - } - if (newStatus != null) { - const oldTxState = - computePeerPullCreditTransactionState(pullCreditRec); - pullCreditRec.status = newStatus; - const newTxState = - computePeerPullCreditTransactionState(pullCreditRec); - await tx.peerPullCredit.put(pullCreditRec); - return { - oldTxState, - newTxState, - }; - } - return undefined; - }, - ); - notifyTransition(ws, transactionId, transitionInfo); - ws.taskScheduler.stopShepherdTask(retryTag); - } - - async resumeTransaction(): Promise<void> { - const { ws, pursePub, retryTag, transactionId } = this; - const transitionInfo = await ws.db.runReadWriteTx( - ["peerPullCredit"], - async (tx) => { - const pullCreditRec = await tx.peerPullCredit.get(pursePub); - if (!pullCreditRec) { - logger.warn(`peer pull credit ${pursePub} not found`); - return; - } - let newStatus: PeerPullPaymentCreditStatus | undefined = undefined; - switch (pullCreditRec.status) { - case PeerPullPaymentCreditStatus.PendingCreatePurse: - case PeerPullPaymentCreditStatus.PendingMergeKycRequired: - case PeerPullPaymentCreditStatus.PendingWithdrawing: - case PeerPullPaymentCreditStatus.PendingReady: - case PeerPullPaymentCreditStatus.AbortingDeletePurse: - case PeerPullPaymentCreditStatus.Done: - case PeerPullPaymentCreditStatus.Failed: - case PeerPullPaymentCreditStatus.Expired: - case PeerPullPaymentCreditStatus.Aborted: - break; - case PeerPullPaymentCreditStatus.SuspendedCreatePurse: - newStatus = PeerPullPaymentCreditStatus.PendingCreatePurse; - break; - case PeerPullPaymentCreditStatus.SuspendedMergeKycRequired: - newStatus = PeerPullPaymentCreditStatus.PendingMergeKycRequired; - break; - case PeerPullPaymentCreditStatus.SuspendedReady: - newStatus = PeerPullPaymentCreditStatus.PendingReady; - break; - case PeerPullPaymentCreditStatus.SuspendedWithdrawing: - newStatus = PeerPullPaymentCreditStatus.PendingWithdrawing; - break; - case PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse: - newStatus = PeerPullPaymentCreditStatus.AbortingDeletePurse; - break; - default: - assertUnreachable(pullCreditRec.status); - } - if (newStatus != null) { - const oldTxState = - computePeerPullCreditTransactionState(pullCreditRec); - pullCreditRec.status = newStatus; - const newTxState = - computePeerPullCreditTransactionState(pullCreditRec); - await tx.peerPullCredit.put(pullCreditRec); - return { - oldTxState, - newTxState, - }; - } - return undefined; - }, - ); - notifyTransition(ws, transactionId, transitionInfo); - ws.taskScheduler.startShepherdTask(retryTag); - } - - async abortTransaction(): Promise<void> { - const { ws, pursePub, retryTag, transactionId } = this; - const transitionInfo = await ws.db.runReadWriteTx( - ["peerPullCredit"], - async (tx) => { - const pullCreditRec = await tx.peerPullCredit.get(pursePub); - if (!pullCreditRec) { - logger.warn(`peer pull credit ${pursePub} not found`); - return; - } - let newStatus: PeerPullPaymentCreditStatus | undefined = undefined; - switch (pullCreditRec.status) { - case PeerPullPaymentCreditStatus.PendingCreatePurse: - case PeerPullPaymentCreditStatus.PendingMergeKycRequired: - newStatus = PeerPullPaymentCreditStatus.AbortingDeletePurse; - break; - case PeerPullPaymentCreditStatus.PendingWithdrawing: - throw Error("can't abort anymore"); - case PeerPullPaymentCreditStatus.PendingReady: - newStatus = PeerPullPaymentCreditStatus.AbortingDeletePurse; - break; - case PeerPullPaymentCreditStatus.Done: - case PeerPullPaymentCreditStatus.SuspendedCreatePurse: - case PeerPullPaymentCreditStatus.SuspendedMergeKycRequired: - case PeerPullPaymentCreditStatus.SuspendedReady: - case PeerPullPaymentCreditStatus.SuspendedWithdrawing: - case PeerPullPaymentCreditStatus.Aborted: - case PeerPullPaymentCreditStatus.AbortingDeletePurse: - case PeerPullPaymentCreditStatus.Failed: - case PeerPullPaymentCreditStatus.Expired: - case PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse: - break; - default: - assertUnreachable(pullCreditRec.status); - } - if (newStatus != null) { - const oldTxState = - computePeerPullCreditTransactionState(pullCreditRec); - pullCreditRec.status = newStatus; - const newTxState = - computePeerPullCreditTransactionState(pullCreditRec); - await tx.peerPullCredit.put(pullCreditRec); - return { - oldTxState, - newTxState, - }; - } - return undefined; - }, - ); - ws.taskScheduler.stopShepherdTask(retryTag); - notifyTransition(ws, transactionId, transitionInfo); - ws.taskScheduler.startShepherdTask(retryTag); - } -} - -async function queryPurseForPeerPullCredit( - ws: InternalWalletState, - pullIni: PeerPullCreditRecord, - cancellationToken: CancellationToken, -): Promise<TaskRunResult> { - const purseDepositUrl = new URL( - `purses/${pullIni.pursePub}/deposit`, - pullIni.exchangeBaseUrl, - ); - purseDepositUrl.searchParams.set("timeout_ms", "30000"); - logger.info(`querying purse status via ${purseDepositUrl.href}`); - const resp = await ws.http.fetch(purseDepositUrl.href, { - timeout: { d_ms: 60000 }, - cancellationToken, - }); - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.PeerPullCredit, - pursePub: pullIni.pursePub, - }); - - logger.info(`purse status code: HTTP ${resp.status}`); - - switch (resp.status) { - case HttpStatusCode.Gone: { - // Exchange says that purse doesn't exist anymore => expired! - const transitionInfo = await ws.db.runReadWriteTx( - ["peerPullCredit"], - async (tx) => { - const finPi = await tx.peerPullCredit.get(pullIni.pursePub); - if (!finPi) { - logger.warn("peerPullCredit not found anymore"); - return; - } - const oldTxState = computePeerPullCreditTransactionState(finPi); - if (finPi.status === PeerPullPaymentCreditStatus.PendingReady) { - finPi.status = PeerPullPaymentCreditStatus.Expired; - } - await tx.peerPullCredit.put(finPi); - const newTxState = computePeerPullCreditTransactionState(finPi); - return { oldTxState, newTxState }; - }, - ); - notifyTransition(ws, transactionId, transitionInfo); - return TaskRunResult.backoff(); - } - case HttpStatusCode.NotFound: - return TaskRunResult.backoff(); - } - - const result = await readSuccessResponseJsonOrThrow( - resp, - codecForExchangePurseStatus(), - ); - - logger.trace(`purse status: ${j2s(result)}`); - - const depositTimestamp = result.deposit_timestamp; - - if (!depositTimestamp || TalerProtocolTimestamp.isNever(depositTimestamp)) { - logger.info("purse not ready yet (no deposit)"); - return TaskRunResult.backoff(); - } - - const reserve = await ws.db.runReadOnlyTx(["reserves"], async (tx) => { - return await tx.reserves.get(pullIni.mergeReserveRowId); - }); - - if (!reserve) { - throw Error("reserve for peer pull credit not found in wallet DB"); - } - - await internalCreateWithdrawalGroup(ws, { - amount: Amounts.parseOrThrow(pullIni.amount), - wgInfo: { - withdrawalType: WithdrawalRecordType.PeerPullCredit, - contractPriv: pullIni.contractPriv, - }, - forcedWithdrawalGroupId: pullIni.withdrawalGroupId, - exchangeBaseUrl: pullIni.exchangeBaseUrl, - reserveStatus: WithdrawalGroupStatus.PendingQueryingStatus, - reserveKeyPair: { - priv: reserve.reservePriv, - pub: reserve.reservePub, - }, - }); - const transitionInfo = await ws.db.runReadWriteTx( - ["peerPullCredit"], - async (tx) => { - const finPi = await tx.peerPullCredit.get(pullIni.pursePub); - if (!finPi) { - logger.warn("peerPullCredit not found anymore"); - return; - } - const oldTxState = computePeerPullCreditTransactionState(finPi); - if (finPi.status === PeerPullPaymentCreditStatus.PendingReady) { - finPi.status = PeerPullPaymentCreditStatus.PendingWithdrawing; - } - await tx.peerPullCredit.put(finPi); - const newTxState = computePeerPullCreditTransactionState(finPi); - return { oldTxState, newTxState }; - }, - ); - notifyTransition(ws, transactionId, transitionInfo); - return TaskRunResult.backoff(); -} - -async function longpollKycStatus( - ws: InternalWalletState, - pursePub: string, - exchangeUrl: string, - kycInfo: KycPendingInfo, - userType: KycUserType, - cancellationToken: CancellationToken, -): Promise<TaskRunResult> { - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.PeerPullCredit, - pursePub, - }); - const retryTag = constructTaskIdentifier({ - tag: PendingTaskType.PeerPullCredit, - pursePub, - }); - - 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, - }); - 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.runReadWriteTx( - ["peerPullCredit"], - async (tx) => { - const peerIni = await tx.peerPullCredit.get(pursePub); - if (!peerIni) { - return; - } - if ( - peerIni.status !== PeerPullPaymentCreditStatus.PendingMergeKycRequired - ) { - return; - } - const oldTxState = computePeerPullCreditTransactionState(peerIni); - peerIni.status = PeerPullPaymentCreditStatus.PendingCreatePurse; - const newTxState = computePeerPullCreditTransactionState(peerIni); - await tx.peerPullCredit.put(peerIni); - return { oldTxState, newTxState }; - }, - ); - notifyTransition(ws, transactionId, transitionInfo); - } else if (kycStatusRes.status === HttpStatusCode.Accepted) { - // FIXME: Do we have to update the URL here? - } else { - throw Error(`unexpected response from kyc-check (${kycStatusRes.status})`); - } - return TaskRunResult.backoff(); -} - -async function processPeerPullCreditAbortingDeletePurse( - ws: InternalWalletState, - peerPullIni: PeerPullCreditRecord, -): Promise<TaskRunResult> { - const { pursePub, pursePriv } = peerPullIni; - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.PeerPullCredit, - pursePub, - }); - - const sigResp = await ws.cryptoApi.signDeletePurse({ - pursePriv, - }); - const purseUrl = new URL(`purses/${pursePub}`, peerPullIni.exchangeBaseUrl); - const resp = await ws.http.fetch(purseUrl.href, { - method: "DELETE", - headers: { - "taler-purse-signature": sigResp.sig, - }, - }); - logger.info(`deleted purse with response status ${resp.status}`); - - const transitionInfo = await ws.db.runReadWriteTx( - [ - "peerPullCredit", - "refreshGroups", - "denominations", - "coinAvailability", - "coins", - ], - async (tx) => { - const ppiRec = await tx.peerPullCredit.get(pursePub); - if (!ppiRec) { - return undefined; - } - if (ppiRec.status !== PeerPullPaymentCreditStatus.AbortingDeletePurse) { - return undefined; - } - const oldTxState = computePeerPullCreditTransactionState(ppiRec); - ppiRec.status = PeerPullPaymentCreditStatus.Aborted; - await tx.peerPullCredit.put(ppiRec); - const newTxState = computePeerPullCreditTransactionState(ppiRec); - return { - oldTxState, - newTxState, - }; - }, - ); - notifyTransition(ws, transactionId, transitionInfo); - - return TaskRunResult.backoff(); -} - -async function handlePeerPullCreditWithdrawing( - ws: InternalWalletState, - pullIni: PeerPullCreditRecord, -): Promise<TaskRunResult> { - if (!pullIni.withdrawalGroupId) { - throw Error("invalid db state (withdrawing, but no withdrawal group ID"); - } - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.PeerPullCredit, - pursePub: pullIni.pursePub, - }); - const wgId = pullIni.withdrawalGroupId; - let finished: boolean = false; - const transitionInfo = await ws.db.runReadWriteTx( - ["peerPullCredit", "withdrawalGroups"], - async (tx) => { - const ppi = await tx.peerPullCredit.get(pullIni.pursePub); - if (!ppi) { - finished = true; - return; - } - if (ppi.status !== PeerPullPaymentCreditStatus.PendingWithdrawing) { - finished = true; - return; - } - const oldTxState = computePeerPullCreditTransactionState(ppi); - const wg = await tx.withdrawalGroups.get(wgId); - if (!wg) { - // FIXME: Fail the operation instead? - return undefined; - } - switch (wg.status) { - case WithdrawalGroupStatus.Done: - finished = true; - ppi.status = PeerPullPaymentCreditStatus.Done; - break; - // FIXME: Also handle other final states! - } - await tx.peerPullCredit.put(ppi); - const newTxState = computePeerPullCreditTransactionState(ppi); - return { - oldTxState, - newTxState, - }; - }, - ); - notifyTransition(ws, transactionId, transitionInfo); - if (finished) { - return TaskRunResult.finished(); - } else { - // FIXME: Return indicator that we depend on the other operation! - return TaskRunResult.backoff(); - } -} - -async function handlePeerPullCreditCreatePurse( - ws: InternalWalletState, - pullIni: PeerPullCreditRecord, -): Promise<TaskRunResult> { - const purseFee = Amounts.stringify(Amounts.zeroOfAmount(pullIni.amount)); - const pursePub = pullIni.pursePub; - const mergeReserve = await ws.db.runReadOnlyTx(["reserves"], async (tx) => { - return tx.reserves.get(pullIni.mergeReserveRowId); - }); - - if (!mergeReserve) { - throw Error("merge reserve for peer pull payment not found in database"); - } - - const contractTermsRecord = await ws.db.runReadOnlyTx( - ["contractTerms"], - async (tx) => { - return tx.contractTerms.get(pullIni.contractTermsHash); - }, - ); - - if (!contractTermsRecord) { - throw Error("contract terms for peer pull payment not found in database"); - } - - const contractTerms: PeerContractTerms = contractTermsRecord.contractTermsRaw; - - const reservePayto = talerPaytoFromExchangeReserve( - pullIni.exchangeBaseUrl, - mergeReserve.reservePub, - ); - - const econtractResp = await ws.cryptoApi.encryptContractForDeposit({ - contractPriv: pullIni.contractPriv, - contractPub: pullIni.contractPub, - contractTerms: contractTermsRecord.contractTermsRaw, - pursePriv: pullIni.pursePriv, - pursePub: pullIni.pursePub, - nonce: pullIni.contractEncNonce, - }); - - const mergeTimestamp = timestampPreciseFromDb(pullIni.mergeTimestamp); - - const purseExpiration = contractTerms.purse_expiration; - const sigRes = await ws.cryptoApi.signReservePurseCreate({ - contractTermsHash: pullIni.contractTermsHash, - flags: WalletAccountMergeFlags.CreateWithPurseFee, - mergePriv: pullIni.mergePriv, - mergeTimestamp: TalerPreciseTimestamp.round(mergeTimestamp), - purseAmount: pullIni.amount, - purseExpiration: purseExpiration, - purseFee: purseFee, - pursePriv: pullIni.pursePriv, - pursePub: pullIni.pursePub, - reservePayto, - reservePriv: mergeReserve.reservePriv, - }); - - const reservePurseReqBody: ExchangeReservePurseRequest = { - merge_sig: sigRes.mergeSig, - merge_timestamp: TalerPreciseTimestamp.round(mergeTimestamp), - h_contract_terms: pullIni.contractTermsHash, - merge_pub: pullIni.mergePub, - min_age: 0, - purse_expiration: purseExpiration, - purse_fee: purseFee, - purse_pub: pullIni.pursePub, - purse_sig: sigRes.purseSig, - purse_value: pullIni.amount, - reserve_sig: sigRes.accountSig, - econtract: econtractResp.econtract, - }; - - logger.info(`reserve purse request: ${j2s(reservePurseReqBody)}`); - - const reservePurseMergeUrl = new URL( - `reserves/${mergeReserve.reservePub}/purse`, - pullIni.exchangeBaseUrl, - ); - - const httpResp = await ws.http.fetch(reservePurseMergeUrl.href, { - method: "POST", - body: reservePurseReqBody, - }); - - if (httpResp.status === HttpStatusCode.UnavailableForLegalReasons) { - const respJson = await httpResp.json(); - const kycPending = codecForWalletKycUuid().decode(respJson); - logger.info(`kyc uuid response: ${j2s(kycPending)}`); - return processPeerPullCreditKycRequired(ws, pullIni, kycPending); - } - - const resp = await readSuccessResponseJsonOrThrow(httpResp, codecForAny()); - - logger.info(`reserve merge response: ${j2s(resp)}`); - - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.PeerPullCredit, - pursePub: pullIni.pursePub, - }); - - const transitionInfo = await ws.db.runReadWriteTx( - ["peerPullCredit"], - async (tx) => { - const pi2 = await tx.peerPullCredit.get(pursePub); - if (!pi2) { - return; - } - const oldTxState = computePeerPullCreditTransactionState(pi2); - pi2.status = PeerPullPaymentCreditStatus.PendingReady; - await tx.peerPullCredit.put(pi2); - const newTxState = computePeerPullCreditTransactionState(pi2); - return { oldTxState, newTxState }; - }, - ); - notifyTransition(ws, transactionId, transitionInfo); - return TaskRunResult.backoff(); -} - -export async function processPeerPullCredit( - ws: InternalWalletState, - pursePub: string, - cancellationToken: CancellationToken, -): Promise<TaskRunResult> { - const pullIni = await ws.db.runReadOnlyTx(["peerPullCredit"], async (tx) => { - return tx.peerPullCredit.get(pursePub); - }); - if (!pullIni) { - throw Error("peer pull payment initiation not found in database"); - } - - const retryTag = constructTaskIdentifier({ - tag: PendingTaskType.PeerPullCredit, - pursePub, - }); - - logger.trace(`processing ${retryTag}, status=${pullIni.status}`); - - switch (pullIni.status) { - case PeerPullPaymentCreditStatus.Done: { - return TaskRunResult.finished(); - } - case PeerPullPaymentCreditStatus.PendingReady: - return queryPurseForPeerPullCredit(ws, pullIni, cancellationToken); - case PeerPullPaymentCreditStatus.PendingMergeKycRequired: { - if (!pullIni.kycInfo) { - throw Error("invalid state, kycInfo required"); - } - return await longpollKycStatus( - ws, - pursePub, - pullIni.exchangeBaseUrl, - pullIni.kycInfo, - "individual", - cancellationToken, - ); - } - case PeerPullPaymentCreditStatus.PendingCreatePurse: - return handlePeerPullCreditCreatePurse(ws, pullIni); - case PeerPullPaymentCreditStatus.AbortingDeletePurse: - return await processPeerPullCreditAbortingDeletePurse(ws, pullIni); - case PeerPullPaymentCreditStatus.PendingWithdrawing: - return handlePeerPullCreditWithdrawing(ws, pullIni); - case PeerPullPaymentCreditStatus.Aborted: - case PeerPullPaymentCreditStatus.Failed: - case PeerPullPaymentCreditStatus.Expired: - case PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse: - case PeerPullPaymentCreditStatus.SuspendedCreatePurse: - case PeerPullPaymentCreditStatus.SuspendedMergeKycRequired: - case PeerPullPaymentCreditStatus.SuspendedReady: - case PeerPullPaymentCreditStatus.SuspendedWithdrawing: - break; - default: - assertUnreachable(pullIni.status); - } - - return TaskRunResult.finished(); -} - -async function processPeerPullCreditKycRequired( - ws: InternalWalletState, - peerIni: PeerPullCreditRecord, - kycPending: WalletKycUuid, -): Promise<TaskRunResult> { - 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 TaskRunResult.backoff(); - } else if (kycStatusRes.status === HttpStatusCode.Accepted) { - const kycStatus = await kycStatusRes.json(); - logger.info(`kyc status: ${j2s(kycStatus)}`); - const { transitionInfo, result } = await ws.db.runReadWriteTx( - ["peerPullCredit"], - async (tx) => { - const peerInc = await tx.peerPullCredit.get(pursePub); - if (!peerInc) { - return { - transitionInfo: undefined, - result: TaskRunResult.finished(), - }; - } - const oldTxState = computePeerPullCreditTransactionState(peerInc); - peerInc.kycInfo = { - paytoHash: kycPending.h_payto, - requirementRow: kycPending.requirement_row, - }; - peerInc.kycUrl = kycStatus.kyc_url; - peerInc.status = PeerPullPaymentCreditStatus.PendingMergeKycRequired; - const newTxState = computePeerPullCreditTransactionState(peerInc); - await tx.peerPullCredit.put(peerInc); - // We'll remove this eventually! New clients should rely on the - // kycUrl field of the transaction, not the error code. - const res: TaskRunResult = { - type: TaskRunResultType.Error, - errorDetail: makeErrorDetail( - TalerErrorCode.WALLET_WITHDRAWAL_KYC_REQUIRED, - { - kycUrl: kycStatus.kyc_url, - }, - ), - }; - return { - transitionInfo: { oldTxState, newTxState }, - result: res, - }; - }, - ); - notifyTransition(ws, transactionId, transitionInfo); - return TaskRunResult.backoff(); - } else { - throw Error(`unexpected response from kyc-check (${kycStatusRes.status})`); - } -} - -/** - * Check fees and available exchanges for a peer push payment initiation. - */ -export async function checkPeerPullPaymentInitiation( - ws: InternalWalletState, - req: CheckPeerPullCreditRequest, -): Promise<CheckPeerPullCreditResponse> { - // FIXME: We don't support exchanges with purse fees yet. - // Select an exchange where we have money in the specified currency - // FIXME: How do we handle regional currency scopes here? Is it an additional input? - - logger.trace("checking peer-pull-credit fees"); - - const currency = Amounts.currencyOf(req.amount); - let exchangeUrl; - if (req.exchangeBaseUrl) { - exchangeUrl = req.exchangeBaseUrl; - } else { - exchangeUrl = await getPreferredExchangeForCurrency(ws, currency); - } - - if (!exchangeUrl) { - throw Error("no exchange found for initiating a peer pull payment"); - } - - logger.trace(`found ${exchangeUrl} as preferred exchange`); - - const wi = await getExchangeWithdrawalInfo( - ws, - exchangeUrl, - Amounts.parseOrThrow(req.amount), - undefined, - ); - - logger.trace(`got withdrawal info`); - - let numCoins = 0; - for (let i = 0; i < wi.selectedDenoms.selectedDenoms.length; i++) { - numCoins += wi.selectedDenoms.selectedDenoms[i].count; - } - - return { - exchangeBaseUrl: exchangeUrl, - amountEffective: wi.withdrawalAmountEffective, - amountRaw: req.amount, - numCoins, - }; -} - -/** - * Find a preferred exchange based on when we withdrew last from this exchange. - */ -async function getPreferredExchangeForCurrency( - ws: InternalWalletState, - currency: string, -): Promise<string | undefined> { - // Find an exchange with the matching currency. - // Prefer exchanges with the most recent withdrawal. - const url = await ws.db.runReadOnlyTx(["exchanges"], async (tx) => { - const exchanges = await tx.exchanges.iter().toArray(); - let candidate = undefined; - for (const e of exchanges) { - if (e.detailsPointer?.currency !== currency) { - continue; - } - if (!candidate) { - candidate = e; - continue; - } - if (candidate.lastWithdrawal && !e.lastWithdrawal) { - continue; - } - const exchangeLastWithdrawal = timestampOptionalPreciseFromDb( - e.lastWithdrawal, - ); - const candidateLastWithdrawal = timestampOptionalPreciseFromDb( - candidate.lastWithdrawal, - ); - if (exchangeLastWithdrawal && candidateLastWithdrawal) { - if ( - AbsoluteTime.cmp( - AbsoluteTime.fromPreciseTimestamp(exchangeLastWithdrawal), - AbsoluteTime.fromPreciseTimestamp(candidateLastWithdrawal), - ) > 0 - ) { - candidate = e; - } - } - } - if (candidate) { - return candidate.baseUrl; - } - return undefined; - }); - return url; -} - -/** - * Initiate a peer pull payment. - */ -export async function initiatePeerPullPayment( - ws: InternalWalletState, - req: InitiatePeerPullCreditRequest, -): Promise<InitiatePeerPullCreditResponse> { - const currency = Amounts.currencyOf(req.partialContractTerms.amount); - let maybeExchangeBaseUrl: string | undefined; - if (req.exchangeBaseUrl) { - maybeExchangeBaseUrl = req.exchangeBaseUrl; - } else { - maybeExchangeBaseUrl = await getPreferredExchangeForCurrency(ws, currency); - } - - if (!maybeExchangeBaseUrl) { - throw Error("no exchange found for initiating a peer pull payment"); - } - - const exchangeBaseUrl = maybeExchangeBaseUrl; - - await fetchFreshExchange(ws, exchangeBaseUrl); - - const mergeReserveInfo = await getMergeReserveInfo(ws, { - exchangeBaseUrl: exchangeBaseUrl, - }); - - const pursePair = await ws.cryptoApi.createEddsaKeypair({}); - const mergePair = await ws.cryptoApi.createEddsaKeypair({}); - - const contractTerms = req.partialContractTerms; - - const hContractTerms = ContractTermsUtil.hashContractTerms(contractTerms); - - const contractKeyPair = await ws.cryptoApi.createEddsaKeypair({}); - - const withdrawalGroupId = encodeCrock(getRandomBytes(32)); - - const mergeReserveRowId = mergeReserveInfo.rowId; - checkDbInvariant(!!mergeReserveRowId); - - const contractEncNonce = encodeCrock(getRandomBytes(24)); - - const wi = await getExchangeWithdrawalInfo( - ws, - exchangeBaseUrl, - Amounts.parseOrThrow(req.partialContractTerms.amount), - undefined, - ); - - const mergeTimestamp = TalerPreciseTimestamp.now(); - - const transitionInfo = await ws.db.runReadWriteTx( - ["peerPullCredit", "contractTerms"], - async (tx) => { - const ppi: PeerPullCreditRecord = { - amount: req.partialContractTerms.amount, - contractTermsHash: hContractTerms, - exchangeBaseUrl: exchangeBaseUrl, - pursePriv: pursePair.priv, - pursePub: pursePair.pub, - mergePriv: mergePair.priv, - mergePub: mergePair.pub, - status: PeerPullPaymentCreditStatus.PendingCreatePurse, - mergeTimestamp: timestampPreciseToDb(mergeTimestamp), - contractEncNonce, - mergeReserveRowId: mergeReserveRowId, - contractPriv: contractKeyPair.priv, - contractPub: contractKeyPair.pub, - withdrawalGroupId, - estimatedAmountEffective: wi.withdrawalAmountEffective, - }; - await tx.peerPullCredit.put(ppi); - const oldTxState: TransactionState = { - major: TransactionMajorState.None, - }; - const newTxState = computePeerPullCreditTransactionState(ppi); - await tx.contractTerms.put({ - contractTermsRaw: contractTerms, - h: hContractTerms, - }); - return { oldTxState, newTxState }; - }, - ); - - const ctx = new PeerPullCreditTransactionContext(ws, pursePair.pub); - - // The pending-incoming balance has changed. - ws.notify({ - type: NotificationType.BalanceChange, - hintTransactionId: ctx.transactionId, - }); - - notifyTransition(ws, ctx.transactionId, transitionInfo); - ws.taskScheduler.startShepherdTask(ctx.retryTag); - - return { - talerUri: stringifyTalerUri({ - type: TalerUriAction.PayPull, - exchangeBaseUrl: exchangeBaseUrl, - contractPriv: contractKeyPair.priv, - }), - transactionId: ctx.transactionId, - }; -} - -export function computePeerPullCreditTransactionState( - pullCreditRecord: PeerPullCreditRecord, -): TransactionState { - switch (pullCreditRecord.status) { - case PeerPullPaymentCreditStatus.PendingCreatePurse: - return { - major: TransactionMajorState.Pending, - minor: TransactionMinorState.CreatePurse, - }; - case PeerPullPaymentCreditStatus.PendingMergeKycRequired: - return { - major: TransactionMajorState.Pending, - minor: TransactionMinorState.MergeKycRequired, - }; - case PeerPullPaymentCreditStatus.PendingReady: - return { - major: TransactionMajorState.Pending, - minor: TransactionMinorState.Ready, - }; - case PeerPullPaymentCreditStatus.Done: - return { - major: TransactionMajorState.Done, - }; - case PeerPullPaymentCreditStatus.PendingWithdrawing: - return { - major: TransactionMajorState.Pending, - minor: TransactionMinorState.Withdraw, - }; - case PeerPullPaymentCreditStatus.SuspendedCreatePurse: - return { - major: TransactionMajorState.Suspended, - minor: TransactionMinorState.CreatePurse, - }; - case PeerPullPaymentCreditStatus.SuspendedReady: - return { - major: TransactionMajorState.Suspended, - minor: TransactionMinorState.Ready, - }; - case PeerPullPaymentCreditStatus.SuspendedWithdrawing: - return { - major: TransactionMajorState.Pending, - minor: TransactionMinorState.Withdraw, - }; - case PeerPullPaymentCreditStatus.SuspendedMergeKycRequired: - return { - major: TransactionMajorState.Suspended, - minor: TransactionMinorState.MergeKycRequired, - }; - case PeerPullPaymentCreditStatus.Aborted: - return { - major: TransactionMajorState.Aborted, - }; - case PeerPullPaymentCreditStatus.AbortingDeletePurse: - return { - major: TransactionMajorState.Aborting, - minor: TransactionMinorState.DeletePurse, - }; - case PeerPullPaymentCreditStatus.Failed: - return { - major: TransactionMajorState.Failed, - }; - case PeerPullPaymentCreditStatus.Expired: - return { - major: TransactionMajorState.Expired, - }; - case PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse: - return { - major: TransactionMajorState.Aborting, - minor: TransactionMinorState.DeletePurse, - }; - } -} - -export function computePeerPullCreditTransactionActions( - pullCreditRecord: PeerPullCreditRecord, -): TransactionAction[] { - switch (pullCreditRecord.status) { - case PeerPullPaymentCreditStatus.PendingCreatePurse: - return [TransactionAction.Abort, TransactionAction.Suspend]; - case PeerPullPaymentCreditStatus.PendingMergeKycRequired: - return [TransactionAction.Abort, TransactionAction.Suspend]; - case PeerPullPaymentCreditStatus.PendingReady: - return [TransactionAction.Abort, TransactionAction.Suspend]; - case PeerPullPaymentCreditStatus.Done: - return [TransactionAction.Delete]; - case PeerPullPaymentCreditStatus.PendingWithdrawing: - return [TransactionAction.Abort, TransactionAction.Suspend]; - case PeerPullPaymentCreditStatus.SuspendedCreatePurse: - return [TransactionAction.Resume, TransactionAction.Abort]; - case PeerPullPaymentCreditStatus.SuspendedReady: - return [TransactionAction.Abort, TransactionAction.Resume]; - case PeerPullPaymentCreditStatus.SuspendedWithdrawing: - return [TransactionAction.Resume, TransactionAction.Fail]; - case PeerPullPaymentCreditStatus.SuspendedMergeKycRequired: - return [TransactionAction.Resume, TransactionAction.Fail]; - case PeerPullPaymentCreditStatus.Aborted: - return [TransactionAction.Delete]; - case PeerPullPaymentCreditStatus.AbortingDeletePurse: - return [TransactionAction.Suspend, TransactionAction.Fail]; - case PeerPullPaymentCreditStatus.Failed: - return [TransactionAction.Delete]; - case PeerPullPaymentCreditStatus.Expired: - return [TransactionAction.Delete]; - case PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse: - return [TransactionAction.Resume, TransactionAction.Fail]; - } -} |