/*
 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 
 */
/**
 * Imports.
 */
import {
  AcceptPeerPullPaymentRequest,
  AcceptPeerPullPaymentResponse,
  AcceptPeerPushPaymentRequest,
  AcceptPeerPushPaymentResponse,
  AgeCommitmentProof,
  AmountJson,
  Amounts,
  AmountString,
  buildCodecForObject,
  CheckPeerPullPaymentRequest,
  CheckPeerPullPaymentResponse,
  CheckPeerPushPaymentRequest,
  CheckPeerPushPaymentResponse,
  Codec,
  codecForAmountString,
  codecForAny,
  codecForExchangeGetContractResponse,
  CoinStatus,
  constructPayPullUri,
  constructPayPushUri,
  ContractTermsUtil,
  decodeCrock,
  eddsaGetPublic,
  encodeCrock,
  ExchangePurseDeposits,
  ExchangePurseMergeRequest,
  ExchangeReservePurseRequest,
  getRandomBytes,
  InitiatePeerPullPaymentRequest,
  InitiatePeerPullPaymentResponse,
  InitiatePeerPushPaymentRequest,
  InitiatePeerPushPaymentResponse,
  j2s,
  Logger,
  parsePayPullUri,
  parsePayPushUri,
  PayPeerInsufficientBalanceDetails,
  PeerContractTerms,
  PreparePeerPullPaymentRequest,
  PreparePeerPullPaymentResponse,
  PreparePeerPushPaymentRequest,
  PreparePeerPushPaymentResponse,
  RefreshReason,
  strcmp,
  TalerErrorCode,
  TalerProtocolTimestamp,
  TransactionType,
  UnblindedSignature,
  WalletAccountMergeFlags,
} from "@gnu-taler/taler-util";
import {
  OperationStatus,
  PeerPullPaymentIncomingStatus,
  PeerPushPaymentIncomingRecord,
  PeerPushPaymentInitiationStatus,
  ReserveRecord,
  WalletStoresV1,
  WithdrawalGroupStatus,
  WithdrawalRecordType,
} from "../db.js";
import { TalerError } from "../errors.js";
import { InternalWalletState } from "../internal-wallet-state.js";
import { makeTransactionId, spendCoins } from "../operations/common.js";
import { readSuccessResponseJsonOrThrow } from "../util/http.js";
import { checkDbInvariant } from "../util/invariants.js";
import { GetReadOnlyAccess } from "../util/query.js";
import { getPeerPaymentBalanceDetailsInTx } from "./balance.js";
import { updateExchangeFromUrl } from "./exchanges.js";
import { internalCreateWithdrawalGroup } from "./withdraw.js";
const logger = new Logger("operations/peer-to-peer.ts");
export interface PeerCoinSelection {
  exchangeBaseUrl: string;
  /**
   * Info of Coins that were selected.
   */
  coins: {
    coinPub: string;
    coinPriv: string;
    contribution: AmountString;
    denomPubHash: string;
    denomSig: UnblindedSignature;
    ageCommitmentProof: AgeCommitmentProof | undefined;
  }[];
  /**
   * How much of the deposit fees is the customer paying?
   */
  depositFees: AmountJson;
}
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: PeerCoinSelection }
  | {
      type: "failure";
      insufficientBalanceDetails: PayPeerInsufficientBalanceDetails;
    };
export async function selectPeerCoins(
  ws: InternalWalletState,
  tx: GetReadOnlyAccess<{
    exchanges: typeof WalletStoresV1.exchanges;
    denominations: typeof WalletStoresV1.denominations;
    coins: typeof WalletStoresV1.coins;
    coinAvailability: typeof WalletStoresV1.coinAvailability;
    refreshGroups: typeof WalletStoresV1.refreshGroups;
  }>,
  instructedAmount: AmountJson,
): Promise {
  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: PeerCoinSelection = {
        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"] = {};
  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.zeroOfAmount(currency);
    }
    perExchange[exch.baseUrl] = {
      balanceAvailable: Amounts.stringify(infoExchange.balanceAvailable),
      balanceMaterial: Amounts.stringify(infoExchange.balanceMaterial),
      feeGapEstimate: Amounts.stringify(gap),
    };
  }
  const errDetails: PayPeerInsufficientBalanceDetails = {
    amountRequested: Amounts.stringify(instructedAmount),
    balanceAvailable: Amounts.stringify(infoGeneral.balanceAvailable),
    balanceMaterial: Amounts.stringify(infoGeneral.balanceMaterial),
    perExchange,
  };
  return { type: "failure", insufficientBalanceDetails: errDetails };
}
export async function preparePeerPushPayment(
  ws: InternalWalletState,
  req: PreparePeerPushPaymentRequest,
): Promise {
  // FIXME: look up for the exchange and calculate fee
  return {
    amountEffective: req.amount,
    amountRaw: req.amount,
  };
}
export async function initiatePeerToPeerPush(
  ws: InternalWalletState,
  req: InitiatePeerPushPaymentRequest,
): Promise {
  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 econtractResp = await ws.cryptoApi.encryptContractForMerge({
    contractTerms,
    mergePriv: mergePair.priv,
    pursePriv: pursePair.priv,
    pursePub: pursePair.pub,
  });
  const coinSelRes: SelectPeerCoinsResult = await ws.db
    .mktx((x) => [
      x.exchanges,
      x.contractTerms,
      x.coins,
      x.coinAvailability,
      x.denominations,
      x.refreshGroups,
      x.peerPullPaymentInitiations,
      x.peerPushPaymentInitiations,
    ])
    .runReadWrite(async (tx) => {
      const selRes = await selectPeerCoins(ws, tx, instructedAmount);
      if (selRes.type === "failure") {
        return selRes;
      }
      const sel = selRes.result;
      await spendCoins(ws, tx, {
        allocationId: `txn:peer-push-debit:${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: econtractResp.contractPriv,
        contractTermsHash: hContractTerms,
        exchangeBaseUrl: sel.exchangeBaseUrl,
        mergePriv: mergePair.priv,
        mergePub: mergePair.pub,
        purseExpiration: purseExpiration,
        pursePriv: pursePair.priv,
        pursePub: pursePair.pub,
        timestampCreated: TalerProtocolTimestamp.now(),
        // FIXME: Only set the later when the purse is actually created!
        status: PeerPushPaymentInitiationStatus.PurseCreated,
      });
      await tx.contractTerms.put({
        h: hContractTerms,
        contractTermsRaw: contractTerms,
      });
      return selRes;
    });
  logger.info(`selected p2p coins (push): ${j2s(coinSelRes)}`);
  if (coinSelRes.type !== "success") {
    throw TalerError.fromDetail(
      TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE,
      {
        insufficientBalanceDetails: coinSelRes.insufficientBalanceDetails,
      },
    );
  }
  const purseSigResp = await ws.cryptoApi.signPurseCreation({
    hContractTerms,
    mergePub: mergePair.pub,
    minAge: 0,
    purseAmount: Amounts.stringify(instructedAmount),
    purseExpiration,
    pursePriv: pursePair.priv,
  });
  const depositSigsResp = await ws.cryptoApi.signPurseDeposits({
    exchangeBaseUrl: coinSelRes.result.exchangeBaseUrl,
    pursePub: pursePair.pub,
    coins: coinSelRes.result.coins,
  });
  const createPurseUrl = new URL(
    `purses/${pursePair.pub}/create`,
    coinSelRes.result.exchangeBaseUrl,
  );
  const httpResp = await ws.http.postJson(createPurseUrl.href, {
    amount: Amounts.stringify(instructedAmount),
    merge_pub: mergePair.pub,
    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 !== 200) {
    throw Error("got error response from exchange");
  }
  return {
    contractPriv: econtractResp.contractPriv,
    mergePriv: mergePair.priv,
    pursePub: pursePair.pub,
    exchangeBaseUrl: coinSelRes.result.exchangeBaseUrl,
    talerUri: constructPayPushUri({
      exchangeBaseUrl: coinSelRes.result.exchangeBaseUrl,
      contractPriv: econtractResp.contractPriv,
    }),
    transactionId: makeTransactionId(
      TransactionType.PeerPushDebit,
      pursePair.pub,
    ),
  };
}
interface ExchangePurseStatus {
  balance: AmountString;
}
export const codecForExchangePurseStatus = (): Codec =>
  buildCodecForObject()
    .property("balance", codecForAmountString())
    .build("ExchangePurseStatus");
export async function checkPeerPushPayment(
  ws: InternalWalletState,
  req: CheckPeerPushPaymentRequest,
): Promise {
  // FIXME: Check if existing record exists!
  const uri = parsePayPushUri(req.talerUri);
  if (!uri) {
    throw Error("got invalid taler://pay-push URI");
  }
  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,
  );
  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: TalerProtocolTimestamp.now(),
        contractTermsHash,
        status: OperationStatus.Finished,
      });
      await tx.contractTerms.put({
        h: contractTermsHash,
        contractTermsRaw: dec.contractTerms,
      });
    });
  return {
    amount: purseStatus.balance,
    contractTerms: dec.contractTerms,
    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 {
  // 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 acceptPeerPushPayment(
  ws: InternalWalletState,
  req: AcceptPeerPushPaymentRequest,
): Promise {
  let peerInc: PeerPushPaymentIncomingRecord | undefined;
  let contractTerms: PeerContractTerms | undefined;
  await ws.db
    .mktx((x) => [x.contractTerms, x.peerPushPaymentIncoming])
    .runReadOnly(async (tx) => {
      peerInc = await tx.peerPushPaymentIncoming.get(
        req.peerPushPaymentIncomingId,
      );
      if (!peerInc) {
        return;
      }
      const ctRec = await tx.contractTerms.get(peerInc.contractTermsHash);
      if (ctRec) {
        contractTerms = ctRec.contractTermsRaw;
      }
    });
  if (!peerInc) {
    throw Error(
      `can't accept unknown incoming p2p push payment (${req.peerPushPaymentIncomingId})`,
    );
  }
  checkDbInvariant(!!contractTerms);
  await updateExchangeFromUrl(ws, peerInc.exchangeBaseUrl);
  const amount = Amounts.parseOrThrow(contractTerms.amount);
  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 mergeHttpReq = await ws.http.postJson(mergePurseUrl.href, mergeReq);
  logger.info(`merge request: ${j2s(mergeReq)}`);
  const res = await readSuccessResponseJsonOrThrow(mergeHttpReq, codecForAny());
  logger.info(`merge response: ${j2s(res)}`);
  const wg = await internalCreateWithdrawalGroup(ws, {
    amount,
    wgInfo: {
      withdrawalType: WithdrawalRecordType.PeerPushCredit,
      contractTerms,
    },
    exchangeBaseUrl: peerInc.exchangeBaseUrl,
    reserveStatus: WithdrawalGroupStatus.QueryingStatus,
    reserveKeyPair: {
      priv: mergeReserveInfo.reservePriv,
      pub: mergeReserveInfo.reservePub,
    },
  });
  return {
    transactionId: makeTransactionId(
      TransactionType.PeerPushCredit,
      wg.withdrawalGroupId,
    ),
  };
}
export async function acceptPeerPullPayment(
  ws: InternalWalletState,
  req: AcceptPeerPullPaymentRequest,
): Promise {
  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: SelectPeerCoinsResult = await ws.db
    .mktx((x) => [
      x.exchanges,
      x.coins,
      x.denominations,
      x.refreshGroups,
      x.peerPullPaymentIncoming,
      x.coinAvailability,
    ])
    .runReadWrite(async (tx) => {
      const selRes = await selectPeerCoins(ws, tx, instructedAmount);
      if (selRes.type !== "success") {
        return selRes;
      }
      const sel = selRes.result;
      await spendCoins(ws, tx, {
        allocationId: `txn:peer-pull-debit:${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();
      }
      pi.status = PeerPullPaymentIncomingStatus.Accepted;
      await tx.peerPullPaymentIncoming.put(pi);
      return selRes;
    });
  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 pursePub = peerPullInc.pursePub;
  const coinSel = coinSelRes.result;
  const depositSigsResp = await ws.cryptoApi.signPurseDeposits({
    exchangeBaseUrl: coinSel.exchangeBaseUrl,
    pursePub,
    coins: coinSel.coins,
  });
  const purseDepositUrl = new URL(
    `purses/${pursePub}/deposit`,
    coinSel.exchangeBaseUrl,
  );
  const depositPayload: ExchangePurseDeposits = {
    deposits: depositSigsResp.deposits,
  };
  const httpResp = await ws.http.postJson(purseDepositUrl.href, depositPayload);
  const resp = await readSuccessResponseJsonOrThrow(httpResp, codecForAny());
  logger.trace(`purse deposit response: ${j2s(resp)}`);
  return {
    transactionId: makeTransactionId(
      TransactionType.PeerPullDebit,
      req.peerPullPaymentIncomingId,
    ),
  };
}
export async function checkPeerPullPayment(
  ws: InternalWalletState,
  req: CheckPeerPullPaymentRequest,
): Promise {
  const uri = parsePayPullUri(req.talerUri);
  if (!uri) {
    throw Error("got invalid taler://pay-push URI");
  }
  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));
  await ws.db
    .mktx((x) => [x.peerPullPaymentIncoming])
    .runReadWrite(async (tx) => {
      await tx.peerPullPaymentIncoming.add({
        peerPullPaymentIncomingId,
        contractPriv: contractPriv,
        exchangeBaseUrl: exchangeBaseUrl,
        pursePub: pursePub,
        timestampCreated: TalerProtocolTimestamp.now(),
        contractTerms: dec.contractTerms,
        status: PeerPullPaymentIncomingStatus.Proposed,
      });
    });
  return {
    amount: purseStatus.balance,
    contractTerms: dec.contractTerms,
    peerPullPaymentIncomingId,
  };
}
export async function preparePeerPullPayment(
  ws: InternalWalletState,
  req: PreparePeerPullPaymentRequest,
): Promise {
  //FIXME: look up for exchange details and use purse fee
  return {
    amountEffective: req.amount,
    amountRaw: req.amount,
  };
}
/**
 * Initiate a peer pull payment.
 */
export async function initiatePeerPullPayment(
  ws: InternalWalletState,
  req: InitiatePeerPullPaymentRequest,
): Promise {
  await updateExchangeFromUrl(ws, req.exchangeBaseUrl);
  const mergeReserveInfo = await getMergeReserveInfo(ws, {
    exchangeBaseUrl: req.exchangeBaseUrl,
  });
  const mergeTimestamp = TalerProtocolTimestamp.now();
  const pursePair = await ws.cryptoApi.createEddsaKeypair({});
  const mergePair = await ws.cryptoApi.createEddsaKeypair({});
  const instructedAmount = Amounts.parseOrThrow(
    req.partialContractTerms.amount,
  );
  const purseExpiration = req.partialContractTerms.purse_expiration;
  const contractTerms = req.partialContractTerms;
  const reservePayto = talerPaytoFromExchangeReserve(
    req.exchangeBaseUrl,
    mergeReserveInfo.reservePub,
  );
  const econtractResp = await ws.cryptoApi.encryptContractForDeposit({
    contractTerms,
    pursePriv: pursePair.priv,
    pursePub: pursePair.pub,
  });
  const hContractTerms = ContractTermsUtil.hashContractTerms(contractTerms);
  const purseFee = Amounts.stringify(
    Amounts.zeroOfCurrency(instructedAmount.currency),
  );
  const sigRes = await ws.cryptoApi.signReservePurseCreate({
    contractTermsHash: hContractTerms,
    flags: WalletAccountMergeFlags.CreateWithPurseFee,
    mergePriv: mergePair.priv,
    mergeTimestamp: mergeTimestamp,
    purseAmount: req.partialContractTerms.amount,
    purseExpiration: purseExpiration,
    purseFee: purseFee,
    pursePriv: pursePair.priv,
    pursePub: pursePair.pub,
    reservePayto,
    reservePriv: mergeReserveInfo.reservePriv,
  });
  await ws.db
    .mktx((x) => [x.peerPullPaymentInitiations, x.contractTerms])
    .runReadWrite(async (tx) => {
      await tx.peerPullPaymentInitiations.put({
        amount: req.partialContractTerms.amount,
        contractTermsHash: hContractTerms,
        exchangeBaseUrl: req.exchangeBaseUrl,
        pursePriv: pursePair.priv,
        pursePub: pursePair.pub,
        status: OperationStatus.Finished,
      });
      await tx.contractTerms.put({
        contractTermsRaw: contractTerms,
        h: hContractTerms,
      });
    });
  const reservePurseReqBody: ExchangeReservePurseRequest = {
    merge_sig: sigRes.mergeSig,
    merge_timestamp: mergeTimestamp,
    h_contract_terms: hContractTerms,
    merge_pub: mergePair.pub,
    min_age: 0,
    purse_expiration: purseExpiration,
    purse_fee: purseFee,
    purse_pub: pursePair.pub,
    purse_sig: sigRes.purseSig,
    purse_value: req.partialContractTerms.amount,
    reserve_sig: sigRes.accountSig,
    econtract: econtractResp.econtract,
  };
  logger.info(`reserve purse request: ${j2s(reservePurseReqBody)}`);
  const reservePurseMergeUrl = new URL(
    `reserves/${mergeReserveInfo.reservePub}/purse`,
    req.exchangeBaseUrl,
  );
  const httpResp = await ws.http.postJson(
    reservePurseMergeUrl.href,
    reservePurseReqBody,
  );
  const resp = await readSuccessResponseJsonOrThrow(httpResp, codecForAny());
  logger.info(`reserve merge response: ${j2s(resp)}`);
  const wg = await internalCreateWithdrawalGroup(ws, {
    amount: instructedAmount,
    wgInfo: {
      withdrawalType: WithdrawalRecordType.PeerPullCredit,
      contractTerms,
      contractPriv: econtractResp.contractPriv,
    },
    exchangeBaseUrl: req.exchangeBaseUrl,
    reserveStatus: WithdrawalGroupStatus.QueryingStatus,
    reserveKeyPair: {
      priv: mergeReserveInfo.reservePriv,
      pub: mergeReserveInfo.reservePub,
    },
  });
  return {
    talerUri: constructPayPullUri({
      exchangeBaseUrl: req.exchangeBaseUrl,
      contractPriv: econtractResp.contractPriv,
    }),
    transactionId: makeTransactionId(
      TransactionType.PeerPullCredit,
      wg.withdrawalGroupId,
    ),
  };
}