gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[taler-wallet-core] branch master updated: fix and simplify coin selecti


From: gnunet
Subject: [taler-wallet-core] branch master updated: fix and simplify coin selection
Date: Wed, 25 Dec 2019 19:11:23 +0100

This is an automated email from the git hooks/post-receive script.

dold pushed a commit to branch master
in repository wallet-core.

The following commit(s) were added to refs/heads/master by this push:
     new adebfab9 fix and simplify coin selection
adebfab9 is described below

commit adebfab94e76ee5d34a4f22d15fc085daef9ae00
Author: Florian Dold <address@hidden>
AuthorDate: Wed Dec 25 19:11:20 2019 +0100

    fix and simplify coin selection
---
 src/crypto/workers/cryptoApi.ts            |  23 +-
 src/crypto/workers/cryptoImplementation.ts | 103 ++------
 src/headless/taler-wallet-cli.ts           |  10 +-
 src/operations/pay.ts                      | 386 ++++++++++++++++++-----------
 src/types/dbTypes.ts                       |   8 +-
 src/types/talerTypes.ts                    |   4 +-
 src/types/walletTypes.ts                   |  82 ++----
 src/util/amounts.ts                        |   6 +-
 src/wallet-test.ts                         | 112 ++++-----
 9 files changed, 364 insertions(+), 370 deletions(-)

diff --git a/src/crypto/workers/cryptoApi.ts b/src/crypto/workers/cryptoApi.ts
index 1c54d286..489d56f5 100644
--- a/src/crypto/workers/cryptoApi.ts
+++ b/src/crypto/workers/cryptoApi.ts
@@ -35,14 +35,13 @@ import {
 
 import { CryptoWorker } from "./cryptoWorker";
 
-import { ContractTerms, PaybackRequest } from "../../types/talerTypes";
+import { ContractTerms, PaybackRequest, CoinDepositPermission } from 
"../../types/talerTypes";
 
 import {
   BenchmarkResult,
-  CoinWithDenom,
-  PaySigInfo,
   PlanchetCreationResult,
   PlanchetCreationRequest,
+  DepositInfo,
 } from "../../types/walletTypes";
 
 import * as timer from "../../util/timer";
@@ -384,19 +383,13 @@ export class CryptoApi {
     );
   }
 
-  signDeposit(
-    contractTermsRaw: string,
-    contractData: WalletContractData,
-    cds: CoinWithDenom[],
-    totalAmount: AmountJson,
-  ): Promise<PaySigInfo> {
-    return this.doRpc<PaySigInfo>(
-      "signDeposit",
+  signDepositPermission(
+    depositInfo: DepositInfo
+  ): Promise<CoinDepositPermission> {
+    return this.doRpc<CoinDepositPermission>(
+      "signDepositPermission",
       3,
-      contractTermsRaw,
-      contractData,
-      cds,
-      totalAmount,
+      depositInfo
     );
   }
 
diff --git a/src/crypto/workers/cryptoImplementation.ts 
b/src/crypto/workers/cryptoImplementation.ts
index 04371186..d3295e74 100644
--- a/src/crypto/workers/cryptoImplementation.ts
+++ b/src/crypto/workers/cryptoImplementation.ts
@@ -36,14 +36,12 @@ import {
   WalletContractData,
 } from "../../types/dbTypes";
 
-import { CoinPaySig, ContractTerms, PaybackRequest } from 
"../../types/talerTypes";
+import { CoinDepositPermission, ContractTerms, PaybackRequest } from 
"../../types/talerTypes";
 import {
   BenchmarkResult,
-  CoinWithDenom,
-  PaySigInfo,
   PlanchetCreationResult,
   PlanchetCreationRequest,
-  CoinPayInfo,
+  DepositInfo,
 } from "../../types/walletTypes";
 import { canonicalJson } from "../../util/helpers";
 import { AmountJson } from "../../util/amounts";
@@ -331,82 +329,29 @@ export class CryptoImplementation {
    * Generate updated coins (to store in the database)
    * and deposit permissions for each given coin.
    */
-  signDeposit(
-    contractTermsRaw: string,
-    contractData: WalletContractData,
-    cds: CoinWithDenom[],
-    totalAmount: AmountJson,
-  ): PaySigInfo {
-    const ret: PaySigInfo = {
-      coinInfo: [],
-    };
-
-    const contractTermsHash = 
this.hashString(canonicalJson(JSON.parse(contractTermsRaw)));
-
-    const feeList: AmountJson[] = cds.map(x => x.denom.feeDeposit);
-    let fees = Amounts.add(Amounts.getZero(feeList[0].currency), ...feeList)
-      .amount;
-    // okay if saturates
-    fees = Amounts.sub(fees, contractData.maxDepositFee).amount;
-    const total = Amounts.add(fees, totalAmount).amount;
-
-    let amountSpent = Amounts.getZero(cds[0].coin.currentAmount.currency);
-    let amountRemaining = total;
-
-    for (const cd of cds) {
-      if (amountRemaining.value === 0 && amountRemaining.fraction === 0) {
-        break;
-      }
-
-      let coinSpend: AmountJson;
-      if (Amounts.cmp(amountRemaining, cd.coin.currentAmount) < 0) {
-        coinSpend = amountRemaining;
-      } else {
-        coinSpend = cd.coin.currentAmount;
-      }
-
-      amountSpent = Amounts.add(amountSpent, coinSpend).amount;
-
-      const feeDeposit = cd.denom.feeDeposit;
-
-      // Give the merchant at least the deposit fee, otherwise it'll reject
-      // the coin.
-
-      if (Amounts.cmp(coinSpend, feeDeposit) < 0) {
-        coinSpend = feeDeposit;
-      }
+  signDepositPermission(depositInfo: DepositInfo): CoinDepositPermission {
+
+    const d = buildSigPS(SignaturePurpose.WALLET_COIN_DEPOSIT)
+      .put(decodeCrock(depositInfo.contractTermsHash))
+      .put(decodeCrock(depositInfo.wireInfoHash))
+      .put(timestampToBuffer(depositInfo.timestamp))
+      .put(timestampToBuffer(depositInfo.refundDeadline))
+      .put(amountToBuffer(depositInfo.spendAmount))
+      .put(amountToBuffer(depositInfo.feeDeposit))
+      .put(decodeCrock(depositInfo.merchantPub))
+      .put(decodeCrock(depositInfo.coinPub))
+      .build();
+    const coinSig = eddsaSign(d, decodeCrock(depositInfo.coinPriv));
 
-      const newAmount = Amounts.sub(cd.coin.currentAmount, coinSpend).amount;
-      cd.coin.currentAmount = newAmount;
-
-      const d = buildSigPS(SignaturePurpose.WALLET_COIN_DEPOSIT)
-        .put(decodeCrock(contractTermsHash))
-        .put(decodeCrock(contractData.wireInfoHash))
-        .put(timestampToBuffer(contractData.timestamp))
-        .put(timestampToBuffer(contractData.refundDeadline))
-        .put(amountToBuffer(coinSpend))
-        .put(amountToBuffer(cd.denom.feeDeposit))
-        .put(decodeCrock(contractData.merchantPub))
-        .put(decodeCrock(cd.coin.coinPub))
-        .build();
-      const coinSig = eddsaSign(d, decodeCrock(cd.coin.coinPriv));
-
-      const s: CoinPaySig = {
-        coin_pub: cd.coin.coinPub,
-        coin_sig: encodeCrock(coinSig),
-        contribution: Amounts.toString(coinSpend),
-        denom_pub: cd.coin.denomPub,
-        exchange_url: cd.denom.exchangeBaseUrl,
-        ub_sig: cd.coin.denomSig,
-      };
-      const coinInfo: CoinPayInfo = {
-        sig: s,
-        coinPub: cd.coin.coinPub,
-        subtractedAmount: coinSpend,
-      };
-      ret.coinInfo.push(coinInfo);
-    }
-    return ret;
+    const s: CoinDepositPermission = {
+      coin_pub: depositInfo.coinPub,
+      coin_sig: encodeCrock(coinSig),
+      contribution: Amounts.toString(depositInfo.spendAmount),
+      denom_pub: depositInfo.denomPub,
+      exchange_url: depositInfo.exchangeBaseUrl,
+      ub_sig: depositInfo.denomSig,
+    };
+    return s;
   }
 
   /**
diff --git a/src/headless/taler-wallet-cli.ts b/src/headless/taler-wallet-cli.ts
index 491f6f55..aad49932 100644
--- a/src/headless/taler-wallet-cli.ts
+++ b/src/headless/taler-wallet-cli.ts
@@ -540,12 +540,18 @@ testCli
   .requiredOption("summary", ["-s", "--summary"], clk.STRING, {
     default: "Test Payment",
   })
+  .requiredOption("merchant", ["-m", "--merchant"], clk.STRING, {
+    default: "https://backend.test.taler.net/";,
+  })
+  .requiredOption("merchantApiKey", ["-k", "--merchant-api-key"], clk.STRING, {
+    default: "sandbox",
+  })
   .action(async args => {
     const cmdArgs = args.genPayUri;
     console.log("creating order");
     const merchantBackend = new MerchantBackendConnection(
-      "https://backend.test.taler.net/";,
-      "sandbox",
+      cmdArgs.merchant,
+      cmdArgs.merchantApiKey,
     );
     const orderResp = await merchantBackend.createOrder(
       cmdArgs.amount,
diff --git a/src/operations/pay.ts b/src/operations/pay.ts
index c7920020..8fed54aa 100644
--- a/src/operations/pay.ts
+++ b/src/operations/pay.ts
@@ -26,9 +26,7 @@
  */
 import { encodeCrock, getRandomBytes } from "../crypto/talerCrypto";
 import {
-  CoinRecord,
   CoinStatus,
-  DenominationRecord,
   initRetryInfo,
   ProposalRecord,
   ProposalStatus,
@@ -41,153 +39,213 @@ import {
 } from "../types/dbTypes";
 import { NotificationType } from "../types/notifications";
 import {
-  Auditor,
-  ContractTerms,
-  ExchangeHandle,
-  MerchantRefundResponse,
   PayReq,
-  Proposal,
   codecForMerchantRefundResponse,
   codecForProposal,
   codecForContractTerms,
+  CoinDepositPermission,
 } from "../types/talerTypes";
 import {
-  CoinSelectionResult,
-  CoinWithDenom,
   ConfirmPayResult,
   OperationError,
-  PaySigInfo,
   PreparePayResult,
   RefreshReason,
 } from "../types/walletTypes";
 import * as Amounts from "../util/amounts";
 import { AmountJson } from "../util/amounts";
-import { amountToPretty, canonicalJson, strcmp } from "../util/helpers";
 import { Logger } from "../util/logging";
 import { getOrderDownloadUrl, parsePayUri } from "../util/taleruri";
 import { guardOperationException } from "./errors";
 import { createRefreshGroup, getTotalRefreshCost } from "./refresh";
 import { acceptRefundResponse } from "./refund";
 import { InternalWalletState } from "./state";
-import { Timestamp, getTimestampNow, timestampAddDuration } from 
"../util/time";
+import { getTimestampNow, timestampAddDuration } from "../util/time";
+import { strcmp, canonicalJson } from "../util/helpers";
 
-interface CoinsForPaymentArgs {
-  allowedAuditors: Auditor[];
-  allowedExchanges: ExchangeHandle[];
-  depositFeeLimit: AmountJson;
+/**
+ * Result of selecting coins, contains the exchange, and selected
+ * coins with their denomination.
+ */
+export interface PayCoinSelection {
+  /**
+   * Amount requested by the merchant.
+   */
   paymentAmount: AmountJson;
-  wireFeeAmortization: number;
-  wireFeeLimit: AmountJson;
-  wireFeeTime: Timestamp;
-  wireMethod: string;
+
+  /**
+   * Public keys of the coins that were selected.
+   */
+  coinPubs: string[];
+
+  /**
+   * Amount that each coin contributes.
+   */
+  coinContributions: AmountJson[];
+
+  /**
+   * How much of the wire fees is the customer paying?
+   */
+  customerWireFees: AmountJson;
+
+  /**
+   * How much of the deposit fees is the customer paying?
+   */
+  customerDepositFees: AmountJson;
 }
 
-interface SelectPayCoinsResult {
-  cds: CoinWithDenom[];
-  totalFees: AmountJson;
+export interface AvailableCoinInfo {
+  coinPub: string;
+  denomPub: string;
+  availableAmount: AmountJson;
+  feeDeposit: AmountJson;
 }
 
 const logger = new Logger("pay.ts");
 
 /**
- * Select coins for a payment under the merchant's constraints.
+ * Compute the total cost of a payment to the customer.
+ */
+export async function getTotalPaymentCost(
+  ws: InternalWalletState,
+  pcs: PayCoinSelection,
+): Promise<AmountJson> {
+  const costs = [
+    pcs.paymentAmount,
+    pcs.customerDepositFees,
+    pcs.customerWireFees,
+  ];
+  for (let i = 0; i < pcs.coinPubs.length; i++) {
+    const coin = await ws.db.get(Stores.coins, pcs.coinPubs[i]);
+    if (!coin) {
+      throw Error("can't calculate payment cost, coin not found");
+    }
+    const denom = await ws.db.get(Stores.denominations, [
+      coin.exchangeBaseUrl,
+      coin.denomPub,
+    ]);
+    if (!denom) {
+      throw Error(
+        "can't calculate payment cost, denomination for coin not found",
+      );
+    }
+    const allDenoms = await ws.db
+      .iterIndex(
+        Stores.denominations.exchangeBaseUrlIndex,
+        coin.exchangeBaseUrl,
+      )
+      .toArray();
+    const amountLeft = Amounts.sub(denom.value, pcs.coinContributions[i])
+      .amount;
+    const refreshCost = getTotalRefreshCost(allDenoms, denom, amountLeft);
+    costs.push(refreshCost);
+  }
+  return Amounts.sum(costs).amount;
+}
+
+/**
+ * Given a list of available coins, select coins to spend under the merchant's
+ * constraints.
+ *
+ * This function is only exported for the sake of unit tests.
  *
  * @param denoms all available denoms, used to compute refresh fees
  */
 export function selectPayCoins(
-  denoms: DenominationRecord[],
-  cds: CoinWithDenom[],
+  acis: AvailableCoinInfo[],
   paymentAmount: AmountJson,
   depositFeeLimit: AmountJson,
-): SelectPayCoinsResult | undefined {
-  if (cds.length === 0) {
+): PayCoinSelection | undefined {
+  if (acis.length === 0) {
     return undefined;
   }
+  const coinPubs: string[] = [];
+  const coinContributions: AmountJson[] = [];
   // Sort by ascending deposit fee and denomPub if deposit fee is the same
   // (to guarantee deterministic results)
-  cds.sort(
+  acis.sort(
     (o1, o2) =>
-      Amounts.cmp(o1.denom.feeDeposit, o2.denom.feeDeposit) ||
-      strcmp(o1.denom.denomPub, o2.denom.denomPub),
+      Amounts.cmp(o1.feeDeposit, o2.feeDeposit) ||
+      strcmp(o1.denomPub, o2.denomPub),
   );
-  const currency = cds[0].denom.value.currency;
-  const cdsResult: CoinWithDenom[] = [];
-  let accDepositFee: AmountJson = Amounts.getZero(currency);
-  let accAmount: AmountJson = Amounts.getZero(currency);
-  for (const { coin, denom } of cds) {
-    if (coin.suspended) {
+  const currency = paymentAmount.currency;
+  let totalFees = Amounts.getZero(currency);
+  let amountPayRemaining = paymentAmount;
+  let amountDepositFeeLimitRemaining = depositFeeLimit;
+  let customerWireFees = Amounts.getZero(currency);
+  let customerDepositFees = Amounts.getZero(currency);
+  for (const aci of acis) {
+    // Don't use this coin if depositing it is more expensive than
+    // the amount it would give the merchant.
+    if (Amounts.cmp(aci.feeDeposit, aci.availableAmount) >= 0) {
       continue;
     }
-    if (coin.status !== CoinStatus.Fresh) {
-      continue;
+    if (amountPayRemaining.value === 0 && amountPayRemaining.fraction === 0) {
+      // We have spent enough!
+      break;
     }
-    if (Amounts.cmp(denom.feeDeposit, coin.currentAmount) >= 0) {
-      continue;
+
+    // How much does the user spend on deposit fees for this coin?
+    const depositFeeSpend = Amounts.sub(
+      aci.feeDeposit,
+      amountDepositFeeLimitRemaining,
+    ).amount;
+
+    if (Amounts.isZero(depositFeeSpend)) {
+      // Fees are still covered by the merchant.
+      amountDepositFeeLimitRemaining = Amounts.sub(
+        amountDepositFeeLimitRemaining,
+        aci.feeDeposit,
+      ).amount;
+    } else {
+      amountDepositFeeLimitRemaining = Amounts.getZero(currency);
     }
-    cdsResult.push({ coin, denom });
-    accDepositFee = Amounts.add(denom.feeDeposit, accDepositFee).amount;
-    let leftAmount = Amounts.sub(
-      coin.currentAmount,
-      Amounts.sub(paymentAmount, accAmount).amount,
+
+    let coinSpend: AmountJson;
+    const amountActualAvailable = Amounts.sub(
+      aci.availableAmount,
+      depositFeeSpend,
     ).amount;
-    accAmount = Amounts.add(coin.currentAmount, accAmount).amount;
-    const coversAmount = Amounts.cmp(accAmount, paymentAmount) >= 0;
-    const coversAmountWithFee =
-      Amounts.cmp(
-        accAmount,
-        Amounts.add(paymentAmount, denom.feeDeposit).amount,
-      ) >= 0;
-    const isBelowFee = Amounts.cmp(accDepositFee, depositFeeLimit) <= 0;
-
-    logger.trace("candidate coin selection", {
-      coversAmount,
-      isBelowFee,
-      accDepositFee,
-      accAmount,
-      paymentAmount,
-    });
 
-    if ((coversAmount && isBelowFee) || coversAmountWithFee) {
-      const depositFeeToCover = Amounts.sub(accDepositFee, depositFeeLimit)
+    if (Amounts.cmp(amountActualAvailable, amountPayRemaining) > 0) {
+      // Partial spending
+      coinSpend = Amounts.add(amountPayRemaining, depositFeeSpend).amount;
+      amountPayRemaining = Amounts.getZero(currency);
+    } else {
+      // Spend the full remaining amount
+      coinSpend = aci.availableAmount;
+      amountPayRemaining = Amounts.add(amountPayRemaining, depositFeeSpend)
+        .amount;
+      amountPayRemaining = Amounts.sub(amountPayRemaining, aci.availableAmount)
         .amount;
-      leftAmount = Amounts.sub(leftAmount, depositFeeToCover).amount;
-      logger.trace("deposit fee to cover", amountToPretty(depositFeeToCover));
-      let totalFees: AmountJson = Amounts.getZero(currency);
-      if (coversAmountWithFee && !isBelowFee) {
-        // these are the fees the customer has to pay
-        // because the merchant doesn't cover them
-        totalFees = Amounts.sub(depositFeeLimit, accDepositFee).amount;
-      }
-      totalFees = Amounts.add(
-        totalFees,
-        getTotalRefreshCost(denoms, denom, leftAmount),
-      ).amount;
-      return { cds: cdsResult, totalFees };
     }
+
+    coinPubs.push(aci.coinPub);
+    coinContributions.push(coinSpend);
+    totalFees = Amounts.add(totalFees, depositFeeSpend).amount;
+  }
+  if (Amounts.isZero(amountPayRemaining)) {
+    return {
+      paymentAmount,
+      coinContributions,
+      coinPubs,
+      customerDepositFees,
+      customerWireFees,
+    };
   }
   return undefined;
 }
 
 /**
- * Get exchanges and associated coins that are still spendable, but only
- * if the sum the coins' remaining value covers the payment amount and fees.
+ * Select coins from the wallet's database that can be used
+ * to pay for the given contract.
+ *
+ * If payment is impossible, undefined is returned.
  */
 async function getCoinsForPayment(
   ws: InternalWalletState,
-  args: WalletContractData,
-): Promise<CoinSelectionResult | undefined> {
-  const {
-    allowedAuditors,
-    allowedExchanges,
-    maxDepositFee,
-    amount,
-    wireFeeAmortization,
-    maxWireFee,
-    timestamp,
-    wireMethod,
-  } = args;
-
-  let remainingAmount = amount;
+  contractData: WalletContractData,
+): Promise<PayCoinSelection | undefined> {
+  let remainingAmount = contractData.amount;
 
   const exchanges = await ws.db.iter(Stores.exchanges).toArray();
 
@@ -203,7 +261,7 @@ async function getCoinsForPayment(
     }
 
     // is the exchange explicitly allowed?
-    for (const allowedExchange of allowedExchanges) {
+    for (const allowedExchange of contractData.allowedExchanges) {
       if (allowedExchange.exchangePub === exchangeDetails.masterPublicKey) {
         isOkay = true;
         break;
@@ -212,7 +270,7 @@ async function getCoinsForPayment(
 
     // is the exchange allowed because of one of its auditors?
     if (!isOkay) {
-      for (const allowedAuditor of allowedAuditors) {
+      for (const allowedAuditor of contractData.allowedAuditors) {
         for (const auditor of exchangeDetails.auditors) {
           if (auditor.auditor_pub === allowedAuditor.auditorPub) {
             isOkay = true;
@@ -251,7 +309,7 @@ async function getCoinsForPayment(
       throw Error("db inconsistent");
     }
     const currency = firstDenom.value.currency;
-    const cds: CoinWithDenom[] = [];
+    const acis: AvailableCoinInfo[] = [];
     for (const coin of coins) {
       const denom = await ws.db.get(Stores.denominations, [
         exchange.baseUrl,
@@ -272,36 +330,45 @@ async function getCoinsForPayment(
       if (coin.status !== CoinStatus.Fresh) {
         continue;
       }
-      cds.push({ coin, denom });
+      acis.push({
+        availableAmount: coin.currentAmount,
+        coinPub: coin.coinPub,
+        denomPub: coin.denomPub,
+        feeDeposit: denom.feeDeposit,
+      });
     }
 
     let totalFees = Amounts.getZero(currency);
     let wireFee: AmountJson | undefined;
-    for (const fee of exchangeFees.feesForType[wireMethod] || []) {
-      if (fee.startStamp <= timestamp && fee.endStamp >= timestamp) {
+    for (const fee of exchangeFees.feesForType[contractData.wireMethod] || []) 
{
+      if (
+        fee.startStamp <= contractData.timestamp &&
+        fee.endStamp >= contractData.timestamp
+      ) {
         wireFee = fee.wireFee;
         break;
       }
     }
 
     if (wireFee) {
-      const amortizedWireFee = Amounts.divide(wireFee, wireFeeAmortization);
-      if (Amounts.cmp(maxWireFee, amortizedWireFee) < 0) {
+      const amortizedWireFee = Amounts.divide(
+        wireFee,
+        contractData.wireFeeAmortization,
+      );
+      if (Amounts.cmp(contractData.maxWireFee, amortizedWireFee) < 0) {
         totalFees = Amounts.add(amortizedWireFee, totalFees).amount;
         remainingAmount = Amounts.add(amortizedWireFee, 
remainingAmount).amount;
       }
     }
 
-    const res = selectPayCoins(denoms, cds, remainingAmount, maxDepositFee);
-
+    // Try if paying using this exchange works
+    const res = selectPayCoins(
+      acis,
+      remainingAmount,
+      contractData.maxDepositFee,
+    );
     if (res) {
-      totalFees = Amounts.add(totalFees, res.totalFees).amount;
-      return {
-        cds: res.cds,
-        exchangeUrl: exchange.baseUrl,
-        totalAmount: remainingAmount,
-        totalFees,
-      };
+      return res;
     }
   }
   return undefined;
@@ -314,7 +381,8 @@ async function getCoinsForPayment(
 async function recordConfirmPay(
   ws: InternalWalletState,
   proposal: ProposalRecord,
-  payCoinInfo: PaySigInfo,
+  coinSelection: PayCoinSelection,
+  coinDepositPermissions: CoinDepositPermission[],
   sessionIdOverride: string | undefined,
 ): Promise<PurchaseRecord> {
   const d = proposal.download;
@@ -329,7 +397,7 @@ async function recordConfirmPay(
   }
   logger.trace(`recording payment with session ID ${sessionId}`);
   const payReq: PayReq = {
-    coins: payCoinInfo.coinInfo.map(x => x.sig),
+    coins: coinDepositPermissions,
     merchant_pub: d.contractData.merchantPub,
     mode: "pay",
     order_id: d.contractData.orderId,
@@ -373,15 +441,15 @@ async function recordConfirmPay(
         await tx.put(Stores.proposals, p);
       }
       await tx.put(Stores.purchases, t);
-      for (let coinInfo of payCoinInfo.coinInfo) {
-        const coin = await tx.get(Stores.coins, coinInfo.coinPub);
+      for (let i = 0; i < coinSelection.coinPubs.length; i++) {
+        const coin = await tx.get(Stores.coins, coinSelection.coinPubs[i]);
         if (!coin) {
           throw Error("coin allocated for payment doesn't exist anymore");
         }
         coin.status = CoinStatus.Dormant;
         const remaining = Amounts.sub(
           coin.currentAmount,
-          coinInfo.subtractedAmount,
+          coinSelection.coinContributions[i],
         );
         if (remaining.saturated) {
           throw Error("not enough remaining balance on coin for payment");
@@ -389,9 +457,7 @@ async function recordConfirmPay(
         coin.currentAmount = remaining.amount;
         await tx.put(Stores.coins, coin);
       }
-      const refreshCoinPubs = payCoinInfo.coinInfo.map(x => ({
-        coinPub: x.coinPub,
-      }));
+      const refreshCoinPubs = coinSelection.coinPubs.map(x => ({ coinPub: x 
}));
       await createRefreshGroup(tx, refreshCoinPubs, RefreshReason.Pay);
     },
   );
@@ -738,6 +804,7 @@ export async function submitPay(
   const payUrl = new URL("pay", purchase.contractData.merchantBaseUrl).href;
 
   try {
+    console.log("pay req", payReq);
     resp = await ws.http.postJson(payUrl, payReq);
   } catch (e) {
     // Gives the user the option to retry / abort and refresh
@@ -745,6 +812,7 @@ export async function submitPay(
     throw e;
   }
   if (resp.status !== 200) {
+    console.log(await resp.json());
     throw Error(`unexpected status (${resp.status}) for /pay`);
   }
   const merchantResp = await resp.json();
@@ -872,11 +940,14 @@ export async function preparePayForUri(
       };
     }
 
+    const totalCost = await getTotalPaymentCost(ws, res);
+    const totalFees = Amounts.sub(totalCost, res.paymentAmount).amount;
+
     return {
       status: "payment-possible",
       contractTermsRaw: d.contractTermsRaw,
       proposalId: proposal.proposalId,
-      totalFees: res.totalFees,
+      totalFees,
     };
   }
 
@@ -957,17 +1028,42 @@ export async function confirmPay(
     throw Error("insufficient balance");
   }
 
-  const { cds, totalAmount } = res;
-  const payCoinInfo = await ws.cryptoApi.signDeposit(
-    d.contractTermsRaw,
-    d.contractData,
-    cds,
-    totalAmount,
-  );
+  const depositPermissions: CoinDepositPermission[] = [];
+  for (let i = 0; i < res.coinPubs.length; i++) {
+    const coin = await ws.db.get(Stores.coins, res.coinPubs[i]);
+    if (!coin) {
+      throw Error("can't pay, allocated coin not found anymore");
+    }
+    const denom = await ws.db.get(Stores.denominations, [
+      coin.exchangeBaseUrl,
+      coin.denomPub,
+    ]);
+    if (!denom) {
+      throw Error(
+        "can't pay, denomination of allocated coin not found anymore",
+      );
+    }
+    const dp = await ws.cryptoApi.signDepositPermission({
+      coinPriv: coin.coinPriv,
+      coinPub: coin.coinPub,
+      contractTermsHash: d.contractData.contractTermsHash,
+      denomPub: coin.denomPub,
+      denomSig: coin.denomSig,
+      exchangeBaseUrl: coin.exchangeBaseUrl,
+      feeDeposit: denom.feeDeposit,
+      merchantPub: d.contractData.merchantPub,
+      refundDeadline: d.contractData.refundDeadline,
+      spendAmount: res.coinContributions[i],
+      timestamp: d.contractData.timestamp,
+      wireInfoHash: d.contractData.wireInfoHash,
+    });
+    depositPermissions.push(dp);
+  }
   purchase = await recordConfirmPay(
     ws,
     proposal,
-    payCoinInfo,
+    res,
+    depositPermissions,
     sessionIdOverride,
   );
 
@@ -1019,23 +1115,29 @@ async function processPurchasePayImpl(
   await submitPay(ws, proposalId);
 }
 
-export async function refuseProposal(ws: InternalWalletState, proposalId: 
string) {
-  const success = await ws.db.runWithWriteTransaction([Stores.proposals], 
async (tx) => {
-    const proposal = await tx.get(Stores.proposals, proposalId);  
-    if (!proposal) {
-      logger.trace(`proposal ${proposalId} not found, won't refuse proposal`);
-      return false ;
-    }
-    if (proposal.proposalStatus !== ProposalStatus.PROPOSED) {
-      return false;
-    }
-    proposal.proposalStatus = ProposalStatus.REFUSED;
-    await tx.put(Stores.proposals, proposal);
-    return true;
-  });
+export async function refuseProposal(
+  ws: InternalWalletState,
+  proposalId: string,
+) {
+  const success = await ws.db.runWithWriteTransaction(
+    [Stores.proposals],
+    async tx => {
+      const proposal = await tx.get(Stores.proposals, proposalId);
+      if (!proposal) {
+        logger.trace(`proposal ${proposalId} not found, won't refuse 
proposal`);
+        return false;
+      }
+      if (proposal.proposalStatus !== ProposalStatus.PROPOSED) {
+        return false;
+      }
+      proposal.proposalStatus = ProposalStatus.REFUSED;
+      await tx.put(Stores.proposals, proposal);
+      return true;
+    },
+  );
   if (success) {
     ws.notify({
       type: NotificationType.Wildcard,
     });
   }
-}
\ No newline at end of file
+}
diff --git a/src/types/dbTypes.ts b/src/types/dbTypes.ts
index 71fe99b6..b8eca2dd 100644
--- a/src/types/dbTypes.ts
+++ b/src/types/dbTypes.ts
@@ -26,7 +26,7 @@
 import { AmountJson } from "../util/amounts";
 import {
   Auditor,
-  CoinPaySig,
+  CoinDepositPermission,
   ContractTerms,
   Denomination,
   MerchantRefundPermission,
@@ -1085,6 +1085,10 @@ export interface AllowedExchangeInfo {
   exchangePub: string;
 }
 
+/**
+ * Data extracted from the contract terms that is relevant for payment
+ * processing in the wallet.
+ */
 export interface WalletContractData {
   fulfillmentUrl: string;
   contractTermsHash: string;
@@ -1230,7 +1234,7 @@ export interface ConfigRecord {
  * Coin that we're depositing ourselves.
  */
 export interface DepositCoin {
-  coinPaySig: CoinPaySig;
+  coinPaySig: CoinDepositPermission;
 
   /**
    * Undefined if coin not deposited, otherwise signature
diff --git a/src/types/talerTypes.ts b/src/types/talerTypes.ts
index f8e2b1c6..f8e44900 100644
--- a/src/types/talerTypes.ts
+++ b/src/types/talerTypes.ts
@@ -211,7 +211,7 @@ export class RecoupConfirmation {
 /**
  * Deposit permission for a single coin.
  */
-export interface CoinPaySig {
+export interface CoinDepositPermission {
   /**
    * Signature by the coin.
    */
@@ -401,7 +401,7 @@ export interface PayReq {
   /**
    * Coins with signature.
    */
-  coins: CoinPaySig[];
+  coins: CoinDepositPermission[];
 
   /**
    * The merchant public key, used to uniquely
diff --git a/src/types/walletTypes.ts b/src/types/walletTypes.ts
index 223ca432..9887474c 100644
--- a/src/types/walletTypes.ts
+++ b/src/types/walletTypes.ts
@@ -33,9 +33,14 @@ import {
   ExchangeRecord,
   ExchangeWireInfo,
 } from "./dbTypes";
-import { CoinPaySig, ContractTerms } from "./talerTypes";
+import { CoinDepositPermission, ContractTerms } from "./talerTypes";
 import { Timestamp } from "../util/time";
-import { typecheckedCodec, makeCodecForObject, codecForString, 
makeCodecOptional } from "../util/codec";
+import {
+  typecheckedCodec,
+  makeCodecForObject,
+  codecForString,
+  makeCodecOptional,
+} from "../util/codec";
 
 /**
  * Response for the create reserve request to the wallet.
@@ -187,32 +192,6 @@ export interface WalletBalanceEntry {
   pendingIncomingDirty: AmountJson;
 }
 
-export interface CoinPayInfo {
-  /**
-   * Amount that will be subtracted from the coin when the payment is 
finalized.
-   */
-  subtractedAmount: AmountJson;
-
-  /**
-   * Public key of the coin that is being spent.
-   */
-  coinPub: string;
-
-  /**
-   * Signature together with the other information needed by the merchant,
-   * directly in the format expected by the merchant.
-   */
-  sig: CoinPaySig;
-}
-
-/**
- * Coins used for a payment, with signatures authorizing the payment and the
- * coins with remaining value updated to accomodate for a payment.
- */
-export interface PaySigInfo {
-  coinInfo: CoinPayInfo[];
-}
-
 /**
  * For terseness.
  */
@@ -302,7 +281,6 @@ export interface ConfirmReserveRequest {
   reservePub: string;
 }
 
-
 export const codecForConfirmReserveRequest = () =>
   typecheckedCodec<ConfirmReserveRequest>(
     makeCodecForObject<ConfirmReserveRequest>()
@@ -337,34 +315,6 @@ export class ReturnCoinsRequest {
   static checked: (obj: any) => ReturnCoinsRequest;
 }
 
-/**
- * Result of selecting coins, contains the exchange, and selected
- * coins with their denomination.
- */
-export interface CoinSelectionResult {
-  exchangeUrl: string;
-  cds: CoinWithDenom[];
-  totalFees: AmountJson;
-  /**
-   * Total amount, including wire fees payed by the customer.
-   */
-  totalAmount: AmountJson;
-}
-
-/**
- * Named tuple of coin and denomination.
- */
-export interface CoinWithDenom {
-  /**
-   * A coin.  Must have the same denomination public key as the associated
-   * denomination.
-   */
-  coin: CoinRecord;
-  /**
-   * An associated denomination.
-   */
-  denom: DenominationRecord;
-}
 
 /**
  * Status of processing a tip.
@@ -511,3 +461,21 @@ export interface CoinPublicKey {
 export interface RefreshGroupId {
   readonly refreshGroupId: string;
 }
+
+/**
+ * Private data required to make a deposit permission.
+ */
+export interface DepositInfo {
+  exchangeBaseUrl: string;
+  contractTermsHash: string;
+  coinPub: string;
+  coinPriv: string;
+  spendAmount: AmountJson;
+  timestamp: Timestamp;
+  refundDeadline: Timestamp;
+  merchantPub: string;
+  feeDeposit: AmountJson;
+  wireInfoHash: string;
+  denomPub: string;
+  denomSig: string;
+}
diff --git a/src/util/amounts.ts b/src/util/amounts.ts
index c85c4839..8deeaecc 100644
--- a/src/util/amounts.ts
+++ b/src/util/amounts.ts
@@ -184,7 +184,7 @@ export function sub(a: AmountJson, ...rest: AmountJson[]): 
Result {
  * Compare two amounts.  Returns 0 when equal, -1 when a < b
  * and +1 when a > b.  Throws when currencies don't match.
  */
-export function cmp(a: AmountJson, b: AmountJson): number {
+export function cmp(a: AmountJson, b: AmountJson): -1 | 0 | 1 {
   if (a.currency !== b.currency) {
     throw Error(`Mismatched currency: ${a.currency} and ${b.currency}`);
   }
@@ -244,6 +244,10 @@ export function isNonZero(a: AmountJson): boolean {
   return a.value > 0 || a.fraction > 0;
 }
 
+export function isZero(a: AmountJson): boolean {
+  return a.value === 0 && a.fraction === 0;
+}
+
 /**
  * Parse an amount like 'EUR:20.5' for 20 Euros and 50 ct.
  */
diff --git a/src/wallet-test.ts b/src/wallet-test.ts
index c937de3f..a465db51 100644
--- a/src/wallet-test.ts
+++ b/src/wallet-test.ts
@@ -19,11 +19,9 @@ import test from "ava";
 import * as dbTypes from "./types/dbTypes";
 import * as types from "./types/walletTypes";
 
-import * as wallet from "./wallet";
-
 import { AmountJson } from "./util/amounts";
 import * as Amounts from "./util/amounts";
-import { selectPayCoins } from "./operations/pay";
+import { selectPayCoins, AvailableCoinInfo } from "./operations/pay";
 
 function a(x: string): AmountJson {
   const amt = Amounts.parse(x);
@@ -33,125 +31,99 @@ function a(x: string): AmountJson {
   return amt;
 }
 
-function fakeCwd(
+
+function fakeAci(
   current: string,
-  value: string,
   feeDeposit: string,
-): types.CoinWithDenom {
+): AvailableCoinInfo {
   return {
-    coin: {
-      blindingKey: "(mock)",
-      coinPriv: "(mock)",
-      coinPub: "(mock)",
-      currentAmount: a(current),
-      denomPub: "(mock)",
-      denomPubHash: "(mock)",
-      denomSig: "(mock)",
-      exchangeBaseUrl: "(mock)",
-      reservePub: "(mock)",
-      coinIndex: -1,
-      withdrawSessionId: "",
-      status: dbTypes.CoinStatus.Fresh,
-    },
-    denom: {
-      denomPub: "(mock)",
-      denomPubHash: "(mock)",
-      exchangeBaseUrl: "(mock)",
-      feeDeposit: a(feeDeposit),
-      feeRefresh: a("EUR:0.0"),
-      feeRefund: a("EUR:0.0"),
-      feeWithdraw: a("EUR:0.0"),
-      isOffered: true,
-      masterSig: "(mock)",
-      stampExpireDeposit: { t_ms: 0 },
-      stampExpireLegal: { t_ms: 0 },
-      stampExpireWithdraw: { t_ms: 0 },
-      stampStart: { t_ms: 0 },
-      status: dbTypes.DenominationStatus.VerifiedGood,
-      value: a(value),
-    },
-  };
+    availableAmount: a(current),
+    coinPub: "foobar",
+    denomPub: "foobar",
+    feeDeposit: a(feeDeposit),
+  }
+
 }
 
 test("coin selection 1", t => {
-  const cds: types.CoinWithDenom[] = [
-    fakeCwd("EUR:1.0", "EUR:1.0", "EUR:0.1"),
-    fakeCwd("EUR:1.0", "EUR:1.0", "EUR:0.0"),
+  const acis: AvailableCoinInfo[] = [
+    fakeAci("EUR:1.0", "EUR:0.1"),
+    fakeAci("EUR:1.0", "EUR:0.0"),
   ];
 
-  const res = selectPayCoins([], cds, a("EUR:2.0"), a("EUR:0.1"));
+  const res = selectPayCoins(acis, a("EUR:2.0"), a("EUR:0.1"));
   if (!res) {
     t.fail();
     return;
   }
-  t.true(res.cds.length === 2);
+  t.true(res.coinPubs.length === 2);
   t.pass();
 });
 
 test("coin selection 2", t => {
-  const cds: types.CoinWithDenom[] = [
-    fakeCwd("EUR:1.0", "EUR:1.0", "EUR:0.5"),
-    fakeCwd("EUR:1.0", "EUR:1.0", "EUR:0.0"),
+  const acis: AvailableCoinInfo[] = [
+    fakeAci("EUR:1.0", "EUR:0.5"),
+    fakeAci("EUR:1.0", "EUR:0.0"),
     // Merchant covers the fee, this one shouldn't be used
-    fakeCwd("EUR:1.0", "EUR:1.0", "EUR:0.0"),
+    fakeAci("EUR:1.0", "EUR:0.0"),
   ];
-  const res = selectPayCoins([], cds, a("EUR:2.0"), a("EUR:0.5"));
+  const res = selectPayCoins(acis, a("EUR:2.0"), a("EUR:0.5"));
   if (!res) {
     t.fail();
     return;
   }
-  t.true(res.cds.length === 2);
+  t.true(res.coinPubs.length === 2);
   t.pass();
 });
 
 test("coin selection 3", t => {
-  const cds: types.CoinWithDenom[] = [
-    fakeCwd("EUR:1.0", "EUR:1.0", "EUR:0.5"),
-    fakeCwd("EUR:1.0", "EUR:1.0", "EUR:0.5"),
+  const acis: AvailableCoinInfo[] = [
+    fakeAci("EUR:1.0", "EUR:0.5"),
+    fakeAci("EUR:1.0", "EUR:0.5"),
     // this coin should be selected instead of previous one with fee
-    fakeCwd("EUR:1.0", "EUR:1.0", "EUR:0.0"),
+    fakeAci("EUR:1.0", "EUR:0.0"),
   ];
-  const res = selectPayCoins([], cds, a("EUR:2.0"), a("EUR:0.5"));
+  const res = selectPayCoins(acis, a("EUR:2.0"), a("EUR:0.5"));
   if (!res) {
     t.fail();
     return;
   }
-  t.true(res.cds.length === 2);
+  t.true(res.coinPubs.length === 2);
   t.pass();
 });
 
 test("coin selection 4", t => {
-  const cds: types.CoinWithDenom[] = [
-    fakeCwd("EUR:1.0", "EUR:1.0", "EUR:0.5"),
-    fakeCwd("EUR:1.0", "EUR:1.0", "EUR:0.5"),
-    fakeCwd("EUR:1.0", "EUR:1.0", "EUR:0.5"),
+  const acis: AvailableCoinInfo[] = [
+    fakeAci("EUR:1.0", "EUR:0.5"),
+    fakeAci("EUR:1.0", "EUR:0.5"),
+    fakeAci("EUR:1.0", "EUR:0.5"),
   ];
-  const res = selectPayCoins([], cds, a("EUR:2.0"), a("EUR:0.2"));
+  const res = selectPayCoins(acis, a("EUR:2.0"), a("EUR:0.5"));
   if (!res) {
     t.fail();
     return;
   }
-  t.true(res.cds.length === 3);
+  t.true(res.coinPubs.length === 3);
   t.pass();
 });
 
 test("coin selection 5", t => {
-  const cds: types.CoinWithDenom[] = [
-    fakeCwd("EUR:1.0", "EUR:1.0", "EUR:0.5"),
-    fakeCwd("EUR:1.0", "EUR:1.0", "EUR:0.5"),
-    fakeCwd("EUR:1.0", "EUR:1.0", "EUR:0.5"),
+  const acis: AvailableCoinInfo[] = [
+    fakeAci("EUR:1.0", "EUR:0.5"),
+    fakeAci("EUR:1.0", "EUR:0.5"),
+    fakeAci("EUR:1.0", "EUR:0.5"),
   ];
-  const res = selectPayCoins([], cds, a("EUR:4.0"), a("EUR:0.2"));
+  const res = selectPayCoins(acis, a("EUR:4.0"), a("EUR:0.2"));
   t.true(!res);
   t.pass();
 });
 
 test("coin selection 6", t => {
-  const cds: types.CoinWithDenom[] = [
-    fakeCwd("EUR:1.0", "EUR:1.0", "EUR:0.5"),
-    fakeCwd("EUR:1.0", "EUR:1.0", "EUR:0.5"),
+  const acis: AvailableCoinInfo[] = [
+    fakeAci("EUR:1.0", "EUR:0.5"),
+    fakeAci("EUR:1.0", "EUR:0.5"),
   ];
-  const res = selectPayCoins([], cds, a("EUR:2.0"), a("EUR:0.2"));
+  const res = selectPayCoins(acis, a("EUR:2.0"), a("EUR:0.2"));
   t.true(!res);
   t.pass();
 });

-- 
To stop receiving notification emails like this one, please contact
address@hidden.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]