gnunet-svn
[Top][All Lists]
Advanced

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

[taler-wallet-core] 01/03: pending operations (pay/proposals)


From: gnunet
Subject: [taler-wallet-core] 01/03: pending operations (pay/proposals)
Date: Tue, 03 Dec 2019 14:40:11 +0100

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

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

commit c33dd75711a39403bd4dd9940caab6d5e6ad2d77
Author: Florian Dold <address@hidden>
AuthorDate: Tue Dec 3 00:52:15 2019 +0100

    pending operations (pay/proposals)
---
 src/android/index.ts        |  12 +-
 src/dbTypes.ts              |  64 ++++++--
 src/util/query.ts           |  42 ++++--
 src/wallet-impl/history.ts  |   2 +
 src/wallet-impl/pay.ts      | 309 +++++++++++++++++++++-----------------
 src/wallet-impl/pending.ts  | 357 ++++++++++++++++++++++++--------------------
 src/wallet-impl/refund.ts   |   9 +-
 src/wallet-impl/reserves.ts |  10 +-
 src/wallet.ts               |   8 +-
 src/walletTypes.ts          |  26 +++-
 10 files changed, 494 insertions(+), 345 deletions(-)

diff --git a/src/android/index.ts b/src/android/index.ts
index 71144176..fb62a5b5 100644
--- a/src/android/index.ts
+++ b/src/android/index.ts
@@ -200,9 +200,11 @@ export function installAndroidWalletListener() {
         const wallet = await wp.promise;
         wallet.stop();
         wp = openPromise<Wallet>();
-        if (walletArgs && walletArgs.persistentStoragePath) {
+        const oldArgs = walletArgs;
+        walletArgs = { ...oldArgs };
+        if (oldArgs && oldArgs.persistentStoragePath) {
           try {
-            fs.unlinkSync(walletArgs.persistentStoragePath);
+            fs.unlinkSync(oldArgs.persistentStoragePath);
           } catch (e) {
             console.error("Error while deleting the wallet db:", e);
           }
@@ -210,6 +212,12 @@ export function installAndroidWalletListener() {
           walletArgs.persistentStoragePath = undefined;
         }
         maybeWallet = undefined;
+        const w = await getDefaultNodeWallet(walletArgs);
+        maybeWallet = w;
+        w.runLoopScheduledRetries().catch((e) => {
+          console.error("Error during wallet retry loop", e);
+        });
+        wp.resolve(w);
         break;
       }
       default:
diff --git a/src/dbTypes.ts b/src/dbTypes.ts
index 731f0358..4f374c26 100644
--- a/src/dbTypes.ts
+++ b/src/dbTypes.ts
@@ -586,22 +586,30 @@ export interface CoinRecord {
 }
 
 export enum ProposalStatus {
+  /**
+   * Not downloaded yet.
+   */
+  DOWNLOADING = "downloading",
+  /**
+   * Proposal downloaded, but the user needs to accept/reject it.
+   */
   PROPOSED = "proposed",
+  /**
+   * The user has accepted the proposal.
+   */
   ACCEPTED = "accepted",
+  /**
+   * The user has rejected the proposal.
+   */
   REJECTED = "rejected",
-}
-
-/**
- * Record for a downloaded order, stored in the wallet's database.
- */
-@Checkable.Class()
-export class ProposalRecord {
   /**
-   * URL where the proposal was downloaded.
+   * Downloaded proposal was detected as a re-purchase.
    */
-  @Checkable.String()
-  url: string;
+  REPURCHASE = "repurchase",
+}
 
+@Checkable.Class()
+export class ProposalDownload {
   /**
    * The contract that was offered by the merchant.
    */
@@ -615,10 +623,27 @@ export class ProposalRecord {
   merchantSig: string;
 
   /**
-   * Hash of the contract terms.
+   * Signature by the merchant over the contract details.
    */
   @Checkable.String()
   contractTermsHash: string;
+}
+
+/**
+ * Record for a downloaded order, stored in the wallet's database.
+ */
+@Checkable.Class()
+export class ProposalRecord {
+  /**
+   * URL where the proposal was downloaded.
+   */
+  @Checkable.String()
+  url: string;
+
+  /**
+   * Downloaded data from the merchant.
+   */
+  download: ProposalDownload | undefined;
 
   /**
    * Unique ID when the order is stored in the wallet DB.
@@ -639,9 +664,18 @@ export class ProposalRecord {
   @Checkable.String()
   noncePriv: string;
 
+  /**
+   * Public key for the nonce.
+   */
+  @Checkable.String()
+  noncePub: string;
+
   @Checkable.String()
   proposalStatus: ProposalStatus;
 
+  @Checkable.String()
+  repurchaseProposalId: string | undefined;
+
   /**
    * Session ID we got when downloading the contract.
    */
@@ -911,6 +945,12 @@ export interface PurchaseRecord {
    * The abort (with refund) was completed for this (incomplete!) purchase.
    */
   abortDone: boolean;
+
+  /**
+   * Proposal ID for this purchase.  Uniquely identifies the
+   * purchase and the proposal.
+   */
+  proposalId: string;
 }
 
 /**
@@ -1076,7 +1116,7 @@ export namespace Stores {
 
   class PurchasesStore extends Store<PurchaseRecord> {
     constructor() {
-      super("purchases", { keyPath: "contractTermsHash" });
+      super("purchases", { keyPath: "proposalId" });
     }
 
     fulfillmentUrlIndex = new Index<string, PurchaseRecord>(
diff --git a/src/util/query.ts b/src/util/query.ts
index 6942d471..b1b19665 100644
--- a/src/util/query.ts
+++ b/src/util/query.ts
@@ -25,7 +25,6 @@
  */
 import { openPromise } from "./promiseUtils";
 
-
 /**
  * Result of an inner join.
  */
@@ -67,7 +66,7 @@ export interface IndexOptions {
 }
 
 function requestToPromise(req: IDBRequest): Promise<any> {
-  const stack = Error("Failed request was started here.")
+  const stack = Error("Failed request was started here.");
   return new Promise((resolve, reject) => {
     req.onsuccess = () => {
       resolve(req.result);
@@ -103,7 +102,7 @@ export async function oneShotGet<T>(
 ): Promise<T | undefined> {
   const tx = db.transaction([store.name], "readonly");
   const req = tx.objectStore(store.name).get(key);
-  const v = await requestToPromise(req)
+  const v = await requestToPromise(req);
   await transactionToPromise(tx);
   return v;
 }
@@ -335,6 +334,17 @@ class TransactionHandle {
     return requestToPromise(req);
   }
 
+  getIndexed<S extends IDBValidKey, T>(
+    index: Index<S, T>,
+    key: any,
+  ): Promise<T | undefined> {
+    const req = this.tx
+      .objectStore(index.storeName)
+      .index(index.indexName)
+      .get(key);
+    return requestToPromise(req);
+  }
+
   iter<T>(store: Store<T>, key?: any): ResultStream<T> {
     const req = this.tx.objectStore(store.name).openCursor(key);
     return new ResultStream<T>(req);
@@ -407,18 +417,20 @@ function runWithTransaction<T>(
     };
     const th = new TransactionHandle(tx);
     const resP = f(th);
-    resP.then(result => {
-      gotFunResult = true;
-      funResult = result;
-    }).catch((e) => {
-      if (e == TransactionAbort) {
-        console.info("aborting transaction");
-      } else {
-        tx.abort();
-        console.error("Transaction failed:", e);
-        console.error(stack);
-      }
-    });
+    resP
+      .then(result => {
+        gotFunResult = true;
+        funResult = result;
+      })
+      .catch(e => {
+        if (e == TransactionAbort) {
+          console.info("aborting transaction");
+        } else {
+          tx.abort();
+          console.error("Transaction failed:", e);
+          console.error(stack);
+        }
+      });
   });
 }
 
diff --git a/src/wallet-impl/history.ts b/src/wallet-impl/history.ts
index 976dab88..f5a4e9d3 100644
--- a/src/wallet-impl/history.ts
+++ b/src/wallet-impl/history.ts
@@ -39,6 +39,7 @@ export async function getHistory(
   // This works as timestamps are guaranteed to be monotonically
   // increasing even
 
+  /*
   const proposals = await oneShotIter(ws.db, Stores.proposals).toArray();
   for (const p of proposals) {
     history.push({
@@ -51,6 +52,7 @@ export async function getHistory(
       explicit: false,
     });
   }
+  */
 
   const withdrawals = await oneShotIter(
     ws.db,
diff --git a/src/wallet-impl/pay.ts b/src/wallet-impl/pay.ts
index 31a1500e..69144d2d 100644
--- a/src/wallet-impl/pay.ts
+++ b/src/wallet-impl/pay.ts
@@ -55,6 +55,7 @@ import {
   strcmp,
   extractTalerStamp,
   canonicalJson,
+  extractTalerStampOrThrow,
 } from "../util/helpers";
 import { Logger } from "../util/logging";
 import { InternalWalletState } from "./state";
@@ -320,31 +321,41 @@ async function recordConfirmPay(
   payCoinInfo: PayCoinInfo,
   chosenExchange: string,
 ): Promise<PurchaseRecord> {
+  const d = proposal.download;
+  if (!d) {
+    throw Error("proposal is in invalid state");
+  }
   const payReq: PayReq = {
     coins: payCoinInfo.sigs,
-    merchant_pub: proposal.contractTerms.merchant_pub,
+    merchant_pub: d.contractTerms.merchant_pub,
     mode: "pay",
-    order_id: proposal.contractTerms.order_id,
+    order_id: d.contractTerms.order_id,
   };
   const t: PurchaseRecord = {
     abortDone: false,
     abortRequested: false,
-    contractTerms: proposal.contractTerms,
-    contractTermsHash: proposal.contractTermsHash,
+    contractTerms: d.contractTerms,
+    contractTermsHash: d.contractTermsHash,
     finished: false,
     lastSessionId: undefined,
-    merchantSig: proposal.merchantSig,
+    merchantSig: d.merchantSig,
     payReq,
     refundsDone: {},
     refundsPending: {},
     timestamp: getTimestampNow(),
     timestamp_refund: undefined,
+    proposalId: proposal.proposalId,
   };
 
   await runWithWriteTransaction(
     ws.db,
-    [Stores.coins, Stores.purchases],
+    [Stores.coins, Stores.purchases, Stores.proposals],
     async tx => {
+      const p = await tx.get(Stores.proposals, proposal.proposalId);
+      if (p) {
+        p.proposalStatus = ProposalStatus.ACCEPTED;
+        await tx.put(Stores.proposals, p);
+      }
       await tx.put(Stores.purchases, t);
       for (let c of payCoinInfo.updatedCoins) {
         await tx.put(Stores.coins, c);
@@ -360,7 +371,7 @@ async function recordConfirmPay(
 function getNextUrl(contractTerms: ContractTerms): string {
   const f = contractTerms.fulfillment_url;
   if (f.startsWith("http://";) || f.startsWith("https://";)) {
-    const fu = new URL(contractTerms.fulfillment_url)
+    const fu = new URL(contractTerms.fulfillment_url);
     fu.searchParams.set("order_id", contractTerms.order_id);
     return fu.href;
   } else {
@@ -370,9 +381,9 @@ function getNextUrl(contractTerms: ContractTerms): string {
 
 export async function abortFailedPayment(
   ws: InternalWalletState,
-  contractTermsHash: string,
+  proposalId: string,
 ): Promise<void> {
-  const purchase = await oneShotGet(ws.db, Stores.purchases, 
contractTermsHash);
+  const purchase = await oneShotGet(ws.db, Stores.purchases, proposalId);
   if (!purchase) {
     throw Error("Purchase not found, unable to abort with refund");
   }
@@ -409,7 +420,7 @@ export async function abortFailedPayment(
   await acceptRefundResponse(ws, refundResponse);
 
   await runWithWriteTransaction(ws.db, [Stores.purchases], async tx => {
-    const p = await tx.get(Stores.purchases, purchase.contractTermsHash);
+    const p = await tx.get(Stores.purchases, proposalId);
     if (!p) {
       return;
     }
@@ -418,6 +429,76 @@ export async function abortFailedPayment(
   });
 }
 
+export async function processDownloadProposal(
+  ws: InternalWalletState,
+  proposalId: string,
+): Promise<void> {
+  const proposal = await oneShotGet(ws.db, Stores.proposals, proposalId);
+  if (!proposal) {
+    return;
+  }
+  if (proposal.proposalStatus != ProposalStatus.DOWNLOADING) {
+    return;
+  }
+  const parsed_url = new URL(proposal.url);
+  parsed_url.searchParams.set("nonce", proposal.noncePub);
+  const urlWithNonce = parsed_url.href;
+  console.log("downloading contract from '" + urlWithNonce + "'");
+  let resp;
+  try {
+    resp = await ws.http.get(urlWithNonce);
+  } catch (e) {
+    console.log("contract download failed", e);
+    throw e;
+  }
+
+  const proposalResp = Proposal.checked(resp.responseJson);
+
+  const contractTermsHash = await ws.cryptoApi.hashString(
+    canonicalJson(proposalResp.contract_terms),
+  );
+
+  const fulfillmentUrl = proposalResp.contract_terms.fulfillment_url;
+
+  await runWithWriteTransaction(
+    ws.db,
+    [Stores.proposals, Stores.purchases],
+    async tx => {
+      const p = await tx.get(Stores.proposals, proposalId);
+      if (!p) {
+        return;
+      }
+      if (p.proposalStatus !== ProposalStatus.DOWNLOADING) {
+        return;
+      }
+      if (
+        fulfillmentUrl.startsWith("http://";) ||
+        fulfillmentUrl.startsWith("https://";)
+      ) {
+        const differentPurchase = await tx.getIndexed(
+          Stores.purchases.fulfillmentUrlIndex,
+          fulfillmentUrl,
+        );
+        if (differentPurchase) {
+          p.proposalStatus = ProposalStatus.REPURCHASE;
+          p.repurchaseProposalId = differentPurchase.proposalId;
+          await tx.put(Stores.proposals, p);
+          return;
+        }
+      }
+      p.download = {
+        contractTerms: proposalResp.contract_terms,
+        merchantSig: proposalResp.sig,
+        contractTermsHash,
+      };
+      p.proposalStatus = ProposalStatus.PROPOSED;
+      await tx.put(Stores.proposals, p);
+    },
+  );
+
+  ws.notifier.notify();
+}
+
 /**
  * Download a proposal and store it in the database.
  * Returns an id for it to retrieve it later.
@@ -425,7 +506,7 @@ export async function abortFailedPayment(
  * @param sessionId Current session ID, if the proposal is being
  *  downloaded in the context of a session ID.
  */
-async function downloadProposal(
+async function startDownloadProposal(
   ws: InternalWalletState,
   url: string,
   sessionId?: string,
@@ -436,55 +517,38 @@ async function downloadProposal(
     url,
   );
   if (oldProposal) {
+    await processDownloadProposal(ws, oldProposal.proposalId);
     return oldProposal.proposalId;
   }
 
   const { priv, pub } = await ws.cryptoApi.createEddsaKeypair();
-  const parsed_url = new URL(url);
-  parsed_url.searchParams.set("nonce", pub);
-  const urlWithNonce = parsed_url.href;
-  console.log("downloading contract from '" + urlWithNonce + "'");
-  let resp;
-  try {
-    resp = await ws.http.get(urlWithNonce);
-  } catch (e) {
-    console.log("contract download failed", e);
-    throw e;
-  }
-
-  const proposal = Proposal.checked(resp.responseJson);
-
-  const contractTermsHash = await ws.cryptoApi.hashString(
-    canonicalJson(proposal.contract_terms),
-  );
-
   const proposalId = encodeCrock(getRandomBytes(32));
 
   const proposalRecord: ProposalRecord = {
-    contractTerms: proposal.contract_terms,
-    contractTermsHash,
-    merchantSig: proposal.sig,
+    download: undefined,
     noncePriv: priv,
+    noncePub: pub,
     timestamp: getTimestampNow(),
     url,
     downloadSessionId: sessionId,
     proposalId: proposalId,
-    proposalStatus: ProposalStatus.PROPOSED,
+    proposalStatus: ProposalStatus.DOWNLOADING,
+    repurchaseProposalId: undefined,
   };
-  await oneShotPut(ws.db, Stores.proposals, proposalRecord);
-  ws.notifier.notify();
 
+  await oneShotPut(ws.db, Stores.proposals, proposalRecord);
+  await processDownloadProposal(ws, proposalId);
   return proposalId;
 }
 
-async function submitPay(
+export async function submitPay(
   ws: InternalWalletState,
-  contractTermsHash: string,
+  proposalId: string,
   sessionId: string | undefined,
 ): Promise<ConfirmPayResult> {
-  const purchase = await oneShotGet(ws.db, Stores.purchases, 
contractTermsHash);
+  const purchase = await oneShotGet(ws.db, Stores.purchases, proposalId);
   if (!purchase) {
-    throw Error("Purchase not found: " + contractTermsHash);
+    throw Error("Purchase not found: " + proposalId);
   }
   if (purchase.abortRequested) {
     throw Error("not submitting payment for aborted purchase");
@@ -507,7 +571,7 @@ async function submitPay(
   const merchantPub = purchase.contractTerms.merchant_pub;
   const valid: boolean = await ws.cryptoApi.isValidPaymentSignature(
     merchantResp.sig,
-    contractTermsHash,
+    purchase.contractTermsHash,
     merchantPub,
   );
   if (!valid) {
@@ -532,14 +596,16 @@ async function submitPay(
     [Stores.coins, Stores.purchases],
     async tx => {
       for (let c of modifiedCoins) {
-        tx.put(Stores.coins, c);
+        await tx.put(Stores.coins, c);
       }
-      tx.put(Stores.purchases, purchase);
+      await tx.put(Stores.purchases, purchase);
     },
   );
 
   for (const c of purchase.payReq.coins) {
-    refresh(ws, c.coin_pub);
+    refresh(ws, c.coin_pub).catch(e => {
+      console.log("error in refreshing after payment:", e);
+    });
   }
 
   const nextUrl = getNextUrl(purchase.contractTerms);
@@ -570,100 +636,67 @@ export async function preparePay(
     };
   }
 
-  let proposalId: string;
-  try {
-    proposalId = await downloadProposal(
-      ws,
-      uriResult.downloadUrl,
-      uriResult.sessionId,
-    );
-  } catch (e) {
-    return {
-      status: "error",
-      error: e.toString(),
-    };
-  }
-  const proposal = await oneShotGet(ws.db, Stores.proposals, proposalId);
-  if (!proposal) {
-    throw Error(`could not get proposal ${proposalId}`);
-  }
-
-  console.log("proposal", proposal);
-
-  const differentPurchase = await oneShotGetIndexed(
-    ws.db,
-    Stores.purchases.fulfillmentUrlIndex,
-    proposal.contractTerms.fulfillment_url,
+  const proposalId = await startDownloadProposal(
+    ws,
+    uriResult.downloadUrl,
+    uriResult.sessionId,
   );
 
-  let fulfillmentUrl = proposal.contractTerms.fulfillment_url;
-  let doublePurchaseDetection = false;
-  if (fulfillmentUrl.startsWith("http")) {
-    doublePurchaseDetection = true;
+  let proposal = await oneShotGet(ws.db, Stores.proposals, proposalId);
+  if (!proposal) {
+    throw Error(`could not get proposal ${proposalId}`);
   }
-
-  if (differentPurchase && doublePurchaseDetection) {
-    // We do this check to prevent merchant B to find out if we bought a
-    // digital product with merchant A by abusing the existing payment
-    // redirect feature.
-    if (
-      differentPurchase.contractTerms.merchant_pub !=
-      proposal.contractTerms.merchant_pub
-    ) {
-      console.warn(
-        "merchant with different public key offered contract with same 
fulfillment URL as an existing purchase",
-      );
-    } else {
-      if (uriResult.sessionId) {
-        await submitPay(
-          ws,
-          differentPurchase.contractTermsHash,
-          uriResult.sessionId,
-        );
-      }
-      return {
-        status: "paid",
-        contractTerms: differentPurchase.contractTerms,
-        nextUrl: getNextUrl(differentPurchase.contractTerms),
-      };
+  if (proposal.proposalStatus === ProposalStatus.REPURCHASE) {
+    const existingProposalId = proposal.repurchaseProposalId;
+    if (!existingProposalId) {
+      throw Error("invalid proposal state");
     }
+    proposal = await oneShotGet(ws.db, Stores.proposals, existingProposalId);
+    if (!proposal) {
+      throw Error("existing proposal is in wrong state");
+    }
+  }
+  const d = proposal.download;
+  if (!d) {
+    console.error("bad proposal", proposal);
+    throw Error("proposal is in invalid state");
+  }
+  const contractTerms = d.contractTerms;
+  const merchantSig = d.merchantSig;
+  if (!contractTerms || !merchantSig) {
+    throw Error("BUG: proposal is in invalid state");
   }
 
+  console.log("proposal", proposal);
+
   // First check if we already payed for it.
-  const purchase = await oneShotGet(
-    ws.db,
-    Stores.purchases,
-    proposal.contractTermsHash,
-  );
+  const purchase = await oneShotGet(ws.db, Stores.purchases, proposalId);
 
   if (!purchase) {
-    const paymentAmount = Amounts.parseOrThrow(proposal.contractTerms.amount);
+    const paymentAmount = Amounts.parseOrThrow(contractTerms.amount);
     let wireFeeLimit;
-    if (proposal.contractTerms.max_wire_fee) {
-      wireFeeLimit = Amounts.parseOrThrow(proposal.contractTerms.max_wire_fee);
+    if (contractTerms.max_wire_fee) {
+      wireFeeLimit = Amounts.parseOrThrow(contractTerms.max_wire_fee);
     } else {
       wireFeeLimit = Amounts.getZero(paymentAmount.currency);
     }
     // If not already payed, check if we could pay for it.
     const res = await getCoinsForPayment(ws, {
-      allowedAuditors: proposal.contractTerms.auditors,
-      allowedExchanges: proposal.contractTerms.exchanges,
-      depositFeeLimit: Amounts.parseOrThrow(proposal.contractTerms.max_fee),
+      allowedAuditors: contractTerms.auditors,
+      allowedExchanges: contractTerms.exchanges,
+      depositFeeLimit: Amounts.parseOrThrow(contractTerms.max_fee),
       paymentAmount,
-      wireFeeAmortization: proposal.contractTerms.wire_fee_amortization || 1,
+      wireFeeAmortization: contractTerms.wire_fee_amortization || 1,
       wireFeeLimit,
-      // FIXME: parse this properly
-      wireFeeTime: extractTalerStamp(proposal.contractTerms.timestamp) || {
-        t_ms: 0,
-      },
-      wireMethod: proposal.contractTerms.wire_method,
+      wireFeeTime: extractTalerStampOrThrow(contractTerms.timestamp),
+      wireMethod: contractTerms.wire_method,
     });
 
     if (!res) {
       console.log("not confirming payment, insufficient coins");
       return {
         status: "insufficient-balance",
-        contractTerms: proposal.contractTerms,
+        contractTerms: contractTerms,
         proposalId: proposal.proposalId,
       };
     }
@@ -676,7 +709,7 @@ export async function preparePay(
     ) {
       const { exchangeUrl, cds, totalAmount } = res;
       const payCoinInfo = await ws.cryptoApi.signDeposit(
-        proposal.contractTerms,
+        contractTerms,
         cds,
         totalAmount,
       );
@@ -691,19 +724,19 @@ export async function preparePay(
 
     return {
       status: "payment-possible",
-      contractTerms: proposal.contractTerms,
+      contractTerms: contractTerms,
       proposalId: proposal.proposalId,
       totalFees: res.totalFees,
     };
   }
 
   if (uriResult.sessionId) {
-    await submitPay(ws, purchase.contractTermsHash, uriResult.sessionId);
+    await submitPay(ws, proposalId, uriResult.sessionId);
   }
 
   return {
     status: "paid",
-    contractTerms: proposal.contractTerms,
+    contractTerms: purchase.contractTerms,
     nextUrl: getNextUrl(purchase.contractTerms),
   };
 }
@@ -762,39 +795,37 @@ export async function confirmPay(
     throw Error(`proposal with id ${proposalId} not found`);
   }
 
+  const d = proposal.download;
+  if (!d) {
+    throw Error("proposal is in invalid state");
+  }
+
   const sessionId = sessionIdOverride || proposal.downloadSessionId;
 
-  let purchase = await oneShotGet(
-    ws.db,
-    Stores.purchases,
-    proposal.contractTermsHash,
-  );
+  let purchase = await oneShotGet(ws.db, Stores.purchases, 
d.contractTermsHash);
 
   if (purchase) {
-    return submitPay(ws, purchase.contractTermsHash, sessionId);
+    return submitPay(ws, proposalId, sessionId);
   }
 
-  const contractAmount = Amounts.parseOrThrow(proposal.contractTerms.amount);
+  const contractAmount = Amounts.parseOrThrow(d.contractTerms.amount);
 
   let wireFeeLimit;
-  if (!proposal.contractTerms.max_wire_fee) {
+  if (!d.contractTerms.max_wire_fee) {
     wireFeeLimit = Amounts.getZero(contractAmount.currency);
   } else {
-    wireFeeLimit = Amounts.parseOrThrow(proposal.contractTerms.max_wire_fee);
+    wireFeeLimit = Amounts.parseOrThrow(d.contractTerms.max_wire_fee);
   }
 
   const res = await getCoinsForPayment(ws, {
-    allowedAuditors: proposal.contractTerms.auditors,
-    allowedExchanges: proposal.contractTerms.exchanges,
-    depositFeeLimit: Amounts.parseOrThrow(proposal.contractTerms.max_fee),
-    paymentAmount: Amounts.parseOrThrow(proposal.contractTerms.amount),
-    wireFeeAmortization: proposal.contractTerms.wire_fee_amortization || 1,
+    allowedAuditors: d.contractTerms.auditors,
+    allowedExchanges: d.contractTerms.exchanges,
+    depositFeeLimit: Amounts.parseOrThrow(d.contractTerms.max_fee),
+    paymentAmount: Amounts.parseOrThrow(d.contractTerms.amount),
+    wireFeeAmortization: d.contractTerms.wire_fee_amortization || 1,
     wireFeeLimit,
-    // FIXME: parse this properly
-    wireFeeTime: extractTalerStamp(proposal.contractTerms.timestamp) || {
-      t_ms: 0,
-    },
-    wireMethod: proposal.contractTerms.wire_method,
+    wireFeeTime: extractTalerStampOrThrow(d.contractTerms.timestamp),
+    wireMethod: d.contractTerms.wire_method,
   });
 
   logger.trace("coin selection result", res);
@@ -809,7 +840,7 @@ export async function confirmPay(
   if (!sd) {
     const { exchangeUrl, cds, totalAmount } = res;
     const payCoinInfo = await ws.cryptoApi.signDeposit(
-      proposal.contractTerms,
+      d.contractTerms,
       cds,
       totalAmount,
     );
@@ -823,5 +854,5 @@ export async function confirmPay(
     );
   }
 
-  return submitPay(ws, purchase.contractTermsHash, sessionId);
+  return submitPay(ws, proposalId, sessionId);
 }
diff --git a/src/wallet-impl/pending.ts b/src/wallet-impl/pending.ts
index dbc6672c..72102e3a 100644
--- a/src/wallet-impl/pending.ts
+++ b/src/wallet-impl/pending.ts
@@ -22,7 +22,7 @@ import {
   PendingOperationsResponse,
   getTimestampNow,
 } from "../walletTypes";
-import { oneShotIter } from "../util/query";
+import { runWithReadTransaction } from "../util/query";
 import { InternalWalletState } from "./state";
 import {
   Stores,
@@ -37,187 +37,212 @@ export async function getPendingOperations(
 ): Promise<PendingOperationsResponse> {
   const pendingOperations: PendingOperationInfo[] = [];
   let minRetryDurationMs = 5000;
-  const exchanges = await oneShotIter(ws.db, Stores.exchanges).toArray();
-  for (let e of exchanges) {
-    switch (e.updateStatus) {
-      case ExchangeUpdateStatus.FINISHED:
-        if (e.lastError) {
-          pendingOperations.push({
-            type: "bug",
-            message:
-              "Exchange record is in FINISHED state but has lastError set",
-            details: {
+  await runWithReadTransaction(
+    ws.db,
+    [
+      Stores.exchanges,
+      Stores.reserves,
+      Stores.refresh,
+      Stores.coins,
+      Stores.withdrawalSession,
+      Stores.proposals,
+      Stores.tips,
+    ],
+    async tx => {
+      await tx.iter(Stores.exchanges).forEach(e => {
+        switch (e.updateStatus) {
+          case ExchangeUpdateStatus.FINISHED:
+            if (e.lastError) {
+              pendingOperations.push({
+                type: "bug",
+                message:
+                  "Exchange record is in FINISHED state but has lastError set",
+                details: {
+                  exchangeBaseUrl: e.baseUrl,
+                },
+              });
+            }
+            if (!e.details) {
+              pendingOperations.push({
+                type: "bug",
+                message:
+                  "Exchange record does not have details, but no update in 
progress.",
+                details: {
+                  exchangeBaseUrl: e.baseUrl,
+                },
+              });
+            }
+            if (!e.wireInfo) {
+              pendingOperations.push({
+                type: "bug",
+                message:
+                  "Exchange record does not have wire info, but no update in 
progress.",
+                details: {
+                  exchangeBaseUrl: e.baseUrl,
+                },
+              });
+            }
+            break;
+          case ExchangeUpdateStatus.FETCH_KEYS:
+            pendingOperations.push({
+              type: "exchange-update",
+              stage: "fetch-keys",
               exchangeBaseUrl: e.baseUrl,
-            },
-          });
-        }
-        if (!e.details) {
-          pendingOperations.push({
-            type: "bug",
-            message:
-              "Exchange record does not have details, but no update in 
progress.",
-            details: {
+              lastError: e.lastError,
+              reason: e.updateReason || "unknown",
+            });
+            break;
+          case ExchangeUpdateStatus.FETCH_WIRE:
+            pendingOperations.push({
+              type: "exchange-update",
+              stage: "fetch-wire",
               exchangeBaseUrl: e.baseUrl,
-            },
-          });
+              lastError: e.lastError,
+              reason: e.updateReason || "unknown",
+            });
+            break;
+          default:
+            pendingOperations.push({
+              type: "bug",
+              message: "Unknown exchangeUpdateStatus",
+              details: {
+                exchangeBaseUrl: e.baseUrl,
+                exchangeUpdateStatus: e.updateStatus,
+              },
+            });
+            break;
         }
-        if (!e.wireInfo) {
-          pendingOperations.push({
-            type: "bug",
-            message:
-              "Exchange record does not have wire info, but no update in 
progress.",
-            details: {
-              exchangeBaseUrl: e.baseUrl,
-            },
-          });
+      });
+      await tx.iter(Stores.reserves).forEach(reserve => {
+        const reserveType = reserve.bankWithdrawStatusUrl
+          ? "taler-bank"
+          : "manual";
+        const now = getTimestampNow();
+        switch (reserve.reserveStatus) {
+          case ReserveRecordStatus.DORMANT:
+            // nothing to report as pending
+            break;
+          case ReserveRecordStatus.WITHDRAWING:
+          case ReserveRecordStatus.UNCONFIRMED:
+          case ReserveRecordStatus.QUERYING_STATUS:
+          case ReserveRecordStatus.REGISTERING_BANK:
+            pendingOperations.push({
+              type: "reserve",
+              stage: reserve.reserveStatus,
+              timestampCreated: reserve.created,
+              reserveType,
+              reservePub: reserve.reservePub,
+            });
+            if (reserve.created.t_ms < now.t_ms - 5000) {
+              minRetryDurationMs = 500;
+            } else if (reserve.created.t_ms < now.t_ms - 30000) {
+              minRetryDurationMs = 2000;
+            }
+            break;
+          case ReserveRecordStatus.WAIT_CONFIRM_BANK:
+            pendingOperations.push({
+              type: "reserve",
+              stage: reserve.reserveStatus,
+              timestampCreated: reserve.created,
+              reserveType,
+              reservePub: reserve.reservePub,
+              bankWithdrawConfirmUrl: reserve.bankWithdrawConfirmUrl,
+            });
+            if (reserve.created.t_ms < now.t_ms - 5000) {
+              minRetryDurationMs = 500;
+            } else if (reserve.created.t_ms < now.t_ms - 30000) {
+              minRetryDurationMs = 2000;
+            }
+            break;
+          default:
+            pendingOperations.push({
+              type: "bug",
+              message: "Unknown reserve record status",
+              details: {
+                reservePub: reserve.reservePub,
+                reserveStatus: reserve.reserveStatus,
+              },
+            });
+            break;
         }
-        break;
-      case ExchangeUpdateStatus.FETCH_KEYS:
-        pendingOperations.push({
-          type: "exchange-update",
-          stage: "fetch-keys",
-          exchangeBaseUrl: e.baseUrl,
-          lastError: e.lastError,
-          reason: e.updateReason || "unknown",
-        });
-        break;
-      case ExchangeUpdateStatus.FETCH_WIRE:
-        pendingOperations.push({
-          type: "exchange-update",
-          stage: "fetch-wire",
-          exchangeBaseUrl: e.baseUrl,
-          lastError: e.lastError,
-          reason: e.updateReason || "unknown",
-        });
-        break;
-      default:
-        pendingOperations.push({
-          type: "bug",
-          message: "Unknown exchangeUpdateStatus",
-          details: {
-            exchangeBaseUrl: e.baseUrl,
-            exchangeUpdateStatus: e.updateStatus,
-          },
-        });
-        break;
-    }
-  }
-  await oneShotIter(ws.db, Stores.reserves).forEach(reserve => {
-    const reserveType = reserve.bankWithdrawStatusUrl ? "taler-bank" : 
"manual";
-    const now = getTimestampNow();
-    switch (reserve.reserveStatus) {
-      case ReserveRecordStatus.DORMANT:
-        // nothing to report as pending
-        break;
-      case ReserveRecordStatus.WITHDRAWING:
-      case ReserveRecordStatus.UNCONFIRMED:
-      case ReserveRecordStatus.QUERYING_STATUS:
-      case ReserveRecordStatus.REGISTERING_BANK:
-        pendingOperations.push({
-          type: "reserve",
-          stage: reserve.reserveStatus,
-          timestampCreated: reserve.created,
-          reserveType,
-          reservePub: reserve.reservePub,
-        });
-        if (reserve.created.t_ms < now.t_ms - 5000) {
-          minRetryDurationMs = 500;
-        } else if (reserve.created.t_ms < now.t_ms - 30000) {
-          minRetryDurationMs = 2000;
+      });
+
+      await tx.iter(Stores.refresh).forEach(r => {
+        if (r.finished) {
+          return;
         }
-        break;
-      case ReserveRecordStatus.WAIT_CONFIRM_BANK:
-        pendingOperations.push({
-          type: "reserve",
-          stage: reserve.reserveStatus,
-          timestampCreated: reserve.created,
-          reserveType,
-          reservePub: reserve.reservePub,
-          bankWithdrawConfirmUrl: reserve.bankWithdrawConfirmUrl,
-        });
-        if (reserve.created.t_ms < now.t_ms - 5000) {
-          minRetryDurationMs = 500;
-        } else if (reserve.created.t_ms < now.t_ms - 30000) {
-          minRetryDurationMs = 2000;
+        let refreshStatus: string;
+        if (r.norevealIndex === undefined) {
+          refreshStatus = "melt";
+        } else {
+          refreshStatus = "reveal";
         }
-        break;
-      default:
+
         pendingOperations.push({
-          type: "bug",
-          message: "Unknown reserve record status",
-          details: {
-            reservePub: reserve.reservePub,
-            reserveStatus: reserve.reserveStatus,
-          },
+          type: "refresh",
+          oldCoinPub: r.meltCoinPub,
+          refreshStatus,
+          refreshOutputSize: r.newDenoms.length,
+          refreshSessionId: r.refreshSessionId,
         });
-        break;
-    }
-  });
-
-  await oneShotIter(ws.db, Stores.refresh).forEach(r => {
-    if (r.finished) {
-      return;
-    }
-    let refreshStatus: string;
-    if (r.norevealIndex === undefined) {
-      refreshStatus = "melt";
-    } else {
-      refreshStatus = "reveal";
-    }
-
-    pendingOperations.push({
-      type: "refresh",
-      oldCoinPub: r.meltCoinPub,
-      refreshStatus,
-      refreshOutputSize: r.newDenoms.length,
-      refreshSessionId: r.refreshSessionId,
-    });
-  });
+      });
 
-  await oneShotIter(ws.db, Stores.coins).forEach(coin => {
-    if (coin.status == CoinStatus.Dirty) {
-      pendingOperations.push({
-        type: "dirty-coin",
-        coinPub: coin.coinPub,
+      await tx.iter(Stores.coins).forEach(coin => {
+        if (coin.status == CoinStatus.Dirty) {
+          pendingOperations.push({
+            type: "dirty-coin",
+            coinPub: coin.coinPub,
+          });
+        }
       });
-    }
-  });
 
-  await oneShotIter(ws.db, Stores.withdrawalSession).forEach(ws => {
-    const numCoinsWithdrawn = ws.withdrawn.reduce((a, x) => a + (x ? 1 : 0), 
0);
-    const numCoinsTotal = ws.withdrawn.length;
-    if (numCoinsWithdrawn < numCoinsTotal) {
-      pendingOperations.push({
-        type: "withdraw",
-        numCoinsTotal,
-        numCoinsWithdrawn,
-        source: ws.source,
-        withdrawSessionId: ws.withdrawSessionId,
+      await tx.iter(Stores.withdrawalSession).forEach(ws => {
+        const numCoinsWithdrawn = ws.withdrawn.reduce(
+          (a, x) => a + (x ? 1 : 0),
+          0,
+        );
+        const numCoinsTotal = ws.withdrawn.length;
+        if (numCoinsWithdrawn < numCoinsTotal) {
+          pendingOperations.push({
+            type: "withdraw",
+            numCoinsTotal,
+            numCoinsWithdrawn,
+            source: ws.source,
+            withdrawSessionId: ws.withdrawSessionId,
+          });
+        }
       });
-    }
-  });
 
-  await oneShotIter(ws.db, Stores.proposals).forEach(proposal => {
-    if (proposal.proposalStatus == ProposalStatus.PROPOSED) {
-      pendingOperations.push({
-        type: "proposal",
-        merchantBaseUrl: proposal.contractTerms.merchant_base_url,
-        proposalId: proposal.proposalId,
-        proposalTimestamp: proposal.timestamp,
+      await tx.iter(Stores.proposals).forEach((proposal) => {
+        if (proposal.proposalStatus == ProposalStatus.PROPOSED) {
+          pendingOperations.push({
+            type: "proposal-choice",
+            merchantBaseUrl: 
proposal.download!!.contractTerms.merchant_base_url,
+            proposalId: proposal.proposalId,
+            proposalTimestamp: proposal.timestamp,
+          });
+        } else if (proposal.proposalStatus == ProposalStatus.DOWNLOADING) {
+          pendingOperations.push({
+            type: "proposal-download",
+            merchantBaseUrl: 
proposal.download!!.contractTerms.merchant_base_url,
+            proposalId: proposal.proposalId,
+            proposalTimestamp: proposal.timestamp,
+          });
+        }
       });
-    }
-  });
 
-  await oneShotIter(ws.db, Stores.tips).forEach(tip => {
-    if (tip.accepted && !tip.pickedUp) {
-      pendingOperations.push({
-        type: "tip",
-        merchantBaseUrl: tip.merchantBaseUrl,
-        tipId: tip.tipId,
-        merchantTipId: tip.merchantTipId,
+      await tx.iter(Stores.tips).forEach((tip) => {
+        if (tip.accepted && !tip.pickedUp) {
+          pendingOperations.push({
+            type: "tip",
+            merchantBaseUrl: tip.merchantBaseUrl,
+            tipId: tip.tipId,
+            merchantTipId: tip.merchantTipId,
+          });
+        }
       });
-    }
-  });
+    },
+  );
 
   return {
     pendingOperations,
diff --git a/src/wallet-impl/refund.ts b/src/wallet-impl/refund.ts
index 2a9dea14..4cd507e4 100644
--- a/src/wallet-impl/refund.ts
+++ b/src/wallet-impl/refund.ts
@@ -91,13 +91,12 @@ export async function getFullRefundFees(
 
 async function submitRefunds(
   ws: InternalWalletState,
-  contractTermsHash: string,
+  proposalId: string,
 ): Promise<void> {
-  const purchase = await oneShotGet(ws.db, Stores.purchases, 
contractTermsHash);
+  const purchase = await oneShotGet(ws.db, Stores.purchases, proposalId);
   if (!purchase) {
     console.error(
-      "not submitting refunds, contract terms not found:",
-      contractTermsHash,
+      "not submitting refunds, payment not found:",
     );
     return;
   }
@@ -160,7 +159,7 @@ async function submitRefunds(
       ws.db,
       [Stores.purchases, Stores.coins],
       async tx => {
-        await tx.mutate(Stores.purchases, contractTermsHash, 
transformPurchase);
+        await tx.mutate(Stores.purchases, proposalId, transformPurchase);
         await tx.mutate(Stores.coins, perm.coin_pub, transformCoin);
       },
     );
diff --git a/src/wallet-impl/reserves.ts b/src/wallet-impl/reserves.ts
index 5d624fe2..c9cd10ca 100644
--- a/src/wallet-impl/reserves.ts
+++ b/src/wallet-impl/reserves.ts
@@ -344,10 +344,16 @@ async function updateReserve(
     resp = await ws.http.get(reqUrl.href);
   } catch (e) {
     if (e.response?.status === 404) {
-      return;
+      const m = "The exchange does not know about this reserve (yet).";
+      await setReserveError(ws, reservePub, {
+        type: "waiting",
+        details: {},
+        message: "The exchange does not know about this reserve (yet).",
+      });
+      throw new OperationFailedAndReportedError(m);
     } else {
       const m = e.message;
-      setReserveError(ws, reservePub, {
+      await setReserveError(ws, reservePub, {
         type: "network",
         details: {},
         message: m,
diff --git a/src/wallet.ts b/src/wallet.ts
index a6eecb8a..432a3e98 100644
--- a/src/wallet.ts
+++ b/src/wallet.ts
@@ -46,6 +46,7 @@ import {
   abortFailedPayment,
   preparePay,
   confirmPay,
+  processDownloadProposal,
 } from "./wallet-impl/pay";
 
 import {
@@ -227,12 +228,17 @@ export class Wallet {
       case "withdraw":
         await processWithdrawSession(this.ws, pending.withdrawSessionId);
         break;
-      case "proposal":
+      case "proposal-choice":
         // Nothing to do, user needs to accept/reject
         break;
+      case "proposal-download":
+        await processDownloadProposal(this.ws, pending.proposalId);
+        break;
       case "tip":
         await processTip(this.ws, pending.tipId);
         break;
+      case "pay":
+        break;
       default:
         assertUnreachable(pending);
     }
diff --git a/src/walletTypes.ts b/src/walletTypes.ts
index b12b29c5..be88fc5b 100644
--- a/src/walletTypes.ts
+++ b/src/walletTypes.ts
@@ -578,13 +578,25 @@ export interface PendingRefreshOperation {
   refreshOutputSize: number;
 }
 
+
 export interface PendingDirtyCoinOperation {
   type: "dirty-coin";
   coinPub: string;
 }
 
-export interface PendingProposalOperation {
-  type: "proposal";
+export interface PendingProposalDownloadOperation {
+  type: "proposal-download";
+  merchantBaseUrl: string;
+  proposalTimestamp: Timestamp;
+  proposalId: string;
+}
+
+/**
+ * User must choose whether to accept or reject the merchant's
+ * proposed contract terms.
+ */
+export interface PendingProposalChoiceOperation {
+  type: "proposal-choice";
   merchantBaseUrl: string;
   proposalTimestamp: Timestamp;
   proposalId: string;
@@ -597,6 +609,12 @@ export interface PendingTipOperation {
   merchantTipId: string;
 }
 
+export interface PendingPayOperation {
+  type: "pay";
+  proposalId: string;
+  isReplay: boolean;
+}
+
 export type PendingOperationInfo =
   | PendingWithdrawOperation
   | PendingReserveOperation
@@ -605,7 +623,9 @@ export type PendingOperationInfo =
   | PendingExchangeUpdateOperation
   | PendingRefreshOperation
   | PendingTipOperation
-  | PendingProposalOperation;
+  | PendingProposalDownloadOperation
+  | PendingProposalChoiceOperation
+  | PendingPayOperation;
 
 export interface PendingOperationsResponse {
   pendingOperations: PendingOperationInfo[];

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



reply via email to

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