summaryrefslogtreecommitdiff
path: root/packages/taler-wallet-core/src/operations/pay-peer-pull-credit.ts
diff options
context:
space:
mode:
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.ts1204
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];
- }
-}