diff options
| author | Florian Dold <florian@dold.me> | 2023-06-05 11:45:16 +0200 | 
|---|---|---|
| committer | Florian Dold <florian@dold.me> | 2023-06-05 11:45:16 +0200 | 
| commit | fda5a0ed87a6473a6b34bd1ac07d5f1d45dfbc19 (patch) | |
| tree | c8b7b09ca441d2a01e340dd3f569e075d3ef278e /packages | |
| parent | f3d4ff4e3a44141ad387ef68a9083b01bf1c818a (diff) | |
wallet-core: restructure p2p impl
Diffstat (limited to 'packages')
9 files changed, 3511 insertions, 3270 deletions
diff --git a/packages/taler-wallet-core/src/operations/pay-peer-common.ts b/packages/taler-wallet-core/src/operations/pay-peer-common.ts new file mode 100644 index 000000000..4b1dd31a5 --- /dev/null +++ b/packages/taler-wallet-core/src/operations/pay-peer-common.ts @@ -0,0 +1,463 @@ +/* + This file is part of GNU Taler + (C) 2022 GNUnet e.V. + + 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/> + */ + +/** + * Imports. + */ +import { +  AgeCommitmentProof, +  AmountJson, +  AmountString, +  Amounts, +  Codec, +  CoinPublicKeyString, +  CoinStatus, +  Logger, +  PayPeerInsufficientBalanceDetails, +  TalerProtocolTimestamp, +  UnblindedSignature, +  buildCodecForObject, +  codecForAmountString, +  codecForTimestamp, +  codecOptional, +  strcmp, +} from "@gnu-taler/taler-util"; +import { SpendCoinDetails } from "../crypto/cryptoImplementation.js"; +import { +  DenominationRecord, +  PeerPushPaymentCoinSelection, +  ReserveRecord, +} from "../db.js"; +import { InternalWalletState } from "../internal-wallet-state.js"; +import { checkDbInvariant } from "../util/invariants.js"; +import { getPeerPaymentBalanceDetailsInTx } from "./balance.js"; +import { getTotalRefreshCost } from "./refresh.js"; + +const logger = new Logger("operations/peer-to-peer.ts"); + +interface SelectedPeerCoin { +  coinPub: string; +  coinPriv: string; +  contribution: AmountString; +  denomPubHash: string; +  denomSig: UnblindedSignature; +  ageCommitmentProof: AgeCommitmentProof | undefined; +} + +interface PeerCoinSelectionDetails { +  exchangeBaseUrl: string; + +  /** +   * Info of Coins that were selected. +   */ +  coins: SelectedPeerCoin[]; + +  /** +   * How much of the deposit fees is the customer paying? +   */ +  depositFees: AmountJson; +} + +/** + * Information about a selected coin for peer to peer payments. + */ +interface CoinInfo { +  /** +   * Public key of the coin. +   */ +  coinPub: string; + +  coinPriv: string; + +  /** +   * Deposit fee for the coin. +   */ +  feeDeposit: AmountJson; + +  value: AmountJson; + +  denomPubHash: string; + +  denomSig: UnblindedSignature; + +  maxAge: number; + +  ageCommitmentProof?: AgeCommitmentProof; +} + +export type SelectPeerCoinsResult = +  | { type: "success"; result: PeerCoinSelectionDetails } +  | { +      type: "failure"; +      insufficientBalanceDetails: PayPeerInsufficientBalanceDetails; +    }; + +export async function queryCoinInfosForSelection( +  ws: InternalWalletState, +  csel: PeerPushPaymentCoinSelection, +): Promise<SpendCoinDetails[]> { +  let infos: SpendCoinDetails[] = []; +  await ws.db +    .mktx((x) => [x.coins, x.denominations]) +    .runReadOnly(async (tx) => { +      for (let i = 0; i < csel.coinPubs.length; i++) { +        const coin = await tx.coins.get(csel.coinPubs[i]); +        if (!coin) { +          throw Error("coin not found anymore"); +        } +        const denom = await ws.getDenomInfo( +          ws, +          tx, +          coin.exchangeBaseUrl, +          coin.denomPubHash, +        ); +        if (!denom) { +          throw Error("denom for coin not found anymore"); +        } +        infos.push({ +          coinPriv: coin.coinPriv, +          coinPub: coin.coinPub, +          denomPubHash: coin.denomPubHash, +          denomSig: coin.denomSig, +          ageCommitmentProof: coin.ageCommitmentProof, +          contribution: csel.contributions[i], +        }); +      } +    }); +  return infos; +} + +export interface PeerCoinSelectionRequest { +  instructedAmount: AmountJson; + +  /** +   * Instruct the coin selection to repair this coin +   * selection instead of selecting completely new coins. +   */ +  repair?: { +    exchangeBaseUrl: string; +    coinPubs: CoinPublicKeyString[]; +    contribs: AmountJson[]; +  }; +} + +export async function selectPeerCoins( +  ws: InternalWalletState, +  req: PeerCoinSelectionRequest, +): Promise<SelectPeerCoinsResult> { +  const instructedAmount = req.instructedAmount; +  if (Amounts.isZero(instructedAmount)) { +    // Other parts of the code assume that we have at least +    // one coin to spend. +    throw new Error("amount of zero not allowed"); +  } +  return await ws.db +    .mktx((x) => [ +      x.exchanges, +      x.contractTerms, +      x.coins, +      x.coinAvailability, +      x.denominations, +      x.refreshGroups, +      x.peerPushPaymentInitiations, +    ]) +    .runReadWrite(async (tx) => { +      const exchanges = await tx.exchanges.iter().toArray(); +      const exchangeFeeGap: { [url: string]: AmountJson } = {}; +      const currency = Amounts.currencyOf(instructedAmount); +      for (const exch of exchanges) { +        if (exch.detailsPointer?.currency !== currency) { +          continue; +        } +        // FIXME: Can't we do this faster by using coinAvailability? +        const coins = ( +          await tx.coins.indexes.byBaseUrl.getAll(exch.baseUrl) +        ).filter((x) => x.status === CoinStatus.Fresh); +        const coinInfos: CoinInfo[] = []; +        for (const coin of coins) { +          const denom = await ws.getDenomInfo( +            ws, +            tx, +            coin.exchangeBaseUrl, +            coin.denomPubHash, +          ); +          if (!denom) { +            throw Error("denom not found"); +          } +          coinInfos.push({ +            coinPub: coin.coinPub, +            feeDeposit: Amounts.parseOrThrow(denom.feeDeposit), +            value: Amounts.parseOrThrow(denom.value), +            denomPubHash: denom.denomPubHash, +            coinPriv: coin.coinPriv, +            denomSig: coin.denomSig, +            maxAge: coin.maxAge, +            ageCommitmentProof: coin.ageCommitmentProof, +          }); +        } +        if (coinInfos.length === 0) { +          continue; +        } +        coinInfos.sort( +          (o1, o2) => +            -Amounts.cmp(o1.value, o2.value) || +            strcmp(o1.denomPubHash, o2.denomPubHash), +        ); +        let amountAcc = Amounts.zeroOfCurrency(currency); +        let depositFeesAcc = Amounts.zeroOfCurrency(currency); +        const resCoins: { +          coinPub: string; +          coinPriv: string; +          contribution: AmountString; +          denomPubHash: string; +          denomSig: UnblindedSignature; +          ageCommitmentProof: AgeCommitmentProof | undefined; +        }[] = []; +        let lastDepositFee = Amounts.zeroOfCurrency(currency); + +        if (req.repair) { +          for (let i = 0; i < req.repair.coinPubs.length; i++) { +            const contrib = req.repair.contribs[i]; +            const coin = await tx.coins.get(req.repair.coinPubs[i]); +            if (!coin) { +              throw Error("repair not possible, coin not found"); +            } +            const denom = await ws.getDenomInfo( +              ws, +              tx, +              coin.exchangeBaseUrl, +              coin.denomPubHash, +            ); +            checkDbInvariant(!!denom); +            resCoins.push({ +              coinPriv: coin.coinPriv, +              coinPub: coin.coinPub, +              contribution: Amounts.stringify(contrib), +              denomPubHash: coin.denomPubHash, +              denomSig: coin.denomSig, +              ageCommitmentProof: coin.ageCommitmentProof, +            }); +            const depositFee = Amounts.parseOrThrow(denom.feeDeposit); +            lastDepositFee = depositFee; +            amountAcc = Amounts.add( +              amountAcc, +              Amounts.sub(contrib, depositFee).amount, +            ).amount; +            depositFeesAcc = Amounts.add(depositFeesAcc, depositFee).amount; +          } +        } + +        for (const coin of coinInfos) { +          if (Amounts.cmp(amountAcc, instructedAmount) >= 0) { +            break; +          } +          const gap = Amounts.add( +            coin.feeDeposit, +            Amounts.sub(instructedAmount, amountAcc).amount, +          ).amount; +          const contrib = Amounts.min(gap, coin.value); +          amountAcc = Amounts.add( +            amountAcc, +            Amounts.sub(contrib, coin.feeDeposit).amount, +          ).amount; +          depositFeesAcc = Amounts.add(depositFeesAcc, coin.feeDeposit).amount; +          resCoins.push({ +            coinPriv: coin.coinPriv, +            coinPub: coin.coinPub, +            contribution: Amounts.stringify(contrib), +            denomPubHash: coin.denomPubHash, +            denomSig: coin.denomSig, +            ageCommitmentProof: coin.ageCommitmentProof, +          }); +          lastDepositFee = coin.feeDeposit; +        } +        if (Amounts.cmp(amountAcc, instructedAmount) >= 0) { +          const res: PeerCoinSelectionDetails = { +            exchangeBaseUrl: exch.baseUrl, +            coins: resCoins, +            depositFees: depositFeesAcc, +          }; +          return { type: "success", result: res }; +        } +        const diff = Amounts.sub(instructedAmount, amountAcc).amount; +        exchangeFeeGap[exch.baseUrl] = Amounts.add(lastDepositFee, diff).amount; + +        continue; +      } + +      // We were unable to select coins. +      // Now we need to produce error details. + +      const infoGeneral = await getPeerPaymentBalanceDetailsInTx(ws, tx, { +        currency, +      }); + +      const perExchange: PayPeerInsufficientBalanceDetails["perExchange"] = {}; + +      let maxFeeGapEstimate = Amounts.zeroOfCurrency(currency); + +      for (const exch of exchanges) { +        if (exch.detailsPointer?.currency !== currency) { +          continue; +        } +        const infoExchange = await getPeerPaymentBalanceDetailsInTx(ws, tx, { +          currency, +          restrictExchangeTo: exch.baseUrl, +        }); +        let gap = +          exchangeFeeGap[exch.baseUrl] ?? Amounts.zeroOfCurrency(currency); +        if (Amounts.cmp(infoExchange.balanceMaterial, instructedAmount) < 0) { +          // Show fee gap only if we should've been able to pay with the material amount +          gap = Amounts.zeroOfCurrency(currency); +        } +        perExchange[exch.baseUrl] = { +          balanceAvailable: Amounts.stringify(infoExchange.balanceAvailable), +          balanceMaterial: Amounts.stringify(infoExchange.balanceMaterial), +          feeGapEstimate: Amounts.stringify(gap), +        }; + +        maxFeeGapEstimate = Amounts.max(maxFeeGapEstimate, gap); +      } + +      const errDetails: PayPeerInsufficientBalanceDetails = { +        amountRequested: Amounts.stringify(instructedAmount), +        balanceAvailable: Amounts.stringify(infoGeneral.balanceAvailable), +        balanceMaterial: Amounts.stringify(infoGeneral.balanceMaterial), +        feeGapEstimate: Amounts.stringify(maxFeeGapEstimate), +        perExchange, +      }; + +      return { type: "failure", insufficientBalanceDetails: errDetails }; +    }); +} + +export async function getTotalPeerPaymentCost( +  ws: InternalWalletState, +  pcs: SelectedPeerCoin[], +): Promise<AmountJson> { +  return ws.db +    .mktx((x) => [x.coins, x.denominations]) +    .runReadOnly(async (tx) => { +      const costs: AmountJson[] = []; +      for (let i = 0; i < pcs.length; i++) { +        const coin = await tx.coins.get(pcs[i].coinPub); +        if (!coin) { +          throw Error("can't calculate payment cost, coin not found"); +        } +        const denom = await tx.denominations.get([ +          coin.exchangeBaseUrl, +          coin.denomPubHash, +        ]); +        if (!denom) { +          throw Error( +            "can't calculate payment cost, denomination for coin not found", +          ); +        } +        const allDenoms = await tx.denominations.indexes.byExchangeBaseUrl +          .iter(coin.exchangeBaseUrl) +          .filter((x) => +            Amounts.isSameCurrency( +              DenominationRecord.getValue(x), +              pcs[i].contribution, +            ), +          ); +        const amountLeft = Amounts.sub( +          DenominationRecord.getValue(denom), +          pcs[i].contribution, +        ).amount; +        const refreshCost = getTotalRefreshCost( +          allDenoms, +          DenominationRecord.toDenomInfo(denom), +          amountLeft, +          ws.config.testing.denomselAllowLate, +        ); +        costs.push(Amounts.parseOrThrow(pcs[i].contribution)); +        costs.push(refreshCost); +      } +      const zero = Amounts.zeroOfAmount(pcs[0].contribution); +      return Amounts.sum([zero, ...costs]).amount; +    }); +} + +interface ExchangePurseStatus { +  balance: AmountString; +  deposit_timestamp?: TalerProtocolTimestamp; +  merge_timestamp?: TalerProtocolTimestamp; +} + +export const codecForExchangePurseStatus = (): Codec<ExchangePurseStatus> => +  buildCodecForObject<ExchangePurseStatus>() +    .property("balance", codecForAmountString()) +    .property("deposit_timestamp", codecOptional(codecForTimestamp)) +    .property("merge_timestamp", codecOptional(codecForTimestamp)) +    .build("ExchangePurseStatus"); + +export function talerPaytoFromExchangeReserve( +  exchangeBaseUrl: string, +  reservePub: string, +): string { +  const url = new URL(exchangeBaseUrl); +  let proto: string; +  if (url.protocol === "http:") { +    proto = "taler-reserve-http"; +  } else if (url.protocol === "https:") { +    proto = "taler-reserve"; +  } else { +    throw Error(`unsupported exchange base URL protocol (${url.protocol})`); +  } + +  let path = url.pathname; +  if (!path.endsWith("/")) { +    path = path + "/"; +  } + +  return `payto://${proto}/${url.host}${url.pathname}${reservePub}`; +} + +export async function getMergeReserveInfo( +  ws: InternalWalletState, +  req: { +    exchangeBaseUrl: string; +  }, +): Promise<ReserveRecord> { +  // We have to eagerly create the key pair outside of the transaction, +  // due to the async crypto API. +  const newReservePair = await ws.cryptoApi.createEddsaKeypair({}); + +  const mergeReserveRecord: ReserveRecord = await ws.db +    .mktx((x) => [x.exchanges, x.reserves, x.withdrawalGroups]) +    .runReadWrite(async (tx) => { +      const ex = await tx.exchanges.get(req.exchangeBaseUrl); +      checkDbInvariant(!!ex); +      if (ex.currentMergeReserveRowId != null) { +        const reserve = await tx.reserves.get(ex.currentMergeReserveRowId); +        checkDbInvariant(!!reserve); +        return reserve; +      } +      const reserve: ReserveRecord = { +        reservePriv: newReservePair.priv, +        reservePub: newReservePair.pub, +      }; +      const insertResp = await tx.reserves.put(reserve); +      checkDbInvariant(typeof insertResp.key === "number"); +      reserve.rowId = insertResp.key; +      ex.currentMergeReserveRowId = reserve.rowId; +      await tx.exchanges.put(ex); +      return reserve; +    }); + +  return mergeReserveRecord; +} 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 new file mode 100644 index 000000000..b9c9728a1 --- /dev/null +++ b/packages/taler-wallet-core/src/operations/pay-peer-pull-credit.ts @@ -0,0 +1,910 @@ +/* + 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, +  TalerPreciseTimestamp, +  TransactionAction, +  TransactionMajorState, +  TransactionMinorState, +  TransactionState, +  TransactionType, +  WalletAccountMergeFlags, +  codecForAny, +  codecForWalletKycUuid, +  constructPayPullUri, +  encodeCrock, +  getRandomBytes, +  j2s, +} from "@gnu-taler/taler-util"; +import { +  readSuccessResponseJsonOrErrorCode, +  readSuccessResponseJsonOrThrow, +  throwUnexpectedRequestError, +} from "@gnu-taler/taler-util/http"; +import { +  PeerPullPaymentInitiationRecord, +  PeerPullPaymentInitiationStatus, +  WithdrawalGroupStatus, +  WithdrawalRecordType, +  updateExchangeFromUrl, +} from "../index.js"; +import { InternalWalletState } from "../internal-wallet-state.js"; +import { PendingTaskType } from "../pending-types.js"; +import { assertUnreachable } from "../util/assertUnreachable.js"; +import { checkDbInvariant } from "../util/invariants.js"; +import { +  OperationAttemptResult, +  OperationAttemptResultType, +  constructTaskIdentifier, +} from "../util/retries.js"; +import { +  LongpollResult, +  resetOperationTimeout, +  runLongpollAsync, +  runOperationWithErrorReporting, +} from "./common.js"; +import { +  codecForExchangePurseStatus, +  getMergeReserveInfo, +  talerPaytoFromExchangeReserve, +} from "./pay-peer-common.js"; +import { +  constructTransactionIdentifier, +  notifyTransition, +  stopLongpolling, +} from "./transactions.js"; +import { +  checkWithdrawalKycStatus, +  getExchangeWithdrawalInfo, +  internalCreateWithdrawalGroup, +  processWithdrawalGroup, +} from "./withdraw.js"; + +const logger = new Logger("pay-peer-pull-credit.ts"); + +export async function queryPurseForPeerPullCredit( +  ws: InternalWalletState, +  pullIni: PeerPullPaymentInitiationRecord, +  cancellationToken: CancellationToken, +): Promise<LongpollResult> { +  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.get(purseDepositUrl.href, { +    timeout: { d_ms: 60000 }, +    cancellationToken, +  }); + +  logger.info(`purse status code: HTTP ${resp.status}`); + +  const result = await readSuccessResponseJsonOrErrorCode( +    resp, +    codecForExchangePurseStatus(), +  ); + +  if (result.isError) { +    logger.info(`got purse status error, EC=${result.talerErrorResponse.code}`); +    if (resp.status === 404) { +      return { ready: false }; +    } else { +      throwUnexpectedRequestError(resp, result.talerErrorResponse); +    } +  } + +  if (!result.response.deposit_timestamp) { +    logger.info("purse not ready yet (no deposit)"); +    return { ready: false }; +  } + +  const reserve = await ws.db +    .mktx((x) => [x.reserves]) +    .runReadOnly(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, +      contractTerms: pullIni.contractTerms, +      contractPriv: pullIni.contractPriv, +    }, +    forcedWithdrawalGroupId: pullIni.withdrawalGroupId, +    exchangeBaseUrl: pullIni.exchangeBaseUrl, +    reserveStatus: WithdrawalGroupStatus.PendingQueryingStatus, +    reserveKeyPair: { +      priv: reserve.reservePriv, +      pub: reserve.reservePub, +    }, +  }); + +  await ws.db +    .mktx((x) => [x.peerPullPaymentInitiations]) +    .runReadWrite(async (tx) => { +      const finPi = await tx.peerPullPaymentInitiations.get(pullIni.pursePub); +      if (!finPi) { +        logger.warn("peerPullPaymentInitiation not found anymore"); +        return; +      } +      if (finPi.status === PeerPullPaymentInitiationStatus.PendingReady) { +        finPi.status = PeerPullPaymentInitiationStatus.DonePurseDeposited; +      } +      await tx.peerPullPaymentInitiations.put(finPi); +    }); +  return { +    ready: true, +  }; +} + +export async function processPeerPullCredit( +  ws: InternalWalletState, +  pursePub: string, +): Promise<OperationAttemptResult> { +  const pullIni = await ws.db +    .mktx((x) => [x.peerPullPaymentInitiations]) +    .runReadOnly(async (tx) => { +      return tx.peerPullPaymentInitiations.get(pursePub); +    }); +  if (!pullIni) { +    throw Error("peer pull payment initiation not found in database"); +  } + +  const retryTag = constructTaskIdentifier({ +    tag: PendingTaskType.PeerPullCredit, +    pursePub, +  }); + +  // We're already running! +  if (ws.activeLongpoll[retryTag]) { +    logger.info("peer-pull-credit already in long-polling, returning!"); +    return { +      type: OperationAttemptResultType.Longpoll, +    }; +  } + +  logger.trace(`processing ${retryTag}, status=${pullIni.status}`); + +  switch (pullIni.status) { +    case PeerPullPaymentInitiationStatus.DonePurseDeposited: { +      // We implement this case so that the "retry" action on a peer-pull-credit transaction +      // also retries the withdrawal task. + +      logger.warn( +        "peer pull payment initiation is already finished, retrying withdrawal", +      ); + +      const withdrawalGroupId = pullIni.withdrawalGroupId; + +      if (withdrawalGroupId) { +        const taskId = constructTaskIdentifier({ +          tag: PendingTaskType.Withdraw, +          withdrawalGroupId, +        }); +        stopLongpolling(ws, taskId); +        await resetOperationTimeout(ws, taskId); +        await runOperationWithErrorReporting(ws, taskId, () => +          processWithdrawalGroup(ws, withdrawalGroupId), +        ); +      } +      return { +        type: OperationAttemptResultType.Finished, +        result: undefined, +      }; +    } +    case PeerPullPaymentInitiationStatus.PendingReady: +      runLongpollAsync(ws, retryTag, async (cancellationToken) => +        queryPurseForPeerPullCredit(ws, pullIni, cancellationToken), +      ); +      logger.trace( +        "returning early from processPeerPullCredit for long-polling in background", +      ); +      return { +        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", +        ); +      } +      break; +    } +    case PeerPullPaymentInitiationStatus.PendingCreatePurse: +      break; +    default: +      throw Error(`unknown PeerPullPaymentInitiationStatus ${pullIni.status}`); +  } + +  const mergeReserve = await ws.db +    .mktx((x) => [x.reserves]) +    .runReadOnly(async (tx) => { +      return tx.reserves.get(pullIni.mergeReserveRowId); +    }); + +  if (!mergeReserve) { +    throw Error("merge reserve for peer pull payment not found in database"); +  } + +  const purseFee = Amounts.stringify(Amounts.zeroOfAmount(pullIni.amount)); + +  const reservePayto = talerPaytoFromExchangeReserve( +    pullIni.exchangeBaseUrl, +    mergeReserve.reservePub, +  ); + +  const econtractResp = await ws.cryptoApi.encryptContractForDeposit({ +    contractPriv: pullIni.contractPriv, +    contractPub: pullIni.contractPub, +    contractTerms: pullIni.contractTerms, +    pursePriv: pullIni.pursePriv, +    pursePub: pullIni.pursePub, +  }); + +  const purseExpiration = pullIni.contractTerms.purse_expiration; +  const sigRes = await ws.cryptoApi.signReservePurseCreate({ +    contractTermsHash: pullIni.contractTermsHash, +    flags: WalletAccountMergeFlags.CreateWithPurseFee, +    mergePriv: pullIni.mergePriv, +    mergeTimestamp: TalerPreciseTimestamp.round(pullIni.mergeTimestamp), +    purseAmount: pullIni.contractTerms.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(pullIni.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.contractTerms.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.postJson( +    reservePurseMergeUrl.href, +    reservePurseReqBody, +  ); + +  if (httpResp.status === HttpStatusCode.UnavailableForLegalReasons) { +    const respJson = await httpResp.json(); +    const kycPending = codecForWalletKycUuid().decode(respJson); +    logger.info(`kyc uuid response: ${j2s(kycPending)}`); + +    await ws.db +      .mktx((x) => [x.peerPullPaymentInitiations]) +      .runReadWrite(async (tx) => { +        const peerIni = await tx.peerPullPaymentInitiations.get(pursePub); +        if (!peerIni) { +          return; +        } +        peerIni.kycInfo = { +          paytoHash: kycPending.h_payto, +          requirementRow: kycPending.requirement_row, +        }; +        peerIni.status = +          PeerPullPaymentInitiationStatus.PendingMergeKycRequired; +        await tx.peerPullPaymentInitiations.put(peerIni); +      }); +    return { +      type: OperationAttemptResultType.Pending, +      result: undefined, +    }; +  } + +  const resp = await readSuccessResponseJsonOrThrow(httpResp, codecForAny()); + +  logger.info(`reserve merge response: ${j2s(resp)}`); + +  await ws.db +    .mktx((x) => [x.peerPullPaymentInitiations]) +    .runReadWrite(async (tx) => { +      const pi2 = await tx.peerPullPaymentInitiations.get(pursePub); +      if (!pi2) { +        return; +      } +      pi2.status = PeerPullPaymentInitiationStatus.PendingReady; +      await tx.peerPullPaymentInitiations.put(pi2); +    }); + +  return { +    type: OperationAttemptResultType.Finished, +    result: undefined, +  }; +} + +/** + * 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`); + +  return { +    exchangeBaseUrl: exchangeUrl, +    amountEffective: wi.withdrawalAmountEffective, +    amountRaw: req.amount, +  }; +} + +/** + * 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 +    .mktx((x) => [x.exchanges]) +    .runReadOnly(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; +        } +        if (candidate.lastWithdrawal && e.lastWithdrawal) { +          if ( +            AbsoluteTime.cmp( +              AbsoluteTime.fromPreciseTimestamp(e.lastWithdrawal), +              AbsoluteTime.fromPreciseTimestamp(candidate.lastWithdrawal), +            ) > 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 updateExchangeFromUrl(ws, exchangeBaseUrl); + +  const mergeReserveInfo = await getMergeReserveInfo(ws, { +    exchangeBaseUrl: exchangeBaseUrl, +  }); + +  const mergeTimestamp = TalerPreciseTimestamp.now(); + +  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 wi = await getExchangeWithdrawalInfo( +    ws, +    exchangeBaseUrl, +    Amounts.parseOrThrow(req.partialContractTerms.amount), +    undefined, +  ); + +  await ws.db +    .mktx((x) => [x.peerPullPaymentInitiations, x.contractTerms]) +    .runReadWrite(async (tx) => { +      await tx.peerPullPaymentInitiations.put({ +        amount: req.partialContractTerms.amount, +        contractTermsHash: hContractTerms, +        exchangeBaseUrl: exchangeBaseUrl, +        pursePriv: pursePair.priv, +        pursePub: pursePair.pub, +        mergePriv: mergePair.priv, +        mergePub: mergePair.pub, +        status: PeerPullPaymentInitiationStatus.PendingCreatePurse, +        contractTerms: contractTerms, +        mergeTimestamp, +        mergeReserveRowId: mergeReserveRowId, +        contractPriv: contractKeyPair.priv, +        contractPub: contractKeyPair.pub, +        withdrawalGroupId, +        estimatedAmountEffective: wi.withdrawalAmountEffective, +      }); +      await tx.contractTerms.put({ +        contractTermsRaw: contractTerms, +        h: hContractTerms, +      }); +    }); + +  // FIXME: Should we somehow signal to the client +  // whether purse creation has failed, or does the client/ +  // check this asynchronously from the transaction status? + +  const taskId = constructTaskIdentifier({ +    tag: PendingTaskType.PeerPullCredit, +    pursePub: pursePair.pub, +  }); + +  await runOperationWithErrorReporting(ws, taskId, async () => { +    return processPeerPullCredit(ws, pursePair.pub); +  }); + +  const transactionId = constructTransactionIdentifier({ +    tag: TransactionType.PeerPullCredit, +    pursePub: pursePair.pub, +  }); + +  return { +    talerUri: constructPayPullUri({ +      exchangeBaseUrl: exchangeBaseUrl, +      contractPriv: contractKeyPair.priv, +    }), +    transactionId, +  }; +} + +export async function suspendPeerPullCreditTransaction( +  ws: InternalWalletState, +  pursePub: string, +) { +  const taskId = constructTaskIdentifier({ +    tag: PendingTaskType.PeerPullCredit, +    pursePub, +  }); +  const transactionId = constructTransactionIdentifier({ +    tag: TransactionType.PeerPullCredit, +    pursePub, +  }); +  stopLongpolling(ws, taskId); +  const transitionInfo = await ws.db +    .mktx((x) => [x.peerPullPaymentInitiations]) +    .runReadWrite(async (tx) => { +      const pullCreditRec = await tx.peerPullPaymentInitiations.get(pursePub); +      if (!pullCreditRec) { +        logger.warn(`peer pull credit ${pursePub} not found`); +        return; +      } +      let newStatus: PeerPullPaymentInitiationStatus | undefined = undefined; +      switch (pullCreditRec.status) { +        case PeerPullPaymentInitiationStatus.PendingCreatePurse: +          newStatus = PeerPullPaymentInitiationStatus.SuspendedCreatePurse; +          break; +        case PeerPullPaymentInitiationStatus.PendingMergeKycRequired: +          newStatus = PeerPullPaymentInitiationStatus.SuspendedMergeKycRequired; +          break; +        case PeerPullPaymentInitiationStatus.PendingWithdrawing: +          newStatus = PeerPullPaymentInitiationStatus.SuspendedWithdrawing; +          break; +        case PeerPullPaymentInitiationStatus.PendingReady: +          newStatus = PeerPullPaymentInitiationStatus.SuspendedReady; +          break; +        case PeerPullPaymentInitiationStatus.AbortingDeletePurse: +          newStatus = +            PeerPullPaymentInitiationStatus.SuspendedAbortingDeletePurse; +          break; +        case PeerPullPaymentInitiationStatus.DonePurseDeposited: +        case PeerPullPaymentInitiationStatus.SuspendedCreatePurse: +        case PeerPullPaymentInitiationStatus.SuspendedMergeKycRequired: +        case PeerPullPaymentInitiationStatus.SuspendedReady: +        case PeerPullPaymentInitiationStatus.SuspendedWithdrawing: +        case PeerPullPaymentInitiationStatus.Aborted: +        case PeerPullPaymentInitiationStatus.Failed: +        case PeerPullPaymentInitiationStatus.SuspendedAbortingDeletePurse: +          break; +        default: +          assertUnreachable(pullCreditRec.status); +      } +      if (newStatus != null) { +        const oldTxState = computePeerPullCreditTransactionState(pullCreditRec); +        pullCreditRec.status = newStatus; +        const newTxState = computePeerPullCreditTransactionState(pullCreditRec); +        await tx.peerPullPaymentInitiations.put(pullCreditRec); +        return { +          oldTxState, +          newTxState, +        }; +      } +      return undefined; +    }); +  notifyTransition(ws, transactionId, transitionInfo); +} + +export async function abortPeerPullCreditTransaction( +  ws: InternalWalletState, +  pursePub: string, +) { +  const taskId = constructTaskIdentifier({ +    tag: PendingTaskType.PeerPullCredit, +    pursePub, +  }); +  const transactionId = constructTransactionIdentifier({ +    tag: TransactionType.PeerPullCredit, +    pursePub, +  }); +  stopLongpolling(ws, taskId); +  const transitionInfo = await ws.db +    .mktx((x) => [x.peerPullPaymentInitiations]) +    .runReadWrite(async (tx) => { +      const pullCreditRec = await tx.peerPullPaymentInitiations.get(pursePub); +      if (!pullCreditRec) { +        logger.warn(`peer pull credit ${pursePub} not found`); +        return; +      } +      let newStatus: PeerPullPaymentInitiationStatus | undefined = undefined; +      switch (pullCreditRec.status) { +        case PeerPullPaymentInitiationStatus.PendingCreatePurse: +        case PeerPullPaymentInitiationStatus.PendingMergeKycRequired: +          newStatus = PeerPullPaymentInitiationStatus.AbortingDeletePurse; +          break; +        case PeerPullPaymentInitiationStatus.PendingWithdrawing: +          throw Error("can't abort anymore"); +        case PeerPullPaymentInitiationStatus.PendingReady: +          newStatus = PeerPullPaymentInitiationStatus.AbortingDeletePurse; +          break; +        case PeerPullPaymentInitiationStatus.DonePurseDeposited: +        case PeerPullPaymentInitiationStatus.SuspendedCreatePurse: +        case PeerPullPaymentInitiationStatus.SuspendedMergeKycRequired: +        case PeerPullPaymentInitiationStatus.SuspendedReady: +        case PeerPullPaymentInitiationStatus.SuspendedWithdrawing: +        case PeerPullPaymentInitiationStatus.Aborted: +        case PeerPullPaymentInitiationStatus.AbortingDeletePurse: +        case PeerPullPaymentInitiationStatus.Failed: +        case PeerPullPaymentInitiationStatus.SuspendedAbortingDeletePurse: +          break; +        default: +          assertUnreachable(pullCreditRec.status); +      } +      if (newStatus != null) { +        const oldTxState = computePeerPullCreditTransactionState(pullCreditRec); +        pullCreditRec.status = newStatus; +        const newTxState = computePeerPullCreditTransactionState(pullCreditRec); +        await tx.peerPullPaymentInitiations.put(pullCreditRec); +        return { +          oldTxState, +          newTxState, +        }; +      } +      return undefined; +    }); +  notifyTransition(ws, transactionId, transitionInfo); +} + +export async function failPeerPullCreditTransaction( +  ws: InternalWalletState, +  pursePub: string, +) { +  const taskId = constructTaskIdentifier({ +    tag: PendingTaskType.PeerPullCredit, +    pursePub, +  }); +  const transactionId = constructTransactionIdentifier({ +    tag: TransactionType.PeerPullCredit, +    pursePub, +  }); +  stopLongpolling(ws, taskId); +  const transitionInfo = await ws.db +    .mktx((x) => [x.peerPullPaymentInitiations]) +    .runReadWrite(async (tx) => { +      const pullCreditRec = await tx.peerPullPaymentInitiations.get(pursePub); +      if (!pullCreditRec) { +        logger.warn(`peer pull credit ${pursePub} not found`); +        return; +      } +      let newStatus: PeerPullPaymentInitiationStatus | undefined = undefined; +      switch (pullCreditRec.status) { +        case PeerPullPaymentInitiationStatus.PendingCreatePurse: +        case PeerPullPaymentInitiationStatus.PendingMergeKycRequired: +        case PeerPullPaymentInitiationStatus.PendingWithdrawing: +        case PeerPullPaymentInitiationStatus.PendingReady: +        case PeerPullPaymentInitiationStatus.DonePurseDeposited: +        case PeerPullPaymentInitiationStatus.SuspendedCreatePurse: +        case PeerPullPaymentInitiationStatus.SuspendedMergeKycRequired: +        case PeerPullPaymentInitiationStatus.SuspendedReady: +        case PeerPullPaymentInitiationStatus.SuspendedWithdrawing: +        case PeerPullPaymentInitiationStatus.Aborted: +        case PeerPullPaymentInitiationStatus.Failed: +          break; +        case PeerPullPaymentInitiationStatus.AbortingDeletePurse: +        case PeerPullPaymentInitiationStatus.SuspendedAbortingDeletePurse: +          newStatus = PeerPullPaymentInitiationStatus.Failed; +          break; +        default: +          assertUnreachable(pullCreditRec.status); +      } +      if (newStatus != null) { +        const oldTxState = computePeerPullCreditTransactionState(pullCreditRec); +        pullCreditRec.status = newStatus; +        const newTxState = computePeerPullCreditTransactionState(pullCreditRec); +        await tx.peerPullPaymentInitiations.put(pullCreditRec); +        return { +          oldTxState, +          newTxState, +        }; +      } +      return undefined; +    }); +  notifyTransition(ws, transactionId, transitionInfo); +} + +export async function resumePeerPullCreditTransaction( +  ws: InternalWalletState, +  pursePub: string, +) { +  const taskId = constructTaskIdentifier({ +    tag: PendingTaskType.PeerPullCredit, +    pursePub, +  }); +  const transactionId = constructTransactionIdentifier({ +    tag: TransactionType.PeerPullCredit, +    pursePub, +  }); +  stopLongpolling(ws, taskId); +  const transitionInfo = await ws.db +    .mktx((x) => [x.peerPullPaymentInitiations]) +    .runReadWrite(async (tx) => { +      const pullCreditRec = await tx.peerPullPaymentInitiations.get(pursePub); +      if (!pullCreditRec) { +        logger.warn(`peer pull credit ${pursePub} not found`); +        return; +      } +      let newStatus: PeerPullPaymentInitiationStatus | undefined = undefined; +      switch (pullCreditRec.status) { +        case PeerPullPaymentInitiationStatus.PendingCreatePurse: +        case PeerPullPaymentInitiationStatus.PendingMergeKycRequired: +        case PeerPullPaymentInitiationStatus.PendingWithdrawing: +        case PeerPullPaymentInitiationStatus.PendingReady: +        case PeerPullPaymentInitiationStatus.AbortingDeletePurse: +        case PeerPullPaymentInitiationStatus.DonePurseDeposited: +        case PeerPullPaymentInitiationStatus.Failed: +        case PeerPullPaymentInitiationStatus.Aborted: +          break; +        case PeerPullPaymentInitiationStatus.SuspendedCreatePurse: +          newStatus = PeerPullPaymentInitiationStatus.PendingCreatePurse; +          break; +        case PeerPullPaymentInitiationStatus.SuspendedMergeKycRequired: +          newStatus = PeerPullPaymentInitiationStatus.PendingMergeKycRequired; +          break; +        case PeerPullPaymentInitiationStatus.SuspendedReady: +          newStatus = PeerPullPaymentInitiationStatus.PendingReady; +          break; +        case PeerPullPaymentInitiationStatus.SuspendedWithdrawing: +          newStatus = PeerPullPaymentInitiationStatus.PendingWithdrawing; +          break; +        case PeerPullPaymentInitiationStatus.SuspendedAbortingDeletePurse: +          newStatus = PeerPullPaymentInitiationStatus.AbortingDeletePurse; +          break; +        default: +          assertUnreachable(pullCreditRec.status); +      } +      if (newStatus != null) { +        const oldTxState = computePeerPullCreditTransactionState(pullCreditRec); +        pullCreditRec.status = newStatus; +        const newTxState = computePeerPullCreditTransactionState(pullCreditRec); +        await tx.peerPullPaymentInitiations.put(pullCreditRec); +        return { +          oldTxState, +          newTxState, +        }; +      } +      return undefined; +    }); +  ws.workAvailable.trigger(); +  notifyTransition(ws, transactionId, transitionInfo); +} + +export function computePeerPullCreditTransactionState( +  pullCreditRecord: PeerPullPaymentInitiationRecord, +): TransactionState { +  switch (pullCreditRecord.status) { +    case PeerPullPaymentInitiationStatus.PendingCreatePurse: +      return { +        major: TransactionMajorState.Pending, +        minor: TransactionMinorState.CreatePurse, +      }; +    case PeerPullPaymentInitiationStatus.PendingMergeKycRequired: +      return { +        major: TransactionMajorState.Pending, +        minor: TransactionMinorState.MergeKycRequired, +      }; +    case PeerPullPaymentInitiationStatus.PendingReady: +      return { +        major: TransactionMajorState.Pending, +        minor: TransactionMinorState.Ready, +      }; +    case PeerPullPaymentInitiationStatus.DonePurseDeposited: +      return { +        major: TransactionMajorState.Done, +      }; +    case PeerPullPaymentInitiationStatus.PendingWithdrawing: +      return { +        major: TransactionMajorState.Pending, +        minor: TransactionMinorState.Withdraw, +      }; +    case PeerPullPaymentInitiationStatus.SuspendedCreatePurse: +      return { +        major: TransactionMajorState.Suspended, +        minor: TransactionMinorState.CreatePurse, +      }; +    case PeerPullPaymentInitiationStatus.SuspendedReady: +      return { +        major: TransactionMajorState.Suspended, +        minor: TransactionMinorState.Ready, +      }; +    case PeerPullPaymentInitiationStatus.SuspendedWithdrawing: +      return { +        major: TransactionMajorState.Pending, +        minor: TransactionMinorState.Withdraw, +      }; +    case PeerPullPaymentInitiationStatus.SuspendedMergeKycRequired: +      return { +        major: TransactionMajorState.Suspended, +        minor: TransactionMinorState.MergeKycRequired, +      }; +    case PeerPullPaymentInitiationStatus.Aborted: +      return { +        major: TransactionMajorState.Aborted, +      }; +    case PeerPullPaymentInitiationStatus.AbortingDeletePurse: +      return { +        major: TransactionMajorState.Aborting, +        minor: TransactionMinorState.DeletePurse, +      }; +    case PeerPullPaymentInitiationStatus.Failed: +      return { +        major: TransactionMajorState.Failed, +      }; +    case PeerPullPaymentInitiationStatus.SuspendedAbortingDeletePurse: +      return { +        major: TransactionMajorState.Aborting, +        minor: TransactionMinorState.DeletePurse, +      }; +  } +} + +export function computePeerPullCreditTransactionActions( +  pullCreditRecord: PeerPullPaymentInitiationRecord, +): TransactionAction[] { +  switch (pullCreditRecord.status) { +    case PeerPullPaymentInitiationStatus.PendingCreatePurse: +      return [TransactionAction.Abort, TransactionAction.Suspend]; +    case PeerPullPaymentInitiationStatus.PendingMergeKycRequired: +      return [TransactionAction.Abort, TransactionAction.Suspend]; +    case PeerPullPaymentInitiationStatus.PendingReady: +      return [TransactionAction.Abort, TransactionAction.Suspend]; +    case PeerPullPaymentInitiationStatus.DonePurseDeposited: +      return [TransactionAction.Delete]; +    case PeerPullPaymentInitiationStatus.PendingWithdrawing: +      return [TransactionAction.Abort, TransactionAction.Suspend]; +    case PeerPullPaymentInitiationStatus.SuspendedCreatePurse: +      return [TransactionAction.Resume, TransactionAction.Abort]; +    case PeerPullPaymentInitiationStatus.SuspendedReady: +      return [TransactionAction.Abort, TransactionAction.Resume]; +    case PeerPullPaymentInitiationStatus.SuspendedWithdrawing: +      return [TransactionAction.Resume, TransactionAction.Fail]; +    case PeerPullPaymentInitiationStatus.SuspendedMergeKycRequired: +      return [TransactionAction.Resume, TransactionAction.Fail]; +    case PeerPullPaymentInitiationStatus.Aborted: +      return [TransactionAction.Delete]; +    case PeerPullPaymentInitiationStatus.AbortingDeletePurse: +      return [TransactionAction.Suspend, TransactionAction.Fail]; +    case PeerPullPaymentInitiationStatus.Failed: +      return [TransactionAction.Delete]; +    case PeerPullPaymentInitiationStatus.SuspendedAbortingDeletePurse: +      return [TransactionAction.Resume, TransactionAction.Fail]; +  } +} diff --git a/packages/taler-wallet-core/src/operations/pay-peer-pull-debit.ts b/packages/taler-wallet-core/src/operations/pay-peer-pull-debit.ts new file mode 100644 index 000000000..fdec42bbd --- /dev/null +++ b/packages/taler-wallet-core/src/operations/pay-peer-pull-debit.ts @@ -0,0 +1,604 @@ +/* + 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 { +  ConfirmPeerPullDebitRequest, +  AcceptPeerPullPaymentResponse, +  Amounts, +  j2s, +  TalerError, +  TalerErrorCode, +  TransactionType, +  RefreshReason, +  Logger, +  PeerContractTerms, +  PreparePeerPullDebitRequest, +  PreparePeerPullDebitResponse, +  TalerPreciseTimestamp, +  codecForExchangeGetContractResponse, +  codecForPeerContractTerms, +  decodeCrock, +  eddsaGetPublic, +  encodeCrock, +  getRandomBytes, +  parsePayPullUri, +  TransactionAction, +  TransactionMajorState, +  TransactionMinorState, +  TransactionState, +} from "@gnu-taler/taler-util"; +import { +  InternalWalletState, +  PeerPullDebitRecordStatus, +  PeerPullPaymentIncomingRecord, +  PendingTaskType, +} from "../index.js"; +import { TaskIdentifiers, constructTaskIdentifier } from "../util/retries.js"; +import { spendCoins, runOperationWithErrorReporting } from "./common.js"; +import { +  codecForExchangePurseStatus, +  getTotalPeerPaymentCost, +  selectPeerCoins, +} from "./pay-peer-common.js"; +import { processPeerPullDebit } from "./pay-peer-push-credit.js"; +import { +  constructTransactionIdentifier, +  notifyTransition, +  stopLongpolling, +} from "./transactions.js"; +import { readSuccessResponseJsonOrThrow } from "@gnu-taler/taler-util/http"; +import { assertUnreachable } from "../util/assertUnreachable.js"; + +const logger = new Logger("pay-peer-pull-debit.ts"); + +export async function confirmPeerPullDebit( +  ws: InternalWalletState, +  req: ConfirmPeerPullDebitRequest, +): Promise<AcceptPeerPullPaymentResponse> { +  const peerPullInc = await ws.db +    .mktx((x) => [x.peerPullPaymentIncoming]) +    .runReadOnly(async (tx) => { +      return tx.peerPullPaymentIncoming.get(req.peerPullPaymentIncomingId); +    }); + +  if (!peerPullInc) { +    throw Error( +      `can't accept unknown incoming p2p pull payment (${req.peerPullPaymentIncomingId})`, +    ); +  } + +  const instructedAmount = Amounts.parseOrThrow( +    peerPullInc.contractTerms.amount, +  ); + +  const coinSelRes = await selectPeerCoins(ws, { instructedAmount }); +  logger.info(`selected p2p coins (pull): ${j2s(coinSelRes)}`); + +  if (coinSelRes.type !== "success") { +    throw TalerError.fromDetail( +      TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE, +      { +        insufficientBalanceDetails: coinSelRes.insufficientBalanceDetails, +      }, +    ); +  } + +  const sel = coinSelRes.result; + +  const totalAmount = await getTotalPeerPaymentCost( +    ws, +    coinSelRes.result.coins, +  ); + +  const ppi = await ws.db +    .mktx((x) => [ +      x.exchanges, +      x.coins, +      x.denominations, +      x.refreshGroups, +      x.peerPullPaymentIncoming, +      x.coinAvailability, +    ]) +    .runReadWrite(async (tx) => { +      await spendCoins(ws, tx, { +        // allocationId: `txn:peer-pull-debit:${req.peerPullPaymentIncomingId}`, +        allocationId: constructTransactionIdentifier({ +          tag: TransactionType.PeerPullDebit, +          peerPullPaymentIncomingId: req.peerPullPaymentIncomingId, +        }), +        coinPubs: sel.coins.map((x) => x.coinPub), +        contributions: sel.coins.map((x) => +          Amounts.parseOrThrow(x.contribution), +        ), +        refreshReason: RefreshReason.PayPeerPull, +      }); + +      const pi = await tx.peerPullPaymentIncoming.get( +        req.peerPullPaymentIncomingId, +      ); +      if (!pi) { +        throw Error(); +      } +      if (pi.status === PeerPullDebitRecordStatus.DialogProposed) { +        pi.status = PeerPullDebitRecordStatus.PendingDeposit; +        pi.coinSel = { +          coinPubs: sel.coins.map((x) => x.coinPub), +          contributions: sel.coins.map((x) => x.contribution), +          totalCost: Amounts.stringify(totalAmount), +        }; +      } +      await tx.peerPullPaymentIncoming.put(pi); +      return pi; +    }); + +  await runOperationWithErrorReporting( +    ws, +    TaskIdentifiers.forPeerPullPaymentDebit(ppi), +    async () => { +      return processPeerPullDebit(ws, ppi.peerPullPaymentIncomingId); +    }, +  ); + +  const transactionId = constructTransactionIdentifier({ +    tag: TransactionType.PeerPullDebit, +    peerPullPaymentIncomingId: req.peerPullPaymentIncomingId, +  }); + +  return { +    transactionId, +  }; +} + +/** + * Look up information about an incoming peer pull payment. + * Store the results in the wallet DB. + */ +export async function preparePeerPullDebit( +  ws: InternalWalletState, +  req: PreparePeerPullDebitRequest, +): Promise<PreparePeerPullDebitResponse> { +  const uri = parsePayPullUri(req.talerUri); + +  if (!uri) { +    throw Error("got invalid taler://pay-pull URI"); +  } + +  const existingPullIncomingRecord = await ws.db +    .mktx((x) => [x.peerPullPaymentIncoming]) +    .runReadOnly(async (tx) => { +      return tx.peerPullPaymentIncoming.indexes.byExchangeAndContractPriv.get([ +        uri.exchangeBaseUrl, +        uri.contractPriv, +      ]); +    }); + +  if (existingPullIncomingRecord) { +    return { +      amount: existingPullIncomingRecord.contractTerms.amount, +      amountRaw: existingPullIncomingRecord.contractTerms.amount, +      amountEffective: existingPullIncomingRecord.totalCostEstimated, +      contractTerms: existingPullIncomingRecord.contractTerms, +      peerPullPaymentIncomingId: +        existingPullIncomingRecord.peerPullPaymentIncomingId, +      transactionId: constructTransactionIdentifier({ +        tag: TransactionType.PeerPullDebit, +        peerPullPaymentIncomingId: +          existingPullIncomingRecord.peerPullPaymentIncomingId, +      }), +    }; +  } + +  const exchangeBaseUrl = uri.exchangeBaseUrl; +  const contractPriv = uri.contractPriv; +  const contractPub = encodeCrock(eddsaGetPublic(decodeCrock(contractPriv))); + +  const getContractUrl = new URL(`contracts/${contractPub}`, exchangeBaseUrl); + +  const contractHttpResp = await ws.http.get(getContractUrl.href); + +  const contractResp = await readSuccessResponseJsonOrThrow( +    contractHttpResp, +    codecForExchangeGetContractResponse(), +  ); + +  const pursePub = contractResp.purse_pub; + +  const dec = await ws.cryptoApi.decryptContractForDeposit({ +    ciphertext: contractResp.econtract, +    contractPriv: contractPriv, +    pursePub: pursePub, +  }); + +  const getPurseUrl = new URL(`purses/${pursePub}/merge`, exchangeBaseUrl); + +  const purseHttpResp = await ws.http.get(getPurseUrl.href); + +  const purseStatus = await readSuccessResponseJsonOrThrow( +    purseHttpResp, +    codecForExchangePurseStatus(), +  ); + +  const peerPullPaymentIncomingId = encodeCrock(getRandomBytes(32)); + +  let contractTerms: PeerContractTerms; + +  if (dec.contractTerms) { +    contractTerms = codecForPeerContractTerms().decode(dec.contractTerms); +    // FIXME: Check that the purseStatus balance matches contract terms amount +  } else { +    // FIXME: In this case, where do we get the purse expiration from?! +    // https://bugs.gnunet.org/view.php?id=7706 +    throw Error("pull payments without contract terms not supported yet"); +  } + +  // FIXME: Why don't we compute the totalCost here?! + +  const instructedAmount = Amounts.parseOrThrow(contractTerms.amount); + +  const coinSelRes = await selectPeerCoins(ws, { instructedAmount }); +  logger.info(`selected p2p coins (pull): ${j2s(coinSelRes)}`); + +  if (coinSelRes.type !== "success") { +    throw TalerError.fromDetail( +      TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE, +      { +        insufficientBalanceDetails: coinSelRes.insufficientBalanceDetails, +      }, +    ); +  } + +  const totalAmount = await getTotalPeerPaymentCost( +    ws, +    coinSelRes.result.coins, +  ); + +  await ws.db +    .mktx((x) => [x.peerPullPaymentIncoming]) +    .runReadWrite(async (tx) => { +      await tx.peerPullPaymentIncoming.add({ +        peerPullPaymentIncomingId, +        contractPriv: contractPriv, +        exchangeBaseUrl: exchangeBaseUrl, +        pursePub: pursePub, +        timestampCreated: TalerPreciseTimestamp.now(), +        contractTerms, +        status: PeerPullDebitRecordStatus.DialogProposed, +        totalCostEstimated: Amounts.stringify(totalAmount), +      }); +    }); + +  return { +    amount: contractTerms.amount, +    amountEffective: Amounts.stringify(totalAmount), +    amountRaw: contractTerms.amount, +    contractTerms: contractTerms, +    peerPullPaymentIncomingId, +    transactionId: constructTransactionIdentifier({ +      tag: TransactionType.PeerPullDebit, +      peerPullPaymentIncomingId: peerPullPaymentIncomingId, +    }), +  }; +} + +export async function suspendPeerPullDebitTransaction( +  ws: InternalWalletState, +  peerPullPaymentIncomingId: string, +) { +  const taskId = constructTaskIdentifier({ +    tag: PendingTaskType.PeerPullDebit, +    peerPullPaymentIncomingId, +  }); +  const transactionId = constructTransactionIdentifier({ +    tag: TransactionType.PeerPullDebit, +    peerPullPaymentIncomingId, +  }); +  stopLongpolling(ws, taskId); +  const transitionInfo = await ws.db +    .mktx((x) => [x.peerPullPaymentIncoming]) +    .runReadWrite(async (tx) => { +      const pullDebitRec = await tx.peerPullPaymentIncoming.get( +        peerPullPaymentIncomingId, +      ); +      if (!pullDebitRec) { +        logger.warn(`peer pull debit ${peerPullPaymentIncomingId} not found`); +        return; +      } +      let newStatus: PeerPullDebitRecordStatus | undefined = undefined; +      switch (pullDebitRec.status) { +        case PeerPullDebitRecordStatus.DialogProposed: +          break; +        case PeerPullDebitRecordStatus.DonePaid: +          break; +        case PeerPullDebitRecordStatus.PendingDeposit: +          newStatus = PeerPullDebitRecordStatus.SuspendedDeposit; +          break; +        case PeerPullDebitRecordStatus.SuspendedDeposit: +          break; +        case PeerPullDebitRecordStatus.Aborted: +          break; +        case PeerPullDebitRecordStatus.AbortingRefresh: +          newStatus = PeerPullDebitRecordStatus.SuspendedAbortingRefresh; +          break; +        case PeerPullDebitRecordStatus.Failed: +          break; +        case PeerPullDebitRecordStatus.SuspendedAbortingRefresh: +          break; +        default: +          assertUnreachable(pullDebitRec.status); +      } +      if (newStatus != null) { +        const oldTxState = computePeerPullDebitTransactionState(pullDebitRec); +        pullDebitRec.status = newStatus; +        const newTxState = computePeerPullDebitTransactionState(pullDebitRec); +        await tx.peerPullPaymentIncoming.put(pullDebitRec); +        return { +          oldTxState, +          newTxState, +        }; +      } +      return undefined; +    }); +  notifyTransition(ws, transactionId, transitionInfo); +} + +export async function abortPeerPullDebitTransaction( +  ws: InternalWalletState, +  peerPullPaymentIncomingId: string, +) { +  const taskId = constructTaskIdentifier({ +    tag: PendingTaskType.PeerPullDebit, +    peerPullPaymentIncomingId, +  }); +  const transactionId = constructTransactionIdentifier({ +    tag: TransactionType.PeerPullDebit, +    peerPullPaymentIncomingId, +  }); +  stopLongpolling(ws, taskId); +  const transitionInfo = await ws.db +    .mktx((x) => [x.peerPullPaymentIncoming]) +    .runReadWrite(async (tx) => { +      const pullDebitRec = await tx.peerPullPaymentIncoming.get( +        peerPullPaymentIncomingId, +      ); +      if (!pullDebitRec) { +        logger.warn(`peer pull debit ${peerPullPaymentIncomingId} not found`); +        return; +      } +      let newStatus: PeerPullDebitRecordStatus | undefined = undefined; +      switch (pullDebitRec.status) { +        case PeerPullDebitRecordStatus.DialogProposed: +          newStatus = PeerPullDebitRecordStatus.Aborted; +          break; +        case PeerPullDebitRecordStatus.DonePaid: +          break; +        case PeerPullDebitRecordStatus.PendingDeposit: +          newStatus = PeerPullDebitRecordStatus.AbortingRefresh; +          break; +        case PeerPullDebitRecordStatus.SuspendedDeposit: +          break; +        case PeerPullDebitRecordStatus.Aborted: +          break; +        case PeerPullDebitRecordStatus.AbortingRefresh: +          break; +        case PeerPullDebitRecordStatus.Failed: +          break; +        case PeerPullDebitRecordStatus.SuspendedAbortingRefresh: +          break; +        default: +          assertUnreachable(pullDebitRec.status); +      } +      if (newStatus != null) { +        const oldTxState = computePeerPullDebitTransactionState(pullDebitRec); +        pullDebitRec.status = newStatus; +        const newTxState = computePeerPullDebitTransactionState(pullDebitRec); +        await tx.peerPullPaymentIncoming.put(pullDebitRec); +        return { +          oldTxState, +          newTxState, +        }; +      } +      return undefined; +    }); +  notifyTransition(ws, transactionId, transitionInfo); +} + +export async function failPeerPullDebitTransaction( +  ws: InternalWalletState, +  peerPullPaymentIncomingId: string, +) { +  const taskId = constructTaskIdentifier({ +    tag: PendingTaskType.PeerPullDebit, +    peerPullPaymentIncomingId, +  }); +  const transactionId = constructTransactionIdentifier({ +    tag: TransactionType.PeerPullDebit, +    peerPullPaymentIncomingId, +  }); +  stopLongpolling(ws, taskId); +  const transitionInfo = await ws.db +    .mktx((x) => [x.peerPullPaymentIncoming]) +    .runReadWrite(async (tx) => { +      const pullDebitRec = await tx.peerPullPaymentIncoming.get( +        peerPullPaymentIncomingId, +      ); +      if (!pullDebitRec) { +        logger.warn(`peer pull debit ${peerPullPaymentIncomingId} not found`); +        return; +      } +      let newStatus: PeerPullDebitRecordStatus | undefined = undefined; +      switch (pullDebitRec.status) { +        case PeerPullDebitRecordStatus.DialogProposed: +          newStatus = PeerPullDebitRecordStatus.Aborted; +          break; +        case PeerPullDebitRecordStatus.DonePaid: +          break; +        case PeerPullDebitRecordStatus.PendingDeposit: +          break; +        case PeerPullDebitRecordStatus.SuspendedDeposit: +          break; +        case PeerPullDebitRecordStatus.Aborted: +          break; +        case PeerPullDebitRecordStatus.Failed: +          break; +        case PeerPullDebitRecordStatus.SuspendedAbortingRefresh: +        case PeerPullDebitRecordStatus.AbortingRefresh: +          // FIXME: abort underlying refresh! +          newStatus = PeerPullDebitRecordStatus.Failed; +          break; +        default: +          assertUnreachable(pullDebitRec.status); +      } +      if (newStatus != null) { +        const oldTxState = computePeerPullDebitTransactionState(pullDebitRec); +        pullDebitRec.status = newStatus; +        const newTxState = computePeerPullDebitTransactionState(pullDebitRec); +        await tx.peerPullPaymentIncoming.put(pullDebitRec); +        return { +          oldTxState, +          newTxState, +        }; +      } +      return undefined; +    }); +  notifyTransition(ws, transactionId, transitionInfo); +} + +export async function resumePeerPullDebitTransaction( +  ws: InternalWalletState, +  peerPullPaymentIncomingId: string, +) { +  const taskId = constructTaskIdentifier({ +    tag: PendingTaskType.PeerPullDebit, +    peerPullPaymentIncomingId, +  }); +  const transactionId = constructTransactionIdentifier({ +    tag: TransactionType.PeerPullDebit, +    peerPullPaymentIncomingId, +  }); +  stopLongpolling(ws, taskId); +  const transitionInfo = await ws.db +    .mktx((x) => [x.peerPullPaymentIncoming]) +    .runReadWrite(async (tx) => { +      const pullDebitRec = await tx.peerPullPaymentIncoming.get( +        peerPullPaymentIncomingId, +      ); +      if (!pullDebitRec) { +        logger.warn(`peer pull debit ${peerPullPaymentIncomingId} not found`); +        return; +      } +      let newStatus: PeerPullDebitRecordStatus | undefined = undefined; +      switch (pullDebitRec.status) { +        case PeerPullDebitRecordStatus.DialogProposed: +        case PeerPullDebitRecordStatus.DonePaid: +        case PeerPullDebitRecordStatus.PendingDeposit: +          break; +        case PeerPullDebitRecordStatus.SuspendedDeposit: +          newStatus = PeerPullDebitRecordStatus.PendingDeposit; +          break; +        case PeerPullDebitRecordStatus.Aborted: +          break; +        case PeerPullDebitRecordStatus.AbortingRefresh: +          break; +        case PeerPullDebitRecordStatus.Failed: +          break; +        case PeerPullDebitRecordStatus.SuspendedAbortingRefresh: +          newStatus = PeerPullDebitRecordStatus.AbortingRefresh; +          break; +        default: +          assertUnreachable(pullDebitRec.status); +      } +      if (newStatus != null) { +        const oldTxState = computePeerPullDebitTransactionState(pullDebitRec); +        pullDebitRec.status = newStatus; +        const newTxState = computePeerPullDebitTransactionState(pullDebitRec); +        await tx.peerPullPaymentIncoming.put(pullDebitRec); +        return { +          oldTxState, +          newTxState, +        }; +      } +      return undefined; +    }); +  ws.workAvailable.trigger(); +  notifyTransition(ws, transactionId, transitionInfo); +} + +export function computePeerPullDebitTransactionState( +  pullDebitRecord: PeerPullPaymentIncomingRecord, +): TransactionState { +  switch (pullDebitRecord.status) { +    case PeerPullDebitRecordStatus.DialogProposed: +      return { +        major: TransactionMajorState.Dialog, +        minor: TransactionMinorState.Proposed, +      }; +    case PeerPullDebitRecordStatus.PendingDeposit: +      return { +        major: TransactionMajorState.Pending, +        minor: TransactionMinorState.Deposit, +      }; +    case PeerPullDebitRecordStatus.DonePaid: +      return { +        major: TransactionMajorState.Done, +      }; +    case PeerPullDebitRecordStatus.SuspendedDeposit: +      return { +        major: TransactionMajorState.Suspended, +        minor: TransactionMinorState.Deposit, +      }; +    case PeerPullDebitRecordStatus.Aborted: +      return { +        major: TransactionMajorState.Aborted, +      }; +    case PeerPullDebitRecordStatus.AbortingRefresh: +      return { +        major: TransactionMajorState.Aborting, +        minor: TransactionMinorState.Refresh, +      }; +    case PeerPullDebitRecordStatus.Failed: +      return { +        major: TransactionMajorState.Failed, +      }; +    case PeerPullDebitRecordStatus.SuspendedAbortingRefresh: +      return { +        major: TransactionMajorState.SuspendedAborting, +        minor: TransactionMinorState.Refresh, +      }; +  } +} + +export function computePeerPullDebitTransactionActions( +  pullDebitRecord: PeerPullPaymentIncomingRecord, +): TransactionAction[] { +  switch (pullDebitRecord.status) { +    case PeerPullDebitRecordStatus.DialogProposed: +      return []; +    case PeerPullDebitRecordStatus.PendingDeposit: +      return [TransactionAction.Abort, TransactionAction.Suspend]; +    case PeerPullDebitRecordStatus.DonePaid: +      return [TransactionAction.Delete]; +    case PeerPullDebitRecordStatus.SuspendedDeposit: +      return [TransactionAction.Resume, TransactionAction.Abort]; +    case PeerPullDebitRecordStatus.Aborted: +      return [TransactionAction.Delete]; +    case PeerPullDebitRecordStatus.AbortingRefresh: +      return [TransactionAction.Fail, TransactionAction.Suspend]; +    case PeerPullDebitRecordStatus.Failed: +      return [TransactionAction.Delete]; +    case PeerPullDebitRecordStatus.SuspendedAbortingRefresh: +      return [TransactionAction.Resume, TransactionAction.Fail]; +  } +} 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 new file mode 100644 index 000000000..69e0f3c27 --- /dev/null +++ b/packages/taler-wallet-core/src/operations/pay-peer-push-credit.ts @@ -0,0 +1,770 @@ +/* + 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 { +  PreparePeerPushCredit, +  PreparePeerPushCreditResponse, +  parsePayPushUri, +  codecForPeerContractTerms, +  TransactionType, +  encodeCrock, +  eddsaGetPublic, +  decodeCrock, +  codecForExchangeGetContractResponse, +  getRandomBytes, +  ContractTermsUtil, +  Amounts, +  TalerPreciseTimestamp, +  AcceptPeerPushPaymentResponse, +  ConfirmPeerPushCreditRequest, +  ExchangePurseMergeRequest, +  HttpStatusCode, +  PeerContractTerms, +  TalerProtocolTimestamp, +  WalletAccountMergeFlags, +  codecForAny, +  codecForWalletKycUuid, +  j2s, +  Logger, +  ExchangePurseDeposits, +  TransactionAction, +  TransactionMajorState, +  TransactionMinorState, +  TransactionState, +} from "@gnu-taler/taler-util"; +import { readSuccessResponseJsonOrThrow } from "@gnu-taler/taler-util/http"; +import { +  InternalWalletState, +  PeerPullDebitRecordStatus, +  PeerPushPaymentIncomingRecord, +  PeerPushPaymentIncomingStatus, +  PendingTaskType, +  WithdrawalGroupStatus, +  WithdrawalRecordType, +} from "../index.js"; +import { updateExchangeFromUrl } from "./exchanges.js"; +import { +  codecForExchangePurseStatus, +  getMergeReserveInfo, +  queryCoinInfosForSelection, +  talerPaytoFromExchangeReserve, +} from "./pay-peer-common.js"; +import { constructTransactionIdentifier, notifyTransition, stopLongpolling } from "./transactions.js"; +import { +  checkWithdrawalKycStatus, +  getExchangeWithdrawalInfo, +  internalCreateWithdrawalGroup, +} from "./withdraw.js"; +import { checkDbInvariant } from "../util/invariants.js"; +import { +  OperationAttemptResult, +  OperationAttemptResultType, +  constructTaskIdentifier, +} from "../util/retries.js"; +import { assertUnreachable } from "../util/assertUnreachable.js"; + +const logger = new Logger("pay-peer-push-credit.ts"); + +export async function preparePeerPushCredit( +  ws: InternalWalletState, +  req: PreparePeerPushCredit, +): Promise<PreparePeerPushCreditResponse> { +  const uri = parsePayPushUri(req.talerUri); + +  if (!uri) { +    throw Error("got invalid taler://pay-push URI"); +  } + +  const existing = await ws.db +    .mktx((x) => [x.contractTerms, x.peerPushPaymentIncoming]) +    .runReadOnly(async (tx) => { +      const existingPushInc = +        await tx.peerPushPaymentIncoming.indexes.byExchangeAndContractPriv.get([ +          uri.exchangeBaseUrl, +          uri.contractPriv, +        ]); +      if (!existingPushInc) { +        return; +      } +      const existingContractTermsRec = await tx.contractTerms.get( +        existingPushInc.contractTermsHash, +      ); +      if (!existingContractTermsRec) { +        throw Error( +          "contract terms for peer push payment credit not found in database", +        ); +      } +      const existingContractTerms = codecForPeerContractTerms().decode( +        existingContractTermsRec.contractTermsRaw, +      ); +      return { existingPushInc, existingContractTerms }; +    }); + +  if (existing) { +    return { +      amount: existing.existingContractTerms.amount, +      amountEffective: existing.existingPushInc.estimatedAmountEffective, +      amountRaw: existing.existingContractTerms.amount, +      contractTerms: existing.existingContractTerms, +      peerPushPaymentIncomingId: +        existing.existingPushInc.peerPushPaymentIncomingId, +      transactionId: constructTransactionIdentifier({ +        tag: TransactionType.PeerPushCredit, +        peerPushPaymentIncomingId: +          existing.existingPushInc.peerPushPaymentIncomingId, +      }), +    }; +  } + +  const exchangeBaseUrl = uri.exchangeBaseUrl; + +  await updateExchangeFromUrl(ws, exchangeBaseUrl); + +  const contractPriv = uri.contractPriv; +  const contractPub = encodeCrock(eddsaGetPublic(decodeCrock(contractPriv))); + +  const getContractUrl = new URL(`contracts/${contractPub}`, exchangeBaseUrl); + +  const contractHttpResp = await ws.http.get(getContractUrl.href); + +  const contractResp = await readSuccessResponseJsonOrThrow( +    contractHttpResp, +    codecForExchangeGetContractResponse(), +  ); + +  const pursePub = contractResp.purse_pub; + +  const dec = await ws.cryptoApi.decryptContractForMerge({ +    ciphertext: contractResp.econtract, +    contractPriv: contractPriv, +    pursePub: pursePub, +  }); + +  const getPurseUrl = new URL(`purses/${pursePub}/deposit`, exchangeBaseUrl); + +  const purseHttpResp = await ws.http.get(getPurseUrl.href); + +  const purseStatus = await readSuccessResponseJsonOrThrow( +    purseHttpResp, +    codecForExchangePurseStatus(), +  ); + +  const peerPushPaymentIncomingId = encodeCrock(getRandomBytes(32)); + +  const contractTermsHash = ContractTermsUtil.hashContractTerms( +    dec.contractTerms, +  ); + +  const withdrawalGroupId = encodeCrock(getRandomBytes(32)); + +  const wi = await getExchangeWithdrawalInfo( +    ws, +    exchangeBaseUrl, +    Amounts.parseOrThrow(purseStatus.balance), +    undefined, +  ); + +  await ws.db +    .mktx((x) => [x.contractTerms, x.peerPushPaymentIncoming]) +    .runReadWrite(async (tx) => { +      await tx.peerPushPaymentIncoming.add({ +        peerPushPaymentIncomingId, +        contractPriv: contractPriv, +        exchangeBaseUrl: exchangeBaseUrl, +        mergePriv: dec.mergePriv, +        pursePub: pursePub, +        timestamp: TalerPreciseTimestamp.now(), +        contractTermsHash, +        status: PeerPushPaymentIncomingStatus.DialogProposed, +        withdrawalGroupId, +        currency: Amounts.currencyOf(purseStatus.balance), +        estimatedAmountEffective: Amounts.stringify( +          wi.withdrawalAmountEffective, +        ), +      }); + +      await tx.contractTerms.put({ +        h: contractTermsHash, +        contractTermsRaw: dec.contractTerms, +      }); +    }); + +  return { +    amount: purseStatus.balance, +    amountEffective: wi.withdrawalAmountEffective, +    amountRaw: purseStatus.balance, +    contractTerms: dec.contractTerms, +    peerPushPaymentIncomingId, +    transactionId: constructTransactionIdentifier({ +      tag: TransactionType.PeerPushCredit, +      peerPushPaymentIncomingId, +    }), +  }; +} + +export async function processPeerPushCredit( +  ws: InternalWalletState, +  peerPushPaymentIncomingId: string, +): Promise<OperationAttemptResult> { +  let peerInc: PeerPushPaymentIncomingRecord | undefined; +  let contractTerms: PeerContractTerms | undefined; +  await ws.db +    .mktx((x) => [x.contractTerms, x.peerPushPaymentIncoming]) +    .runReadWrite(async (tx) => { +      peerInc = await tx.peerPushPaymentIncoming.get(peerPushPaymentIncomingId); +      if (!peerInc) { +        return; +      } +      const ctRec = await tx.contractTerms.get(peerInc.contractTermsHash); +      if (ctRec) { +        contractTerms = ctRec.contractTermsRaw; +      } +      await tx.peerPushPaymentIncoming.put(peerInc); +    }); + +  if (!peerInc) { +    throw Error( +      `can't accept unknown incoming p2p push payment (${peerPushPaymentIncomingId})`, +    ); +  } + +  checkDbInvariant(!!contractTerms); + +  const amount = Amounts.parseOrThrow(contractTerms.amount); + +  if ( +    peerInc.status === PeerPushPaymentIncomingStatus.PendingMergeKycRequired && +    peerInc.kycInfo +  ) { +    const txId = constructTransactionIdentifier({ +      tag: TransactionType.PeerPushCredit, +      peerPushPaymentIncomingId: peerInc.peerPushPaymentIncomingId, +    }); +    await checkWithdrawalKycStatus( +      ws, +      peerInc.exchangeBaseUrl, +      txId, +      peerInc.kycInfo, +      "individual", +    ); +  } + +  const mergeReserveInfo = await getMergeReserveInfo(ws, { +    exchangeBaseUrl: peerInc.exchangeBaseUrl, +  }); + +  const mergeTimestamp = TalerProtocolTimestamp.now(); + +  const reservePayto = talerPaytoFromExchangeReserve( +    peerInc.exchangeBaseUrl, +    mergeReserveInfo.reservePub, +  ); + +  const sigRes = await ws.cryptoApi.signPurseMerge({ +    contractTermsHash: ContractTermsUtil.hashContractTerms(contractTerms), +    flags: WalletAccountMergeFlags.MergeFullyPaidPurse, +    mergePriv: peerInc.mergePriv, +    mergeTimestamp: mergeTimestamp, +    purseAmount: Amounts.stringify(amount), +    purseExpiration: contractTerms.purse_expiration, +    purseFee: Amounts.stringify(Amounts.zeroOfCurrency(amount.currency)), +    pursePub: peerInc.pursePub, +    reservePayto, +    reservePriv: mergeReserveInfo.reservePriv, +  }); + +  const mergePurseUrl = new URL( +    `purses/${peerInc.pursePub}/merge`, +    peerInc.exchangeBaseUrl, +  ); + +  const mergeReq: ExchangePurseMergeRequest = { +    payto_uri: reservePayto, +    merge_timestamp: mergeTimestamp, +    merge_sig: sigRes.mergeSig, +    reserve_sig: sigRes.accountSig, +  }; + +  const mergeHttpResp = await ws.http.postJson(mergePurseUrl.href, mergeReq); + +  if (mergeHttpResp.status === HttpStatusCode.UnavailableForLegalReasons) { +    const respJson = await mergeHttpResp.json(); +    const kycPending = codecForWalletKycUuid().decode(respJson); +    logger.info(`kyc uuid response: ${j2s(kycPending)}`); + +    await ws.db +      .mktx((x) => [x.peerPushPaymentIncoming]) +      .runReadWrite(async (tx) => { +        const peerInc = await tx.peerPushPaymentIncoming.get( +          peerPushPaymentIncomingId, +        ); +        if (!peerInc) { +          return; +        } +        peerInc.kycInfo = { +          paytoHash: kycPending.h_payto, +          requirementRow: kycPending.requirement_row, +        }; +        peerInc.status = PeerPushPaymentIncomingStatus.PendingMergeKycRequired; +        await tx.peerPushPaymentIncoming.put(peerInc); +      }); +    return { +      type: OperationAttemptResultType.Pending, +      result: undefined, +    }; +  } + +  logger.trace(`merge request: ${j2s(mergeReq)}`); +  const res = await readSuccessResponseJsonOrThrow( +    mergeHttpResp, +    codecForAny(), +  ); +  logger.trace(`merge response: ${j2s(res)}`); + +  await internalCreateWithdrawalGroup(ws, { +    amount, +    wgInfo: { +      withdrawalType: WithdrawalRecordType.PeerPushCredit, +      contractTerms, +    }, +    forcedWithdrawalGroupId: peerInc.withdrawalGroupId, +    exchangeBaseUrl: peerInc.exchangeBaseUrl, +    reserveStatus: WithdrawalGroupStatus.PendingQueryingStatus, +    reserveKeyPair: { +      priv: mergeReserveInfo.reservePriv, +      pub: mergeReserveInfo.reservePub, +    }, +  }); + +  await ws.db +    .mktx((x) => [x.contractTerms, x.peerPushPaymentIncoming]) +    .runReadWrite(async (tx) => { +      const peerInc = await tx.peerPushPaymentIncoming.get( +        peerPushPaymentIncomingId, +      ); +      if (!peerInc) { +        return; +      } +      if ( +        peerInc.status === PeerPushPaymentIncomingStatus.PendingMerge || +        peerInc.status === PeerPushPaymentIncomingStatus.PendingMergeKycRequired +      ) { +        peerInc.status = PeerPushPaymentIncomingStatus.Done; +      } +      await tx.peerPushPaymentIncoming.put(peerInc); +    }); + +  return { +    type: OperationAttemptResultType.Finished, +    result: undefined, +  }; +} + +export async function confirmPeerPushCredit( +  ws: InternalWalletState, +  req: ConfirmPeerPushCreditRequest, +): Promise<AcceptPeerPushPaymentResponse> { +  let peerInc: PeerPushPaymentIncomingRecord | undefined; + +  await ws.db +    .mktx((x) => [x.contractTerms, x.peerPushPaymentIncoming]) +    .runReadWrite(async (tx) => { +      peerInc = await tx.peerPushPaymentIncoming.get( +        req.peerPushPaymentIncomingId, +      ); +      if (!peerInc) { +        return; +      } +      if (peerInc.status === PeerPushPaymentIncomingStatus.DialogProposed) { +        peerInc.status = PeerPushPaymentIncomingStatus.PendingMerge; +      } +      await tx.peerPushPaymentIncoming.put(peerInc); +    }); + +  if (!peerInc) { +    throw Error( +      `can't accept unknown incoming p2p push payment (${req.peerPushPaymentIncomingId})`, +    ); +  } + +  ws.workAvailable.trigger(); + +  const transactionId = constructTransactionIdentifier({ +    tag: TransactionType.PeerPushCredit, +    peerPushPaymentIncomingId: req.peerPushPaymentIncomingId, +  }); + +  return { +    transactionId, +  }; +} + + +export async function processPeerPullDebit( +  ws: InternalWalletState, +  peerPullPaymentIncomingId: string, +): Promise<OperationAttemptResult> { +  const peerPullInc = await ws.db +    .mktx((x) => [x.peerPullPaymentIncoming]) +    .runReadOnly(async (tx) => { +      return tx.peerPullPaymentIncoming.get(peerPullPaymentIncomingId); +    }); +  if (!peerPullInc) { +    throw Error("peer pull debit not found"); +  } +  if (peerPullInc.status === PeerPullDebitRecordStatus.PendingDeposit) { +    const pursePub = peerPullInc.pursePub; + +    const coinSel = peerPullInc.coinSel; +    if (!coinSel) { +      throw Error("invalid state, no coins selected"); +    } + +    const coins = await queryCoinInfosForSelection(ws, coinSel); + +    const depositSigsResp = await ws.cryptoApi.signPurseDeposits({ +      exchangeBaseUrl: peerPullInc.exchangeBaseUrl, +      pursePub: peerPullInc.pursePub, +      coins, +    }); + +    const purseDepositUrl = new URL( +      `purses/${pursePub}/deposit`, +      peerPullInc.exchangeBaseUrl, +    ); + +    const depositPayload: ExchangePurseDeposits = { +      deposits: depositSigsResp.deposits, +    }; + +    if (logger.shouldLogTrace()) { +      logger.trace(`purse deposit payload: ${j2s(depositPayload)}`); +    } + +    const httpResp = await ws.http.postJson( +      purseDepositUrl.href, +      depositPayload, +    ); +    const resp = await readSuccessResponseJsonOrThrow(httpResp, codecForAny()); +    logger.trace(`purse deposit response: ${j2s(resp)}`); +  } + +  await ws.db +    .mktx((x) => [x.peerPullPaymentIncoming]) +    .runReadWrite(async (tx) => { +      const pi = await tx.peerPullPaymentIncoming.get( +        peerPullPaymentIncomingId, +      ); +      if (!pi) { +        throw Error("peer pull payment not found anymore"); +      } +      if (pi.status === PeerPullDebitRecordStatus.PendingDeposit) { +        pi.status = PeerPullDebitRecordStatus.DonePaid; +      } +      await tx.peerPullPaymentIncoming.put(pi); +    }); + +  return { +    type: OperationAttemptResultType.Finished, +    result: undefined, +  }; +} + + +export async function suspendPeerPushCreditTransaction( +  ws: InternalWalletState, +  peerPushPaymentIncomingId: string, +) { +  const taskId = constructTaskIdentifier({ +    tag: PendingTaskType.PeerPushCredit, +    peerPushPaymentIncomingId, +  }); +  const transactionId = constructTransactionIdentifier({ +    tag: TransactionType.PeerPushCredit, +    peerPushPaymentIncomingId, +  }); +  stopLongpolling(ws, taskId); +  const transitionInfo = await ws.db +    .mktx((x) => [x.peerPushPaymentIncoming]) +    .runReadWrite(async (tx) => { +      const pushCreditRec = await tx.peerPushPaymentIncoming.get( +        peerPushPaymentIncomingId, +      ); +      if (!pushCreditRec) { +        logger.warn(`peer push credit ${peerPushPaymentIncomingId} not found`); +        return; +      } +      let newStatus: PeerPushPaymentIncomingStatus | undefined = undefined; +      switch (pushCreditRec.status) { +        case PeerPushPaymentIncomingStatus.DialogProposed: +        case PeerPushPaymentIncomingStatus.Done: +        case PeerPushPaymentIncomingStatus.SuspendedMerge: +        case PeerPushPaymentIncomingStatus.SuspendedMergeKycRequired: +        case PeerPushPaymentIncomingStatus.SuspendedWithdrawing: +          break; +        case PeerPushPaymentIncomingStatus.PendingMergeKycRequired: +          newStatus = PeerPushPaymentIncomingStatus.SuspendedMergeKycRequired; +          break; +        case PeerPushPaymentIncomingStatus.PendingMerge: +          newStatus = PeerPushPaymentIncomingStatus.SuspendedMerge; +          break; +        case PeerPushPaymentIncomingStatus.PendingWithdrawing: +          // FIXME: Suspend internal withdrawal transaction! +          newStatus = PeerPushPaymentIncomingStatus.SuspendedWithdrawing; +          break; +        case PeerPushPaymentIncomingStatus.Aborted: +          break; +        case PeerPushPaymentIncomingStatus.Failed: +          break; +        default: +          assertUnreachable(pushCreditRec.status); +      } +      if (newStatus != null) { +        const oldTxState = computePeerPushCreditTransactionState(pushCreditRec); +        pushCreditRec.status = newStatus; +        const newTxState = computePeerPushCreditTransactionState(pushCreditRec); +        await tx.peerPushPaymentIncoming.put(pushCreditRec); +        return { +          oldTxState, +          newTxState, +        }; +      } +      return undefined; +    }); +  notifyTransition(ws, transactionId, transitionInfo); +} + +export async function abortPeerPushCreditTransaction( +  ws: InternalWalletState, +  peerPushPaymentIncomingId: string, +) { +  const taskId = constructTaskIdentifier({ +    tag: PendingTaskType.PeerPushCredit, +    peerPushPaymentIncomingId, +  }); +  const transactionId = constructTransactionIdentifier({ +    tag: TransactionType.PeerPushCredit, +    peerPushPaymentIncomingId, +  }); +  stopLongpolling(ws, taskId); +  const transitionInfo = await ws.db +    .mktx((x) => [x.peerPushPaymentIncoming]) +    .runReadWrite(async (tx) => { +      const pushCreditRec = await tx.peerPushPaymentIncoming.get( +        peerPushPaymentIncomingId, +      ); +      if (!pushCreditRec) { +        logger.warn(`peer push credit ${peerPushPaymentIncomingId} not found`); +        return; +      } +      let newStatus: PeerPushPaymentIncomingStatus | undefined = undefined; +      switch (pushCreditRec.status) { +        case PeerPushPaymentIncomingStatus.DialogProposed: +          newStatus = PeerPushPaymentIncomingStatus.Aborted; +          break; +        case PeerPushPaymentIncomingStatus.Done: +          break; +        case PeerPushPaymentIncomingStatus.SuspendedMerge: +        case PeerPushPaymentIncomingStatus.SuspendedMergeKycRequired: +        case PeerPushPaymentIncomingStatus.SuspendedWithdrawing: +          newStatus = PeerPushPaymentIncomingStatus.Aborted; +          break; +        case PeerPushPaymentIncomingStatus.PendingMergeKycRequired: +          newStatus = PeerPushPaymentIncomingStatus.Aborted; +          break; +        case PeerPushPaymentIncomingStatus.PendingMerge: +          newStatus = PeerPushPaymentIncomingStatus.Aborted; +          break; +        case PeerPushPaymentIncomingStatus.PendingWithdrawing: +          newStatus = PeerPushPaymentIncomingStatus.Aborted; +          break; +        case PeerPushPaymentIncomingStatus.Aborted: +          break; +        case PeerPushPaymentIncomingStatus.Failed: +          break; +        default: +          assertUnreachable(pushCreditRec.status); +      } +      if (newStatus != null) { +        const oldTxState = computePeerPushCreditTransactionState(pushCreditRec); +        pushCreditRec.status = newStatus; +        const newTxState = computePeerPushCreditTransactionState(pushCreditRec); +        await tx.peerPushPaymentIncoming.put(pushCreditRec); +        return { +          oldTxState, +          newTxState, +        }; +      } +      return undefined; +    }); +  notifyTransition(ws, transactionId, transitionInfo); +} + +export async function failPeerPushCreditTransaction( +  ws: InternalWalletState, +  peerPushPaymentIncomingId: string, +) { +  // We don't have any "aborting" states! +  throw Error("can't run cancel-aborting on peer-push-credit transaction"); +} + +export async function resumePeerPushCreditTransaction( +  ws: InternalWalletState, +  peerPushPaymentIncomingId: string, +) { +  const taskId = constructTaskIdentifier({ +    tag: PendingTaskType.PeerPushCredit, +    peerPushPaymentIncomingId, +  }); +  const transactionId = constructTransactionIdentifier({ +    tag: TransactionType.PeerPushCredit, +    peerPushPaymentIncomingId, +  }); +  stopLongpolling(ws, taskId); +  const transitionInfo = await ws.db +    .mktx((x) => [x.peerPushPaymentIncoming]) +    .runReadWrite(async (tx) => { +      const pushCreditRec = await tx.peerPushPaymentIncoming.get( +        peerPushPaymentIncomingId, +      ); +      if (!pushCreditRec) { +        logger.warn(`peer push credit ${peerPushPaymentIncomingId} not found`); +        return; +      } +      let newStatus: PeerPushPaymentIncomingStatus | undefined = undefined; +      switch (pushCreditRec.status) { +        case PeerPushPaymentIncomingStatus.DialogProposed: +        case PeerPushPaymentIncomingStatus.Done: +        case PeerPushPaymentIncomingStatus.PendingMergeKycRequired: +        case PeerPushPaymentIncomingStatus.PendingMerge: +        case PeerPushPaymentIncomingStatus.PendingWithdrawing: +        case PeerPushPaymentIncomingStatus.SuspendedMerge: +          newStatus = PeerPushPaymentIncomingStatus.PendingMerge; +          break; +        case PeerPushPaymentIncomingStatus.SuspendedMergeKycRequired: +          newStatus = PeerPushPaymentIncomingStatus.PendingMergeKycRequired; +          break; +        case PeerPushPaymentIncomingStatus.SuspendedWithdrawing: +          // FIXME: resume underlying "internal-withdrawal" transaction. +          newStatus = PeerPushPaymentIncomingStatus.PendingWithdrawing; +          break; +        case PeerPushPaymentIncomingStatus.Aborted: +          break; +        case PeerPushPaymentIncomingStatus.Failed: +          break; +        default: +          assertUnreachable(pushCreditRec.status); +      } +      if (newStatus != null) { +        const oldTxState = computePeerPushCreditTransactionState(pushCreditRec); +        pushCreditRec.status = newStatus; +        const newTxState = computePeerPushCreditTransactionState(pushCreditRec); +        await tx.peerPushPaymentIncoming.put(pushCreditRec); +        return { +          oldTxState, +          newTxState, +        }; +      } +      return undefined; +    }); +  ws.workAvailable.trigger(); +  notifyTransition(ws, transactionId, transitionInfo); +} + +export function computePeerPushCreditTransactionState( +  pushCreditRecord: PeerPushPaymentIncomingRecord, +): TransactionState { +  switch (pushCreditRecord.status) { +    case PeerPushPaymentIncomingStatus.DialogProposed: +      return { +        major: TransactionMajorState.Dialog, +        minor: TransactionMinorState.Proposed, +      }; +    case PeerPushPaymentIncomingStatus.PendingMerge: +      return { +        major: TransactionMajorState.Pending, +        minor: TransactionMinorState.Merge, +      }; +    case PeerPushPaymentIncomingStatus.Done: +      return { +        major: TransactionMajorState.Done, +      }; +    case PeerPushPaymentIncomingStatus.PendingMergeKycRequired: +      return { +        major: TransactionMajorState.Pending, +        minor: TransactionMinorState.KycRequired, +      }; +    case PeerPushPaymentIncomingStatus.PendingWithdrawing: +      return { +        major: TransactionMajorState.Pending, +        minor: TransactionMinorState.Withdraw, +      }; +    case PeerPushPaymentIncomingStatus.SuspendedMerge: +      return { +        major: TransactionMajorState.Suspended, +        minor: TransactionMinorState.Merge, +      }; +    case PeerPushPaymentIncomingStatus.SuspendedMergeKycRequired: +      return { +        major: TransactionMajorState.Suspended, +        minor: TransactionMinorState.MergeKycRequired, +      }; +    case PeerPushPaymentIncomingStatus.SuspendedWithdrawing: +      return { +        major: TransactionMajorState.Suspended, +        minor: TransactionMinorState.Withdraw, +      }; +    case PeerPushPaymentIncomingStatus.Aborted: +      return { +        major: TransactionMajorState.Aborted, +      }; +    case PeerPushPaymentIncomingStatus.Failed: +      return { +        major: TransactionMajorState.Failed, +      }; +    default: +      assertUnreachable(pushCreditRecord.status); +  } +} + +export function computePeerPushCreditTransactionActions( +  pushCreditRecord: PeerPushPaymentIncomingRecord, +): TransactionAction[] { +  switch (pushCreditRecord.status) { +    case PeerPushPaymentIncomingStatus.DialogProposed: +      return []; +    case PeerPushPaymentIncomingStatus.PendingMerge: +      return [TransactionAction.Abort, TransactionAction.Suspend]; +    case PeerPushPaymentIncomingStatus.Done: +      return [TransactionAction.Delete]; +    case PeerPushPaymentIncomingStatus.PendingMergeKycRequired: +      return [TransactionAction.Abort, TransactionAction.Suspend]; +    case PeerPushPaymentIncomingStatus.PendingWithdrawing: +      return [TransactionAction.Suspend, TransactionAction.Fail]; +    case PeerPushPaymentIncomingStatus.SuspendedMerge: +      return [TransactionAction.Resume, TransactionAction.Abort]; +    case PeerPushPaymentIncomingStatus.SuspendedMergeKycRequired: +      return [TransactionAction.Resume, TransactionAction.Abort]; +    case PeerPushPaymentIncomingStatus.SuspendedWithdrawing: +      return [TransactionAction.Resume, TransactionAction.Fail]; +    case PeerPushPaymentIncomingStatus.Aborted: +      return [TransactionAction.Delete]; +    case PeerPushPaymentIncomingStatus.Failed: +      return [TransactionAction.Delete]; +    default: +      assertUnreachable(pushCreditRecord.status); +  } +}
\ No newline at end of file diff --git a/packages/taler-wallet-core/src/operations/pay-peer-push-debit.ts b/packages/taler-wallet-core/src/operations/pay-peer-push-debit.ts new file mode 100644 index 000000000..dead6313d --- /dev/null +++ b/packages/taler-wallet-core/src/operations/pay-peer-push-debit.ts @@ -0,0 +1,742 @@ +/* + 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 { +  Amounts, +  CheckPeerPushDebitRequest, +  CheckPeerPushDebitResponse, +  ContractTermsUtil, +  HttpStatusCode, +  InitiatePeerPushDebitRequest, +  InitiatePeerPushDebitResponse, +  Logger, +  RefreshReason, +  TalerError, +  TalerErrorCode, +  TalerPreciseTimestamp, +  TransactionAction, +  TransactionMajorState, +  TransactionMinorState, +  TransactionState, +  TransactionType, +  constructPayPushUri, +  j2s, +} from "@gnu-taler/taler-util"; +import { InternalWalletState } from "../internal-wallet-state.js"; +import { +  selectPeerCoins, +  getTotalPeerPaymentCost, +  codecForExchangePurseStatus, +  queryCoinInfosForSelection, +} from "./pay-peer-common.js"; +import { readSuccessResponseJsonOrThrow } from "@gnu-taler/taler-util/http"; +import { +  PeerPushPaymentInitiationRecord, +  PeerPushPaymentInitiationStatus, +} from "../index.js"; +import { PendingTaskType } from "../pending-types.js"; +import { +  OperationAttemptResult, +  OperationAttemptResultType, +  constructTaskIdentifier, +} from "../util/retries.js"; +import { +  runLongpollAsync, +  spendCoins, +  runOperationWithErrorReporting, +} from "./common.js"; +import { +  constructTransactionIdentifier, +  notifyTransition, +  stopLongpolling, +} from "./transactions.js"; +import { assertUnreachable } from "../util/assertUnreachable.js"; + +const logger = new Logger("pay-peer-push-debit.ts"); + +export async function checkPeerPushDebit( +  ws: InternalWalletState, +  req: CheckPeerPushDebitRequest, +): Promise<CheckPeerPushDebitResponse> { +  const instructedAmount = Amounts.parseOrThrow(req.amount); +  const coinSelRes = await selectPeerCoins(ws, { instructedAmount }); +  if (coinSelRes.type === "failure") { +    throw TalerError.fromDetail( +      TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE, +      { +        insufficientBalanceDetails: coinSelRes.insufficientBalanceDetails, +      }, +    ); +  } +  const totalAmount = await getTotalPeerPaymentCost( +    ws, +    coinSelRes.result.coins, +  ); +  return { +    amountEffective: Amounts.stringify(totalAmount), +    amountRaw: req.amount, +  }; +} + +async function processPeerPushDebitCreateReserve( +  ws: InternalWalletState, +  peerPushInitiation: PeerPushPaymentInitiationRecord, +): Promise<OperationAttemptResult> { +  const pursePub = peerPushInitiation.pursePub; +  const purseExpiration = peerPushInitiation.purseExpiration; +  const hContractTerms = peerPushInitiation.contractTermsHash; + +  const purseSigResp = await ws.cryptoApi.signPurseCreation({ +    hContractTerms, +    mergePub: peerPushInitiation.mergePub, +    minAge: 0, +    purseAmount: peerPushInitiation.amount, +    purseExpiration, +    pursePriv: peerPushInitiation.pursePriv, +  }); + +  const coins = await queryCoinInfosForSelection( +    ws, +    peerPushInitiation.coinSel, +  ); + +  const depositSigsResp = await ws.cryptoApi.signPurseDeposits({ +    exchangeBaseUrl: peerPushInitiation.exchangeBaseUrl, +    pursePub: peerPushInitiation.pursePub, +    coins, +  }); + +  const econtractResp = await ws.cryptoApi.encryptContractForMerge({ +    contractTerms: peerPushInitiation.contractTerms, +    mergePriv: peerPushInitiation.mergePriv, +    pursePriv: peerPushInitiation.pursePriv, +    pursePub: peerPushInitiation.pursePub, +    contractPriv: peerPushInitiation.contractPriv, +    contractPub: peerPushInitiation.contractPub, +  }); + +  const createPurseUrl = new URL( +    `purses/${peerPushInitiation.pursePub}/create`, +    peerPushInitiation.exchangeBaseUrl, +  ); + +  const httpResp = await ws.http.fetch(createPurseUrl.href, { +    method: "POST", +    body: { +      amount: peerPushInitiation.amount, +      merge_pub: peerPushInitiation.mergePub, +      purse_sig: purseSigResp.sig, +      h_contract_terms: hContractTerms, +      purse_expiration: purseExpiration, +      deposits: depositSigsResp.deposits, +      min_age: 0, +      econtract: econtractResp.econtract, +    }, +  }); + +  const resp = await httpResp.json(); + +  logger.info(`resp: ${j2s(resp)}`); + +  if (httpResp.status !== HttpStatusCode.Ok) { +    throw Error("got error response from exchange"); +  } + +  await ws.db +    .mktx((x) => [x.peerPushPaymentInitiations]) +    .runReadWrite(async (tx) => { +      const ppi = await tx.peerPushPaymentInitiations.get(pursePub); +      if (!ppi) { +        return; +      } +      ppi.status = PeerPushPaymentInitiationStatus.Done; +      await tx.peerPushPaymentInitiations.put(ppi); +    }); + +  return { +    type: OperationAttemptResultType.Finished, +    result: undefined, +  }; +} + +async function transitionPeerPushDebitFromReadyToDone( +  ws: InternalWalletState, +  pursePub: string, +): Promise<void> { +  const transactionId = constructTransactionIdentifier({ +    tag: TransactionType.PeerPushDebit, +    pursePub, +  }); +  const transitionInfo = await ws.db +    .mktx((x) => [x.peerPushPaymentInitiations]) +    .runReadWrite(async (tx) => { +      const ppiRec = await tx.peerPushPaymentInitiations.get(pursePub); +      if (!ppiRec) { +        return undefined; +      } +      if (ppiRec.status !== PeerPushPaymentInitiationStatus.PendingReady) { +        return undefined; +      } +      const oldTxState = computePeerPushDebitTransactionState(ppiRec); +      ppiRec.status = PeerPushPaymentInitiationStatus.Done; +      const newTxState = computePeerPushDebitTransactionState(ppiRec); +      return { +        oldTxState, +        newTxState, +      }; +    }); +  notifyTransition(ws, transactionId, transitionInfo); +} + +/** + * Process the "pending(ready)" state of a peer-push-debit transaction. + */ +async function processPeerPushDebitReady( +  ws: InternalWalletState, +  peerPushInitiation: PeerPushPaymentInitiationRecord, +): Promise<OperationAttemptResult> { +  const pursePub = peerPushInitiation.pursePub; +  const retryTag = constructTaskIdentifier({ +    tag: PendingTaskType.PeerPushDebit, +    pursePub, +  }); +  runLongpollAsync(ws, retryTag, async (ct) => { +    const mergeUrl = new URL(`purses/${pursePub}/merge`); +    mergeUrl.searchParams.set("timeout_ms", "30000"); +    const resp = await ws.http.fetch(mergeUrl.href, { +      // timeout: getReserveRequestTimeout(withdrawalGroup), +      cancellationToken: ct, +    }); +    if (resp.status === HttpStatusCode.Ok) { +      const purseStatus = await readSuccessResponseJsonOrThrow( +        resp, +        codecForExchangePurseStatus(), +      ); +      if (purseStatus.deposit_timestamp) { +        await transitionPeerPushDebitFromReadyToDone( +          ws, +          peerPushInitiation.pursePub, +        ); +        return { +          ready: true, +        }; +      } +    } else if (resp.status === HttpStatusCode.Gone) { +      // FIXME: transition the reserve into the expired state +    } +    return { +      ready: false, +    }; +  }); +  logger.trace( +    "returning early from peer-push-debit for long-polling in background", +  ); +  return { +    type: OperationAttemptResultType.Longpoll, +  }; +} + +export async function processPeerPushDebit( +  ws: InternalWalletState, +  pursePub: string, +): Promise<OperationAttemptResult> { +  const peerPushInitiation = await ws.db +    .mktx((x) => [x.peerPushPaymentInitiations]) +    .runReadOnly(async (tx) => { +      return tx.peerPushPaymentInitiations.get(pursePub); +    }); +  if (!peerPushInitiation) { +    throw Error("peer push payment not found"); +  } + +  const retryTag = constructTaskIdentifier({ +    tag: PendingTaskType.PeerPushDebit, +    pursePub, +  }); + +  // We're already running! +  if (ws.activeLongpoll[retryTag]) { +    logger.info("peer-push-debit task already in long-polling, returning!"); +    return { +      type: OperationAttemptResultType.Longpoll, +    }; +  } + +  switch (peerPushInitiation.status) { +    case PeerPushPaymentInitiationStatus.PendingCreatePurse: +      return processPeerPushDebitCreateReserve(ws, peerPushInitiation); +    case PeerPushPaymentInitiationStatus.PendingReady: +      return processPeerPushDebitReady(ws, peerPushInitiation); +  } + +  return { +    type: OperationAttemptResultType.Finished, +    result: undefined, +  }; +} + +/** + * Initiate sending a peer-to-peer push payment. + */ +export async function initiatePeerPushDebit( +  ws: InternalWalletState, +  req: InitiatePeerPushDebitRequest, +): Promise<InitiatePeerPushDebitResponse> { +  const instructedAmount = Amounts.parseOrThrow( +    req.partialContractTerms.amount, +  ); +  const purseExpiration = req.partialContractTerms.purse_expiration; +  const contractTerms = req.partialContractTerms; + +  const pursePair = await ws.cryptoApi.createEddsaKeypair({}); +  const mergePair = await ws.cryptoApi.createEddsaKeypair({}); + +  const hContractTerms = ContractTermsUtil.hashContractTerms(contractTerms); + +  const contractKeyPair = await ws.cryptoApi.createEddsaKeypair({}); + +  const coinSelRes = await selectPeerCoins(ws, { instructedAmount }); + +  if (coinSelRes.type !== "success") { +    throw TalerError.fromDetail( +      TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE, +      { +        insufficientBalanceDetails: coinSelRes.insufficientBalanceDetails, +      }, +    ); +  } + +  const sel = coinSelRes.result; + +  logger.info(`selected p2p coins (push): ${j2s(coinSelRes)}`); + +  const totalAmount = await getTotalPeerPaymentCost( +    ws, +    coinSelRes.result.coins, +  ); + +  await ws.db +    .mktx((x) => [ +      x.exchanges, +      x.contractTerms, +      x.coins, +      x.coinAvailability, +      x.denominations, +      x.refreshGroups, +      x.peerPushPaymentInitiations, +    ]) +    .runReadWrite(async (tx) => { +      // FIXME: Instead of directly doing a spendCoin here, +      // we might want to mark the coins as used and spend them +      // after we've been able to create the purse. +      await spendCoins(ws, tx, { +        // allocationId: `txn:peer-push-debit:${pursePair.pub}`, +        allocationId: constructTransactionIdentifier({ +          tag: TransactionType.PeerPushDebit, +          pursePub: pursePair.pub, +        }), +        coinPubs: sel.coins.map((x) => x.coinPub), +        contributions: sel.coins.map((x) => +          Amounts.parseOrThrow(x.contribution), +        ), +        refreshReason: RefreshReason.PayPeerPush, +      }); + +      await tx.peerPushPaymentInitiations.add({ +        amount: Amounts.stringify(instructedAmount), +        contractPriv: contractKeyPair.priv, +        contractPub: contractKeyPair.pub, +        contractTermsHash: hContractTerms, +        exchangeBaseUrl: sel.exchangeBaseUrl, +        mergePriv: mergePair.priv, +        mergePub: mergePair.pub, +        purseExpiration: purseExpiration, +        pursePriv: pursePair.priv, +        pursePub: pursePair.pub, +        timestampCreated: TalerPreciseTimestamp.now(), +        status: PeerPushPaymentInitiationStatus.PendingCreatePurse, +        contractTerms: contractTerms, +        coinSel: { +          coinPubs: sel.coins.map((x) => x.coinPub), +          contributions: sel.coins.map((x) => x.contribution), +        }, +        totalCost: Amounts.stringify(totalAmount), +      }); + +      await tx.contractTerms.put({ +        h: hContractTerms, +        contractTermsRaw: contractTerms, +      }); +    }); + +  const taskId = constructTaskIdentifier({ +    tag: PendingTaskType.PeerPushDebit, +    pursePub: pursePair.pub, +  }); + +  await runOperationWithErrorReporting(ws, taskId, async () => { +    return await processPeerPushDebit(ws, pursePair.pub); +  }); + +  return { +    contractPriv: contractKeyPair.priv, +    mergePriv: mergePair.priv, +    pursePub: pursePair.pub, +    exchangeBaseUrl: coinSelRes.result.exchangeBaseUrl, +    talerUri: constructPayPushUri({ +      exchangeBaseUrl: coinSelRes.result.exchangeBaseUrl, +      contractPriv: contractKeyPair.priv, +    }), +    transactionId: constructTransactionIdentifier({ +      tag: TransactionType.PeerPushDebit, +      pursePub: pursePair.pub, +    }), +  }; +} + +export function computePeerPushDebitTransactionActions( +  ppiRecord: PeerPushPaymentInitiationRecord, +): TransactionAction[] { +  switch (ppiRecord.status) { +    case PeerPushPaymentInitiationStatus.PendingCreatePurse: +      return [TransactionAction.Abort, TransactionAction.Suspend]; +    case PeerPushPaymentInitiationStatus.PendingReady: +      return [TransactionAction.Abort, TransactionAction.Suspend]; +    case PeerPushPaymentInitiationStatus.Aborted: +      return [TransactionAction.Delete]; +    case PeerPushPaymentInitiationStatus.AbortingDeletePurse: +      return [TransactionAction.Suspend, TransactionAction.Fail]; +    case PeerPushPaymentInitiationStatus.AbortingRefresh: +      return [TransactionAction.Suspend, TransactionAction.Fail]; +    case PeerPushPaymentInitiationStatus.SuspendedAbortingDeletePurse: +      return [TransactionAction.Resume, TransactionAction.Fail]; +    case PeerPushPaymentInitiationStatus.SuspendedAbortingRefresh: +      return [TransactionAction.Resume, TransactionAction.Fail]; +    case PeerPushPaymentInitiationStatus.SuspendedCreatePurse: +      return [TransactionAction.Resume, TransactionAction.Abort]; +    case PeerPushPaymentInitiationStatus.SuspendedReady: +      return [TransactionAction.Suspend, TransactionAction.Abort]; +    case PeerPushPaymentInitiationStatus.Done: +      return [TransactionAction.Delete]; +    case PeerPushPaymentInitiationStatus.Failed: +      return [TransactionAction.Delete]; +  } +} + +export async function abortPeerPushDebitTransaction( +  ws: InternalWalletState, +  pursePub: string, +) { +  const taskId = constructTaskIdentifier({ +    tag: PendingTaskType.PeerPushDebit, +    pursePub, +  }); +  const transactionId = constructTransactionIdentifier({ +    tag: TransactionType.PeerPushDebit, +    pursePub, +  }); +  stopLongpolling(ws, taskId); +  const transitionInfo = await ws.db +    .mktx((x) => [x.peerPushPaymentInitiations]) +    .runReadWrite(async (tx) => { +      const pushDebitRec = await tx.peerPushPaymentInitiations.get(pursePub); +      if (!pushDebitRec) { +        logger.warn(`peer push debit ${pursePub} not found`); +        return; +      } +      let newStatus: PeerPushPaymentInitiationStatus | undefined = undefined; +      switch (pushDebitRec.status) { +        case PeerPushPaymentInitiationStatus.PendingReady: +        case PeerPushPaymentInitiationStatus.SuspendedReady: +          newStatus = PeerPushPaymentInitiationStatus.AbortingDeletePurse; +          break; +        case PeerPushPaymentInitiationStatus.SuspendedCreatePurse: +        case PeerPushPaymentInitiationStatus.PendingCreatePurse: +          // Network request might already be in-flight! +          newStatus = PeerPushPaymentInitiationStatus.AbortingDeletePurse; +          break; +        case PeerPushPaymentInitiationStatus.SuspendedAbortingRefresh: +        case PeerPushPaymentInitiationStatus.SuspendedAbortingDeletePurse: +        case PeerPushPaymentInitiationStatus.AbortingRefresh: +        case PeerPushPaymentInitiationStatus.Done: +        case PeerPushPaymentInitiationStatus.AbortingDeletePurse: +        case PeerPushPaymentInitiationStatus.Aborted: +          // Do nothing +          break; +        case PeerPushPaymentInitiationStatus.Failed: +          break; +        default: +          assertUnreachable(pushDebitRec.status); +      } +      if (newStatus != null) { +        const oldTxState = computePeerPushDebitTransactionState(pushDebitRec); +        pushDebitRec.status = newStatus; +        const newTxState = computePeerPushDebitTransactionState(pushDebitRec); +        await tx.peerPushPaymentInitiations.put(pushDebitRec); +        return { +          oldTxState, +          newTxState, +        }; +      } +      return undefined; +    }); +  notifyTransition(ws, transactionId, transitionInfo); +} + +export async function failPeerPushDebitTransaction( +  ws: InternalWalletState, +  pursePub: string, +) { +  const taskId = constructTaskIdentifier({ +    tag: PendingTaskType.PeerPushDebit, +    pursePub, +  }); +  const transactionId = constructTransactionIdentifier({ +    tag: TransactionType.PeerPushDebit, +    pursePub, +  }); +  stopLongpolling(ws, taskId); +  const transitionInfo = await ws.db +    .mktx((x) => [x.peerPushPaymentInitiations]) +    .runReadWrite(async (tx) => { +      const pushDebitRec = await tx.peerPushPaymentInitiations.get(pursePub); +      if (!pushDebitRec) { +        logger.warn(`peer push debit ${pursePub} not found`); +        return; +      } +      let newStatus: PeerPushPaymentInitiationStatus | undefined = undefined; +      switch (pushDebitRec.status) { +        case PeerPushPaymentInitiationStatus.AbortingRefresh: +        case PeerPushPaymentInitiationStatus.SuspendedAbortingRefresh: +          // FIXME: We also need to abort the refresh group! +          newStatus = PeerPushPaymentInitiationStatus.Aborted; +          break; +        case PeerPushPaymentInitiationStatus.AbortingDeletePurse: +        case PeerPushPaymentInitiationStatus.SuspendedAbortingDeletePurse: +          newStatus = PeerPushPaymentInitiationStatus.Aborted; +          break; +        case PeerPushPaymentInitiationStatus.PendingReady: +        case PeerPushPaymentInitiationStatus.SuspendedReady: +        case PeerPushPaymentInitiationStatus.SuspendedCreatePurse: +        case PeerPushPaymentInitiationStatus.PendingCreatePurse: +        case PeerPushPaymentInitiationStatus.Done: +        case PeerPushPaymentInitiationStatus.Aborted: +        case PeerPushPaymentInitiationStatus.Failed: +          // Do nothing +          break; +        default: +          assertUnreachable(pushDebitRec.status); +      } +      if (newStatus != null) { +        const oldTxState = computePeerPushDebitTransactionState(pushDebitRec); +        pushDebitRec.status = newStatus; +        const newTxState = computePeerPushDebitTransactionState(pushDebitRec); +        await tx.peerPushPaymentInitiations.put(pushDebitRec); +        return { +          oldTxState, +          newTxState, +        }; +      } +      return undefined; +    }); +  notifyTransition(ws, transactionId, transitionInfo); +} + +export async function suspendPeerPushDebitTransaction( +  ws: InternalWalletState, +  pursePub: string, +) { +  const taskId = constructTaskIdentifier({ +    tag: PendingTaskType.PeerPushDebit, +    pursePub, +  }); +  const transactionId = constructTransactionIdentifier({ +    tag: TransactionType.PeerPushDebit, +    pursePub, +  }); +  stopLongpolling(ws, taskId); +  const transitionInfo = await ws.db +    .mktx((x) => [x.peerPushPaymentInitiations]) +    .runReadWrite(async (tx) => { +      const pushDebitRec = await tx.peerPushPaymentInitiations.get(pursePub); +      if (!pushDebitRec) { +        logger.warn(`peer push debit ${pursePub} not found`); +        return; +      } +      let newStatus: PeerPushPaymentInitiationStatus | undefined = undefined; +      switch (pushDebitRec.status) { +        case PeerPushPaymentInitiationStatus.PendingCreatePurse: +          newStatus = PeerPushPaymentInitiationStatus.SuspendedCreatePurse; +          break; +        case PeerPushPaymentInitiationStatus.AbortingRefresh: +          newStatus = PeerPushPaymentInitiationStatus.SuspendedAbortingRefresh; +          break; +        case PeerPushPaymentInitiationStatus.AbortingDeletePurse: +          newStatus = +            PeerPushPaymentInitiationStatus.SuspendedAbortingDeletePurse; +          break; +        case PeerPushPaymentInitiationStatus.PendingReady: +          newStatus = PeerPushPaymentInitiationStatus.SuspendedReady; +          break; +        case PeerPushPaymentInitiationStatus.SuspendedAbortingDeletePurse: +        case PeerPushPaymentInitiationStatus.SuspendedAbortingRefresh: +        case PeerPushPaymentInitiationStatus.SuspendedReady: +        case PeerPushPaymentInitiationStatus.SuspendedCreatePurse: +        case PeerPushPaymentInitiationStatus.Done: +        case PeerPushPaymentInitiationStatus.Aborted: +        case PeerPushPaymentInitiationStatus.Failed: +          // Do nothing +          break; +        default: +          assertUnreachable(pushDebitRec.status); +      } +      if (newStatus != null) { +        const oldTxState = computePeerPushDebitTransactionState(pushDebitRec); +        pushDebitRec.status = newStatus; +        const newTxState = computePeerPushDebitTransactionState(pushDebitRec); +        await tx.peerPushPaymentInitiations.put(pushDebitRec); +        return { +          oldTxState, +          newTxState, +        }; +      } +      return undefined; +    }); +  notifyTransition(ws, transactionId, transitionInfo); +} + +export async function resumePeerPushDebitTransaction( +  ws: InternalWalletState, +  pursePub: string, +) { +  const taskId = constructTaskIdentifier({ +    tag: PendingTaskType.PeerPushDebit, +    pursePub, +  }); +  const transactionId = constructTransactionIdentifier({ +    tag: TransactionType.PeerPushDebit, +    pursePub, +  }); +  stopLongpolling(ws, taskId); +  const transitionInfo = await ws.db +    .mktx((x) => [x.peerPushPaymentInitiations]) +    .runReadWrite(async (tx) => { +      const pushDebitRec = await tx.peerPushPaymentInitiations.get(pursePub); +      if (!pushDebitRec) { +        logger.warn(`peer push debit ${pursePub} not found`); +        return; +      } +      let newStatus: PeerPushPaymentInitiationStatus | undefined = undefined; +      switch (pushDebitRec.status) { +        case PeerPushPaymentInitiationStatus.SuspendedAbortingDeletePurse: +          newStatus = PeerPushPaymentInitiationStatus.AbortingDeletePurse; +          break; +        case PeerPushPaymentInitiationStatus.SuspendedAbortingRefresh: +          newStatus = PeerPushPaymentInitiationStatus.AbortingRefresh; +          break; +        case PeerPushPaymentInitiationStatus.SuspendedReady: +          newStatus = PeerPushPaymentInitiationStatus.PendingReady; +          break; +        case PeerPushPaymentInitiationStatus.SuspendedCreatePurse: +          newStatus = PeerPushPaymentInitiationStatus.PendingCreatePurse; +          break; +        case PeerPushPaymentInitiationStatus.PendingCreatePurse: +        case PeerPushPaymentInitiationStatus.AbortingRefresh: +        case PeerPushPaymentInitiationStatus.AbortingDeletePurse: +        case PeerPushPaymentInitiationStatus.PendingReady: +        case PeerPushPaymentInitiationStatus.Done: +        case PeerPushPaymentInitiationStatus.Aborted: +        case PeerPushPaymentInitiationStatus.Failed: +          // Do nothing +          break; +        default: +          assertUnreachable(pushDebitRec.status); +      } +      if (newStatus != null) { +        const oldTxState = computePeerPushDebitTransactionState(pushDebitRec); +        pushDebitRec.status = newStatus; +        const newTxState = computePeerPushDebitTransactionState(pushDebitRec); +        await tx.peerPushPaymentInitiations.put(pushDebitRec); +        return { +          oldTxState, +          newTxState, +        }; +      } +      return undefined; +    }); +  ws.workAvailable.trigger(); +  notifyTransition(ws, transactionId, transitionInfo); +} + + +export function computePeerPushDebitTransactionState( +  ppiRecord: PeerPushPaymentInitiationRecord, +): TransactionState { +  switch (ppiRecord.status) { +    case PeerPushPaymentInitiationStatus.PendingCreatePurse: +      return { +        major: TransactionMajorState.Pending, +        minor: TransactionMinorState.CreatePurse, +      }; +    case PeerPushPaymentInitiationStatus.PendingReady: +      return { +        major: TransactionMajorState.Pending, +        minor: TransactionMinorState.Ready, +      }; +    case PeerPushPaymentInitiationStatus.Aborted: +      return { +        major: TransactionMajorState.Aborted, +      }; +    case PeerPushPaymentInitiationStatus.AbortingDeletePurse: +      return { +        major: TransactionMajorState.Aborting, +        minor: TransactionMinorState.DeletePurse, +      }; +    case PeerPushPaymentInitiationStatus.AbortingRefresh: +      return { +        major: TransactionMajorState.Aborting, +        minor: TransactionMinorState.Refresh, +      }; +    case PeerPushPaymentInitiationStatus.SuspendedAbortingDeletePurse: +      return { +        major: TransactionMajorState.SuspendedAborting, +        minor: TransactionMinorState.DeletePurse, +      }; +    case PeerPushPaymentInitiationStatus.SuspendedAbortingRefresh: +      return { +        major: TransactionMajorState.SuspendedAborting, +        minor: TransactionMinorState.Refresh, +      }; +    case PeerPushPaymentInitiationStatus.SuspendedCreatePurse: +      return { +        major: TransactionMajorState.Suspended, +        minor: TransactionMinorState.CreatePurse, +      }; +    case PeerPushPaymentInitiationStatus.SuspendedReady: +      return { +        major: TransactionMajorState.Suspended, +        minor: TransactionMinorState.Ready, +      }; +    case PeerPushPaymentInitiationStatus.Done: +      return { +        major: TransactionMajorState.Done, +      }; +    case PeerPushPaymentInitiationStatus.Failed: +      return { +        major: TransactionMajorState.Failed, +      }; +  } +}
\ No newline at end of file diff --git a/packages/taler-wallet-core/src/operations/pay-peer.ts b/packages/taler-wallet-core/src/operations/pay-peer.ts deleted file mode 100644 index 28fef6afc..000000000 --- a/packages/taler-wallet-core/src/operations/pay-peer.ts +++ /dev/null @@ -1,3226 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2022 GNUnet e.V. - - 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/> - */ - -/** - * Imports. - */ -import { -  AbsoluteTime, -  ConfirmPeerPullDebitRequest, -  AcceptPeerPullPaymentResponse, -  ConfirmPeerPushCreditRequest, -  AcceptPeerPushPaymentResponse, -  AgeCommitmentProof, -  AmountJson, -  Amounts, -  AmountString, -  buildCodecForObject, -  PreparePeerPullDebitRequest, -  PreparePeerPullDebitResponse, -  PreparePeerPushCredit, -  PreparePeerPushCreditResponse, -  Codec, -  codecForAmountString, -  codecForAny, -  codecForExchangeGetContractResponse, -  codecForPeerContractTerms, -  CoinStatus, -  constructPayPullUri, -  constructPayPushUri, -  ContractTermsUtil, -  decodeCrock, -  eddsaGetPublic, -  encodeCrock, -  ExchangePurseDeposits, -  ExchangePurseMergeRequest, -  ExchangeReservePurseRequest, -  getRandomBytes, -  InitiatePeerPullCreditRequest, -  InitiatePeerPullCreditResponse, -  InitiatePeerPushDebitRequest, -  InitiatePeerPushDebitResponse, -  j2s, -  Logger, -  parsePayPullUri, -  parsePayPushUri, -  PayPeerInsufficientBalanceDetails, -  PeerContractTerms, -  CheckPeerPullCreditRequest, -  CheckPeerPullCreditResponse, -  CheckPeerPushDebitRequest, -  CheckPeerPushDebitResponse, -  RefreshReason, -  strcmp, -  TalerErrorCode, -  TalerProtocolTimestamp, -  TransactionType, -  UnblindedSignature, -  WalletAccountMergeFlags, -  codecOptional, -  codecForTimestamp, -  CancellationToken, -  NotificationType, -  HttpStatusCode, -  codecForWalletKycUuid, -  TransactionState, -  TransactionMajorState, -  TransactionMinorState, -  TalerPreciseTimestamp, -  TransactionAction, -} from "@gnu-taler/taler-util"; -import { SpendCoinDetails } from "../crypto/cryptoImplementation.js"; -import { -  DenominationRecord, -  PeerPullPaymentIncomingRecord, -  PeerPullDebitRecordStatus, -  PeerPullPaymentInitiationRecord, -  PeerPullPaymentInitiationStatus, -  PeerPushPaymentCoinSelection, -  PeerPushPaymentIncomingRecord, -  PeerPushPaymentIncomingStatus, -  PeerPushPaymentInitiationRecord, -  PeerPushPaymentInitiationStatus, -  ReserveRecord, -  WithdrawalGroupStatus, -  WithdrawalRecordType, -} from "../db.js"; -import { TalerError } from "@gnu-taler/taler-util"; -import { InternalWalletState } from "../internal-wallet-state.js"; -import { -  LongpollResult, -  resetOperationTimeout, -  runLongpollAsync, -  runOperationWithErrorReporting, -  spendCoins, -} from "../operations/common.js"; -import { -  readSuccessResponseJsonOrErrorCode, -  readSuccessResponseJsonOrThrow, -  throwUnexpectedRequestError, -} from "@gnu-taler/taler-util/http"; -import { checkDbInvariant } from "../util/invariants.js"; -import { -  constructTaskIdentifier, -  OperationAttemptResult, -  OperationAttemptResultType, -  TaskIdentifiers, -} from "../util/retries.js"; -import { getPeerPaymentBalanceDetailsInTx } from "./balance.js"; -import { updateExchangeFromUrl } from "./exchanges.js"; -import { getTotalRefreshCost } from "./refresh.js"; -import { -  checkWithdrawalKycStatus, -  getExchangeWithdrawalInfo, -  internalCreateWithdrawalGroup, -  processWithdrawalGroup, -} from "./withdraw.js"; -import { PendingTaskType } from "../pending-types.js"; -import { -  constructTransactionIdentifier, -  notifyTransition, -  stopLongpolling, -} from "./transactions.js"; -import { assertUnreachable } from "../util/assertUnreachable.js"; - -const logger = new Logger("operations/peer-to-peer.ts"); - -interface SelectedPeerCoin { -  coinPub: string; -  coinPriv: string; -  contribution: AmountString; -  denomPubHash: string; -  denomSig: UnblindedSignature; -  ageCommitmentProof: AgeCommitmentProof | undefined; -} - -interface PeerCoinSelectionDetails { -  exchangeBaseUrl: string; - -  /** -   * Info of Coins that were selected. -   */ -  coins: SelectedPeerCoin[]; - -  /** -   * How much of the deposit fees is the customer paying? -   */ -  depositFees: AmountJson; -} - -/** - * Information about a selected coin for peer to peer payments. - */ -interface CoinInfo { -  /** -   * Public key of the coin. -   */ -  coinPub: string; - -  coinPriv: string; - -  /** -   * Deposit fee for the coin. -   */ -  feeDeposit: AmountJson; - -  value: AmountJson; - -  denomPubHash: string; - -  denomSig: UnblindedSignature; - -  maxAge: number; - -  ageCommitmentProof?: AgeCommitmentProof; -} - -export type SelectPeerCoinsResult = -  | { type: "success"; result: PeerCoinSelectionDetails } -  | { -      type: "failure"; -      insufficientBalanceDetails: PayPeerInsufficientBalanceDetails; -    }; - -export async function queryCoinInfosForSelection( -  ws: InternalWalletState, -  csel: PeerPushPaymentCoinSelection, -): Promise<SpendCoinDetails[]> { -  let infos: SpendCoinDetails[] = []; -  await ws.db -    .mktx((x) => [x.coins, x.denominations]) -    .runReadOnly(async (tx) => { -      for (let i = 0; i < csel.coinPubs.length; i++) { -        const coin = await tx.coins.get(csel.coinPubs[i]); -        if (!coin) { -          throw Error("coin not found anymore"); -        } -        const denom = await ws.getDenomInfo( -          ws, -          tx, -          coin.exchangeBaseUrl, -          coin.denomPubHash, -        ); -        if (!denom) { -          throw Error("denom for coin not found anymore"); -        } -        infos.push({ -          coinPriv: coin.coinPriv, -          coinPub: coin.coinPub, -          denomPubHash: coin.denomPubHash, -          denomSig: coin.denomSig, -          ageCommitmentProof: coin.ageCommitmentProof, -          contribution: csel.contributions[i], -        }); -      } -    }); -  return infos; -} - -export async function selectPeerCoins( -  ws: InternalWalletState, -  instructedAmount: AmountJson, -): Promise<SelectPeerCoinsResult> { -  if (Amounts.isZero(instructedAmount)) { -    // Other parts of the code assume that we have at least -    // one coin to spend. -    throw new Error("amount of zero not allowed"); -  } -  return await ws.db -    .mktx((x) => [ -      x.exchanges, -      x.contractTerms, -      x.coins, -      x.coinAvailability, -      x.denominations, -      x.refreshGroups, -      x.peerPushPaymentInitiations, -    ]) -    .runReadWrite(async (tx) => { -      const exchanges = await tx.exchanges.iter().toArray(); -      const exchangeFeeGap: { [url: string]: AmountJson } = {}; -      const currency = Amounts.currencyOf(instructedAmount); -      for (const exch of exchanges) { -        if (exch.detailsPointer?.currency !== currency) { -          continue; -        } -        const coins = ( -          await tx.coins.indexes.byBaseUrl.getAll(exch.baseUrl) -        ).filter((x) => x.status === CoinStatus.Fresh); -        const coinInfos: CoinInfo[] = []; -        for (const coin of coins) { -          const denom = await ws.getDenomInfo( -            ws, -            tx, -            coin.exchangeBaseUrl, -            coin.denomPubHash, -          ); -          if (!denom) { -            throw Error("denom not found"); -          } -          coinInfos.push({ -            coinPub: coin.coinPub, -            feeDeposit: Amounts.parseOrThrow(denom.feeDeposit), -            value: Amounts.parseOrThrow(denom.value), -            denomPubHash: denom.denomPubHash, -            coinPriv: coin.coinPriv, -            denomSig: coin.denomSig, -            maxAge: coin.maxAge, -            ageCommitmentProof: coin.ageCommitmentProof, -          }); -        } -        if (coinInfos.length === 0) { -          continue; -        } -        coinInfos.sort( -          (o1, o2) => -            -Amounts.cmp(o1.value, o2.value) || -            strcmp(o1.denomPubHash, o2.denomPubHash), -        ); -        let amountAcc = Amounts.zeroOfCurrency(currency); -        let depositFeesAcc = Amounts.zeroOfCurrency(currency); -        const resCoins: { -          coinPub: string; -          coinPriv: string; -          contribution: AmountString; -          denomPubHash: string; -          denomSig: UnblindedSignature; -          ageCommitmentProof: AgeCommitmentProof | undefined; -        }[] = []; -        let lastDepositFee = Amounts.zeroOfCurrency(currency); -        for (const coin of coinInfos) { -          if (Amounts.cmp(amountAcc, instructedAmount) >= 0) { -            break; -          } -          const gap = Amounts.add( -            coin.feeDeposit, -            Amounts.sub(instructedAmount, amountAcc).amount, -          ).amount; -          const contrib = Amounts.min(gap, coin.value); -          amountAcc = Amounts.add( -            amountAcc, -            Amounts.sub(contrib, coin.feeDeposit).amount, -          ).amount; -          depositFeesAcc = Amounts.add(depositFeesAcc, coin.feeDeposit).amount; -          resCoins.push({ -            coinPriv: coin.coinPriv, -            coinPub: coin.coinPub, -            contribution: Amounts.stringify(contrib), -            denomPubHash: coin.denomPubHash, -            denomSig: coin.denomSig, -            ageCommitmentProof: coin.ageCommitmentProof, -          }); -          lastDepositFee = coin.feeDeposit; -        } -        if (Amounts.cmp(amountAcc, instructedAmount) >= 0) { -          const res: PeerCoinSelectionDetails = { -            exchangeBaseUrl: exch.baseUrl, -            coins: resCoins, -            depositFees: depositFeesAcc, -          }; -          return { type: "success", result: res }; -        } -        const diff = Amounts.sub(instructedAmount, amountAcc).amount; -        exchangeFeeGap[exch.baseUrl] = Amounts.add(lastDepositFee, diff).amount; - -        continue; -      } -      // We were unable to select coins. -      // Now we need to produce error details. - -      const infoGeneral = await getPeerPaymentBalanceDetailsInTx(ws, tx, { -        currency, -      }); - -      const perExchange: PayPeerInsufficientBalanceDetails["perExchange"] = {}; - -      let maxFeeGapEstimate = Amounts.zeroOfCurrency(currency); - -      for (const exch of exchanges) { -        if (exch.detailsPointer?.currency !== currency) { -          continue; -        } -        const infoExchange = await getPeerPaymentBalanceDetailsInTx(ws, tx, { -          currency, -          restrictExchangeTo: exch.baseUrl, -        }); -        let gap = -          exchangeFeeGap[exch.baseUrl] ?? Amounts.zeroOfCurrency(currency); -        if (Amounts.cmp(infoExchange.balanceMaterial, instructedAmount) < 0) { -          // Show fee gap only if we should've been able to pay with the material amount -          gap = Amounts.zeroOfCurrency(currency); -        } -        perExchange[exch.baseUrl] = { -          balanceAvailable: Amounts.stringify(infoExchange.balanceAvailable), -          balanceMaterial: Amounts.stringify(infoExchange.balanceMaterial), -          feeGapEstimate: Amounts.stringify(gap), -        }; - -        maxFeeGapEstimate = Amounts.max(maxFeeGapEstimate, gap); -      } - -      const errDetails: PayPeerInsufficientBalanceDetails = { -        amountRequested: Amounts.stringify(instructedAmount), -        balanceAvailable: Amounts.stringify(infoGeneral.balanceAvailable), -        balanceMaterial: Amounts.stringify(infoGeneral.balanceMaterial), -        feeGapEstimate: Amounts.stringify(maxFeeGapEstimate), -        perExchange, -      }; - -      return { type: "failure", insufficientBalanceDetails: errDetails }; -    }); -} - -export async function getTotalPeerPaymentCost( -  ws: InternalWalletState, -  pcs: SelectedPeerCoin[], -): Promise<AmountJson> { -  return ws.db -    .mktx((x) => [x.coins, x.denominations]) -    .runReadOnly(async (tx) => { -      const costs: AmountJson[] = []; -      for (let i = 0; i < pcs.length; i++) { -        const coin = await tx.coins.get(pcs[i].coinPub); -        if (!coin) { -          throw Error("can't calculate payment cost, coin not found"); -        } -        const denom = await tx.denominations.get([ -          coin.exchangeBaseUrl, -          coin.denomPubHash, -        ]); -        if (!denom) { -          throw Error( -            "can't calculate payment cost, denomination for coin not found", -          ); -        } -        const allDenoms = await tx.denominations.indexes.byExchangeBaseUrl -          .iter(coin.exchangeBaseUrl) -          .filter((x) => -            Amounts.isSameCurrency( -              DenominationRecord.getValue(x), -              pcs[i].contribution, -            ), -          ); -        const amountLeft = Amounts.sub( -          DenominationRecord.getValue(denom), -          pcs[i].contribution, -        ).amount; -        const refreshCost = getTotalRefreshCost( -          allDenoms, -          DenominationRecord.toDenomInfo(denom), -          amountLeft, -          ws.config.testing.denomselAllowLate, -        ); -        costs.push(Amounts.parseOrThrow(pcs[i].contribution)); -        costs.push(refreshCost); -      } -      const zero = Amounts.zeroOfAmount(pcs[0].contribution); -      return Amounts.sum([zero, ...costs]).amount; -    }); -} - -export async function checkPeerPushDebit( -  ws: InternalWalletState, -  req: CheckPeerPushDebitRequest, -): Promise<CheckPeerPushDebitResponse> { -  const instructedAmount = Amounts.parseOrThrow(req.amount); -  const coinSelRes = await selectPeerCoins(ws, instructedAmount); -  if (coinSelRes.type === "failure") { -    throw TalerError.fromDetail( -      TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE, -      { -        insufficientBalanceDetails: coinSelRes.insufficientBalanceDetails, -      }, -    ); -  } -  const totalAmount = await getTotalPeerPaymentCost( -    ws, -    coinSelRes.result.coins, -  ); -  return { -    amountEffective: Amounts.stringify(totalAmount), -    amountRaw: req.amount, -  }; -} - -async function processPeerPushDebitCreateReserve( -  ws: InternalWalletState, -  peerPushInitiation: PeerPushPaymentInitiationRecord, -): Promise<OperationAttemptResult> { -  const pursePub = peerPushInitiation.pursePub; -  const purseExpiration = peerPushInitiation.purseExpiration; -  const hContractTerms = peerPushInitiation.contractTermsHash; - -  const purseSigResp = await ws.cryptoApi.signPurseCreation({ -    hContractTerms, -    mergePub: peerPushInitiation.mergePub, -    minAge: 0, -    purseAmount: peerPushInitiation.amount, -    purseExpiration, -    pursePriv: peerPushInitiation.pursePriv, -  }); - -  const coins = await queryCoinInfosForSelection( -    ws, -    peerPushInitiation.coinSel, -  ); - -  const depositSigsResp = await ws.cryptoApi.signPurseDeposits({ -    exchangeBaseUrl: peerPushInitiation.exchangeBaseUrl, -    pursePub: peerPushInitiation.pursePub, -    coins, -  }); - -  const econtractResp = await ws.cryptoApi.encryptContractForMerge({ -    contractTerms: peerPushInitiation.contractTerms, -    mergePriv: peerPushInitiation.mergePriv, -    pursePriv: peerPushInitiation.pursePriv, -    pursePub: peerPushInitiation.pursePub, -    contractPriv: peerPushInitiation.contractPriv, -    contractPub: peerPushInitiation.contractPub, -  }); - -  const createPurseUrl = new URL( -    `purses/${peerPushInitiation.pursePub}/create`, -    peerPushInitiation.exchangeBaseUrl, -  ); - -  const httpResp = await ws.http.fetch(createPurseUrl.href, { -    method: "POST", -    body: { -      amount: peerPushInitiation.amount, -      merge_pub: peerPushInitiation.mergePub, -      purse_sig: purseSigResp.sig, -      h_contract_terms: hContractTerms, -      purse_expiration: purseExpiration, -      deposits: depositSigsResp.deposits, -      min_age: 0, -      econtract: econtractResp.econtract, -    }, -  }); - -  const resp = await httpResp.json(); - -  logger.info(`resp: ${j2s(resp)}`); - -  if (httpResp.status !== HttpStatusCode.Ok) { -    throw Error("got error response from exchange"); -  } - -  await ws.db -    .mktx((x) => [x.peerPushPaymentInitiations]) -    .runReadWrite(async (tx) => { -      const ppi = await tx.peerPushPaymentInitiations.get(pursePub); -      if (!ppi) { -        return; -      } -      ppi.status = PeerPushPaymentInitiationStatus.Done; -      await tx.peerPushPaymentInitiations.put(ppi); -    }); - -  return { -    type: OperationAttemptResultType.Finished, -    result: undefined, -  }; -} - -async function transitionPeerPushDebitFromReadyToDone( -  ws: InternalWalletState, -  pursePub: string, -): Promise<void> { -  const transactionId = constructTransactionIdentifier({ -    tag: TransactionType.PeerPushDebit, -    pursePub, -  }); -  const transitionInfo = await ws.db -    .mktx((x) => [x.peerPushPaymentInitiations]) -    .runReadWrite(async (tx) => { -      const ppiRec = await tx.peerPushPaymentInitiations.get(pursePub); -      if (!ppiRec) { -        return undefined; -      } -      if (ppiRec.status !== PeerPushPaymentInitiationStatus.PendingReady) { -        return undefined; -      } -      const oldTxState = computePeerPushDebitTransactionState(ppiRec); -      ppiRec.status = PeerPushPaymentInitiationStatus.Done; -      const newTxState = computePeerPushDebitTransactionState(ppiRec); -      return { -        oldTxState, -        newTxState, -      }; -    }); -  notifyTransition(ws, transactionId, transitionInfo); -} - -/** - * Process the "pending(ready)" state of a peer-push-debit transaction. - */ -async function processPeerPushDebitReady( -  ws: InternalWalletState, -  peerPushInitiation: PeerPushPaymentInitiationRecord, -): Promise<OperationAttemptResult> { -  const pursePub = peerPushInitiation.pursePub; -  const retryTag = constructTaskIdentifier({ -    tag: PendingTaskType.PeerPushDebit, -    pursePub, -  }); -  runLongpollAsync(ws, retryTag, async (ct) => { -    const mergeUrl = new URL(`purses/${pursePub}/merge`); -    mergeUrl.searchParams.set("timeout_ms", "30000"); -    const resp = await ws.http.fetch(mergeUrl.href, { -      // timeout: getReserveRequestTimeout(withdrawalGroup), -      cancellationToken: ct, -    }); -    if (resp.status === HttpStatusCode.Ok) { -      const purseStatus = await readSuccessResponseJsonOrThrow( -        resp, -        codecForExchangePurseStatus(), -      ); -      if (purseStatus.deposit_timestamp) { -        await transitionPeerPushDebitFromReadyToDone( -          ws, -          peerPushInitiation.pursePub, -        ); -        return { -          ready: true, -        }; -      } -    } else if (resp.status === HttpStatusCode.Gone) { -      // FIXME: transition the reserve into the expired state -    } -    return { -      ready: false, -    }; -  }); -  logger.trace( -    "returning early from withdrawal for long-polling in background", -  ); -  return { -    type: OperationAttemptResultType.Longpoll, -  }; -} - -export async function processPeerPushDebit( -  ws: InternalWalletState, -  pursePub: string, -): Promise<OperationAttemptResult> { -  const peerPushInitiation = await ws.db -    .mktx((x) => [x.peerPushPaymentInitiations]) -    .runReadOnly(async (tx) => { -      return tx.peerPushPaymentInitiations.get(pursePub); -    }); -  if (!peerPushInitiation) { -    throw Error("peer push payment not found"); -  } - -  const retryTag = constructTaskIdentifier({ -    tag: PendingTaskType.PeerPushDebit, -    pursePub, -  }); - -  // We're already running! -  if (ws.activeLongpoll[retryTag]) { -    logger.info("peer-push-debit task already in long-polling, returning!"); -    return { -      type: OperationAttemptResultType.Longpoll, -    }; -  } - -  switch (peerPushInitiation.status) { -    case PeerPushPaymentInitiationStatus.PendingCreatePurse: -      return processPeerPushDebitCreateReserve(ws, peerPushInitiation); -    case PeerPushPaymentInitiationStatus.PendingReady: -      return processPeerPushDebitReady(ws, peerPushInitiation); -  } - -  return { -    type: OperationAttemptResultType.Finished, -    result: undefined, -  }; -} - -/** - * Initiate sending a peer-to-peer push payment. - */ -export async function initiatePeerPushDebit( -  ws: InternalWalletState, -  req: InitiatePeerPushDebitRequest, -): Promise<InitiatePeerPushDebitResponse> { -  const instructedAmount = Amounts.parseOrThrow( -    req.partialContractTerms.amount, -  ); -  const purseExpiration = req.partialContractTerms.purse_expiration; -  const contractTerms = req.partialContractTerms; - -  const pursePair = await ws.cryptoApi.createEddsaKeypair({}); -  const mergePair = await ws.cryptoApi.createEddsaKeypair({}); - -  const hContractTerms = ContractTermsUtil.hashContractTerms(contractTerms); - -  const contractKeyPair = await ws.cryptoApi.createEddsaKeypair({}); - -  const coinSelRes = await selectPeerCoins(ws, instructedAmount); - -  if (coinSelRes.type !== "success") { -    throw TalerError.fromDetail( -      TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE, -      { -        insufficientBalanceDetails: coinSelRes.insufficientBalanceDetails, -      }, -    ); -  } - -  const sel = coinSelRes.result; - -  logger.info(`selected p2p coins (push): ${j2s(coinSelRes)}`); - -  const totalAmount = await getTotalPeerPaymentCost( -    ws, -    coinSelRes.result.coins, -  ); - -  await ws.db -    .mktx((x) => [ -      x.exchanges, -      x.contractTerms, -      x.coins, -      x.coinAvailability, -      x.denominations, -      x.refreshGroups, -      x.peerPushPaymentInitiations, -    ]) -    .runReadWrite(async (tx) => { -      // FIXME: Instead of directly doing a spendCoin here, -      // we might want to mark the coins as used and spend them -      // after we've been able to create the purse. -      await spendCoins(ws, tx, { -        // allocationId: `txn:peer-push-debit:${pursePair.pub}`, -        allocationId: constructTransactionIdentifier({ -          tag: TransactionType.PeerPushDebit, -          pursePub: pursePair.pub, -        }), -        coinPubs: sel.coins.map((x) => x.coinPub), -        contributions: sel.coins.map((x) => -          Amounts.parseOrThrow(x.contribution), -        ), -        refreshReason: RefreshReason.PayPeerPush, -      }); - -      await tx.peerPushPaymentInitiations.add({ -        amount: Amounts.stringify(instructedAmount), -        contractPriv: contractKeyPair.priv, -        contractPub: contractKeyPair.pub, -        contractTermsHash: hContractTerms, -        exchangeBaseUrl: sel.exchangeBaseUrl, -        mergePriv: mergePair.priv, -        mergePub: mergePair.pub, -        purseExpiration: purseExpiration, -        pursePriv: pursePair.priv, -        pursePub: pursePair.pub, -        timestampCreated: TalerPreciseTimestamp.now(), -        status: PeerPushPaymentInitiationStatus.PendingCreatePurse, -        contractTerms: contractTerms, -        coinSel: { -          coinPubs: sel.coins.map((x) => x.coinPub), -          contributions: sel.coins.map((x) => x.contribution), -        }, -        totalCost: Amounts.stringify(totalAmount), -      }); - -      await tx.contractTerms.put({ -        h: hContractTerms, -        contractTermsRaw: contractTerms, -      }); -    }); - -  const taskId = constructTaskIdentifier({ -    tag: PendingTaskType.PeerPushDebit, -    pursePub: pursePair.pub, -  }); - -  await runOperationWithErrorReporting(ws, taskId, async () => { -    return await processPeerPushDebit(ws, pursePair.pub); -  }); - -  return { -    contractPriv: contractKeyPair.priv, -    mergePriv: mergePair.priv, -    pursePub: pursePair.pub, -    exchangeBaseUrl: coinSelRes.result.exchangeBaseUrl, -    talerUri: constructPayPushUri({ -      exchangeBaseUrl: coinSelRes.result.exchangeBaseUrl, -      contractPriv: contractKeyPair.priv, -    }), -    transactionId: constructTransactionIdentifier({ -      tag: TransactionType.PeerPushDebit, -      pursePub: pursePair.pub, -    }), -  }; -} - -interface ExchangePurseStatus { -  balance: AmountString; -  deposit_timestamp?: TalerProtocolTimestamp; -  merge_timestamp?: TalerProtocolTimestamp; -} - -export const codecForExchangePurseStatus = (): Codec<ExchangePurseStatus> => -  buildCodecForObject<ExchangePurseStatus>() -    .property("balance", codecForAmountString()) -    .property("deposit_timestamp", codecOptional(codecForTimestamp)) -    .property("merge_timestamp", codecOptional(codecForTimestamp)) -    .build("ExchangePurseStatus"); - -export async function preparePeerPushCredit( -  ws: InternalWalletState, -  req: PreparePeerPushCredit, -): Promise<PreparePeerPushCreditResponse> { -  const uri = parsePayPushUri(req.talerUri); - -  if (!uri) { -    throw Error("got invalid taler://pay-push URI"); -  } - -  const existing = await ws.db -    .mktx((x) => [x.contractTerms, x.peerPushPaymentIncoming]) -    .runReadOnly(async (tx) => { -      const existingPushInc = -        await tx.peerPushPaymentIncoming.indexes.byExchangeAndContractPriv.get([ -          uri.exchangeBaseUrl, -          uri.contractPriv, -        ]); -      if (!existingPushInc) { -        return; -      } -      const existingContractTermsRec = await tx.contractTerms.get( -        existingPushInc.contractTermsHash, -      ); -      if (!existingContractTermsRec) { -        throw Error( -          "contract terms for peer push payment credit not found in database", -        ); -      } -      const existingContractTerms = codecForPeerContractTerms().decode( -        existingContractTermsRec.contractTermsRaw, -      ); -      return { existingPushInc, existingContractTerms }; -    }); - -  if (existing) { -    return { -      amount: existing.existingContractTerms.amount, -      amountEffective: existing.existingPushInc.estimatedAmountEffective, -      amountRaw: existing.existingContractTerms.amount, -      contractTerms: existing.existingContractTerms, -      peerPushPaymentIncomingId: -        existing.existingPushInc.peerPushPaymentIncomingId, -      transactionId: constructTransactionIdentifier({ -        tag: TransactionType.PeerPushCredit, -        peerPushPaymentIncomingId: -          existing.existingPushInc.peerPushPaymentIncomingId, -      }), -    }; -  } - -  const exchangeBaseUrl = uri.exchangeBaseUrl; - -  await updateExchangeFromUrl(ws, exchangeBaseUrl); - -  const contractPriv = uri.contractPriv; -  const contractPub = encodeCrock(eddsaGetPublic(decodeCrock(contractPriv))); - -  const getContractUrl = new URL(`contracts/${contractPub}`, exchangeBaseUrl); - -  const contractHttpResp = await ws.http.get(getContractUrl.href); - -  const contractResp = await readSuccessResponseJsonOrThrow( -    contractHttpResp, -    codecForExchangeGetContractResponse(), -  ); - -  const pursePub = contractResp.purse_pub; - -  const dec = await ws.cryptoApi.decryptContractForMerge({ -    ciphertext: contractResp.econtract, -    contractPriv: contractPriv, -    pursePub: pursePub, -  }); - -  const getPurseUrl = new URL(`purses/${pursePub}/deposit`, exchangeBaseUrl); - -  const purseHttpResp = await ws.http.get(getPurseUrl.href); - -  const purseStatus = await readSuccessResponseJsonOrThrow( -    purseHttpResp, -    codecForExchangePurseStatus(), -  ); - -  const peerPushPaymentIncomingId = encodeCrock(getRandomBytes(32)); - -  const contractTermsHash = ContractTermsUtil.hashContractTerms( -    dec.contractTerms, -  ); - -  const withdrawalGroupId = encodeCrock(getRandomBytes(32)); - -  const wi = await getExchangeWithdrawalInfo( -    ws, -    exchangeBaseUrl, -    Amounts.parseOrThrow(purseStatus.balance), -    undefined, -  ); - -  await ws.db -    .mktx((x) => [x.contractTerms, x.peerPushPaymentIncoming]) -    .runReadWrite(async (tx) => { -      await tx.peerPushPaymentIncoming.add({ -        peerPushPaymentIncomingId, -        contractPriv: contractPriv, -        exchangeBaseUrl: exchangeBaseUrl, -        mergePriv: dec.mergePriv, -        pursePub: pursePub, -        timestamp: TalerPreciseTimestamp.now(), -        contractTermsHash, -        status: PeerPushPaymentIncomingStatus.DialogProposed, -        withdrawalGroupId, -        currency: Amounts.currencyOf(purseStatus.balance), -        estimatedAmountEffective: Amounts.stringify( -          wi.withdrawalAmountEffective, -        ), -      }); - -      await tx.contractTerms.put({ -        h: contractTermsHash, -        contractTermsRaw: dec.contractTerms, -      }); -    }); - -  return { -    amount: purseStatus.balance, -    amountEffective: wi.withdrawalAmountEffective, -    amountRaw: purseStatus.balance, -    contractTerms: dec.contractTerms, -    peerPushPaymentIncomingId, -    transactionId: constructTransactionIdentifier({ -      tag: TransactionType.PeerPushCredit, -      peerPushPaymentIncomingId, -    }), -  }; -} - -export function talerPaytoFromExchangeReserve( -  exchangeBaseUrl: string, -  reservePub: string, -): string { -  const url = new URL(exchangeBaseUrl); -  let proto: string; -  if (url.protocol === "http:") { -    proto = "taler-reserve-http"; -  } else if (url.protocol === "https:") { -    proto = "taler-reserve"; -  } else { -    throw Error(`unsupported exchange base URL protocol (${url.protocol})`); -  } - -  let path = url.pathname; -  if (!path.endsWith("/")) { -    path = path + "/"; -  } - -  return `payto://${proto}/${url.host}${url.pathname}${reservePub}`; -} - -async function getMergeReserveInfo( -  ws: InternalWalletState, -  req: { -    exchangeBaseUrl: string; -  }, -): Promise<ReserveRecord> { -  // We have to eagerly create the key pair outside of the transaction, -  // due to the async crypto API. -  const newReservePair = await ws.cryptoApi.createEddsaKeypair({}); - -  const mergeReserveRecord: ReserveRecord = await ws.db -    .mktx((x) => [x.exchanges, x.reserves, x.withdrawalGroups]) -    .runReadWrite(async (tx) => { -      const ex = await tx.exchanges.get(req.exchangeBaseUrl); -      checkDbInvariant(!!ex); -      if (ex.currentMergeReserveRowId != null) { -        const reserve = await tx.reserves.get(ex.currentMergeReserveRowId); -        checkDbInvariant(!!reserve); -        return reserve; -      } -      const reserve: ReserveRecord = { -        reservePriv: newReservePair.priv, -        reservePub: newReservePair.pub, -      }; -      const insertResp = await tx.reserves.put(reserve); -      checkDbInvariant(typeof insertResp.key === "number"); -      reserve.rowId = insertResp.key; -      ex.currentMergeReserveRowId = reserve.rowId; -      await tx.exchanges.put(ex); -      return reserve; -    }); - -  return mergeReserveRecord; -} - -export async function processPeerPushCredit( -  ws: InternalWalletState, -  peerPushPaymentIncomingId: string, -): Promise<OperationAttemptResult> { -  let peerInc: PeerPushPaymentIncomingRecord | undefined; -  let contractTerms: PeerContractTerms | undefined; -  await ws.db -    .mktx((x) => [x.contractTerms, x.peerPushPaymentIncoming]) -    .runReadWrite(async (tx) => { -      peerInc = await tx.peerPushPaymentIncoming.get(peerPushPaymentIncomingId); -      if (!peerInc) { -        return; -      } -      const ctRec = await tx.contractTerms.get(peerInc.contractTermsHash); -      if (ctRec) { -        contractTerms = ctRec.contractTermsRaw; -      } -      await tx.peerPushPaymentIncoming.put(peerInc); -    }); - -  if (!peerInc) { -    throw Error( -      `can't accept unknown incoming p2p push payment (${peerPushPaymentIncomingId})`, -    ); -  } - -  checkDbInvariant(!!contractTerms); - -  const amount = Amounts.parseOrThrow(contractTerms.amount); - -  if ( -    peerInc.status === PeerPushPaymentIncomingStatus.PendingMergeKycRequired && -    peerInc.kycInfo -  ) { -    const txId = constructTransactionIdentifier({ -      tag: TransactionType.PeerPushCredit, -      peerPushPaymentIncomingId: peerInc.peerPushPaymentIncomingId, -    }); -    await checkWithdrawalKycStatus( -      ws, -      peerInc.exchangeBaseUrl, -      txId, -      peerInc.kycInfo, -      "individual", -    ); -  } - -  const mergeReserveInfo = await getMergeReserveInfo(ws, { -    exchangeBaseUrl: peerInc.exchangeBaseUrl, -  }); - -  const mergeTimestamp = TalerProtocolTimestamp.now(); - -  const reservePayto = talerPaytoFromExchangeReserve( -    peerInc.exchangeBaseUrl, -    mergeReserveInfo.reservePub, -  ); - -  const sigRes = await ws.cryptoApi.signPurseMerge({ -    contractTermsHash: ContractTermsUtil.hashContractTerms(contractTerms), -    flags: WalletAccountMergeFlags.MergeFullyPaidPurse, -    mergePriv: peerInc.mergePriv, -    mergeTimestamp: mergeTimestamp, -    purseAmount: Amounts.stringify(amount), -    purseExpiration: contractTerms.purse_expiration, -    purseFee: Amounts.stringify(Amounts.zeroOfCurrency(amount.currency)), -    pursePub: peerInc.pursePub, -    reservePayto, -    reservePriv: mergeReserveInfo.reservePriv, -  }); - -  const mergePurseUrl = new URL( -    `purses/${peerInc.pursePub}/merge`, -    peerInc.exchangeBaseUrl, -  ); - -  const mergeReq: ExchangePurseMergeRequest = { -    payto_uri: reservePayto, -    merge_timestamp: mergeTimestamp, -    merge_sig: sigRes.mergeSig, -    reserve_sig: sigRes.accountSig, -  }; - -  const mergeHttpResp = await ws.http.postJson(mergePurseUrl.href, mergeReq); - -  if (mergeHttpResp.status === HttpStatusCode.UnavailableForLegalReasons) { -    const respJson = await mergeHttpResp.json(); -    const kycPending = codecForWalletKycUuid().decode(respJson); -    logger.info(`kyc uuid response: ${j2s(kycPending)}`); - -    await ws.db -      .mktx((x) => [x.peerPushPaymentIncoming]) -      .runReadWrite(async (tx) => { -        const peerInc = await tx.peerPushPaymentIncoming.get( -          peerPushPaymentIncomingId, -        ); -        if (!peerInc) { -          return; -        } -        peerInc.kycInfo = { -          paytoHash: kycPending.h_payto, -          requirementRow: kycPending.requirement_row, -        }; -        peerInc.status = PeerPushPaymentIncomingStatus.PendingMergeKycRequired; -        await tx.peerPushPaymentIncoming.put(peerInc); -      }); -    return { -      type: OperationAttemptResultType.Pending, -      result: undefined, -    }; -  } - -  logger.trace(`merge request: ${j2s(mergeReq)}`); -  const res = await readSuccessResponseJsonOrThrow( -    mergeHttpResp, -    codecForAny(), -  ); -  logger.trace(`merge response: ${j2s(res)}`); - -  await internalCreateWithdrawalGroup(ws, { -    amount, -    wgInfo: { -      withdrawalType: WithdrawalRecordType.PeerPushCredit, -      contractTerms, -    }, -    forcedWithdrawalGroupId: peerInc.withdrawalGroupId, -    exchangeBaseUrl: peerInc.exchangeBaseUrl, -    reserveStatus: WithdrawalGroupStatus.PendingQueryingStatus, -    reserveKeyPair: { -      priv: mergeReserveInfo.reservePriv, -      pub: mergeReserveInfo.reservePub, -    }, -  }); - -  await ws.db -    .mktx((x) => [x.contractTerms, x.peerPushPaymentIncoming]) -    .runReadWrite(async (tx) => { -      const peerInc = await tx.peerPushPaymentIncoming.get( -        peerPushPaymentIncomingId, -      ); -      if (!peerInc) { -        return; -      } -      if ( -        peerInc.status === PeerPushPaymentIncomingStatus.PendingMerge || -        peerInc.status === PeerPushPaymentIncomingStatus.PendingMergeKycRequired -      ) { -        peerInc.status = PeerPushPaymentIncomingStatus.Done; -      } -      await tx.peerPushPaymentIncoming.put(peerInc); -    }); - -  return { -    type: OperationAttemptResultType.Finished, -    result: undefined, -  }; -} - -export async function confirmPeerPushCredit( -  ws: InternalWalletState, -  req: ConfirmPeerPushCreditRequest, -): Promise<AcceptPeerPushPaymentResponse> { -  let peerInc: PeerPushPaymentIncomingRecord | undefined; - -  await ws.db -    .mktx((x) => [x.contractTerms, x.peerPushPaymentIncoming]) -    .runReadWrite(async (tx) => { -      peerInc = await tx.peerPushPaymentIncoming.get( -        req.peerPushPaymentIncomingId, -      ); -      if (!peerInc) { -        return; -      } -      if (peerInc.status === PeerPushPaymentIncomingStatus.DialogProposed) { -        peerInc.status = PeerPushPaymentIncomingStatus.PendingMerge; -      } -      await tx.peerPushPaymentIncoming.put(peerInc); -    }); - -  if (!peerInc) { -    throw Error( -      `can't accept unknown incoming p2p push payment (${req.peerPushPaymentIncomingId})`, -    ); -  } - -  ws.workAvailable.trigger(); - -  const transactionId = constructTransactionIdentifier({ -    tag: TransactionType.PeerPushCredit, -    peerPushPaymentIncomingId: req.peerPushPaymentIncomingId, -  }); - -  return { -    transactionId, -  }; -} - -export async function processPeerPullDebit( -  ws: InternalWalletState, -  peerPullPaymentIncomingId: string, -): Promise<OperationAttemptResult> { -  const peerPullInc = await ws.db -    .mktx((x) => [x.peerPullPaymentIncoming]) -    .runReadOnly(async (tx) => { -      return tx.peerPullPaymentIncoming.get(peerPullPaymentIncomingId); -    }); -  if (!peerPullInc) { -    throw Error("peer pull debit not found"); -  } -  if (peerPullInc.status === PeerPullDebitRecordStatus.PendingDeposit) { -    const pursePub = peerPullInc.pursePub; - -    const coinSel = peerPullInc.coinSel; -    if (!coinSel) { -      throw Error("invalid state, no coins selected"); -    } - -    const coins = await queryCoinInfosForSelection(ws, coinSel); - -    const depositSigsResp = await ws.cryptoApi.signPurseDeposits({ -      exchangeBaseUrl: peerPullInc.exchangeBaseUrl, -      pursePub: peerPullInc.pursePub, -      coins, -    }); - -    const purseDepositUrl = new URL( -      `purses/${pursePub}/deposit`, -      peerPullInc.exchangeBaseUrl, -    ); - -    const depositPayload: ExchangePurseDeposits = { -      deposits: depositSigsResp.deposits, -    }; - -    if (logger.shouldLogTrace()) { -      logger.trace(`purse deposit payload: ${j2s(depositPayload)}`); -    } - -    const httpResp = await ws.http.postJson( -      purseDepositUrl.href, -      depositPayload, -    ); -    const resp = await readSuccessResponseJsonOrThrow(httpResp, codecForAny()); -    logger.trace(`purse deposit response: ${j2s(resp)}`); -  } - -  await ws.db -    .mktx((x) => [x.peerPullPaymentIncoming]) -    .runReadWrite(async (tx) => { -      const pi = await tx.peerPullPaymentIncoming.get( -        peerPullPaymentIncomingId, -      ); -      if (!pi) { -        throw Error("peer pull payment not found anymore"); -      } -      if (pi.status === PeerPullDebitRecordStatus.PendingDeposit) { -        pi.status = PeerPullDebitRecordStatus.DonePaid; -      } -      await tx.peerPullPaymentIncoming.put(pi); -    }); - -  return { -    type: OperationAttemptResultType.Finished, -    result: undefined, -  }; -} - -export async function confirmPeerPullDebit( -  ws: InternalWalletState, -  req: ConfirmPeerPullDebitRequest, -): Promise<AcceptPeerPullPaymentResponse> { -  const peerPullInc = await ws.db -    .mktx((x) => [x.peerPullPaymentIncoming]) -    .runReadOnly(async (tx) => { -      return tx.peerPullPaymentIncoming.get(req.peerPullPaymentIncomingId); -    }); - -  if (!peerPullInc) { -    throw Error( -      `can't accept unknown incoming p2p pull payment (${req.peerPullPaymentIncomingId})`, -    ); -  } - -  const instructedAmount = Amounts.parseOrThrow( -    peerPullInc.contractTerms.amount, -  ); - -  const coinSelRes = await selectPeerCoins(ws, instructedAmount); -  logger.info(`selected p2p coins (pull): ${j2s(coinSelRes)}`); - -  if (coinSelRes.type !== "success") { -    throw TalerError.fromDetail( -      TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE, -      { -        insufficientBalanceDetails: coinSelRes.insufficientBalanceDetails, -      }, -    ); -  } - -  const sel = coinSelRes.result; - -  const totalAmount = await getTotalPeerPaymentCost( -    ws, -    coinSelRes.result.coins, -  ); - -  const ppi = await ws.db -    .mktx((x) => [ -      x.exchanges, -      x.coins, -      x.denominations, -      x.refreshGroups, -      x.peerPullPaymentIncoming, -      x.coinAvailability, -    ]) -    .runReadWrite(async (tx) => { -      await spendCoins(ws, tx, { -        // allocationId: `txn:peer-pull-debit:${req.peerPullPaymentIncomingId}`, -        allocationId: constructTransactionIdentifier({ -          tag: TransactionType.PeerPullDebit, -          peerPullPaymentIncomingId: req.peerPullPaymentIncomingId, -        }), -        coinPubs: sel.coins.map((x) => x.coinPub), -        contributions: sel.coins.map((x) => -          Amounts.parseOrThrow(x.contribution), -        ), -        refreshReason: RefreshReason.PayPeerPull, -      }); - -      const pi = await tx.peerPullPaymentIncoming.get( -        req.peerPullPaymentIncomingId, -      ); -      if (!pi) { -        throw Error(); -      } -      if (pi.status === PeerPullDebitRecordStatus.DialogProposed) { -        pi.status = PeerPullDebitRecordStatus.PendingDeposit; -        pi.coinSel = { -          coinPubs: sel.coins.map((x) => x.coinPub), -          contributions: sel.coins.map((x) => x.contribution), -          totalCost: Amounts.stringify(totalAmount), -        }; -      } -      await tx.peerPullPaymentIncoming.put(pi); -      return pi; -    }); - -  await runOperationWithErrorReporting( -    ws, -    TaskIdentifiers.forPeerPullPaymentDebit(ppi), -    async () => { -      return processPeerPullDebit(ws, ppi.peerPullPaymentIncomingId); -    }, -  ); - -  const transactionId = constructTransactionIdentifier({ -    tag: TransactionType.PeerPullDebit, -    peerPullPaymentIncomingId: req.peerPullPaymentIncomingId, -  }); - -  return { -    transactionId, -  }; -} - -/** - * Look up information about an incoming peer pull payment. - * Store the results in the wallet DB. - */ -export async function preparePeerPullDebit( -  ws: InternalWalletState, -  req: PreparePeerPullDebitRequest, -): Promise<PreparePeerPullDebitResponse> { -  const uri = parsePayPullUri(req.talerUri); - -  if (!uri) { -    throw Error("got invalid taler://pay-pull URI"); -  } - -  const existingPullIncomingRecord = await ws.db -    .mktx((x) => [x.peerPullPaymentIncoming]) -    .runReadOnly(async (tx) => { -      return tx.peerPullPaymentIncoming.indexes.byExchangeAndContractPriv.get([ -        uri.exchangeBaseUrl, -        uri.contractPriv, -      ]); -    }); - -  if (existingPullIncomingRecord) { -    return { -      amount: existingPullIncomingRecord.contractTerms.amount, -      amountRaw: existingPullIncomingRecord.contractTerms.amount, -      amountEffective: existingPullIncomingRecord.totalCostEstimated, -      contractTerms: existingPullIncomingRecord.contractTerms, -      peerPullPaymentIncomingId: -        existingPullIncomingRecord.peerPullPaymentIncomingId, -      transactionId: constructTransactionIdentifier({ -        tag: TransactionType.PeerPullDebit, -        peerPullPaymentIncomingId: -          existingPullIncomingRecord.peerPullPaymentIncomingId, -      }), -    }; -  } - -  const exchangeBaseUrl = uri.exchangeBaseUrl; -  const contractPriv = uri.contractPriv; -  const contractPub = encodeCrock(eddsaGetPublic(decodeCrock(contractPriv))); - -  const getContractUrl = new URL(`contracts/${contractPub}`, exchangeBaseUrl); - -  const contractHttpResp = await ws.http.get(getContractUrl.href); - -  const contractResp = await readSuccessResponseJsonOrThrow( -    contractHttpResp, -    codecForExchangeGetContractResponse(), -  ); - -  const pursePub = contractResp.purse_pub; - -  const dec = await ws.cryptoApi.decryptContractForDeposit({ -    ciphertext: contractResp.econtract, -    contractPriv: contractPriv, -    pursePub: pursePub, -  }); - -  const getPurseUrl = new URL(`purses/${pursePub}/merge`, exchangeBaseUrl); - -  const purseHttpResp = await ws.http.get(getPurseUrl.href); - -  const purseStatus = await readSuccessResponseJsonOrThrow( -    purseHttpResp, -    codecForExchangePurseStatus(), -  ); - -  const peerPullPaymentIncomingId = encodeCrock(getRandomBytes(32)); - -  let contractTerms: PeerContractTerms; - -  if (dec.contractTerms) { -    contractTerms = codecForPeerContractTerms().decode(dec.contractTerms); -    // FIXME: Check that the purseStatus balance matches contract terms amount -  } else { -    // FIXME: In this case, where do we get the purse expiration from?! -    // https://bugs.gnunet.org/view.php?id=7706 -    throw Error("pull payments without contract terms not supported yet"); -  } - -  // FIXME: Why don't we compute the totalCost here?! - -  const instructedAmount = Amounts.parseOrThrow(contractTerms.amount); - -  const coinSelRes = await selectPeerCoins(ws, instructedAmount); -  logger.info(`selected p2p coins (pull): ${j2s(coinSelRes)}`); - -  if (coinSelRes.type !== "success") { -    throw TalerError.fromDetail( -      TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE, -      { -        insufficientBalanceDetails: coinSelRes.insufficientBalanceDetails, -      }, -    ); -  } - -  const totalAmount = await getTotalPeerPaymentCost( -    ws, -    coinSelRes.result.coins, -  ); - -  await ws.db -    .mktx((x) => [x.peerPullPaymentIncoming]) -    .runReadWrite(async (tx) => { -      await tx.peerPullPaymentIncoming.add({ -        peerPullPaymentIncomingId, -        contractPriv: contractPriv, -        exchangeBaseUrl: exchangeBaseUrl, -        pursePub: pursePub, -        timestampCreated: TalerPreciseTimestamp.now(), -        contractTerms, -        status: PeerPullDebitRecordStatus.DialogProposed, -        totalCostEstimated: Amounts.stringify(totalAmount), -      }); -    }); - -  return { -    amount: contractTerms.amount, -    amountEffective: Amounts.stringify(totalAmount), -    amountRaw: contractTerms.amount, -    contractTerms: contractTerms, -    peerPullPaymentIncomingId, -    transactionId: constructTransactionIdentifier({ -      tag: TransactionType.PeerPullDebit, -      peerPullPaymentIncomingId: peerPullPaymentIncomingId, -    }), -  }; -} - -export async function queryPurseForPeerPullCredit( -  ws: InternalWalletState, -  pullIni: PeerPullPaymentInitiationRecord, -  cancellationToken: CancellationToken, -): Promise<LongpollResult> { -  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.get(purseDepositUrl.href, { -    timeout: { d_ms: 60000 }, -    cancellationToken, -  }); - -  logger.info(`purse status code: HTTP ${resp.status}`); - -  const result = await readSuccessResponseJsonOrErrorCode( -    resp, -    codecForExchangePurseStatus(), -  ); - -  if (result.isError) { -    logger.info(`got purse status error, EC=${result.talerErrorResponse.code}`); -    if (resp.status === 404) { -      return { ready: false }; -    } else { -      throwUnexpectedRequestError(resp, result.talerErrorResponse); -    } -  } - -  if (!result.response.deposit_timestamp) { -    logger.info("purse not ready yet (no deposit)"); -    return { ready: false }; -  } - -  const reserve = await ws.db -    .mktx((x) => [x.reserves]) -    .runReadOnly(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, -      contractTerms: pullIni.contractTerms, -      contractPriv: pullIni.contractPriv, -    }, -    forcedWithdrawalGroupId: pullIni.withdrawalGroupId, -    exchangeBaseUrl: pullIni.exchangeBaseUrl, -    reserveStatus: WithdrawalGroupStatus.PendingQueryingStatus, -    reserveKeyPair: { -      priv: reserve.reservePriv, -      pub: reserve.reservePub, -    }, -  }); - -  await ws.db -    .mktx((x) => [x.peerPullPaymentInitiations]) -    .runReadWrite(async (tx) => { -      const finPi = await tx.peerPullPaymentInitiations.get(pullIni.pursePub); -      if (!finPi) { -        logger.warn("peerPullPaymentInitiation not found anymore"); -        return; -      } -      if (finPi.status === PeerPullPaymentInitiationStatus.PendingReady) { -        finPi.status = PeerPullPaymentInitiationStatus.DonePurseDeposited; -      } -      await tx.peerPullPaymentInitiations.put(finPi); -    }); -  return { -    ready: true, -  }; -} - -export async function processPeerPullCredit( -  ws: InternalWalletState, -  pursePub: string, -): Promise<OperationAttemptResult> { -  const pullIni = await ws.db -    .mktx((x) => [x.peerPullPaymentInitiations]) -    .runReadOnly(async (tx) => { -      return tx.peerPullPaymentInitiations.get(pursePub); -    }); -  if (!pullIni) { -    throw Error("peer pull payment initiation not found in database"); -  } - -  const retryTag = constructTaskIdentifier({ -    tag: PendingTaskType.PeerPullCredit, -    pursePub, -  }); - -  // We're already running! -  if (ws.activeLongpoll[retryTag]) { -    logger.info("peer-pull-credit already in long-polling, returning!"); -    return { -      type: OperationAttemptResultType.Longpoll, -    }; -  } - -  logger.trace(`processing ${retryTag}, status=${pullIni.status}`); - -  switch (pullIni.status) { -    case PeerPullPaymentInitiationStatus.DonePurseDeposited: { -      // We implement this case so that the "retry" action on a peer-pull-credit transaction -      // also retries the withdrawal task. - -      logger.warn( -        "peer pull payment initiation is already finished, retrying withdrawal", -      ); - -      const withdrawalGroupId = pullIni.withdrawalGroupId; - -      if (withdrawalGroupId) { -        const taskId = constructTaskIdentifier({ -          tag: PendingTaskType.Withdraw, -          withdrawalGroupId, -        }); -        stopLongpolling(ws, taskId); -        await resetOperationTimeout(ws, taskId); -        await runOperationWithErrorReporting(ws, taskId, () => -          processWithdrawalGroup(ws, withdrawalGroupId), -        ); -      } -      return { -        type: OperationAttemptResultType.Finished, -        result: undefined, -      }; -    } -    case PeerPullPaymentInitiationStatus.PendingReady: -      runLongpollAsync(ws, retryTag, async (cancellationToken) => -        queryPurseForPeerPullCredit(ws, pullIni, cancellationToken), -      ); -      logger.trace( -        "returning early from processPeerPullCredit for long-polling in background", -      ); -      return { -        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", -        ); -      } -      break; -    } -    case PeerPullPaymentInitiationStatus.PendingCreatePurse: -      break; -    default: -      throw Error(`unknown PeerPullPaymentInitiationStatus ${pullIni.status}`); -  } - -  const mergeReserve = await ws.db -    .mktx((x) => [x.reserves]) -    .runReadOnly(async (tx) => { -      return tx.reserves.get(pullIni.mergeReserveRowId); -    }); - -  if (!mergeReserve) { -    throw Error("merge reserve for peer pull payment not found in database"); -  } - -  const purseFee = Amounts.stringify(Amounts.zeroOfAmount(pullIni.amount)); - -  const reservePayto = talerPaytoFromExchangeReserve( -    pullIni.exchangeBaseUrl, -    mergeReserve.reservePub, -  ); - -  const econtractResp = await ws.cryptoApi.encryptContractForDeposit({ -    contractPriv: pullIni.contractPriv, -    contractPub: pullIni.contractPub, -    contractTerms: pullIni.contractTerms, -    pursePriv: pullIni.pursePriv, -    pursePub: pullIni.pursePub, -  }); - -  const purseExpiration = pullIni.contractTerms.purse_expiration; -  const sigRes = await ws.cryptoApi.signReservePurseCreate({ -    contractTermsHash: pullIni.contractTermsHash, -    flags: WalletAccountMergeFlags.CreateWithPurseFee, -    mergePriv: pullIni.mergePriv, -    mergeTimestamp: TalerPreciseTimestamp.round(pullIni.mergeTimestamp), -    purseAmount: pullIni.contractTerms.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(pullIni.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.contractTerms.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.postJson( -    reservePurseMergeUrl.href, -    reservePurseReqBody, -  ); - -  if (httpResp.status === HttpStatusCode.UnavailableForLegalReasons) { -    const respJson = await httpResp.json(); -    const kycPending = codecForWalletKycUuid().decode(respJson); -    logger.info(`kyc uuid response: ${j2s(kycPending)}`); - -    await ws.db -      .mktx((x) => [x.peerPullPaymentInitiations]) -      .runReadWrite(async (tx) => { -        const peerIni = await tx.peerPullPaymentInitiations.get(pursePub); -        if (!peerIni) { -          return; -        } -        peerIni.kycInfo = { -          paytoHash: kycPending.h_payto, -          requirementRow: kycPending.requirement_row, -        }; -        peerIni.status = -          PeerPullPaymentInitiationStatus.PendingMergeKycRequired; -        await tx.peerPullPaymentInitiations.put(peerIni); -      }); -    return { -      type: OperationAttemptResultType.Pending, -      result: undefined, -    }; -  } - -  const resp = await readSuccessResponseJsonOrThrow(httpResp, codecForAny()); - -  logger.info(`reserve merge response: ${j2s(resp)}`); - -  await ws.db -    .mktx((x) => [x.peerPullPaymentInitiations]) -    .runReadWrite(async (tx) => { -      const pi2 = await tx.peerPullPaymentInitiations.get(pursePub); -      if (!pi2) { -        return; -      } -      pi2.status = PeerPullPaymentInitiationStatus.PendingReady; -      await tx.peerPullPaymentInitiations.put(pi2); -    }); - -  return { -    type: OperationAttemptResultType.Finished, -    result: undefined, -  }; -} - -/** - * 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 -    .mktx((x) => [x.exchanges]) -    .runReadOnly(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; -        } -        if (candidate.lastWithdrawal && e.lastWithdrawal) { -          if ( -            AbsoluteTime.cmp( -              AbsoluteTime.fromPreciseTimestamp(e.lastWithdrawal), -              AbsoluteTime.fromPreciseTimestamp(candidate.lastWithdrawal), -            ) > 0 -          ) { -            candidate = e; -          } -        } -      } -      if (candidate) { -        return candidate.baseUrl; -      } -      return undefined; -    }); -  return url; -} - -/** - * 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`); - -  return { -    exchangeBaseUrl: exchangeUrl, -    amountEffective: wi.withdrawalAmountEffective, -    amountRaw: req.amount, -  }; -} - -/** - * 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 updateExchangeFromUrl(ws, exchangeBaseUrl); - -  const mergeReserveInfo = await getMergeReserveInfo(ws, { -    exchangeBaseUrl: exchangeBaseUrl, -  }); - -  const mergeTimestamp = TalerPreciseTimestamp.now(); - -  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 wi = await getExchangeWithdrawalInfo( -    ws, -    exchangeBaseUrl, -    Amounts.parseOrThrow(req.partialContractTerms.amount), -    undefined, -  ); - -  await ws.db -    .mktx((x) => [x.peerPullPaymentInitiations, x.contractTerms]) -    .runReadWrite(async (tx) => { -      await tx.peerPullPaymentInitiations.put({ -        amount: req.partialContractTerms.amount, -        contractTermsHash: hContractTerms, -        exchangeBaseUrl: exchangeBaseUrl, -        pursePriv: pursePair.priv, -        pursePub: pursePair.pub, -        mergePriv: mergePair.priv, -        mergePub: mergePair.pub, -        status: PeerPullPaymentInitiationStatus.PendingCreatePurse, -        contractTerms: contractTerms, -        mergeTimestamp, -        mergeReserveRowId: mergeReserveRowId, -        contractPriv: contractKeyPair.priv, -        contractPub: contractKeyPair.pub, -        withdrawalGroupId, -        estimatedAmountEffective: wi.withdrawalAmountEffective, -      }); -      await tx.contractTerms.put({ -        contractTermsRaw: contractTerms, -        h: hContractTerms, -      }); -    }); - -  // FIXME: Should we somehow signal to the client -  // whether purse creation has failed, or does the client/ -  // check this asynchronously from the transaction status? - -  const taskId = constructTaskIdentifier({ -    tag: PendingTaskType.PeerPullCredit, -    pursePub: pursePair.pub, -  }); - -  await runOperationWithErrorReporting(ws, taskId, async () => { -    return processPeerPullCredit(ws, pursePair.pub); -  }); - -  const transactionId = constructTransactionIdentifier({ -    tag: TransactionType.PeerPullCredit, -    pursePub: pursePair.pub, -  }); - -  return { -    talerUri: constructPayPullUri({ -      exchangeBaseUrl: exchangeBaseUrl, -      contractPriv: contractKeyPair.priv, -    }), -    transactionId, -  }; -} - -export function computePeerPushDebitTransactionState( -  ppiRecord: PeerPushPaymentInitiationRecord, -): TransactionState { -  switch (ppiRecord.status) { -    case PeerPushPaymentInitiationStatus.PendingCreatePurse: -      return { -        major: TransactionMajorState.Pending, -        minor: TransactionMinorState.CreatePurse, -      }; -    case PeerPushPaymentInitiationStatus.PendingReady: -      return { -        major: TransactionMajorState.Pending, -        minor: TransactionMinorState.Ready, -      }; -    case PeerPushPaymentInitiationStatus.Aborted: -      return { -        major: TransactionMajorState.Aborted, -      }; -    case PeerPushPaymentInitiationStatus.AbortingDeletePurse: -      return { -        major: TransactionMajorState.Aborting, -        minor: TransactionMinorState.DeletePurse, -      }; -    case PeerPushPaymentInitiationStatus.AbortingRefresh: -      return { -        major: TransactionMajorState.Aborting, -        minor: TransactionMinorState.Refresh, -      }; -    case PeerPushPaymentInitiationStatus.SuspendedAbortingDeletePurse: -      return { -        major: TransactionMajorState.SuspendedAborting, -        minor: TransactionMinorState.DeletePurse, -      }; -    case PeerPushPaymentInitiationStatus.SuspendedAbortingRefresh: -      return { -        major: TransactionMajorState.SuspendedAborting, -        minor: TransactionMinorState.Refresh, -      }; -    case PeerPushPaymentInitiationStatus.SuspendedCreatePurse: -      return { -        major: TransactionMajorState.Suspended, -        minor: TransactionMinorState.CreatePurse, -      }; -    case PeerPushPaymentInitiationStatus.SuspendedReady: -      return { -        major: TransactionMajorState.Suspended, -        minor: TransactionMinorState.Ready, -      }; -    case PeerPushPaymentInitiationStatus.Done: -      return { -        major: TransactionMajorState.Done, -      }; -    case PeerPushPaymentInitiationStatus.Failed: -      return { -        major: TransactionMajorState.Failed, -      }; -  } -} - -export function computePeerPushDebitTransactionActions( -  ppiRecord: PeerPushPaymentInitiationRecord, -): TransactionAction[] { -  switch (ppiRecord.status) { -    case PeerPushPaymentInitiationStatus.PendingCreatePurse: -      return [TransactionAction.Abort, TransactionAction.Suspend]; -    case PeerPushPaymentInitiationStatus.PendingReady: -      return [TransactionAction.Abort, TransactionAction.Suspend]; -    case PeerPushPaymentInitiationStatus.Aborted: -      return [TransactionAction.Delete]; -    case PeerPushPaymentInitiationStatus.AbortingDeletePurse: -      return [TransactionAction.Suspend, TransactionAction.Fail]; -    case PeerPushPaymentInitiationStatus.AbortingRefresh: -      return [TransactionAction.Suspend, TransactionAction.Fail]; -    case PeerPushPaymentInitiationStatus.SuspendedAbortingDeletePurse: -      return [TransactionAction.Resume, TransactionAction.Fail]; -    case PeerPushPaymentInitiationStatus.SuspendedAbortingRefresh: -      return [TransactionAction.Resume, TransactionAction.Fail]; -    case PeerPushPaymentInitiationStatus.SuspendedCreatePurse: -      return [TransactionAction.Resume, TransactionAction.Abort]; -    case PeerPushPaymentInitiationStatus.SuspendedReady: -      return [TransactionAction.Suspend, TransactionAction.Abort]; -    case PeerPushPaymentInitiationStatus.Done: -      return [TransactionAction.Delete]; -    case PeerPushPaymentInitiationStatus.Failed: -      return [TransactionAction.Delete]; -  } -} - -export async function abortPeerPushDebitTransaction( -  ws: InternalWalletState, -  pursePub: string, -) { -  const taskId = constructTaskIdentifier({ -    tag: PendingTaskType.PeerPushDebit, -    pursePub, -  }); -  const transactionId = constructTransactionIdentifier({ -    tag: TransactionType.PeerPushDebit, -    pursePub, -  }); -  stopLongpolling(ws, taskId); -  const transitionInfo = await ws.db -    .mktx((x) => [x.peerPushPaymentInitiations]) -    .runReadWrite(async (tx) => { -      const pushDebitRec = await tx.peerPushPaymentInitiations.get(pursePub); -      if (!pushDebitRec) { -        logger.warn(`peer push debit ${pursePub} not found`); -        return; -      } -      let newStatus: PeerPushPaymentInitiationStatus | undefined = undefined; -      switch (pushDebitRec.status) { -        case PeerPushPaymentInitiationStatus.PendingReady: -        case PeerPushPaymentInitiationStatus.SuspendedReady: -          newStatus = PeerPushPaymentInitiationStatus.AbortingDeletePurse; -          break; -        case PeerPushPaymentInitiationStatus.SuspendedCreatePurse: -        case PeerPushPaymentInitiationStatus.PendingCreatePurse: -          // Network request might already be in-flight! -          newStatus = PeerPushPaymentInitiationStatus.AbortingDeletePurse; -          break; -        case PeerPushPaymentInitiationStatus.SuspendedAbortingRefresh: -        case PeerPushPaymentInitiationStatus.SuspendedAbortingDeletePurse: -        case PeerPushPaymentInitiationStatus.AbortingRefresh: -        case PeerPushPaymentInitiationStatus.Done: -        case PeerPushPaymentInitiationStatus.AbortingDeletePurse: -        case PeerPushPaymentInitiationStatus.Aborted: -          // Do nothing -          break; -        case PeerPushPaymentInitiationStatus.Failed: -          break; -        default: -          assertUnreachable(pushDebitRec.status); -      } -      if (newStatus != null) { -        const oldTxState = computePeerPushDebitTransactionState(pushDebitRec); -        pushDebitRec.status = newStatus; -        const newTxState = computePeerPushDebitTransactionState(pushDebitRec); -        await tx.peerPushPaymentInitiations.put(pushDebitRec); -        return { -          oldTxState, -          newTxState, -        }; -      } -      return undefined; -    }); -  notifyTransition(ws, transactionId, transitionInfo); -} - -export async function failPeerPushDebitTransaction( -  ws: InternalWalletState, -  pursePub: string, -) { -  const taskId = constructTaskIdentifier({ -    tag: PendingTaskType.PeerPushDebit, -    pursePub, -  }); -  const transactionId = constructTransactionIdentifier({ -    tag: TransactionType.PeerPushDebit, -    pursePub, -  }); -  stopLongpolling(ws, taskId); -  const transitionInfo = await ws.db -    .mktx((x) => [x.peerPushPaymentInitiations]) -    .runReadWrite(async (tx) => { -      const pushDebitRec = await tx.peerPushPaymentInitiations.get(pursePub); -      if (!pushDebitRec) { -        logger.warn(`peer push debit ${pursePub} not found`); -        return; -      } -      let newStatus: PeerPushPaymentInitiationStatus | undefined = undefined; -      switch (pushDebitRec.status) { -        case PeerPushPaymentInitiationStatus.AbortingRefresh: -        case PeerPushPaymentInitiationStatus.SuspendedAbortingRefresh: -          // FIXME: We also need to abort the refresh group! -          newStatus = PeerPushPaymentInitiationStatus.Aborted; -          break; -        case PeerPushPaymentInitiationStatus.AbortingDeletePurse: -        case PeerPushPaymentInitiationStatus.SuspendedAbortingDeletePurse: -          newStatus = PeerPushPaymentInitiationStatus.Aborted; -          break; -        case PeerPushPaymentInitiationStatus.PendingReady: -        case PeerPushPaymentInitiationStatus.SuspendedReady: -        case PeerPushPaymentInitiationStatus.SuspendedCreatePurse: -        case PeerPushPaymentInitiationStatus.PendingCreatePurse: -        case PeerPushPaymentInitiationStatus.Done: -        case PeerPushPaymentInitiationStatus.Aborted: -        case PeerPushPaymentInitiationStatus.Failed: -          // Do nothing -          break; -        default: -          assertUnreachable(pushDebitRec.status); -      } -      if (newStatus != null) { -        const oldTxState = computePeerPushDebitTransactionState(pushDebitRec); -        pushDebitRec.status = newStatus; -        const newTxState = computePeerPushDebitTransactionState(pushDebitRec); -        await tx.peerPushPaymentInitiations.put(pushDebitRec); -        return { -          oldTxState, -          newTxState, -        }; -      } -      return undefined; -    }); -  notifyTransition(ws, transactionId, transitionInfo); -} - -export async function suspendPeerPushDebitTransaction( -  ws: InternalWalletState, -  pursePub: string, -) { -  const taskId = constructTaskIdentifier({ -    tag: PendingTaskType.PeerPushDebit, -    pursePub, -  }); -  const transactionId = constructTransactionIdentifier({ -    tag: TransactionType.PeerPushDebit, -    pursePub, -  }); -  stopLongpolling(ws, taskId); -  const transitionInfo = await ws.db -    .mktx((x) => [x.peerPushPaymentInitiations]) -    .runReadWrite(async (tx) => { -      const pushDebitRec = await tx.peerPushPaymentInitiations.get(pursePub); -      if (!pushDebitRec) { -        logger.warn(`peer push debit ${pursePub} not found`); -        return; -      } -      let newStatus: PeerPushPaymentInitiationStatus | undefined = undefined; -      switch (pushDebitRec.status) { -        case PeerPushPaymentInitiationStatus.PendingCreatePurse: -          newStatus = PeerPushPaymentInitiationStatus.SuspendedCreatePurse; -          break; -        case PeerPushPaymentInitiationStatus.AbortingRefresh: -          newStatus = PeerPushPaymentInitiationStatus.SuspendedAbortingRefresh; -          break; -        case PeerPushPaymentInitiationStatus.AbortingDeletePurse: -          newStatus = -            PeerPushPaymentInitiationStatus.SuspendedAbortingDeletePurse; -          break; -        case PeerPushPaymentInitiationStatus.PendingReady: -          newStatus = PeerPushPaymentInitiationStatus.SuspendedReady; -          break; -        case PeerPushPaymentInitiationStatus.SuspendedAbortingDeletePurse: -        case PeerPushPaymentInitiationStatus.SuspendedAbortingRefresh: -        case PeerPushPaymentInitiationStatus.SuspendedReady: -        case PeerPushPaymentInitiationStatus.SuspendedCreatePurse: -        case PeerPushPaymentInitiationStatus.Done: -        case PeerPushPaymentInitiationStatus.Aborted: -        case PeerPushPaymentInitiationStatus.Failed: -          // Do nothing -          break; -        default: -          assertUnreachable(pushDebitRec.status); -      } -      if (newStatus != null) { -        const oldTxState = computePeerPushDebitTransactionState(pushDebitRec); -        pushDebitRec.status = newStatus; -        const newTxState = computePeerPushDebitTransactionState(pushDebitRec); -        await tx.peerPushPaymentInitiations.put(pushDebitRec); -        return { -          oldTxState, -          newTxState, -        }; -      } -      return undefined; -    }); -  notifyTransition(ws, transactionId, transitionInfo); -} - -export async function suspendPeerPullDebitTransaction( -  ws: InternalWalletState, -  peerPullPaymentIncomingId: string, -) { -  const taskId = constructTaskIdentifier({ -    tag: PendingTaskType.PeerPullDebit, -    peerPullPaymentIncomingId, -  }); -  const transactionId = constructTransactionIdentifier({ -    tag: TransactionType.PeerPullDebit, -    peerPullPaymentIncomingId, -  }); -  stopLongpolling(ws, taskId); -  const transitionInfo = await ws.db -    .mktx((x) => [x.peerPullPaymentIncoming]) -    .runReadWrite(async (tx) => { -      const pullDebitRec = await tx.peerPullPaymentIncoming.get( -        peerPullPaymentIncomingId, -      ); -      if (!pullDebitRec) { -        logger.warn(`peer pull debit ${peerPullPaymentIncomingId} not found`); -        return; -      } -      let newStatus: PeerPullDebitRecordStatus | undefined = undefined; -      switch (pullDebitRec.status) { -        case PeerPullDebitRecordStatus.DialogProposed: -          break; -        case PeerPullDebitRecordStatus.DonePaid: -          break; -        case PeerPullDebitRecordStatus.PendingDeposit: -          newStatus = PeerPullDebitRecordStatus.SuspendedDeposit; -          break; -        case PeerPullDebitRecordStatus.SuspendedDeposit: -          break; -        case PeerPullDebitRecordStatus.Aborted: -          break; -        case PeerPullDebitRecordStatus.AbortingRefresh: -          newStatus = PeerPullDebitRecordStatus.SuspendedAbortingRefresh; -          break; -        case PeerPullDebitRecordStatus.Failed: -          break; -        case PeerPullDebitRecordStatus.SuspendedAbortingRefresh: -          break; -        default: -          assertUnreachable(pullDebitRec.status); -      } -      if (newStatus != null) { -        const oldTxState = computePeerPullDebitTransactionState(pullDebitRec); -        pullDebitRec.status = newStatus; -        const newTxState = computePeerPullDebitTransactionState(pullDebitRec); -        await tx.peerPullPaymentIncoming.put(pullDebitRec); -        return { -          oldTxState, -          newTxState, -        }; -      } -      return undefined; -    }); -  notifyTransition(ws, transactionId, transitionInfo); -} - -export async function abortPeerPullDebitTransaction( -  ws: InternalWalletState, -  peerPullPaymentIncomingId: string, -) { -  const taskId = constructTaskIdentifier({ -    tag: PendingTaskType.PeerPullDebit, -    peerPullPaymentIncomingId, -  }); -  const transactionId = constructTransactionIdentifier({ -    tag: TransactionType.PeerPullDebit, -    peerPullPaymentIncomingId, -  }); -  stopLongpolling(ws, taskId); -  const transitionInfo = await ws.db -    .mktx((x) => [x.peerPullPaymentIncoming]) -    .runReadWrite(async (tx) => { -      const pullDebitRec = await tx.peerPullPaymentIncoming.get( -        peerPullPaymentIncomingId, -      ); -      if (!pullDebitRec) { -        logger.warn(`peer pull debit ${peerPullPaymentIncomingId} not found`); -        return; -      } -      let newStatus: PeerPullDebitRecordStatus | undefined = undefined; -      switch (pullDebitRec.status) { -        case PeerPullDebitRecordStatus.DialogProposed: -          newStatus = PeerPullDebitRecordStatus.Aborted; -          break; -        case PeerPullDebitRecordStatus.DonePaid: -          break; -        case PeerPullDebitRecordStatus.PendingDeposit: -          newStatus = PeerPullDebitRecordStatus.AbortingRefresh; -          break; -        case PeerPullDebitRecordStatus.SuspendedDeposit: -          break; -        case PeerPullDebitRecordStatus.Aborted: -          break; -        case PeerPullDebitRecordStatus.AbortingRefresh: -          break; -        case PeerPullDebitRecordStatus.Failed: -          break; -        case PeerPullDebitRecordStatus.SuspendedAbortingRefresh: -          break; -        default: -          assertUnreachable(pullDebitRec.status); -      } -      if (newStatus != null) { -        const oldTxState = computePeerPullDebitTransactionState(pullDebitRec); -        pullDebitRec.status = newStatus; -        const newTxState = computePeerPullDebitTransactionState(pullDebitRec); -        await tx.peerPullPaymentIncoming.put(pullDebitRec); -        return { -          oldTxState, -          newTxState, -        }; -      } -      return undefined; -    }); -  notifyTransition(ws, transactionId, transitionInfo); -} - -export async function failPeerPullDebitTransaction( -  ws: InternalWalletState, -  peerPullPaymentIncomingId: string, -) { -  const taskId = constructTaskIdentifier({ -    tag: PendingTaskType.PeerPullDebit, -    peerPullPaymentIncomingId, -  }); -  const transactionId = constructTransactionIdentifier({ -    tag: TransactionType.PeerPullDebit, -    peerPullPaymentIncomingId, -  }); -  stopLongpolling(ws, taskId); -  const transitionInfo = await ws.db -    .mktx((x) => [x.peerPullPaymentIncoming]) -    .runReadWrite(async (tx) => { -      const pullDebitRec = await tx.peerPullPaymentIncoming.get( -        peerPullPaymentIncomingId, -      ); -      if (!pullDebitRec) { -        logger.warn(`peer pull debit ${peerPullPaymentIncomingId} not found`); -        return; -      } -      let newStatus: PeerPullDebitRecordStatus | undefined = undefined; -      switch (pullDebitRec.status) { -        case PeerPullDebitRecordStatus.DialogProposed: -          newStatus = PeerPullDebitRecordStatus.Aborted; -          break; -        case PeerPullDebitRecordStatus.DonePaid: -          break; -        case PeerPullDebitRecordStatus.PendingDeposit: -          break; -        case PeerPullDebitRecordStatus.SuspendedDeposit: -          break; -        case PeerPullDebitRecordStatus.Aborted: -          break; -        case PeerPullDebitRecordStatus.Failed: -          break; -        case PeerPullDebitRecordStatus.SuspendedAbortingRefresh: -        case PeerPullDebitRecordStatus.AbortingRefresh: -          // FIXME: abort underlying refresh! -          newStatus = PeerPullDebitRecordStatus.Failed; -          break; -        default: -          assertUnreachable(pullDebitRec.status); -      } -      if (newStatus != null) { -        const oldTxState = computePeerPullDebitTransactionState(pullDebitRec); -        pullDebitRec.status = newStatus; -        const newTxState = computePeerPullDebitTransactionState(pullDebitRec); -        await tx.peerPullPaymentIncoming.put(pullDebitRec); -        return { -          oldTxState, -          newTxState, -        }; -      } -      return undefined; -    }); -  notifyTransition(ws, transactionId, transitionInfo); -} - -export async function resumePeerPullDebitTransaction( -  ws: InternalWalletState, -  peerPullPaymentIncomingId: string, -) { -  const taskId = constructTaskIdentifier({ -    tag: PendingTaskType.PeerPullDebit, -    peerPullPaymentIncomingId, -  }); -  const transactionId = constructTransactionIdentifier({ -    tag: TransactionType.PeerPullDebit, -    peerPullPaymentIncomingId, -  }); -  stopLongpolling(ws, taskId); -  const transitionInfo = await ws.db -    .mktx((x) => [x.peerPullPaymentIncoming]) -    .runReadWrite(async (tx) => { -      const pullDebitRec = await tx.peerPullPaymentIncoming.get( -        peerPullPaymentIncomingId, -      ); -      if (!pullDebitRec) { -        logger.warn(`peer pull debit ${peerPullPaymentIncomingId} not found`); -        return; -      } -      let newStatus: PeerPullDebitRecordStatus | undefined = undefined; -      switch (pullDebitRec.status) { -        case PeerPullDebitRecordStatus.DialogProposed: -        case PeerPullDebitRecordStatus.DonePaid: -        case PeerPullDebitRecordStatus.PendingDeposit: -          break; -        case PeerPullDebitRecordStatus.SuspendedDeposit: -          newStatus = PeerPullDebitRecordStatus.PendingDeposit; -          break; -        case PeerPullDebitRecordStatus.Aborted: -          break; -        case PeerPullDebitRecordStatus.AbortingRefresh: -          break; -        case PeerPullDebitRecordStatus.Failed: -          break; -        case PeerPullDebitRecordStatus.SuspendedAbortingRefresh: -          newStatus = PeerPullDebitRecordStatus.AbortingRefresh; -          break; -        default: -          assertUnreachable(pullDebitRec.status); -      } -      if (newStatus != null) { -        const oldTxState = computePeerPullDebitTransactionState(pullDebitRec); -        pullDebitRec.status = newStatus; -        const newTxState = computePeerPullDebitTransactionState(pullDebitRec); -        await tx.peerPullPaymentIncoming.put(pullDebitRec); -        return { -          oldTxState, -          newTxState, -        }; -      } -      return undefined; -    }); -  ws.workAvailable.trigger(); -  notifyTransition(ws, transactionId, transitionInfo); -} - -export async function suspendPeerPushCreditTransaction( -  ws: InternalWalletState, -  peerPushPaymentIncomingId: string, -) { -  const taskId = constructTaskIdentifier({ -    tag: PendingTaskType.PeerPushCredit, -    peerPushPaymentIncomingId, -  }); -  const transactionId = constructTransactionIdentifier({ -    tag: TransactionType.PeerPushCredit, -    peerPushPaymentIncomingId, -  }); -  stopLongpolling(ws, taskId); -  const transitionInfo = await ws.db -    .mktx((x) => [x.peerPushPaymentIncoming]) -    .runReadWrite(async (tx) => { -      const pushCreditRec = await tx.peerPushPaymentIncoming.get( -        peerPushPaymentIncomingId, -      ); -      if (!pushCreditRec) { -        logger.warn(`peer push credit ${peerPushPaymentIncomingId} not found`); -        return; -      } -      let newStatus: PeerPushPaymentIncomingStatus | undefined = undefined; -      switch (pushCreditRec.status) { -        case PeerPushPaymentIncomingStatus.DialogProposed: -        case PeerPushPaymentIncomingStatus.Done: -        case PeerPushPaymentIncomingStatus.SuspendedMerge: -        case PeerPushPaymentIncomingStatus.SuspendedMergeKycRequired: -        case PeerPushPaymentIncomingStatus.SuspendedWithdrawing: -          break; -        case PeerPushPaymentIncomingStatus.PendingMergeKycRequired: -          newStatus = PeerPushPaymentIncomingStatus.SuspendedMergeKycRequired; -          break; -        case PeerPushPaymentIncomingStatus.PendingMerge: -          newStatus = PeerPushPaymentIncomingStatus.SuspendedMerge; -          break; -        case PeerPushPaymentIncomingStatus.PendingWithdrawing: -          // FIXME: Suspend internal withdrawal transaction! -          newStatus = PeerPushPaymentIncomingStatus.SuspendedWithdrawing; -          break; -        case PeerPushPaymentIncomingStatus.Aborted: -          break; -        case PeerPushPaymentIncomingStatus.Failed: -          break; -        default: -          assertUnreachable(pushCreditRec.status); -      } -      if (newStatus != null) { -        const oldTxState = computePeerPushCreditTransactionState(pushCreditRec); -        pushCreditRec.status = newStatus; -        const newTxState = computePeerPushCreditTransactionState(pushCreditRec); -        await tx.peerPushPaymentIncoming.put(pushCreditRec); -        return { -          oldTxState, -          newTxState, -        }; -      } -      return undefined; -    }); -  notifyTransition(ws, transactionId, transitionInfo); -} - -export async function abortPeerPushCreditTransaction( -  ws: InternalWalletState, -  peerPushPaymentIncomingId: string, -) { -  const taskId = constructTaskIdentifier({ -    tag: PendingTaskType.PeerPushCredit, -    peerPushPaymentIncomingId, -  }); -  const transactionId = constructTransactionIdentifier({ -    tag: TransactionType.PeerPushCredit, -    peerPushPaymentIncomingId, -  }); -  stopLongpolling(ws, taskId); -  const transitionInfo = await ws.db -    .mktx((x) => [x.peerPushPaymentIncoming]) -    .runReadWrite(async (tx) => { -      const pushCreditRec = await tx.peerPushPaymentIncoming.get( -        peerPushPaymentIncomingId, -      ); -      if (!pushCreditRec) { -        logger.warn(`peer push credit ${peerPushPaymentIncomingId} not found`); -        return; -      } -      let newStatus: PeerPushPaymentIncomingStatus | undefined = undefined; -      switch (pushCreditRec.status) { -        case PeerPushPaymentIncomingStatus.DialogProposed: -          newStatus = PeerPushPaymentIncomingStatus.Aborted; -          break; -        case PeerPushPaymentIncomingStatus.Done: -          break; -        case PeerPushPaymentIncomingStatus.SuspendedMerge: -        case PeerPushPaymentIncomingStatus.SuspendedMergeKycRequired: -        case PeerPushPaymentIncomingStatus.SuspendedWithdrawing: -          newStatus = PeerPushPaymentIncomingStatus.Aborted; -          break; -        case PeerPushPaymentIncomingStatus.PendingMergeKycRequired: -          newStatus = PeerPushPaymentIncomingStatus.Aborted; -          break; -        case PeerPushPaymentIncomingStatus.PendingMerge: -          newStatus = PeerPushPaymentIncomingStatus.Aborted; -          break; -        case PeerPushPaymentIncomingStatus.PendingWithdrawing: -          newStatus = PeerPushPaymentIncomingStatus.Aborted; -          break; -        case PeerPushPaymentIncomingStatus.Aborted: -          break; -        case PeerPushPaymentIncomingStatus.Failed: -          break; -        default: -          assertUnreachable(pushCreditRec.status); -      } -      if (newStatus != null) { -        const oldTxState = computePeerPushCreditTransactionState(pushCreditRec); -        pushCreditRec.status = newStatus; -        const newTxState = computePeerPushCreditTransactionState(pushCreditRec); -        await tx.peerPushPaymentIncoming.put(pushCreditRec); -        return { -          oldTxState, -          newTxState, -        }; -      } -      return undefined; -    }); -  notifyTransition(ws, transactionId, transitionInfo); -} - -export async function failPeerPushCreditTransaction( -  ws: InternalWalletState, -  peerPushPaymentIncomingId: string, -) { -  // We don't have any "aborting" states! -  throw Error("can't run cancel-aborting on peer-push-credit transaction"); -} - -export async function resumePeerPushCreditTransaction( -  ws: InternalWalletState, -  peerPushPaymentIncomingId: string, -) { -  const taskId = constructTaskIdentifier({ -    tag: PendingTaskType.PeerPushCredit, -    peerPushPaymentIncomingId, -  }); -  const transactionId = constructTransactionIdentifier({ -    tag: TransactionType.PeerPushCredit, -    peerPushPaymentIncomingId, -  }); -  stopLongpolling(ws, taskId); -  const transitionInfo = await ws.db -    .mktx((x) => [x.peerPushPaymentIncoming]) -    .runReadWrite(async (tx) => { -      const pushCreditRec = await tx.peerPushPaymentIncoming.get( -        peerPushPaymentIncomingId, -      ); -      if (!pushCreditRec) { -        logger.warn(`peer push credit ${peerPushPaymentIncomingId} not found`); -        return; -      } -      let newStatus: PeerPushPaymentIncomingStatus | undefined = undefined; -      switch (pushCreditRec.status) { -        case PeerPushPaymentIncomingStatus.DialogProposed: -        case PeerPushPaymentIncomingStatus.Done: -        case PeerPushPaymentIncomingStatus.PendingMergeKycRequired: -        case PeerPushPaymentIncomingStatus.PendingMerge: -        case PeerPushPaymentIncomingStatus.PendingWithdrawing: -        case PeerPushPaymentIncomingStatus.SuspendedMerge: -          newStatus = PeerPushPaymentIncomingStatus.PendingMerge; -          break; -        case PeerPushPaymentIncomingStatus.SuspendedMergeKycRequired: -          newStatus = PeerPushPaymentIncomingStatus.PendingMergeKycRequired; -          break; -        case PeerPushPaymentIncomingStatus.SuspendedWithdrawing: -          // FIXME: resume underlying "internal-withdrawal" transaction. -          newStatus = PeerPushPaymentIncomingStatus.PendingWithdrawing; -          break; -        case PeerPushPaymentIncomingStatus.Aborted: -          break; -        case PeerPushPaymentIncomingStatus.Failed: -          break; -        default: -          assertUnreachable(pushCreditRec.status); -      } -      if (newStatus != null) { -        const oldTxState = computePeerPushCreditTransactionState(pushCreditRec); -        pushCreditRec.status = newStatus; -        const newTxState = computePeerPushCreditTransactionState(pushCreditRec); -        await tx.peerPushPaymentIncoming.put(pushCreditRec); -        return { -          oldTxState, -          newTxState, -        }; -      } -      return undefined; -    }); -  ws.workAvailable.trigger(); -  notifyTransition(ws, transactionId, transitionInfo); -} - -export async function suspendPeerPullCreditTransaction( -  ws: InternalWalletState, -  pursePub: string, -) { -  const taskId = constructTaskIdentifier({ -    tag: PendingTaskType.PeerPullCredit, -    pursePub, -  }); -  const transactionId = constructTransactionIdentifier({ -    tag: TransactionType.PeerPullCredit, -    pursePub, -  }); -  stopLongpolling(ws, taskId); -  const transitionInfo = await ws.db -    .mktx((x) => [x.peerPullPaymentInitiations]) -    .runReadWrite(async (tx) => { -      const pullCreditRec = await tx.peerPullPaymentInitiations.get(pursePub); -      if (!pullCreditRec) { -        logger.warn(`peer pull credit ${pursePub} not found`); -        return; -      } -      let newStatus: PeerPullPaymentInitiationStatus | undefined = undefined; -      switch (pullCreditRec.status) { -        case PeerPullPaymentInitiationStatus.PendingCreatePurse: -          newStatus = PeerPullPaymentInitiationStatus.SuspendedCreatePurse; -          break; -        case PeerPullPaymentInitiationStatus.PendingMergeKycRequired: -          newStatus = PeerPullPaymentInitiationStatus.SuspendedMergeKycRequired; -          break; -        case PeerPullPaymentInitiationStatus.PendingWithdrawing: -          newStatus = PeerPullPaymentInitiationStatus.SuspendedWithdrawing; -          break; -        case PeerPullPaymentInitiationStatus.PendingReady: -          newStatus = PeerPullPaymentInitiationStatus.SuspendedReady; -          break; -        case PeerPullPaymentInitiationStatus.AbortingDeletePurse: -          newStatus = -            PeerPullPaymentInitiationStatus.SuspendedAbortingDeletePurse; -          break; -        case PeerPullPaymentInitiationStatus.DonePurseDeposited: -        case PeerPullPaymentInitiationStatus.SuspendedCreatePurse: -        case PeerPullPaymentInitiationStatus.SuspendedMergeKycRequired: -        case PeerPullPaymentInitiationStatus.SuspendedReady: -        case PeerPullPaymentInitiationStatus.SuspendedWithdrawing: -        case PeerPullPaymentInitiationStatus.Aborted: -        case PeerPullPaymentInitiationStatus.Failed: -        case PeerPullPaymentInitiationStatus.SuspendedAbortingDeletePurse: -          break; -        default: -          assertUnreachable(pullCreditRec.status); -      } -      if (newStatus != null) { -        const oldTxState = computePeerPullCreditTransactionState(pullCreditRec); -        pullCreditRec.status = newStatus; -        const newTxState = computePeerPullCreditTransactionState(pullCreditRec); -        await tx.peerPullPaymentInitiations.put(pullCreditRec); -        return { -          oldTxState, -          newTxState, -        }; -      } -      return undefined; -    }); -  notifyTransition(ws, transactionId, transitionInfo); -} - -export async function abortPeerPullCreditTransaction( -  ws: InternalWalletState, -  pursePub: string, -) { -  const taskId = constructTaskIdentifier({ -    tag: PendingTaskType.PeerPullCredit, -    pursePub, -  }); -  const transactionId = constructTransactionIdentifier({ -    tag: TransactionType.PeerPullCredit, -    pursePub, -  }); -  stopLongpolling(ws, taskId); -  const transitionInfo = await ws.db -    .mktx((x) => [x.peerPullPaymentInitiations]) -    .runReadWrite(async (tx) => { -      const pullCreditRec = await tx.peerPullPaymentInitiations.get(pursePub); -      if (!pullCreditRec) { -        logger.warn(`peer pull credit ${pursePub} not found`); -        return; -      } -      let newStatus: PeerPullPaymentInitiationStatus | undefined = undefined; -      switch (pullCreditRec.status) { -        case PeerPullPaymentInitiationStatus.PendingCreatePurse: -        case PeerPullPaymentInitiationStatus.PendingMergeKycRequired: -          newStatus = PeerPullPaymentInitiationStatus.AbortingDeletePurse; -          break; -        case PeerPullPaymentInitiationStatus.PendingWithdrawing: -          throw Error("can't abort anymore"); -        case PeerPullPaymentInitiationStatus.PendingReady: -          newStatus = PeerPullPaymentInitiationStatus.AbortingDeletePurse; -          break; -        case PeerPullPaymentInitiationStatus.DonePurseDeposited: -        case PeerPullPaymentInitiationStatus.SuspendedCreatePurse: -        case PeerPullPaymentInitiationStatus.SuspendedMergeKycRequired: -        case PeerPullPaymentInitiationStatus.SuspendedReady: -        case PeerPullPaymentInitiationStatus.SuspendedWithdrawing: -        case PeerPullPaymentInitiationStatus.Aborted: -        case PeerPullPaymentInitiationStatus.AbortingDeletePurse: -        case PeerPullPaymentInitiationStatus.Failed: -        case PeerPullPaymentInitiationStatus.SuspendedAbortingDeletePurse: -          break; -        default: -          assertUnreachable(pullCreditRec.status); -      } -      if (newStatus != null) { -        const oldTxState = computePeerPullCreditTransactionState(pullCreditRec); -        pullCreditRec.status = newStatus; -        const newTxState = computePeerPullCreditTransactionState(pullCreditRec); -        await tx.peerPullPaymentInitiations.put(pullCreditRec); -        return { -          oldTxState, -          newTxState, -        }; -      } -      return undefined; -    }); -  notifyTransition(ws, transactionId, transitionInfo); -} - -export async function failPeerPullCreditTransaction( -  ws: InternalWalletState, -  pursePub: string, -) { -  const taskId = constructTaskIdentifier({ -    tag: PendingTaskType.PeerPullCredit, -    pursePub, -  }); -  const transactionId = constructTransactionIdentifier({ -    tag: TransactionType.PeerPullCredit, -    pursePub, -  }); -  stopLongpolling(ws, taskId); -  const transitionInfo = await ws.db -    .mktx((x) => [x.peerPullPaymentInitiations]) -    .runReadWrite(async (tx) => { -      const pullCreditRec = await tx.peerPullPaymentInitiations.get(pursePub); -      if (!pullCreditRec) { -        logger.warn(`peer pull credit ${pursePub} not found`); -        return; -      } -      let newStatus: PeerPullPaymentInitiationStatus | undefined = undefined; -      switch (pullCreditRec.status) { -        case PeerPullPaymentInitiationStatus.PendingCreatePurse: -        case PeerPullPaymentInitiationStatus.PendingMergeKycRequired: -        case PeerPullPaymentInitiationStatus.PendingWithdrawing: -        case PeerPullPaymentInitiationStatus.PendingReady: -        case PeerPullPaymentInitiationStatus.DonePurseDeposited: -        case PeerPullPaymentInitiationStatus.SuspendedCreatePurse: -        case PeerPullPaymentInitiationStatus.SuspendedMergeKycRequired: -        case PeerPullPaymentInitiationStatus.SuspendedReady: -        case PeerPullPaymentInitiationStatus.SuspendedWithdrawing: -        case PeerPullPaymentInitiationStatus.Aborted: -        case PeerPullPaymentInitiationStatus.Failed: -          break; -        case PeerPullPaymentInitiationStatus.AbortingDeletePurse: -        case PeerPullPaymentInitiationStatus.SuspendedAbortingDeletePurse: -          newStatus = PeerPullPaymentInitiationStatus.Failed; -          break; -        default: -          assertUnreachable(pullCreditRec.status); -      } -      if (newStatus != null) { -        const oldTxState = computePeerPullCreditTransactionState(pullCreditRec); -        pullCreditRec.status = newStatus; -        const newTxState = computePeerPullCreditTransactionState(pullCreditRec); -        await tx.peerPullPaymentInitiations.put(pullCreditRec); -        return { -          oldTxState, -          newTxState, -        }; -      } -      return undefined; -    }); -  notifyTransition(ws, transactionId, transitionInfo); -} - -export async function resumePeerPullCreditTransaction( -  ws: InternalWalletState, -  pursePub: string, -) { -  const taskId = constructTaskIdentifier({ -    tag: PendingTaskType.PeerPullCredit, -    pursePub, -  }); -  const transactionId = constructTransactionIdentifier({ -    tag: TransactionType.PeerPullCredit, -    pursePub, -  }); -  stopLongpolling(ws, taskId); -  const transitionInfo = await ws.db -    .mktx((x) => [x.peerPullPaymentInitiations]) -    .runReadWrite(async (tx) => { -      const pullCreditRec = await tx.peerPullPaymentInitiations.get(pursePub); -      if (!pullCreditRec) { -        logger.warn(`peer pull credit ${pursePub} not found`); -        return; -      } -      let newStatus: PeerPullPaymentInitiationStatus | undefined = undefined; -      switch (pullCreditRec.status) { -        case PeerPullPaymentInitiationStatus.PendingCreatePurse: -        case PeerPullPaymentInitiationStatus.PendingMergeKycRequired: -        case PeerPullPaymentInitiationStatus.PendingWithdrawing: -        case PeerPullPaymentInitiationStatus.PendingReady: -        case PeerPullPaymentInitiationStatus.AbortingDeletePurse: -        case PeerPullPaymentInitiationStatus.DonePurseDeposited: -        case PeerPullPaymentInitiationStatus.Failed: -        case PeerPullPaymentInitiationStatus.Aborted: -          break; -        case PeerPullPaymentInitiationStatus.SuspendedCreatePurse: -          newStatus = PeerPullPaymentInitiationStatus.PendingCreatePurse; -          break; -        case PeerPullPaymentInitiationStatus.SuspendedMergeKycRequired: -          newStatus = PeerPullPaymentInitiationStatus.PendingMergeKycRequired; -          break; -        case PeerPullPaymentInitiationStatus.SuspendedReady: -          newStatus = PeerPullPaymentInitiationStatus.PendingReady; -          break; -        case PeerPullPaymentInitiationStatus.SuspendedWithdrawing: -          newStatus = PeerPullPaymentInitiationStatus.PendingWithdrawing; -          break; -        case PeerPullPaymentInitiationStatus.SuspendedAbortingDeletePurse: -          newStatus = PeerPullPaymentInitiationStatus.AbortingDeletePurse; -          break; -        default: -          assertUnreachable(pullCreditRec.status); -      } -      if (newStatus != null) { -        const oldTxState = computePeerPullCreditTransactionState(pullCreditRec); -        pullCreditRec.status = newStatus; -        const newTxState = computePeerPullCreditTransactionState(pullCreditRec); -        await tx.peerPullPaymentInitiations.put(pullCreditRec); -        return { -          oldTxState, -          newTxState, -        }; -      } -      return undefined; -    }); -  ws.workAvailable.trigger(); -  notifyTransition(ws, transactionId, transitionInfo); -} - -export async function resumePeerPushDebitTransaction( -  ws: InternalWalletState, -  pursePub: string, -) { -  const taskId = constructTaskIdentifier({ -    tag: PendingTaskType.PeerPushDebit, -    pursePub, -  }); -  const transactionId = constructTransactionIdentifier({ -    tag: TransactionType.PeerPushDebit, -    pursePub, -  }); -  stopLongpolling(ws, taskId); -  const transitionInfo = await ws.db -    .mktx((x) => [x.peerPushPaymentInitiations]) -    .runReadWrite(async (tx) => { -      const pushDebitRec = await tx.peerPushPaymentInitiations.get(pursePub); -      if (!pushDebitRec) { -        logger.warn(`peer push debit ${pursePub} not found`); -        return; -      } -      let newStatus: PeerPushPaymentInitiationStatus | undefined = undefined; -      switch (pushDebitRec.status) { -        case PeerPushPaymentInitiationStatus.SuspendedAbortingDeletePurse: -          newStatus = PeerPushPaymentInitiationStatus.AbortingDeletePurse; -          break; -        case PeerPushPaymentInitiationStatus.SuspendedAbortingRefresh: -          newStatus = PeerPushPaymentInitiationStatus.AbortingRefresh; -          break; -        case PeerPushPaymentInitiationStatus.SuspendedReady: -          newStatus = PeerPushPaymentInitiationStatus.PendingReady; -          break; -        case PeerPushPaymentInitiationStatus.SuspendedCreatePurse: -          newStatus = PeerPushPaymentInitiationStatus.PendingCreatePurse; -          break; -        case PeerPushPaymentInitiationStatus.PendingCreatePurse: -        case PeerPushPaymentInitiationStatus.AbortingRefresh: -        case PeerPushPaymentInitiationStatus.AbortingDeletePurse: -        case PeerPushPaymentInitiationStatus.PendingReady: -        case PeerPushPaymentInitiationStatus.Done: -        case PeerPushPaymentInitiationStatus.Aborted: -        case PeerPushPaymentInitiationStatus.Failed: -          // Do nothing -          break; -        default: -          assertUnreachable(pushDebitRec.status); -      } -      if (newStatus != null) { -        const oldTxState = computePeerPushDebitTransactionState(pushDebitRec); -        pushDebitRec.status = newStatus; -        const newTxState = computePeerPushDebitTransactionState(pushDebitRec); -        await tx.peerPushPaymentInitiations.put(pushDebitRec); -        return { -          oldTxState, -          newTxState, -        }; -      } -      return undefined; -    }); -  ws.workAvailable.trigger(); -  notifyTransition(ws, transactionId, transitionInfo); -} - -export function computePeerPushCreditTransactionState( -  pushCreditRecord: PeerPushPaymentIncomingRecord, -): TransactionState { -  switch (pushCreditRecord.status) { -    case PeerPushPaymentIncomingStatus.DialogProposed: -      return { -        major: TransactionMajorState.Dialog, -        minor: TransactionMinorState.Proposed, -      }; -    case PeerPushPaymentIncomingStatus.PendingMerge: -      return { -        major: TransactionMajorState.Pending, -        minor: TransactionMinorState.Merge, -      }; -    case PeerPushPaymentIncomingStatus.Done: -      return { -        major: TransactionMajorState.Done, -      }; -    case PeerPushPaymentIncomingStatus.PendingMergeKycRequired: -      return { -        major: TransactionMajorState.Pending, -        minor: TransactionMinorState.KycRequired, -      }; -    case PeerPushPaymentIncomingStatus.PendingWithdrawing: -      return { -        major: TransactionMajorState.Pending, -        minor: TransactionMinorState.Withdraw, -      }; -    case PeerPushPaymentIncomingStatus.SuspendedMerge: -      return { -        major: TransactionMajorState.Suspended, -        minor: TransactionMinorState.Merge, -      }; -    case PeerPushPaymentIncomingStatus.SuspendedMergeKycRequired: -      return { -        major: TransactionMajorState.Suspended, -        minor: TransactionMinorState.MergeKycRequired, -      }; -    case PeerPushPaymentIncomingStatus.SuspendedWithdrawing: -      return { -        major: TransactionMajorState.Suspended, -        minor: TransactionMinorState.Withdraw, -      }; -    case PeerPushPaymentIncomingStatus.Aborted: -      return { -        major: TransactionMajorState.Aborted, -      }; -    case PeerPushPaymentIncomingStatus.Failed: -      return { -        major: TransactionMajorState.Failed, -      }; -    default: -      assertUnreachable(pushCreditRecord.status); -  } -} - -export function computePeerPushCreditTransactionActions( -  pushCreditRecord: PeerPushPaymentIncomingRecord, -): TransactionAction[] { -  switch (pushCreditRecord.status) { -    case PeerPushPaymentIncomingStatus.DialogProposed: -      return []; -    case PeerPushPaymentIncomingStatus.PendingMerge: -      return [TransactionAction.Abort, TransactionAction.Suspend]; -    case PeerPushPaymentIncomingStatus.Done: -      return [TransactionAction.Delete]; -    case PeerPushPaymentIncomingStatus.PendingMergeKycRequired: -      return [TransactionAction.Abort, TransactionAction.Suspend]; -    case PeerPushPaymentIncomingStatus.PendingWithdrawing: -      return [TransactionAction.Suspend, TransactionAction.Fail]; -    case PeerPushPaymentIncomingStatus.SuspendedMerge: -      return [TransactionAction.Resume, TransactionAction.Abort]; -    case PeerPushPaymentIncomingStatus.SuspendedMergeKycRequired: -      return [TransactionAction.Resume, TransactionAction.Abort]; -    case PeerPushPaymentIncomingStatus.SuspendedWithdrawing: -      return [TransactionAction.Resume, TransactionAction.Fail]; -    case PeerPushPaymentIncomingStatus.Aborted: -      return [TransactionAction.Delete]; -    case PeerPushPaymentIncomingStatus.Failed: -      return [TransactionAction.Delete]; -    default: -      assertUnreachable(pushCreditRecord.status); -  } -} - -export function computePeerPullCreditTransactionState( -  pullCreditRecord: PeerPullPaymentInitiationRecord, -): TransactionState { -  switch (pullCreditRecord.status) { -    case PeerPullPaymentInitiationStatus.PendingCreatePurse: -      return { -        major: TransactionMajorState.Pending, -        minor: TransactionMinorState.CreatePurse, -      }; -    case PeerPullPaymentInitiationStatus.PendingMergeKycRequired: -      return { -        major: TransactionMajorState.Pending, -        minor: TransactionMinorState.MergeKycRequired, -      }; -    case PeerPullPaymentInitiationStatus.PendingReady: -      return { -        major: TransactionMajorState.Pending, -        minor: TransactionMinorState.Ready, -      }; -    case PeerPullPaymentInitiationStatus.DonePurseDeposited: -      return { -        major: TransactionMajorState.Done, -      }; -    case PeerPullPaymentInitiationStatus.PendingWithdrawing: -      return { -        major: TransactionMajorState.Pending, -        minor: TransactionMinorState.Withdraw, -      }; -    case PeerPullPaymentInitiationStatus.SuspendedCreatePurse: -      return { -        major: TransactionMajorState.Suspended, -        minor: TransactionMinorState.CreatePurse, -      }; -    case PeerPullPaymentInitiationStatus.SuspendedReady: -      return { -        major: TransactionMajorState.Suspended, -        minor: TransactionMinorState.Ready, -      }; -    case PeerPullPaymentInitiationStatus.SuspendedWithdrawing: -      return { -        major: TransactionMajorState.Pending, -        minor: TransactionMinorState.Withdraw, -      }; -    case PeerPullPaymentInitiationStatus.SuspendedMergeKycRequired: -      return { -        major: TransactionMajorState.Suspended, -        minor: TransactionMinorState.MergeKycRequired, -      }; -    case PeerPullPaymentInitiationStatus.Aborted: -      return { -        major: TransactionMajorState.Aborted, -      }; -    case PeerPullPaymentInitiationStatus.AbortingDeletePurse: -      return { -        major: TransactionMajorState.Aborting, -        minor: TransactionMinorState.DeletePurse, -      }; -    case PeerPullPaymentInitiationStatus.Failed: -      return { -        major: TransactionMajorState.Failed, -      }; -    case PeerPullPaymentInitiationStatus.SuspendedAbortingDeletePurse: -      return { -        major: TransactionMajorState.Aborting, -        minor: TransactionMinorState.DeletePurse, -      }; -  } -} - -export function computePeerPullCreditTransactionActions( -  pullCreditRecord: PeerPullPaymentInitiationRecord, -): TransactionAction[] { -  switch (pullCreditRecord.status) { -    case PeerPullPaymentInitiationStatus.PendingCreatePurse: -      return [TransactionAction.Abort, TransactionAction.Suspend]; -    case PeerPullPaymentInitiationStatus.PendingMergeKycRequired: -      return [TransactionAction.Abort, TransactionAction.Suspend]; -    case PeerPullPaymentInitiationStatus.PendingReady: -      return [TransactionAction.Abort, TransactionAction.Suspend]; -    case PeerPullPaymentInitiationStatus.DonePurseDeposited: -      return [TransactionAction.Delete]; -    case PeerPullPaymentInitiationStatus.PendingWithdrawing: -      return [TransactionAction.Abort, TransactionAction.Suspend]; -    case PeerPullPaymentInitiationStatus.SuspendedCreatePurse: -      return [TransactionAction.Resume, TransactionAction.Abort]; -    case PeerPullPaymentInitiationStatus.SuspendedReady: -      return [TransactionAction.Abort, TransactionAction.Resume]; -    case PeerPullPaymentInitiationStatus.SuspendedWithdrawing: -      return [TransactionAction.Resume, TransactionAction.Fail]; -    case PeerPullPaymentInitiationStatus.SuspendedMergeKycRequired: -      return [TransactionAction.Resume, TransactionAction.Fail]; -    case PeerPullPaymentInitiationStatus.Aborted: -      return [TransactionAction.Delete]; -    case PeerPullPaymentInitiationStatus.AbortingDeletePurse: -      return [TransactionAction.Suspend, TransactionAction.Fail]; -    case PeerPullPaymentInitiationStatus.Failed: -      return [TransactionAction.Delete]; -    case PeerPullPaymentInitiationStatus.SuspendedAbortingDeletePurse: -      return [TransactionAction.Resume, TransactionAction.Fail]; -  } -} - -export function computePeerPullDebitTransactionState( -  pullDebitRecord: PeerPullPaymentIncomingRecord, -): TransactionState { -  switch (pullDebitRecord.status) { -    case PeerPullDebitRecordStatus.DialogProposed: -      return { -        major: TransactionMajorState.Dialog, -        minor: TransactionMinorState.Proposed, -      }; -    case PeerPullDebitRecordStatus.PendingDeposit: -      return { -        major: TransactionMajorState.Pending, -        minor: TransactionMinorState.Deposit, -      }; -    case PeerPullDebitRecordStatus.DonePaid: -      return { -        major: TransactionMajorState.Done, -      }; -    case PeerPullDebitRecordStatus.SuspendedDeposit: -      return { -        major: TransactionMajorState.Suspended, -        minor: TransactionMinorState.Deposit, -      }; -    case PeerPullDebitRecordStatus.Aborted: -      return { -        major: TransactionMajorState.Aborted, -      }; -    case PeerPullDebitRecordStatus.AbortingRefresh: -      return { -        major: TransactionMajorState.Aborting, -        minor: TransactionMinorState.Refresh, -      }; -    case PeerPullDebitRecordStatus.Failed: -      return { -        major: TransactionMajorState.Failed, -      }; -    case PeerPullDebitRecordStatus.SuspendedAbortingRefresh: -      return { -        major: TransactionMajorState.SuspendedAborting, -        minor: TransactionMinorState.Refresh, -      }; -  } -} - -export function computePeerPullDebitTransactionActions( -  pullDebitRecord: PeerPullPaymentIncomingRecord, -): TransactionAction[] { -  switch (pullDebitRecord.status) { -    case PeerPullDebitRecordStatus.DialogProposed: -      return []; -    case PeerPullDebitRecordStatus.PendingDeposit: -      return [TransactionAction.Abort, TransactionAction.Suspend]; -    case PeerPullDebitRecordStatus.DonePaid: -      return [TransactionAction.Delete]; -    case PeerPullDebitRecordStatus.SuspendedDeposit: -      return [TransactionAction.Resume, TransactionAction.Abort]; -    case PeerPullDebitRecordStatus.Aborted: -      return [TransactionAction.Delete]; -    case PeerPullDebitRecordStatus.AbortingRefresh: -      return [TransactionAction.Fail, TransactionAction.Suspend]; -    case PeerPullDebitRecordStatus.Failed: -      return [TransactionAction.Delete]; -    case PeerPullDebitRecordStatus.SuspendedAbortingRefresh: -      return [TransactionAction.Resume, TransactionAction.Fail]; -  } -} diff --git a/packages/taler-wallet-core/src/operations/testing.ts b/packages/taler-wallet-core/src/operations/testing.ts index ef5aa907d..238a5dc66 100644 --- a/packages/taler-wallet-core/src/operations/testing.ts +++ b/packages/taler-wallet-core/src/operations/testing.ts @@ -50,14 +50,10 @@ import { getBalances } from "./balance.js";  import { checkLogicInvariant } from "../util/invariants.js";  import { acceptWithdrawalFromUri } from "./withdraw.js";  import { updateExchangeFromUrl } from "./exchanges.js"; -import { -  confirmPeerPullDebit, -  confirmPeerPushCredit, -  initiatePeerPullPayment, -  initiatePeerPushDebit, -  preparePeerPullDebit, -  preparePeerPushCredit, -} from "./pay-peer.js"; +import { initiatePeerPullPayment } from "./pay-peer-pull-credit.js"; +import { preparePeerPullDebit, confirmPeerPullDebit } from "./pay-peer-pull-debit.js"; +import { preparePeerPushCredit, confirmPeerPushCredit } from "./pay-peer-push-credit.js"; +import { initiatePeerPushDebit } from "./pay-peer-push-debit.js";  const logger = new Logger("operations/testing.ts"); diff --git a/packages/taler-wallet-core/src/operations/transactions.ts b/packages/taler-wallet-core/src/operations/transactions.ts index a0da95799..1bd024d28 100644 --- a/packages/taler-wallet-core/src/operations/transactions.ts +++ b/packages/taler-wallet-core/src/operations/transactions.ts @@ -93,32 +93,6 @@ import {    computePayMerchantTransactionActions,  } from "./pay-merchant.js";  import { -  abortPeerPullCreditTransaction, -  abortPeerPullDebitTransaction, -  abortPeerPushCreditTransaction, -  abortPeerPushDebitTransaction, -  failPeerPullCreditTransaction, -  failPeerPullDebitTransaction, -  failPeerPushCreditTransaction, -  failPeerPushDebitTransaction, -  computePeerPullCreditTransactionState, -  computePeerPullDebitTransactionState, -  computePeerPushCreditTransactionState, -  computePeerPushDebitTransactionState, -  resumePeerPullCreditTransaction, -  resumePeerPullDebitTransaction, -  resumePeerPushCreditTransaction, -  resumePeerPushDebitTransaction, -  suspendPeerPullCreditTransaction, -  suspendPeerPullDebitTransaction, -  suspendPeerPushCreditTransaction, -  suspendPeerPushDebitTransaction, -  computePeerPushDebitTransactionActions, -  computePeerPullDebitTransactionActions, -  computePeerPullCreditTransactionActions, -  computePeerPushCreditTransactionActions, -} from "./pay-peer.js"; -import {    abortRefreshGroup,    failRefreshGroup,    computeRefreshTransactionState, @@ -143,6 +117,10 @@ 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";  const logger = new Logger("taler-wallet-core:transactions.ts"); diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts index df48c0e19..d0c34588b 100644 --- a/packages/taler-wallet-core/src/wallet.ts +++ b/packages/taler-wallet-core/src/wallet.ts @@ -63,8 +63,6 @@ import {    codecForAddKnownBankAccounts,    codecForAny,    codecForApplyDevExperiment, -  codecForApplyRefundFromPurchaseIdRequest, -  codecForApplyRefundRequest,    codecForCancelAbortingTransactionRequest,    codecForCheckPeerPullPaymentRequest,    codecForCheckPeerPushDebitRequest, @@ -196,22 +194,29 @@ import {    getContractTermsDetails,    preparePayForUri,    processPurchase, +  startQueryRefund,    startRefundQueryForUri,  } from "./operations/pay-merchant.js";  import {    checkPeerPullPaymentInitiation, -  checkPeerPushDebit, -  confirmPeerPullDebit, -  confirmPeerPushCredit,    initiatePeerPullPayment, -  initiatePeerPushDebit, +  processPeerPullCredit, +} from "./operations/pay-peer-pull-credit.js"; +import { +  confirmPeerPullDebit,    preparePeerPullDebit, +} from "./operations/pay-peer-pull-debit.js"; +import { +  confirmPeerPushCredit,    preparePeerPushCredit, -  processPeerPullCredit,    processPeerPullDebit,    processPeerPushCredit, +} from "./operations/pay-peer-push-credit.js"; +import { +  checkPeerPushDebit, +  initiatePeerPushDebit,    processPeerPushDebit, -} from "./operations/pay-peer.js"; +} from "./operations/pay-peer-push-debit.js";  import { getPendingOperations } from "./operations/pending.js";  import {    createRecoupGroup, @@ -232,8 +237,8 @@ import {  import { acceptTip, prepareTip, processTip } from "./operations/tip.js";  import {    abortTransaction, -  failTransaction,    deleteTransaction, +  failTransaction,    getTransactionById,    getTransactions,    parseTransactionIdentifier, @@ -280,7 +285,6 @@ import {    WalletCoreApiClient,    WalletCoreResponseType,  } from "./wallet-api-types.js"; -import { startQueryRefund } from "./operations/pay-merchant.js";  const logger = new Logger("wallet.ts");  | 
