gnunet-svn
[Top][All Lists]
Advanced

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

[taler-wallet-core] branch master updated: consistent error handling for


From: gnunet
Subject: [taler-wallet-core] branch master updated: consistent error handling for HTTP request (and some other things)
Date: Wed, 22 Jul 2020 10:52:08 +0200

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 e60563fb consistent error handling for HTTP request (and some other 
things)
e60563fb is described below

commit e60563fb540c04d9ba751fea69c1fc0f1de598b5
Author: Florian Dold <florian.dold@gmail.com>
AuthorDate: Wed Jul 22 14:22:03 2020 +0530

    consistent error handling for HTTP request (and some other things)
---
 src/android/index.ts             |  17 ++-
 src/headless/NodeHttpLib.ts      |  36 +++++-
 src/headless/helpers.ts          |   5 +-
 src/headless/integrationtest.ts  |   7 +-
 src/headless/merchant.ts         |   8 +-
 src/headless/taler-wallet-cli.ts |   7 +-
 src/operations/errors.ts         |  61 ++++++---
 src/operations/exchanges.ts      | 118 +++++------------
 src/operations/pay.ts            |  57 +++++----
 src/operations/recoup.ts         |  36 +++---
 src/operations/refresh.ts        |  76 ++++-------
 src/operations/refund.ts         |  97 ++++----------
 src/operations/reserves.ts       |  82 ++++++------
 src/operations/tip.ts            |  18 ++-
 src/operations/transactions.ts   |  87 +++++++------
 src/operations/withdraw.ts       |  30 ++---
 src/types/dbTypes.ts             |  29 ++---
 src/types/notifications.ts       |  23 +++-
 src/types/pending.ts             |  16 +--
 src/types/talerTypes.ts          |  68 +++++++++-
 src/types/transactions.ts        |   2 +-
 src/types/walletTypes.ts         |   9 +-
 src/util/amounts-test.ts         |   4 +-
 src/util/amounts.ts              |   2 +-
 src/util/http.ts                 | 267 +++++++++++++++++++++++----------------
 src/wallet.ts                    |  18 +--
 src/webex/pages/withdraw.tsx     |   4 +-
 src/webex/wxBackend.ts           |   5 +-
 28 files changed, 636 insertions(+), 553 deletions(-)

diff --git a/src/android/index.ts b/src/android/index.ts
index 63d88d70..d7a5897a 100644
--- a/src/android/index.ts
+++ b/src/android/index.ts
@@ -114,6 +114,8 @@ export class AndroidHttpLib implements HttpRequestLibrary {
     const headers = new Headers();
     if (msg.status != 0) {
       const resp: HttpResponse = {
+        // FIXME: pass through this URL
+        requestUrl: "",
         headers,
         status: msg.status,
         json: async () => JSON.parse(msg.responseText),
@@ -196,7 +198,10 @@ class AndroidWalletMessageHandler {
       }
       case "getWithdrawalDetailsForAmount": {
         const wallet = await this.wp.promise;
-        return await 
wallet.getWithdrawalDetailsForAmount(args.exchangeBaseUrl, args.amount);
+        return await wallet.getWithdrawalDetailsForAmount(
+          args.exchangeBaseUrl,
+          args.amount,
+        );
       }
       case "withdrawTestkudos": {
         const wallet = await this.wp.promise;
@@ -218,7 +223,10 @@ class AndroidWalletMessageHandler {
       }
       case "setExchangeTosAccepted": {
         const wallet = await this.wp.promise;
-        await wallet.acceptExchangeTermsOfService(args.exchangeBaseUrl, 
args.acceptedEtag);
+        await wallet.acceptExchangeTermsOfService(
+          args.exchangeBaseUrl,
+          args.acceptedEtag,
+        );
         return {};
       }
       case "retryPendingNow": {
@@ -237,7 +245,10 @@ class AndroidWalletMessageHandler {
       }
       case "acceptManualWithdrawal": {
         const wallet = await this.wp.promise;
-        const res = await wallet.acceptManualWithdrawal(args.exchangeBaseUrl, 
Amounts.parseOrThrow(args.amount));
+        const res = await wallet.acceptManualWithdrawal(
+          args.exchangeBaseUrl,
+          Amounts.parseOrThrow(args.amount),
+        );
         return res;
       }
       case "startTunnel": {
diff --git a/src/headless/NodeHttpLib.ts b/src/headless/NodeHttpLib.ts
index 118fb9e9..d109c3b7 100644
--- a/src/headless/NodeHttpLib.ts
+++ b/src/headless/NodeHttpLib.ts
@@ -27,6 +27,8 @@ import {
 } from "../util/http";
 import { RequestThrottler } from "../util/RequestThrottler";
 import Axios from "axios";
+import { OperationFailedError, makeErrorDetails } from "../operations/errors";
+import { TalerErrorCode } from "../TalerErrorCode";
 
 /**
  * Implementation of the HTTP request library interface for node.
@@ -63,17 +65,44 @@ export class NodeHttpLib implements HttpRequestLibrary {
 
     const respText = resp.data;
     if (typeof respText !== "string") {
-      throw Error("unexpected response type");
+      throw new OperationFailedError(
+        makeErrorDetails(
+          TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+          "unexpected response type",
+          {
+            httpStatusCode: resp.status,
+            requestUrl: url,
+          },
+        ),
+      );
     }
     const makeJson = async (): Promise<any> => {
       let responseJson;
       try {
         responseJson = JSON.parse(respText);
       } catch (e) {
-        throw Error("Invalid JSON from HTTP response");
+        throw new OperationFailedError(
+          makeErrorDetails(
+            TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+            "invalid JSON",
+            {
+              httpStatusCode: resp.status,
+              requestUrl: url,
+            },
+          ),
+        );
       }
       if (responseJson === null || typeof responseJson !== "object") {
-        throw Error("Invalid JSON from HTTP response");
+        throw new OperationFailedError(
+          makeErrorDetails(
+            TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+            "invalid JSON",
+            {
+              httpStatusCode: resp.status,
+              requestUrl: url,
+            },
+          ),
+        );
       }
       return responseJson;
     };
@@ -82,6 +111,7 @@ export class NodeHttpLib implements HttpRequestLibrary {
       headers.set(hn, resp.headers[hn]);
     }
     return {
+      requestUrl: url,
       headers,
       status: resp.status,
       text: async () => resp.data,
diff --git a/src/headless/helpers.ts b/src/headless/helpers.ts
index 67ba62df..e451db55 100644
--- a/src/headless/helpers.ts
+++ b/src/headless/helpers.ts
@@ -143,7 +143,10 @@ export async function withdrawTestBalance(
   exchangeBaseUrl = "https://exchange.test.taler.net/";,
 ): Promise<void> {
   await myWallet.updateExchangeFromUrl(exchangeBaseUrl, true);
-  const reserveResponse = await 
myWallet.acceptManualWithdrawal(exchangeBaseUrl, Amounts.parseOrThrow(amount));
+  const reserveResponse = await myWallet.acceptManualWithdrawal(
+    exchangeBaseUrl,
+    Amounts.parseOrThrow(amount),
+  );
 
   const reservePub = reserveResponse.reservePub;
 
diff --git a/src/headless/integrationtest.ts b/src/headless/integrationtest.ts
index 786907a0..db96d57c 100644
--- a/src/headless/integrationtest.ts
+++ b/src/headless/integrationtest.ts
@@ -25,7 +25,6 @@ import { NodeHttpLib } from "./NodeHttpLib";
 import { Wallet } from "../wallet";
 import { Configuration } from "../util/talerconfig";
 import { Amounts, AmountJson } from "../util/amounts";
-import { OperationFailedAndReportedError, OperationFailedError } from 
"../operations/errors";
 
 const logger = new Logger("integrationtest.ts");
 
@@ -70,9 +69,9 @@ async function makePayment(
   }
 
   const confirmPayResult = await wallet.confirmPay(
-      preparePayResult.proposalId,
-      undefined,
-    );
+    preparePayResult.proposalId,
+    undefined,
+  );
 
   console.log("confirmPayResult", confirmPayResult);
 
diff --git a/src/headless/merchant.ts b/src/headless/merchant.ts
index 3324924f..34ca5564 100644
--- a/src/headless/merchant.ts
+++ b/src/headless/merchant.ts
@@ -37,7 +37,10 @@ export class MerchantBackendConnection {
     reason: string,
     refundAmount: string,
   ): Promise<string> {
-    const reqUrl = new URL(`private/orders/${orderId}/refund`, 
this.merchantBaseUrl);
+    const reqUrl = new URL(
+      `private/orders/${orderId}/refund`,
+      this.merchantBaseUrl,
+    );
     const refundReq = {
       reason,
       refund: refundAmount,
@@ -123,7 +126,8 @@ export class MerchantBackendConnection {
   }
 
   async checkPayment(orderId: string): Promise<CheckPaymentResponse> {
-    const reqUrl = new URL(`private/orders/${orderId}`, 
this.merchantBaseUrl).href;
+    const reqUrl = new URL(`private/orders/${orderId}`, this.merchantBaseUrl)
+      .href;
     const resp = await axios({
       method: "get",
       url: reqUrl,
diff --git a/src/headless/taler-wallet-cli.ts b/src/headless/taler-wallet-cli.ts
index fc13d77f..b8ae84d7 100644
--- a/src/headless/taler-wallet-cli.ts
+++ b/src/headless/taler-wallet-cli.ts
@@ -30,7 +30,7 @@ import {
   setupRefreshPlanchet,
   encodeCrock,
 } from "../crypto/talerCrypto";
-import { OperationFailedAndReportedError, OperationFailedError } from 
"../operations/errors";
+import { OperationFailedAndReportedError } from "../operations/errors";
 import { Bank } from "./bank";
 import { classifyTalerUri, TalerUriType } from "../util/taleruri";
 import { Configuration } from "../util/talerconfig";
@@ -527,7 +527,10 @@ advancedCli
   .maybeOption("sessionIdOverride", ["--session-id"], clk.STRING)
   .action(async (args) => {
     await withWallet(args, async (wallet) => {
-      wallet.confirmPay(args.payConfirm.proposalId, 
args.payConfirm.sessionIdOverride);
+      wallet.confirmPay(
+        args.payConfirm.proposalId,
+        args.payConfirm.sessionIdOverride,
+      );
     });
   });
 
diff --git a/src/operations/errors.ts b/src/operations/errors.ts
index 01a8283c..198d3f8c 100644
--- a/src/operations/errors.ts
+++ b/src/operations/errors.ts
@@ -23,14 +23,15 @@
 /**
  * Imports.
  */
-import { OperationError } from "../types/walletTypes";
+import { OperationErrorDetails } from "../types/walletTypes";
+import { TalerErrorCode } from "../TalerErrorCode";
 
 /**
  * This exception is there to let the caller know that an error happened,
  * but the error has already been reported by writing it to the database.
  */
 export class OperationFailedAndReportedError extends Error {
-  constructor(public operationError: OperationError) {
+  constructor(public operationError: OperationErrorDetails) {
     super(operationError.message);
 
     // Set the prototype explicitly.
@@ -43,7 +44,15 @@ export class OperationFailedAndReportedError extends Error {
  * responsible for recording the failure in the database.
  */
 export class OperationFailedError extends Error {
-  constructor(public operationError: OperationError) {
+  static fromCode(
+    ec: TalerErrorCode,
+    message: string,
+    details: Record<string, unknown>,
+  ): OperationFailedError {
+    return new OperationFailedError(makeErrorDetails(ec, message, details));
+  }
+
+  constructor(public operationError: OperationErrorDetails) {
     super(operationError.message);
 
     // Set the prototype explicitly.
@@ -51,6 +60,19 @@ export class OperationFailedError extends Error {
   }
 }
 
+export function makeErrorDetails(
+  ec: TalerErrorCode,
+  message: string,
+  details: Record<string, unknown>,
+): OperationErrorDetails {
+  return {
+    talerErrorCode: ec,
+    talerErrorHint: `Error: ${TalerErrorCode[ec]}`,
+    details: details,
+    message,
+  };
+}
+
 /**
  * Run an operation and call the onOpError callback
  * when there was an exception or operation error that must be reported.
@@ -58,7 +80,7 @@ export class OperationFailedError extends Error {
  */
 export async function guardOperationException<T>(
   op: () => Promise<T>,
-  onOpError: (e: OperationError) => Promise<void>,
+  onOpError: (e: OperationErrorDetails) => Promise<void>,
 ): Promise<T> {
   try {
     return await op();
@@ -71,21 +93,28 @@ export async function guardOperationException<T>(
       throw new OperationFailedAndReportedError(e.operationError);
     }
     if (e instanceof Error) {
-      const opErr = {
-        type: "exception",
-        message: e.message,
-        details: {},
-      };
+      const opErr = makeErrorDetails(
+        TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION,
+        `unexpected exception (message: ${e.message})`,
+        {},
+      );
       await onOpError(opErr);
       throw new OperationFailedAndReportedError(opErr);
     }
-    const opErr = {
-      type: "exception",
-      message: "unexpected exception thrown",
-      details: {
-        value: e.toString(),
-      },
-    };
+    // Something was thrown that is not even an exception!
+    // Try to stringify it.
+    let excString: string;
+    try {
+      excString = e.toString();
+    } catch (e) {
+      // Something went horribly wrong.
+      excString = "can't stringify exception";
+    }
+    const opErr = makeErrorDetails(
+      TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION,
+      `unexpected exception (not an exception, ${excString})`,
+      {},
+    );
     await onOpError(opErr);
     throw new OperationFailedAndReportedError(opErr);
   }
diff --git a/src/operations/exchanges.ts b/src/operations/exchanges.ts
index 6f5ff1d3..ff2cd3da 100644
--- a/src/operations/exchanges.ts
+++ b/src/operations/exchanges.ts
@@ -16,12 +16,11 @@
 
 import { InternalWalletState } from "./state";
 import {
-  ExchangeKeysJson,
   Denomination,
   codecForExchangeKeysJson,
   codecForExchangeWireJson,
 } from "../types/talerTypes";
-import { OperationError } from "../types/walletTypes";
+import { OperationErrorDetails } from "../types/walletTypes";
 import {
   ExchangeRecord,
   ExchangeUpdateStatus,
@@ -38,6 +37,7 @@ import { parsePaytoUri } from "../util/payto";
 import {
   OperationFailedAndReportedError,
   guardOperationException,
+  makeErrorDetails,
 } from "./errors";
 import {
   WALLET_CACHE_BREAKER_CLIENT_VERSION,
@@ -46,6 +46,11 @@ import {
 import { getTimestampNow } from "../util/time";
 import { compare } from "../util/libtoolVersion";
 import { createRecoupGroup, processRecoupGroup } from "./recoup";
+import { TalerErrorCode } from "../TalerErrorCode";
+import {
+  readSuccessResponseJsonOrThrow,
+  readSuccessResponseTextOrThrow,
+} from "../util/http";
 
 async function denominationRecordFromKeys(
   ws: InternalWalletState,
@@ -77,7 +82,7 @@ async function denominationRecordFromKeys(
 async function setExchangeError(
   ws: InternalWalletState,
   baseUrl: string,
-  err: OperationError,
+  err: OperationErrorDetails,
 ): Promise<void> {
   console.log(`last error for exchange ${baseUrl}:`, err);
   const mut = (exchange: ExchangeRecord): ExchangeRecord => {
@@ -102,88 +107,40 @@ async function updateExchangeWithKeys(
   if (existingExchangeRecord?.updateStatus != ExchangeUpdateStatus.FetchKeys) {
     return;
   }
+
   const keysUrl = new URL("keys", baseUrl);
   keysUrl.searchParams.set("cacheBreaker", 
WALLET_CACHE_BREAKER_CLIENT_VERSION);
 
-  let keysResp;
-  try {
-    const r = await ws.http.get(keysUrl.href);
-    if (r.status !== 200) {
-      throw Error(`unexpected status for keys: ${r.status}`);
-    }
-    keysResp = await r.json();
-  } catch (e) {
-    const m = `Fetching keys failed: ${e.message}`;
-    const opErr = {
-      type: "network",
-      details: {
-        requestUrl: e.config?.url,
-      },
-      message: m,
-    };
-    await setExchangeError(ws, baseUrl, opErr);
-    throw new OperationFailedAndReportedError(opErr);
-  }
-  let exchangeKeysJson: ExchangeKeysJson;
-  try {
-    exchangeKeysJson = codecForExchangeKeysJson().decode(keysResp);
-  } catch (e) {
-    const m = `Parsing /keys response failed: ${e.message}`;
-    const opErr = {
-      type: "protocol-violation",
-      details: {},
-      message: m,
-    };
-    await setExchangeError(ws, baseUrl, opErr);
-    throw new OperationFailedAndReportedError(opErr);
-  }
-
-  const lastUpdateTimestamp = exchangeKeysJson.list_issue_date;
-  if (!lastUpdateTimestamp) {
-    const m = `Parsing /keys response failed: invalid list_issue_date.`;
-    const opErr = {
-      type: "protocol-violation",
-      details: {},
-      message: m,
-    };
-    await setExchangeError(ws, baseUrl, opErr);
-    throw new OperationFailedAndReportedError(opErr);
-  }
+  const resp = await ws.http.get(keysUrl.href);
+  const exchangeKeysJson = await readSuccessResponseJsonOrThrow(
+    resp,
+    codecForExchangeKeysJson(),
+  );
 
   if (exchangeKeysJson.denoms.length === 0) {
-    const m = "exchange doesn't offer any denominations";
-    const opErr = {
-      type: "protocol-violation",
-      details: {},
-      message: m,
-    };
+    const opErr = makeErrorDetails(
+      TalerErrorCode.WALLET_EXCHANGE_DENOMINATIONS_INSUFFICIENT,
+      "exchange doesn't offer any denominations",
+      {
+        exchangeBaseUrl: baseUrl,
+      },
+    );
     await setExchangeError(ws, baseUrl, opErr);
     throw new OperationFailedAndReportedError(opErr);
   }
 
   const protocolVersion = exchangeKeysJson.version;
-  if (!protocolVersion) {
-    const m = "outdate exchange, no version in /keys response";
-    const opErr = {
-      type: "protocol-violation",
-      details: {},
-      message: m,
-    };
-    await setExchangeError(ws, baseUrl, opErr);
-    throw new OperationFailedAndReportedError(opErr);
-  }
 
   const versionRes = compare(WALLET_EXCHANGE_PROTOCOL_VERSION, 
protocolVersion);
   if (versionRes?.compatible != true) {
-    const m = "exchange protocol version not compatible with wallet";
-    const opErr = {
-      type: "protocol-incompatible",
-      details: {
+    const opErr = makeErrorDetails(
+      TalerErrorCode.WALLET_EXCHANGE_PROTOCOL_VERSION_INCOMPATIBLE,
+      "exchange protocol version not compatible with wallet",
+      {
         exchangeProtocolVersion: protocolVersion,
         walletProtocolVersion: WALLET_EXCHANGE_PROTOCOL_VERSION,
       },
-      message: m,
-    };
+    );
     await setExchangeError(ws, baseUrl, opErr);
     throw new OperationFailedAndReportedError(opErr);
   }
@@ -197,6 +154,8 @@ async function updateExchangeWithKeys(
     ),
   );
 
+  const lastUpdateTimestamp = getTimestampNow();
+
   const recoupGroupId: string | undefined = undefined;
 
   await ws.db.runWithWriteTransaction(
@@ -331,11 +290,7 @@ async function updateExchangeWithTermsOfService(
   };
 
   const resp = await ws.http.get(reqUrl.href, { headers });
-  if (resp.status !== 200) {
-    throw Error(`/terms response has unexpected status code (${resp.status})`);
-  }
-
-  const tosText = await resp.text();
+  const tosText = await readSuccessResponseTextOrThrow(resp);
   const tosEtag = resp.headers.get("etag") || undefined;
 
   await ws.db.runWithWriteTransaction([Stores.exchanges], async (tx) => {
@@ -393,14 +348,11 @@ async function updateExchangeWithWireInfo(
   reqUrl.searchParams.set("cacheBreaker", WALLET_CACHE_BREAKER_CLIENT_VERSION);
 
   const resp = await ws.http.get(reqUrl.href);
-  if (resp.status !== 200) {
-    throw Error(`/wire response has unexpected status code (${resp.status})`);
-  }
-  const wiJson = await resp.json();
-  if (!wiJson) {
-    throw Error("/wire response malformed");
-  }
-  const wireInfo = codecForExchangeWireJson().decode(wiJson);
+  const wireInfo = await readSuccessResponseJsonOrThrow(
+    resp,
+    codecForExchangeWireJson(),
+  );
+
   for (const a of wireInfo.accounts) {
     console.log("validating exchange acct");
     const isValid = await ws.cryptoApi.isValidWireAccount(
@@ -461,7 +413,7 @@ export async function updateExchangeFromUrl(
   baseUrl: string,
   forceNow = false,
 ): Promise<ExchangeRecord> {
-  const onOpErr = (e: OperationError): Promise<void> =>
+  const onOpErr = (e: OperationErrorDetails): Promise<void> =>
     setExchangeError(ws, baseUrl, e);
   return await guardOperationException(
     () => updateExchangeFromUrlImpl(ws, baseUrl, forceNow),
diff --git a/src/operations/pay.ts b/src/operations/pay.ts
index 74bfcc70..29b69783 100644
--- a/src/operations/pay.ts
+++ b/src/operations/pay.ts
@@ -38,7 +38,6 @@ import {
 } from "../types/dbTypes";
 import { NotificationType } from "../types/notifications";
 import {
-  PayReq,
   codecForProposal,
   codecForContractTerms,
   CoinDepositPermission,
@@ -46,7 +45,7 @@ import {
 } from "../types/talerTypes";
 import {
   ConfirmPayResult,
-  OperationError,
+  OperationErrorDetails,
   PreparePayResult,
   RefreshReason,
 } from "../types/walletTypes";
@@ -59,7 +58,10 @@ import { createRefreshGroup, getTotalRefreshCost } from 
"./refresh";
 import { InternalWalletState } from "./state";
 import { getTimestampNow, timestampAddDuration } from "../util/time";
 import { strcmp, canonicalJson } from "../util/helpers";
-import { httpPostTalerJson } from "../util/http";
+import {
+  readSuccessResponseJsonOrErrorCode,
+  readSuccessResponseJsonOrThrow,
+} from "../util/http";
 
 /**
  * Logger.
@@ -515,7 +517,7 @@ function getNextUrl(contractData: WalletContractData): 
string {
 async function incrementProposalRetry(
   ws: InternalWalletState,
   proposalId: string,
-  err: OperationError | undefined,
+  err: OperationErrorDetails | undefined,
 ): Promise<void> {
   await ws.db.runWithWriteTransaction([Stores.proposals], async (tx) => {
     const pr = await tx.get(Stores.proposals, proposalId);
@@ -538,7 +540,7 @@ async function incrementProposalRetry(
 async function incrementPurchasePayRetry(
   ws: InternalWalletState,
   proposalId: string,
-  err: OperationError | undefined,
+  err: OperationErrorDetails | undefined,
 ): Promise<void> {
   console.log("incrementing purchase pay retry with error", err);
   await ws.db.runWithWriteTransaction([Stores.purchases], async (tx) => {
@@ -554,7 +556,9 @@ async function incrementPurchasePayRetry(
     pr.lastPayError = err;
     await tx.put(Stores.purchases, pr);
   });
-  ws.notify({ type: NotificationType.PayOperationError });
+  if (err) {
+    ws.notify({ type: NotificationType.PayOperationError, error: err });
+  }
 }
 
 export async function processDownloadProposal(
@@ -562,7 +566,7 @@ export async function processDownloadProposal(
   proposalId: string,
   forceNow = false,
 ): Promise<void> {
-  const onOpErr = (err: OperationError): Promise<void> =>
+  const onOpErr = (err: OperationErrorDetails): Promise<void> =>
     incrementProposalRetry(ws, proposalId, err);
   await guardOperationException(
     () => processDownloadProposalImpl(ws, proposalId, forceNow),
@@ -604,14 +608,15 @@ async function processDownloadProposalImpl(
   ).href;
   logger.trace("downloading contract from '" + orderClaimUrl + "'");
 
-  const proposalResp = await httpPostTalerJson({
-    url: orderClaimUrl,
-    body: {
-      nonce: proposal.noncePub,
-    },
-    codec: codecForProposal(),
-    http: ws.http,
-  });
+  const reqestBody = {
+    nonce: proposal.noncePub,
+  };
+
+  const resp = await ws.http.postJson(orderClaimUrl, reqestBody);
+  const proposalResp = await readSuccessResponseJsonOrThrow(
+    resp,
+    codecForProposal(),
+  );
 
   // The proposalResp contains the contract terms as raw JSON,
   // as the coded to parse them doesn't necessarily round-trip.
@@ -779,15 +784,17 @@ export async function submitPay(
     purchase.contractData.merchantBaseUrl,
   ).href;
 
-  const merchantResp = await httpPostTalerJson({
-    url: payUrl,
-    body: {
-      coins: purchase.coinDepositPermissions,
-      session_id: purchase.lastSessionId,
-    },
-    codec: codecForMerchantPayResponse(),
-    http: ws.http,
-  });
+  const reqBody = {
+    coins: purchase.coinDepositPermissions,
+    session_id: purchase.lastSessionId,
+  };
+
+  const resp = await ws.http.postJson(payUrl, reqBody);
+
+  const merchantResp = await readSuccessResponseJsonOrThrow(
+    resp,
+    codecForMerchantPayResponse(),
+  );
 
   console.log("got success from pay URL", merchantResp);
 
@@ -1050,7 +1057,7 @@ export async function processPurchasePay(
   proposalId: string,
   forceNow = false,
 ): Promise<void> {
-  const onOpErr = (e: OperationError): Promise<void> =>
+  const onOpErr = (e: OperationErrorDetails): Promise<void> =>
     incrementPurchasePayRetry(ws, proposalId, e);
   await guardOperationException(
     () => processPurchasePayImpl(ws, proposalId, forceNow),
diff --git a/src/operations/recoup.ts b/src/operations/recoup.ts
index d1b3c3bd..445d029c 100644
--- a/src/operations/recoup.ts
+++ b/src/operations/recoup.ts
@@ -44,17 +44,17 @@ import { forceQueryReserve } from "./reserves";
 
 import { Amounts } from "../util/amounts";
 import { createRefreshGroup, processRefreshGroup } from "./refresh";
-import { RefreshReason, OperationError } from "../types/walletTypes";
+import { RefreshReason, OperationErrorDetails } from "../types/walletTypes";
 import { TransactionHandle } from "../util/query";
 import { encodeCrock, getRandomBytes } from "../crypto/talerCrypto";
 import { getTimestampNow } from "../util/time";
 import { guardOperationException } from "./errors";
-import { httpPostTalerJson } from "../util/http";
+import { readSuccessResponseJsonOrThrow } from "../util/http";
 
 async function incrementRecoupRetry(
   ws: InternalWalletState,
   recoupGroupId: string,
-  err: OperationError | undefined,
+  err: OperationErrorDetails | undefined,
 ): Promise<void> {
   await ws.db.runWithWriteTransaction([Stores.recoupGroups], async (tx) => {
     const r = await tx.get(Stores.recoupGroups, recoupGroupId);
@@ -69,7 +69,9 @@ async function incrementRecoupRetry(
     r.lastError = err;
     await tx.put(Stores.recoupGroups, r);
   });
-  ws.notify({ type: NotificationType.RecoupOperationError });
+  if (err) {
+    ws.notify({ type: NotificationType.RecoupOperationError, error: err });
+  }
 }
 
 async function putGroupAsFinished(
@@ -147,12 +149,11 @@ async function recoupWithdrawCoin(
 
   const recoupRequest = await ws.cryptoApi.createRecoupRequest(coin);
   const reqUrl = new URL(`/coins/${coin.coinPub}/recoup`, 
coin.exchangeBaseUrl);
-  const recoupConfirmation = await httpPostTalerJson({
-    url: reqUrl.href,
-    body: recoupRequest,
-    codec: codecForRecoupConfirmation(),
-    http: ws.http,
-  });
+  const resp = await ws.http.postJson(reqUrl.href, recoupRequest);
+  const recoupConfirmation = await readSuccessResponseJsonOrThrow(
+    resp,
+    codecForRecoupConfirmation(),
+  );
 
   if (recoupConfirmation.reserve_pub !== reservePub) {
     throw Error(`Coin's reserve doesn't match reserve on recoup`);
@@ -222,13 +223,12 @@ async function recoupRefreshCoin(
   const recoupRequest = await ws.cryptoApi.createRecoupRequest(coin);
   const reqUrl = new URL(`/coins/${coin.coinPub}/recoup`, 
coin.exchangeBaseUrl);
   console.log("making recoup request");
-  
-  const recoupConfirmation = await httpPostTalerJson({
-    url: reqUrl.href,
-    body: recoupRequest,
-    codec: codecForRecoupConfirmation(),
-    http: ws.http,
-  });
+
+  const resp = await ws.http.postJson(reqUrl.href, recoupRequest);
+  const recoupConfirmation = await readSuccessResponseJsonOrThrow(
+    resp,
+    codecForRecoupConfirmation(),
+  );
 
   if (recoupConfirmation.old_coin_pub != cs.oldCoinPub) {
     throw Error(`Coin's oldCoinPub doesn't match reserve on recoup`);
@@ -298,7 +298,7 @@ export async function processRecoupGroup(
   forceNow = false,
 ): Promise<void> {
   await ws.memoProcessRecoup.memo(recoupGroupId, async () => {
-    const onOpErr = (e: OperationError): Promise<void> =>
+    const onOpErr = (e: OperationErrorDetails): Promise<void> =>
       incrementRecoupRetry(ws, recoupGroupId, e);
     return await guardOperationException(
       async () => await processRecoupGroupImpl(ws, recoupGroupId, forceNow),
diff --git a/src/operations/refresh.ts b/src/operations/refresh.ts
index 2d7ffad2..4d477d64 100644
--- a/src/operations/refresh.ts
+++ b/src/operations/refresh.ts
@@ -34,7 +34,7 @@ import { Logger } from "../util/logging";
 import { getWithdrawDenomList } from "./withdraw";
 import { updateExchangeFromUrl } from "./exchanges";
 import {
-  OperationError,
+  OperationErrorDetails,
   CoinPublicKey,
   RefreshReason,
   RefreshGroupId,
@@ -43,6 +43,11 @@ import { guardOperationException } from "./errors";
 import { NotificationType } from "../types/notifications";
 import { getRandomBytes, encodeCrock } from "../crypto/talerCrypto";
 import { getTimestampNow } from "../util/time";
+import { readSuccessResponseJsonOrThrow } from "../util/http";
+import {
+  codecForExchangeMeltResponse,
+  codecForExchangeRevealResponse,
+} from "../types/talerTypes";
 
 const logger = new Logger("refresh.ts");
 
@@ -243,34 +248,12 @@ async function refreshMelt(
   };
   logger.trace(`melt request for coin:`, meltReq);
   const resp = await ws.http.postJson(reqUrl.href, meltReq);
-  if (resp.status !== 200) {
-    console.log(`got status ${resp.status} for refresh/melt`);
-    try {
-      const respJson = await resp.json();
-      console.log(
-        `body of refresh/melt error response:`,
-        JSON.stringify(respJson, undefined, 2),
-      );
-    } catch (e) {
-      console.log(`body of refresh/melt error response is not JSON`);
-    }
-    throw Error(`unexpected status code ${resp.status} for refresh/melt`);
-  }
-
-  const respJson = await resp.json();
-
-  logger.trace("melt response:", respJson);
-
-  if (resp.status !== 200) {
-    console.error(respJson);
-    throw Error("refresh failed");
-  }
-
-  const norevealIndex = respJson.noreveal_index;
+  const meltResponse = await readSuccessResponseJsonOrThrow(
+    resp,
+    codecForExchangeMeltResponse(),
+  );
 
-  if (typeof norevealIndex !== "number") {
-    throw Error("invalid response");
-  }
+  const norevealIndex = meltResponse.noreveal_index;
 
   refreshSession.norevealIndex = norevealIndex;
 
@@ -355,30 +338,15 @@ async function refreshReveal(
     refreshSession.exchangeBaseUrl,
   );
 
-  let resp;
-  try {
-    resp = await ws.http.postJson(reqUrl.href, req);
-  } catch (e) {
-    console.error("got error during /refresh/reveal request");
-    console.error(e);
-    return;
-  }
-
-  if (resp.status !== 200) {
-    console.error("error: /refresh/reveal returned status " + resp.status);
-    return;
-  }
-
-  const respJson = await resp.json();
-
-  if (!respJson.ev_sigs || !Array.isArray(respJson.ev_sigs)) {
-    console.error("/refresh/reveal did not contain ev_sigs");
-    return;
-  }
+  const resp = await ws.http.postJson(reqUrl.href, req);
+  const reveal = await readSuccessResponseJsonOrThrow(
+    resp,
+    codecForExchangeRevealResponse(),
+  );
 
   const coins: CoinRecord[] = [];
 
-  for (let i = 0; i < respJson.ev_sigs.length; i++) {
+  for (let i = 0; i < reveal.ev_sigs.length; i++) {
     const denom = await ws.db.get(Stores.denominations, [
       refreshSession.exchangeBaseUrl,
       refreshSession.newDenoms[i],
@@ -389,7 +357,7 @@ async function refreshReveal(
     }
     const pc = refreshSession.planchetsForGammas[norevealIndex][i];
     const denomSig = await ws.cryptoApi.rsaUnblind(
-      respJson.ev_sigs[i].ev_sig,
+      reveal.ev_sigs[i].ev_sig,
       pc.blindingKey,
       denom.denomPub,
     );
@@ -457,7 +425,7 @@ async function refreshReveal(
 async function incrementRefreshRetry(
   ws: InternalWalletState,
   refreshGroupId: string,
-  err: OperationError | undefined,
+  err: OperationErrorDetails | undefined,
 ): Promise<void> {
   await ws.db.runWithWriteTransaction([Stores.refreshGroups], async (tx) => {
     const r = await tx.get(Stores.refreshGroups, refreshGroupId);
@@ -472,7 +440,9 @@ async function incrementRefreshRetry(
     r.lastError = err;
     await tx.put(Stores.refreshGroups, r);
   });
-  ws.notify({ type: NotificationType.RefreshOperationError });
+  if (err) {
+    ws.notify({ type: NotificationType.RefreshOperationError, error: err });
+  }
 }
 
 export async function processRefreshGroup(
@@ -481,7 +451,7 @@ export async function processRefreshGroup(
   forceNow = false,
 ): Promise<void> {
   await ws.memoProcessRefresh.memo(refreshGroupId, async () => {
-    const onOpErr = (e: OperationError): Promise<void> =>
+    const onOpErr = (e: OperationErrorDetails): Promise<void> =>
       incrementRefreshRetry(ws, refreshGroupId, e);
     return await guardOperationException(
       async () => await processRefreshGroupImpl(ws, refreshGroupId, forceNow),
diff --git a/src/operations/refund.ts b/src/operations/refund.ts
index 5f6ccf9d..1d6561bd 100644
--- a/src/operations/refund.ts
+++ b/src/operations/refund.ts
@@ -25,7 +25,7 @@
  */
 import { InternalWalletState } from "./state";
 import {
-  OperationError,
+  OperationErrorDetails,
   RefreshReason,
   CoinPublicKey,
 } from "../types/walletTypes";
@@ -52,15 +52,18 @@ import { randomBytes } from 
"../crypto/primitives/nacl-fast";
 import { encodeCrock } from "../crypto/talerCrypto";
 import { getTimestampNow } from "../util/time";
 import { Logger } from "../util/logging";
+import { readSuccessResponseJsonOrThrow } from "../util/http";
 
 const logger = new Logger("refund.ts");
 
+/**
+ * Retry querying and applying refunds for an order later.
+ */
 async function incrementPurchaseQueryRefundRetry(
   ws: InternalWalletState,
   proposalId: string,
-  err: OperationError | undefined,
+  err: OperationErrorDetails | undefined,
 ): Promise<void> {
-  console.log("incrementing purchase refund query retry with error", err);
   await ws.db.runWithWriteTransaction([Stores.purchases], async (tx) => {
     const pr = await tx.get(Stores.purchases, proposalId);
     if (!pr) {
@@ -74,54 +77,12 @@ async function incrementPurchaseQueryRefundRetry(
     pr.lastRefundStatusError = err;
     await tx.put(Stores.purchases, pr);
   });
-  ws.notify({ type: NotificationType.RefundStatusOperationError });
-}
-
-export async function getFullRefundFees(
-  ws: InternalWalletState,
-  refundPermissions: MerchantRefundDetails[],
-): Promise<AmountJson> {
-  if (refundPermissions.length === 0) {
-    throw Error("no refunds given");
-  }
-  const coin0 = await ws.db.get(Stores.coins, refundPermissions[0].coin_pub);
-  if (!coin0) {
-    throw Error("coin not found");
-  }
-  let feeAcc = Amounts.getZero(
-    Amounts.parseOrThrow(refundPermissions[0].refund_amount).currency,
-  );
-
-  const denoms = await ws.db
-    .iterIndex(Stores.denominations.exchangeBaseUrlIndex, 
coin0.exchangeBaseUrl)
-    .toArray();
-
-  for (const rp of refundPermissions) {
-    const coin = await ws.db.get(Stores.coins, rp.coin_pub);
-    if (!coin) {
-      throw Error("coin not found");
-    }
-    const denom = await ws.db.get(Stores.denominations, [
-      coin0.exchangeBaseUrl,
-      coin.denomPub,
-    ]);
-    if (!denom) {
-      throw Error(`denom not found (${coin.denomPub})`);
-    }
-    // FIXME:  this assumes that the refund already happened.
-    // When it hasn't, the refresh cost is inaccurate.  To fix this,
-    // we need introduce a flag to tell if a coin was refunded or
-    // refreshed normally (and what about incremental refunds?)
-    const refundAmount = Amounts.parseOrThrow(rp.refund_amount);
-    const refundFee = Amounts.parseOrThrow(rp.refund_fee);
-    const refreshCost = getTotalRefreshCost(
-      denoms,
-      denom,
-      Amounts.sub(refundAmount, refundFee).amount,
-    );
-    feeAcc = Amounts.add(feeAcc, refreshCost, refundFee).amount;
+  if (err) {
+    ws.notify({
+      type: NotificationType.RefundStatusOperationError,
+      error: err,
+    });
   }
-  return feeAcc;
 }
 
 function getRefundKey(d: MerchantRefundDetails): string {
@@ -310,14 +271,14 @@ async function acceptRefundResponse(
         p.lastRefundStatusError = undefined;
         p.refundStatusRetryInfo = initRetryInfo(false);
         p.refundStatusRequested = false;
-        console.log("refund query done");
+        logger.trace("refund query done");
       } else {
         // No error, but we need to try again!
         p.timestampLastRefundStatus = now;
         p.refundStatusRetryInfo.retryCounter++;
         updateRetryInfoTimeout(p.refundStatusRetryInfo);
         p.lastRefundStatusError = undefined;
-        console.log("refund query not done");
+        logger.trace("refund query not done");
       }
 
       p.refundsRefreshCost = { ...p.refundsRefreshCost, ...refundsRefreshCost 
};
@@ -369,7 +330,7 @@ async function startRefundQuery(
     async (tx) => {
       const p = await tx.get(Stores.purchases, proposalId);
       if (!p) {
-        console.log("no purchase found for refund URL");
+        logger.error("no purchase found for refund URL");
         return false;
       }
       p.refundStatusRequested = true;
@@ -401,7 +362,7 @@ export async function applyRefund(
 ): Promise<{ contractTermsHash: string; proposalId: string }> {
   const parseResult = parseRefundUri(talerRefundUri);
 
-  console.log("applying refund", parseResult);
+  logger.trace("applying refund", parseResult);
 
   if (!parseResult) {
     throw Error("invalid refund URI");
@@ -432,7 +393,7 @@ export async function processPurchaseQueryRefund(
   proposalId: string,
   forceNow = false,
 ): Promise<void> {
-  const onOpErr = (e: OperationError): Promise<void> =>
+  const onOpErr = (e: OperationErrorDetails): Promise<void> =>
     incrementPurchaseQueryRefundRetry(ws, proposalId, e);
   await guardOperationException(
     () => processPurchaseQueryRefundImpl(ws, proposalId, forceNow),
@@ -464,27 +425,23 @@ async function processPurchaseQueryRefundImpl(
   if (!purchase) {
     return;
   }
+
   if (!purchase.refundStatusRequested) {
     return;
   }
 
-  const refundUrlObj = new URL("refund", 
purchase.contractData.merchantBaseUrl);
-  refundUrlObj.searchParams.set("order_id", purchase.contractData.orderId);
-  const refundUrl = refundUrlObj.href;
-  let resp;
-  try {
-    resp = await ws.http.get(refundUrl);
-  } catch (e) {
-    console.error("error downloading refund permission", e);
-    throw e;
-  }
-  if (resp.status !== 200) {
-    throw Error(`unexpected status code (${resp.status}) for /refund`);
-  }
+  const request = await ws.http.get(
+    new URL(
+      `orders/${purchase.contractData.orderId}`,
+      purchase.contractData.merchantBaseUrl,
+    ).href,
+  );
 
-  const refundResponse = codecForMerchantRefundResponse().decode(
-    await resp.json(),
+  const refundResponse = await readSuccessResponseJsonOrThrow(
+    request,
+    codecForMerchantRefundResponse(),
   );
+
   await acceptRefundResponse(
     ws,
     proposalId,
diff --git a/src/operations/reserves.ts b/src/operations/reserves.ts
index 365d6e22..e6b09316 100644
--- a/src/operations/reserves.ts
+++ b/src/operations/reserves.ts
@@ -17,7 +17,7 @@
 import {
   CreateReserveRequest,
   CreateReserveResponse,
-  OperationError,
+  OperationErrorDetails,
   AcceptWithdrawalResponse,
 } from "../types/walletTypes";
 import { canonicalizeBaseUrl } from "../util/helpers";
@@ -56,7 +56,7 @@ import {
 import {
   guardOperationException,
   OperationFailedAndReportedError,
-  OperationFailedError,
+  makeErrorDetails,
 } from "./errors";
 import { NotificationType } from "../types/notifications";
 import { codecForReserveStatus } from "../types/ReserveStatus";
@@ -67,6 +67,11 @@ import {
 } from "../util/reserveHistoryUtil";
 import { TransactionHandle } from "../util/query";
 import { addPaytoQueryParams } from "../util/payto";
+import { TalerErrorCode } from "../TalerErrorCode";
+import {
+  readSuccessResponseJsonOrErrorCode,
+  throwUnexpectedRequestError,
+} from "../util/http";
 
 const logger = new Logger("reserves.ts");
 
@@ -107,7 +112,9 @@ export async function createReserve(
 
   if (req.bankWithdrawStatusUrl) {
     if (!req.exchangePaytoUri) {
-      throw Error("Exchange payto URI must be specified for a bank-integrated 
withdrawal");
+      throw Error(
+        "Exchange payto URI must be specified for a bank-integrated 
withdrawal",
+      );
     }
     bankInfo = {
       statusUrl: req.bankWithdrawStatusUrl,
@@ -285,7 +292,7 @@ export async function processReserve(
   forceNow = false,
 ): Promise<void> {
   return ws.memoProcessReserve.memo(reservePub, async () => {
-    const onOpError = (err: OperationError): Promise<void> =>
+    const onOpError = (err: OperationErrorDetails): Promise<void> =>
       incrementReserveRetry(ws, reservePub, err);
     await guardOperationException(
       () => processReserveImpl(ws, reservePub, forceNow),
@@ -344,7 +351,7 @@ export async function processReserveBankStatus(
   ws: InternalWalletState,
   reservePub: string,
 ): Promise<void> {
-  const onOpError = (err: OperationError): Promise<void> =>
+  const onOpError = (err: OperationErrorDetails): Promise<void> =>
     incrementReserveRetry(ws, reservePub, err);
   await guardOperationException(
     () => processReserveBankStatusImpl(ws, reservePub),
@@ -423,7 +430,7 @@ async function processReserveBankStatusImpl(
 async function incrementReserveRetry(
   ws: InternalWalletState,
   reservePub: string,
-  err: OperationError | undefined,
+  err: OperationErrorDetails | undefined,
 ): Promise<void> {
   await ws.db.runWithWriteTransaction([Stores.reserves], async (tx) => {
     const r = await tx.get(Stores.reserves, reservePub);
@@ -444,7 +451,7 @@ async function incrementReserveRetry(
   if (err) {
     ws.notify({
       type: NotificationType.ReserveOperationError,
-      operationError: err,
+      error: err,
     });
   }
 }
@@ -466,35 +473,32 @@ async function updateReserve(
     return;
   }
 
-  const reqUrl = new URL(`reserves/${reservePub}`, reserve.exchangeBaseUrl);
-  let resp;
-  try {
-    resp = await ws.http.get(reqUrl.href);
-    console.log("got reserves/${RESERVE_PUB} response", await resp.json());
-    if (resp.status === 404) {
-      const m = "reserve not known to the exchange yet";
-      throw new OperationFailedError({
-        type: "waiting",
-        message: m,
-        details: {},
+  const resp = await ws.http.get(
+    new URL(`reserves/${reservePub}`, reserve.exchangeBaseUrl).href,
+  );
+
+  const result = await readSuccessResponseJsonOrErrorCode(
+    resp,
+    codecForReserveStatus(),
+  );
+  if (result.isError) {
+    if (
+      resp.status === 404 &&
+      result.talerErrorResponse.code === TalerErrorCode.RESERVE_STATUS_UNKNOWN
+    ) {
+      ws.notify({
+        type: NotificationType.ReserveNotYetFound,
+        reservePub,
       });
+      await incrementReserveRetry(ws, reservePub, undefined);
+      return;
+    } else {
+      throwUnexpectedRequestError(resp, result.talerErrorResponse);
     }
-    if (resp.status !== 200) {
-      throw Error(`unexpected status code ${resp.status} for reserve/status`);
-    }
-  } catch (e) {
-    logger.trace("caught exception for reserve/status");
-    const m = e.message;
-    const opErr = {
-      type: "network",
-      details: {},
-      message: m,
-    };
-    await incrementReserveRetry(ws, reservePub, opErr);
-    throw new OperationFailedAndReportedError(opErr);
   }
-  const respJson = await resp.json();
-  const reserveInfo = codecForReserveStatus().decode(respJson);
+
+  const reserveInfo = result.response;
+
   const balance = Amounts.parseOrThrow(reserveInfo.balance);
   const currency = balance.currency;
   await ws.db.runWithWriteTransaction(
@@ -656,14 +660,12 @@ async function depleteReserve(
     // Only complain about inability to withdraw if we
     // didn't withdraw before.
     if (Amounts.isZero(summary.withdrawnAmount)) {
-      const m = `Unable to withdraw from reserve, no denominations are 
available to withdraw.`;
-      const opErr = {
-        type: "internal",
-        message: m,
-        details: {},
-      };
+      const opErr = makeErrorDetails(
+        TalerErrorCode.WALLET_EXCHANGE_DENOMINATIONS_INSUFFICIENT,
+        `Unable to withdraw from reserve, no denominations are available to 
withdraw.`,
+        {},
+      );
       await incrementReserveRetry(ws, reserve.reservePub, opErr);
-      console.log(m);
       throw new OperationFailedAndReportedError(opErr);
     }
     return;
diff --git a/src/operations/tip.ts b/src/operations/tip.ts
index 1ae7700a..d121b1cb 100644
--- a/src/operations/tip.ts
+++ b/src/operations/tip.ts
@@ -16,7 +16,7 @@
 
 import { InternalWalletState } from "./state";
 import { parseTipUri } from "../util/taleruri";
-import { TipStatus, OperationError } from "../types/walletTypes";
+import { TipStatus, OperationErrorDetails } from "../types/walletTypes";
 import {
   TipPlanchetDetail,
   codecForTipPickupGetResponse,
@@ -43,6 +43,7 @@ import { getRandomBytes, encodeCrock } from 
"../crypto/talerCrypto";
 import { guardOperationException } from "./errors";
 import { NotificationType } from "../types/notifications";
 import { getTimestampNow } from "../util/time";
+import { readSuccessResponseJsonOrThrow } from "../util/http";
 
 export async function getTipStatus(
   ws: InternalWalletState,
@@ -57,13 +58,10 @@ export async function getTipStatus(
   tipStatusUrl.searchParams.set("tip_id", res.merchantTipId);
   console.log("checking tip status from", tipStatusUrl.href);
   const merchantResp = await ws.http.get(tipStatusUrl.href);
-  if (merchantResp.status !== 200) {
-    throw Error(`unexpected status ${merchantResp.status} for tip-pickup`);
-  }
-  const respJson = await merchantResp.json();
-  console.log("resp:", respJson);
-  const tipPickupStatus = codecForTipPickupGetResponse().decode(respJson);
-
+  const tipPickupStatus = await readSuccessResponseJsonOrThrow(
+    merchantResp,
+    codecForTipPickupGetResponse(),
+  );
   console.log("status", tipPickupStatus);
 
   const amount = Amounts.parseOrThrow(tipPickupStatus.amount);
@@ -133,7 +131,7 @@ export async function getTipStatus(
 async function incrementTipRetry(
   ws: InternalWalletState,
   refreshSessionId: string,
-  err: OperationError | undefined,
+  err: OperationErrorDetails | undefined,
 ): Promise<void> {
   await ws.db.runWithWriteTransaction([Stores.tips], async (tx) => {
     const t = await tx.get(Stores.tips, refreshSessionId);
@@ -156,7 +154,7 @@ export async function processTip(
   tipId: string,
   forceNow = false,
 ): Promise<void> {
-  const onOpErr = (e: OperationError): Promise<void> =>
+  const onOpErr = (e: OperationErrorDetails): Promise<void> =>
     incrementTipRetry(ws, tipId, e);
   await guardOperationException(
     () => processTipImpl(ws, tipId, forceNow),
diff --git a/src/operations/transactions.ts b/src/operations/transactions.ts
index 85cd8716..f104f107 100644
--- a/src/operations/transactions.ts
+++ b/src/operations/transactions.ts
@@ -177,50 +177,57 @@ export async function getTransactions(
         }
 
         switch (wsr.source.type) {
-          case WithdrawalSourceType.Reserve: {
-            const r = await tx.get(Stores.reserves, wsr.source.reservePub);
-            if (!r) {
-              break;
-            }
-            let amountRaw: AmountJson | undefined = undefined;
-            if (wsr.withdrawalGroupId === r.initialWithdrawalGroupId) {
-              amountRaw = r.instructedAmount;
-            } else {
-              amountRaw = wsr.denomsSel.totalWithdrawCost;
-            }
-            let withdrawalDetails: WithdrawalDetails;
-            if (r.bankInfo) {
+          case WithdrawalSourceType.Reserve:
+            {
+              const r = await tx.get(Stores.reserves, wsr.source.reservePub);
+              if (!r) {
+                break;
+              }
+              let amountRaw: AmountJson | undefined = undefined;
+              if (wsr.withdrawalGroupId === r.initialWithdrawalGroupId) {
+                amountRaw = r.instructedAmount;
+              } else {
+                amountRaw = wsr.denomsSel.totalWithdrawCost;
+              }
+              let withdrawalDetails: WithdrawalDetails;
+              if (r.bankInfo) {
                 withdrawalDetails = {
                   type: WithdrawalType.TalerBankIntegrationApi,
                   confirmed: true,
                   bankConfirmationUrl: r.bankInfo.confirmUrl,
                 };
-            } else {
-              const exchange = await tx.get(Stores.exchanges, 
r.exchangeBaseUrl);
-              if (!exchange) {
-                // FIXME: report somehow
-                break;
+              } else {
+                const exchange = await tx.get(
+                  Stores.exchanges,
+                  r.exchangeBaseUrl,
+                );
+                if (!exchange) {
+                  // FIXME: report somehow
+                  break;
+                }
+                withdrawalDetails = {
+                  type: WithdrawalType.ManualTransfer,
+                  exchangePaytoUris:
+                    exchange.wireInfo?.accounts.map((x) => x.payto_uri) ?? [],
+                };
               }
-              withdrawalDetails = {
-                type: WithdrawalType.ManualTransfer,
-                exchangePaytoUris: exchange.wireInfo?.accounts.map((x) => 
x.payto_uri) ?? [],
-              };
+              transactions.push({
+                type: TransactionType.Withdrawal,
+                amountEffective: Amounts.stringify(
+                  wsr.denomsSel.totalCoinValue,
+                ),
+                amountRaw: Amounts.stringify(amountRaw),
+                withdrawalDetails,
+                exchangeBaseUrl: wsr.exchangeBaseUrl,
+                pending: !wsr.timestampFinish,
+                timestamp: wsr.timestampStart,
+                transactionId: makeEventId(
+                  TransactionType.Withdrawal,
+                  wsr.withdrawalGroupId,
+                ),
+              });
             }
-            transactions.push({
-              type: TransactionType.Withdrawal,
-              amountEffective: Amounts.stringify(wsr.denomsSel.totalCoinValue),
-              amountRaw: Amounts.stringify(amountRaw),
-              withdrawalDetails,
-              exchangeBaseUrl: wsr.exchangeBaseUrl,
-              pending: !wsr.timestampFinish,
-              timestamp: wsr.timestampStart,
-              transactionId: makeEventId(
-                TransactionType.Withdrawal,
-                wsr.withdrawalGroupId,
-              ),
-            });
-          }
-          break;
+            break;
           default:
             // Tips are reported via their own event
             break;
@@ -254,7 +261,7 @@ export async function getTransactions(
             type: WithdrawalType.TalerBankIntegrationApi,
             confirmed: false,
             bankConfirmationUrl: r.bankInfo.confirmUrl,
-          }
+          };
         } else {
           withdrawalDetails = {
             type: WithdrawalType.ManualTransfer,
@@ -264,9 +271,7 @@ export async function getTransactions(
         transactions.push({
           type: TransactionType.Withdrawal,
           amountRaw: Amounts.stringify(r.instructedAmount),
-          amountEffective: Amounts.stringify(
-            r.initialDenomSel.totalCoinValue,
-          ),
+          amountEffective: Amounts.stringify(r.initialDenomSel.totalCoinValue),
           exchangeBaseUrl: r.exchangeBaseUrl,
           pending: true,
           timestamp: r.timestampCreated,
diff --git a/src/operations/withdraw.ts b/src/operations/withdraw.ts
index 98969d21..f7879dfe 100644
--- a/src/operations/withdraw.ts
+++ b/src/operations/withdraw.ts
@@ -33,7 +33,7 @@ import {
   BankWithdrawDetails,
   ExchangeWithdrawDetails,
   WithdrawalDetailsResponse,
-  OperationError,
+  OperationErrorDetails,
 } from "../types/walletTypes";
 import {
   codecForWithdrawOperationStatusResponse,
@@ -54,7 +54,7 @@ import {
   timestampCmp,
   timestampSubtractDuraction,
 } from "../util/time";
-import { httpPostTalerJson } from "../util/http";
+import { readSuccessResponseJsonOrThrow } from "../util/http";
 
 const logger = new Logger("withdraw.ts");
 
@@ -142,14 +142,11 @@ export async function getBankWithdrawalInfo(
     throw Error(`can't parse URL ${talerWithdrawUri}`);
   }
   const resp = await ws.http.get(uriResult.statusUrl);
-  if (resp.status !== 200) {
-    throw Error(
-      `unexpected status (${resp.status}) from bank for 
${uriResult.statusUrl}`,
-    );
-  }
-  const respJson = await resp.json();
+  const status = await readSuccessResponseJsonOrThrow(
+    resp,
+    codecForWithdrawOperationStatusResponse(),
+  );
 
-  const status = codecForWithdrawOperationStatusResponse().decode(respJson);
   return {
     amount: Amounts.parseOrThrow(status.amount),
     confirmTransferUrl: status.confirm_transfer_url,
@@ -310,12 +307,11 @@ async function processPlanchet(
     exchange.baseUrl,
   ).href;
 
-  const r = await httpPostTalerJson({
-    url: reqUrl,
-    body: wd,
-    codec: codecForWithdrawResponse(),
-    http: ws.http,
-  });
+  const resp = await ws.http.postJson(reqUrl, wd);
+  const r = await readSuccessResponseJsonOrThrow(
+    resp,
+    codecForWithdrawResponse(),
+  );
 
   logger.trace(`got response for /withdraw`);
 
@@ -505,7 +501,7 @@ export async function selectWithdrawalDenoms(
 async function incrementWithdrawalRetry(
   ws: InternalWalletState,
   withdrawalGroupId: string,
-  err: OperationError | undefined,
+  err: OperationErrorDetails | undefined,
 ): Promise<void> {
   await ws.db.runWithWriteTransaction([Stores.withdrawalGroups], async (tx) => 
{
     const wsr = await tx.get(Stores.withdrawalGroups, withdrawalGroupId);
@@ -530,7 +526,7 @@ export async function processWithdrawGroup(
   withdrawalGroupId: string,
   forceNow = false,
 ): Promise<void> {
-  const onOpErr = (e: OperationError): Promise<void> =>
+  const onOpErr = (e: OperationErrorDetails): Promise<void> =>
     incrementWithdrawalRetry(ws, withdrawalGroupId, e);
   await guardOperationException(
     () => processWithdrawGroupImpl(ws, withdrawalGroupId, forceNow),
diff --git a/src/types/dbTypes.ts b/src/types/dbTypes.ts
index b085f83d..252649b0 100644
--- a/src/types/dbTypes.ts
+++ b/src/types/dbTypes.ts
@@ -28,7 +28,6 @@ import {
   Auditor,
   CoinDepositPermission,
   MerchantRefundDetails,
-  PayReq,
   TipResponse,
   ExchangeSignKeyJson,
   MerchantInfo,
@@ -36,7 +35,7 @@ import {
 } from "./talerTypes";
 
 import { Index, Store } from "../util/query";
-import { OperationError, RefreshReason } from "./walletTypes";
+import { OperationErrorDetails, RefreshReason } from "./walletTypes";
 import {
   ReserveTransaction,
   ReserveCreditTransaction,
@@ -319,7 +318,7 @@ export interface ReserveRecord {
    * Last error that happened in a reserve operation
    * (either talking to the bank or the exchange).
    */
-  lastError: OperationError | undefined;
+  lastError: OperationErrorDetails | undefined;
 }
 
 /**
@@ -633,7 +632,7 @@ export interface ExchangeRecord {
    */
   updateDiff: ExchangeUpdateDiff | undefined;
 
-  lastError?: OperationError;
+  lastError?: OperationErrorDetails;
 }
 
 /**
@@ -890,14 +889,14 @@ export interface ProposalRecord {
    */
   retryInfo: RetryInfo;
 
-  lastError: OperationError | undefined;
+  lastError: OperationErrorDetails | undefined;
 }
 
 /**
  * Status of a tip we got from a merchant.
  */
 export interface TipRecord {
-  lastError: OperationError | undefined;
+  lastError: OperationErrorDetails | undefined;
 
   /**
    * Has the user accepted the tip?  Only after the tip has been accepted coins
@@ -982,9 +981,9 @@ export interface RefreshGroupRecord {
    */
   retryInfo: RetryInfo;
 
-  lastError: OperationError | undefined;
+  lastError: OperationErrorDetails | undefined;
 
-  lastErrorPerCoin: { [coinIndex: number]: OperationError };
+  lastErrorPerCoin: { [coinIndex: number]: OperationErrorDetails };
 
   refreshGroupId: string;
 
@@ -1012,7 +1011,7 @@ export interface RefreshGroupRecord {
  * Ongoing refresh
  */
 export interface RefreshSessionRecord {
-  lastError: OperationError | undefined;
+  lastError: OperationErrorDetails | undefined;
 
   /**
    * Public key that's being melted in this session.
@@ -1330,7 +1329,7 @@ export interface PurchaseRecord {
 
   payRetryInfo: RetryInfo;
 
-  lastPayError: OperationError | undefined;
+  lastPayError: OperationErrorDetails | undefined;
 
   /**
    * Retry information for querying the refund status with the merchant.
@@ -1340,7 +1339,7 @@ export interface PurchaseRecord {
   /**
    * Last error (or undefined) for querying the refund status with the 
merchant.
    */
-  lastRefundStatusError: OperationError | undefined;
+  lastRefundStatusError: OperationErrorDetails | undefined;
 
   /**
    * Continue querying the refund status until this deadline has expired.
@@ -1448,7 +1447,7 @@ export interface DenomSelectionState {
 /**
  * Group of withdrawal operations that need to be executed.
  * (Either for a normal withdrawal or from a tip.)
- * 
+ *
  * The withdrawal group record is only created after we know
  * the coin selection we want to withdraw.
  */
@@ -1492,9 +1491,9 @@ export interface WithdrawalGroupRecord {
    * Last error per coin/planchet, or undefined if no error occured for
    * the coin/planchet.
    */
-  lastErrorPerCoin: { [coinIndex: number]: OperationError };
+  lastErrorPerCoin: { [coinIndex: number]: OperationErrorDetails };
 
-  lastError: OperationError | undefined;
+  lastError: OperationErrorDetails | undefined;
 }
 
 export interface BankWithdrawUriRecord {
@@ -1559,7 +1558,7 @@ export interface RecoupGroupRecord {
   /**
    * Last error that occured, if any.
    */
-  lastError: OperationError | undefined;
+  lastError: OperationErrorDetails | undefined;
 }
 
 export const enum ImportPayloadType {
diff --git a/src/types/notifications.ts b/src/types/notifications.ts
index ac30b6fe..5d6d2ee1 100644
--- a/src/types/notifications.ts
+++ b/src/types/notifications.ts
@@ -22,7 +22,7 @@
 /**
  * Imports.
  */
-import { OperationError } from "./walletTypes";
+import { OperationErrorDetails } from "./walletTypes";
 import { WithdrawalSource } from "./dbTypes";
 
 export const enum NotificationType {
@@ -54,6 +54,7 @@ export const enum NotificationType {
   TipOperationError = "tip-error",
   PayOperationError = "pay-error",
   WithdrawOperationError = "withdraw-error",
+  ReserveNotYetFound = "reserve-not-yet-found",
   ReserveOperationError = "reserve-error",
   InternalError = "internal-error",
   PendingOperationProcessed = "pending-operation-processed",
@@ -72,6 +73,11 @@ export interface InternalErrorNotification {
   exception: any;
 }
 
+export interface ReserveNotYetFoundNotification {
+  type: NotificationType.ReserveNotYetFound;
+  reservePub: string;
+}
+
 export interface CoinWithdrawnNotification {
   type: NotificationType.CoinWithdrawn;
 }
@@ -148,27 +154,32 @@ export interface RefundFinishedNotification {
 
 export interface ExchangeOperationErrorNotification {
   type: NotificationType.ExchangeOperationError;
+  error: OperationErrorDetails;
 }
 
 export interface RefreshOperationErrorNotification {
   type: NotificationType.RefreshOperationError;
+  error: OperationErrorDetails;
 }
 
 export interface RefundStatusOperationErrorNotification {
   type: NotificationType.RefundStatusOperationError;
+  error: OperationErrorDetails;
 }
 
 export interface RefundApplyOperationErrorNotification {
   type: NotificationType.RefundApplyOperationError;
+  error: OperationErrorDetails;
 }
 
 export interface PayOperationErrorNotification {
   type: NotificationType.PayOperationError;
+  error: OperationErrorDetails;
 }
 
 export interface ProposalOperationErrorNotification {
   type: NotificationType.ProposalOperationError;
-  error: OperationError;
+  error: OperationErrorDetails;
 }
 
 export interface TipOperationErrorNotification {
@@ -177,16 +188,17 @@ export interface TipOperationErrorNotification {
 
 export interface WithdrawOperationErrorNotification {
   type: NotificationType.WithdrawOperationError;
-  error: OperationError,
+  error: OperationErrorDetails;
 }
 
 export interface RecoupOperationErrorNotification {
   type: NotificationType.RecoupOperationError;
+  error: OperationErrorDetails;
 }
 
 export interface ReserveOperationErrorNotification {
   type: NotificationType.ReserveOperationError;
-  operationError: OperationError;
+  error: OperationErrorDetails;
 }
 
 export interface ReserveCreatedNotification {
@@ -238,4 +250,5 @@ export type WalletNotification =
   | InternalErrorNotification
   | PendingOperationProcessedNotification
   | ProposalRefusedNotification
-  | ReserveRegisteredWithBankNotification;
\ No newline at end of file
+  | ReserveRegisteredWithBankNotification
+  | ReserveNotYetFoundNotification;
diff --git a/src/types/pending.ts b/src/types/pending.ts
index f949b7c1..8a1e8436 100644
--- a/src/types/pending.ts
+++ b/src/types/pending.ts
@@ -21,7 +21,7 @@
 /**
  * Imports.
  */
-import { OperationError, WalletBalance } from "./walletTypes";
+import { OperationErrorDetails, WalletBalance } from "./walletTypes";
 import { WithdrawalSource, RetryInfo, ReserveRecordStatus } from "./dbTypes";
 import { Timestamp, Duration } from "../util/time";
 import { ReserveType } from "./history";
@@ -68,7 +68,7 @@ export interface PendingExchangeUpdateOperation {
   stage: ExchangeUpdateOperationStage;
   reason: string;
   exchangeBaseUrl: string;
-  lastError: OperationError | undefined;
+  lastError: OperationErrorDetails | undefined;
 }
 
 /**
@@ -112,7 +112,7 @@ export interface PendingReserveOperation {
  */
 export interface PendingRefreshOperation {
   type: PendingOperationType.Refresh;
-  lastError?: OperationError;
+  lastError?: OperationErrorDetails;
   refreshGroupId: string;
   finishedPerCoin: boolean[];
   retryInfo: RetryInfo;
@@ -127,7 +127,7 @@ export interface PendingProposalDownloadOperation {
   proposalTimestamp: Timestamp;
   proposalId: string;
   orderId: string;
-  lastError?: OperationError;
+  lastError?: OperationErrorDetails;
   retryInfo: RetryInfo;
 }
 
@@ -172,7 +172,7 @@ export interface PendingPayOperation {
   proposalId: string;
   isReplay: boolean;
   retryInfo: RetryInfo;
-  lastError: OperationError | undefined;
+  lastError: OperationErrorDetails | undefined;
 }
 
 /**
@@ -183,14 +183,14 @@ export interface PendingRefundQueryOperation {
   type: PendingOperationType.RefundQuery;
   proposalId: string;
   retryInfo: RetryInfo;
-  lastError: OperationError | undefined;
+  lastError: OperationErrorDetails | undefined;
 }
 
 export interface PendingRecoupOperation {
   type: PendingOperationType.Recoup;
   recoupGroupId: string;
   retryInfo: RetryInfo;
-  lastError: OperationError | undefined;
+  lastError: OperationErrorDetails | undefined;
 }
 
 /**
@@ -199,7 +199,7 @@ export interface PendingRecoupOperation {
 export interface PendingWithdrawOperation {
   type: PendingOperationType.Withdraw;
   source: WithdrawalSource;
-  lastError: OperationError | undefined;
+  lastError: OperationErrorDetails | undefined;
   withdrawalGroupId: string;
   numCoinsWithdrawn: number;
   numCoinsTotal: number;
diff --git a/src/types/talerTypes.ts b/src/types/talerTypes.ts
index 232f5f31..ef14684f 100644
--- a/src/types/talerTypes.ts
+++ b/src/types/talerTypes.ts
@@ -433,7 +433,6 @@ export class ContractTerms {
   extra: any;
 }
 
-
 /**
  * Refund permission in the format that the merchant gives it to us.
  */
@@ -788,6 +787,53 @@ export interface MerchantPayResponse {
   sig: string;
 }
 
+export interface ExchangeMeltResponse {
+  /**
+   * Which of the kappa indices does the client not have to reveal.
+   */
+  noreveal_index: number;
+
+  /**
+   * Signature of TALER_RefreshMeltConfirmationPS whereby the exchange
+   * affirms the successful melt and confirming the noreveal_index
+   */
+  exchange_sig: EddsaSignatureString;
+
+  /*
+   * public EdDSA key of the exchange that was used to generate the signature.
+   * Should match one of the exchange's signing keys from /keys.  Again given
+   * explicitly as the client might otherwise be confused by clock skew as to
+   * which signing key was used.
+   */
+  exchange_pub: EddsaPublicKeyString;
+
+  /*
+   * Base URL to use for operations on the refresh context
+   * (so the reveal operation).  If not given,
+   * the base URL is the same as the one used for this request.
+   * Can be used if the base URL for /refreshes/ differs from that
+   * for /coins/, i.e. for load balancing.  Clients SHOULD
+   * respect the refresh_base_url if provided.  Any HTTP server
+   * belonging to an exchange MUST generate a 307 or 308 redirection
+   * to the correct base URL should a client uses the wrong base
+   * URL, or if the base URL has changed since the melt.
+   *
+   * When melting the same coin twice (technically allowed
+   * as the response might have been lost on the network),
+   * the exchange may return different values for the refresh_base_url.
+   */
+  refresh_base_url?: string;
+}
+
+export interface ExchangeRevealItem {
+  ev_sig: string;
+}
+
+export interface ExchangeRevealResponse {
+  // List of the exchange's blinded RSA signatures on the new coins.
+  ev_sigs: ExchangeRevealItem[];
+}
+
 export type AmountString = string;
 export type Base32String = string;
 export type EddsaSignatureString = string;
@@ -1028,3 +1074,23 @@ export const codecForMerchantPayResponse = (): 
Codec<MerchantPayResponse> =>
   makeCodecForObject<MerchantPayResponse>()
     .property("sig", codecForString)
     .build("MerchantPayResponse");
+
+export const codecForExchangeMeltResponse = (): Codec<ExchangeMeltResponse> =>
+  makeCodecForObject<ExchangeMeltResponse>()
+    .property("exchange_pub", codecForString)
+    .property("exchange_sig", codecForString)
+    .property("noreveal_index", codecForNumber)
+    .property("refresh_base_url", makeCodecOptional(codecForString))
+    .build("ExchangeMeltResponse");
+
+export const codecForExchangeRevealItem = (): Codec<ExchangeRevealItem> =>
+  makeCodecForObject<ExchangeRevealItem>()
+    .property("ev_sig", codecForString)
+    .build("ExchangeRevealItem");
+
+export const codecForExchangeRevealResponse = (): Codec<
+  ExchangeRevealResponse
+> =>
+  makeCodecForObject<ExchangeRevealResponse>()
+    .property("ev_sigs", makeCodecForList(codecForExchangeRevealItem()))
+    .build("ExchangeRevealResponse");
diff --git a/src/types/transactions.ts b/src/types/transactions.ts
index b87726ba..d6262264 100644
--- a/src/types/transactions.ts
+++ b/src/types/transactions.ts
@@ -119,7 +119,7 @@ interface WithdrawalDetailsForManualTransfer {
 
   /**
    * Payto URIs that the exchange supports.
-   * 
+   *
    * Already contains the amount and message.
    */
   exchangePaytoUris: string[];
diff --git a/src/types/walletTypes.ts b/src/types/walletTypes.ts
index ee7d071c..95ec47b6 100644
--- a/src/types/walletTypes.ts
+++ b/src/types/walletTypes.ts
@@ -303,7 +303,7 @@ export class ReturnCoinsRequest {
    * Wire details for the bank account of the customer that will
    * receive the funds.
    */
-  senderWire?: object;
+  senderWire?: string;
 
   /**
    * Verify that a value matches the schema of this class and convert it into a
@@ -406,10 +406,11 @@ export interface WalletDiagnostics {
   dbOutdated: boolean;
 }
 
-export interface OperationError {
-  type: string;
+export interface OperationErrorDetails {
+  talerErrorCode: number;
+  talerErrorHint: string;
   message: string;
-  details: any;
+  details: unknown;
 }
 
 export interface PlanchetCreationResult {
diff --git a/src/util/amounts-test.ts b/src/util/amounts-test.ts
index e10ee596..afd8caa5 100644
--- a/src/util/amounts-test.ts
+++ b/src/util/amounts-test.ts
@@ -24,9 +24,7 @@ const jAmt = (
   currency: string,
 ): AmountJson => ({ value, fraction, currency });
 
-const sAmt = (
-  s: string
-): AmountJson => Amounts.parseOrThrow(s);
+const sAmt = (s: string): AmountJson => Amounts.parseOrThrow(s);
 
 test("amount addition (simple)", (t) => {
   const a1 = jAmt(1, 0, "EUR");
diff --git a/src/util/amounts.ts b/src/util/amounts.ts
index 94aefb3c..1e7f192f 100644
--- a/src/util/amounts.ts
+++ b/src/util/amounts.ts
@@ -349,7 +349,7 @@ function mult(a: AmountJson, n: number): Result {
       n = n / 2;
     } else {
       n = (n - 1) / 2;
-      const r2 = add(acc, x)
+      const r2 = add(acc, x);
       if (r2.saturated) {
         return r2;
       }
diff --git a/src/util/http.ts b/src/util/http.ts
index bc054096..abbc8df0 100644
--- a/src/util/http.ts
+++ b/src/util/http.ts
@@ -14,18 +14,26 @@
  TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
-import { Codec } from "./codec";
-import { OperationFailedError } from "../operations/errors";
-
 /**
  * Helpers for doing XMLHttpRequest-s that are based on ES6 promises.
  * Allows for easy mocking for test cases.
  */
 
+/**
+ * Imports
+ */
+import { Codec } from "./codec";
+import { OperationFailedError, makeErrorDetails } from "../operations/errors";
+import { TalerErrorCode } from "../TalerErrorCode";
+import { Logger } from "./logging";
+
+const logger = new Logger("http.ts");
+
 /**
  * An HTTP response that is returned by all request methods of this library.
  */
 export interface HttpResponse {
+  requestUrl: string;
   status: number;
   headers: Headers;
   json(): Promise<any>;
@@ -67,10 +75,20 @@ export class Headers {
 }
 
 /**
- * The request library is bundled into an interface to m  responseJson: object 
& any;ake mocking easy.
+ * Interface for the HTTP request library used by the wallet.
+ *
+ * The request library is bundled into an interface to make mocking and
+ * request tunneling easy.
  */
 export interface HttpRequestLibrary {
+  /**
+   * Make an HTTP GET request.
+   */
   get(url: string, opt?: HttpRequestOptions): Promise<HttpResponse>;
+
+  /**
+   * Make an HTTP POST request with a JSON body.
+   */
   postJson(
     url: string,
     body: any,
@@ -105,18 +123,29 @@ export class BrowserHttpLib implements HttpRequestLibrary 
{
       }
 
       myRequest.onerror = (e) => {
-        console.error("http request error");
-        reject(Error("could not make XMLHttpRequest"));
+        logger.error("http request error");
+        reject(
+          OperationFailedError.fromCode(
+            TalerErrorCode.WALLET_NETWORK_ERROR,
+            "Could not make request",
+            {
+              requestUrl: url,
+            },
+          ),
+        );
       };
 
       myRequest.addEventListener("readystatechange", (e) => {
         if (myRequest.readyState === XMLHttpRequest.DONE) {
           if (myRequest.status === 0) {
-            reject(
-              Error(
-                "HTTP Request failed (status code 0, maybe URI scheme is 
wrong?)",
-              ),
+            const exc = OperationFailedError.fromCode(
+              TalerErrorCode.WALLET_NETWORK_ERROR,
+              "HTTP request failed (status 0, maybe URI scheme was wrong?)",
+              {
+                requestUrl: url,
+              },
             );
+            reject(exc);
             return;
           }
           const makeJson = async (): Promise<any> => {
@@ -124,10 +153,24 @@ export class BrowserHttpLib implements HttpRequestLibrary 
{
             try {
               responseJson = JSON.parse(myRequest.responseText);
             } catch (e) {
-              throw Error("Invalid JSON from HTTP response");
+              throw OperationFailedError.fromCode(
+                TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+                "Invalid JSON from HTTP response",
+                {
+                  requestUrl: url,
+                  httpStatusCode: myRequest.status,
+                },
+              );
             }
             if (responseJson === null || typeof responseJson !== "object") {
-              throw Error("Invalid JSON from HTTP response");
+              throw OperationFailedError.fromCode(
+                TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+                "Invalid JSON from HTTP response",
+                {
+                  requestUrl: url,
+                  httpStatusCode: myRequest.status,
+                },
+              );
             }
             return responseJson;
           };
@@ -141,13 +184,14 @@ export class BrowserHttpLib implements HttpRequestLibrary 
{
             const parts = line.split(": ");
             const headerName = parts.shift();
             if (!headerName) {
-              console.error("invalid header");
+              logger.warn("skipping invalid header");
               return;
             }
             const value = parts.join(": ");
             headerMap.set(headerName, value);
           });
           const resp: HttpResponse = {
+            requestUrl: url,
             status: myRequest.status,
             headers: headerMap,
             json: makeJson,
@@ -165,7 +209,7 @@ export class BrowserHttpLib implements HttpRequestLibrary {
 
   postJson(
     url: string,
-    body: any,
+    body: unknown,
     opt?: HttpRequestOptions,
   ): Promise<HttpResponse> {
     return this.req("post", url, JSON.stringify(body), opt);
@@ -176,114 +220,121 @@ export class BrowserHttpLib implements 
HttpRequestLibrary {
   }
 }
 
-export interface PostJsonRequest<RespType> {
-  http: HttpRequestLibrary;
-  url: string;
-  body: any;
-  codec: Codec<RespType>;
-}
+type TalerErrorResponse = {
+  code: number;
+} & unknown;
 
-/**
- * Helper for making Taler-style HTTP POST requests with a JSON payload and 
response.
- */
-export async function httpPostTalerJson<RespType>(
-  req: PostJsonRequest<RespType>,
-): Promise<RespType> {
-  const resp = await req.http.postJson(req.url, req.body);
+type ResponseOrError<T> =
+  | { isError: false; response: T }
+  | { isError: true; talerErrorResponse: TalerErrorResponse };
 
-  if (resp.status !== 200) {
-    let exc: OperationFailedError | undefined = undefined;
-    try {
-      const errorJson = await resp.json();
-      const m = `received error response (status ${resp.status})`;
-      exc = new OperationFailedError({
-        type: "protocol",
-        message: m,
-        details: {
-          httpStatusCode: resp.status,
-          errorResponse: errorJson,
-        },
-      });
-    } catch (e) {
-      const m = "could not parse response JSON";
-      exc = new OperationFailedError({
-        type: "network",
-        message: m,
-        details: {
-          status: resp.status,
-        },
-      });
+export async function readSuccessResponseJsonOrErrorCode<T>(
+  httpResponse: HttpResponse,
+  codec: Codec<T>,
+): Promise<ResponseOrError<T>> {
+  if (!(httpResponse.status >= 200 && httpResponse.status < 300)) {
+    const errJson = await httpResponse.json();
+    const talerErrorCode = errJson.code;
+    if (typeof talerErrorCode !== "number") {
+      throw new OperationFailedError(
+        makeErrorDetails(
+          TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+          "Error response did not contain error code",
+          {
+            requestUrl: httpResponse.requestUrl,
+          },
+        ),
+      );
     }
-    throw exc;
+    return {
+      isError: true,
+      talerErrorResponse: errJson,
+    };
   }
-  let json: any;
+  const respJson = await httpResponse.json();
+  let parsedResponse: T;
   try {
-    json = await resp.json();
+    parsedResponse = codec.decode(respJson);
   } catch (e) {
-    const m = "could not parse response JSON";
-    throw new OperationFailedError({
-      type: "network",
-      message: m,
-      details: {
-        status: resp.status,
+    throw OperationFailedError.fromCode(
+      TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+      "Response invalid",
+      {
+        requestUrl: httpResponse.requestUrl,
+        httpStatusCode: httpResponse.status,
+        validationError: e.toString(),
       },
-    });
+    );
   }
-  return req.codec.decode(json);
+  return {
+    isError: false,
+    response: parsedResponse,
+  };
 }
 
-
-export interface GetJsonRequest<RespType> {
-  http: HttpRequestLibrary;
-  url: string;
-  codec: Codec<RespType>;
+export function throwUnexpectedRequestError(
+  httpResponse: HttpResponse,
+  talerErrorResponse: TalerErrorResponse,
+): never {
+  throw new OperationFailedError(
+    makeErrorDetails(
+      TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR,
+      "Unexpected error code in response",
+      {
+        requestUrl: httpResponse.requestUrl,
+        httpStatusCode: httpResponse.status,
+        errorResponse: talerErrorResponse,
+      },
+    ),
+  );
 }
 
-/**
- * Helper for making Taler-style HTTP GET requests with a JSON payload.
- */
-export async function httpGetTalerJson<RespType>(
-  req: GetJsonRequest<RespType>,
-): Promise<RespType> {
-  const resp = await req.http.get(req.url);
+export async function readSuccessResponseJsonOrThrow<T>(
+  httpResponse: HttpResponse,
+  codec: Codec<T>,
+): Promise<T> {
+  const r = await readSuccessResponseJsonOrErrorCode(httpResponse, codec);
+  if (!r.isError) {
+    return r.response;
+  }
+  throwUnexpectedRequestError(httpResponse, r.talerErrorResponse);
+}
 
-  if (resp.status !== 200) {
-    let exc: OperationFailedError | undefined = undefined;
-    try {
-      const errorJson = await resp.json();
-      const m = `received error response (status ${resp.status})`;
-      exc = new OperationFailedError({
-        type: "protocol",
-        message: m,
-        details: {
-          httpStatusCode: resp.status,
-          errorResponse: errorJson,
-        },
-      });
-    } catch (e) {
-      const m = "could not parse response JSON";
-      exc = new OperationFailedError({
-        type: "network",
-        message: m,
-        details: {
-          status: resp.status,
-        },
-      });
+export async function readSuccessResponseTextOrErrorCode<T>(
+  httpResponse: HttpResponse,
+): Promise<ResponseOrError<string>> {
+  if (!(httpResponse.status >= 200 && httpResponse.status < 300)) {
+    const errJson = await httpResponse.json();
+    const talerErrorCode = errJson.code;
+    if (typeof talerErrorCode !== "number") {
+      throw new OperationFailedError(
+        makeErrorDetails(
+          TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+          "Error response did not contain error code",
+          {
+            requestUrl: httpResponse.requestUrl,
+          },
+        ),
+      );
     }
-    throw exc;
+    return {
+      isError: true,
+      talerErrorResponse: errJson,
+    };
   }
-  let json: any;
-  try {
-    json = await resp.json();
-  } catch (e) {
-    const m = "could not parse response JSON";
-    throw new OperationFailedError({
-      type: "network",
-      message: m,
-      details: {
-        status: resp.status,
-      },
-    });
+  const respJson = await httpResponse.text();
+  return {
+    isError: false,
+    response: respJson,
+  };
+}
+
+export async function readSuccessResponseTextOrThrow<T>(
+  httpResponse: HttpResponse,
+): Promise<string> {
+  const r = await readSuccessResponseTextOrErrorCode(httpResponse);
+  if (!r.isError) {
+    return r.response;
   }
-  return req.codec.decode(json);
+  throwUnexpectedRequestError(httpResponse, r.talerErrorResponse);
 }
diff --git a/src/wallet.ts b/src/wallet.ts
index 9df27989..ff72f3c7 100644
--- a/src/wallet.ts
+++ b/src/wallet.ts
@@ -52,7 +52,7 @@ import {
   ReserveRecordStatus,
   CoinSourceType,
 } from "./types/dbTypes";
-import { MerchantRefundDetails, CoinDumpJson } from "./types/talerTypes";
+import { CoinDumpJson } from "./types/talerTypes";
 import {
   BenchmarkResult,
   ConfirmPayResult,
@@ -106,11 +106,7 @@ import {
 } from "./types/pending";
 import { WalletNotification, NotificationType } from "./types/notifications";
 import { HistoryQuery, HistoryEvent } from "./types/history";
-import {
-  processPurchaseQueryRefund,
-  getFullRefundFees,
-  applyRefund,
-} from "./operations/refund";
+import { processPurchaseQueryRefund, applyRefund } from "./operations/refund";
 import { durationMin, Duration } from "./util/time";
 import { processRecoupGroup } from "./operations/recoup";
 import { OperationFailedAndReportedError } from "./operations/errors";
@@ -372,12 +368,12 @@ export class Wallet {
                 type: NotificationType.InternalError,
                 message: "uncaught exception",
                 exception: e,
-               });
+              });
             }
           }
           this.ws.notify({
             type: NotificationType.PendingOperationProcessed,
-           });
+          });
         }
       }
     }
@@ -712,12 +708,6 @@ export class Wallet {
     return this.db.get(Stores.purchases, contractTermsHash);
   }
 
-  async getFullRefundFees(
-    refundPermissions: MerchantRefundDetails[],
-  ): Promise<AmountJson> {
-    return getFullRefundFees(this.ws, refundPermissions);
-  }
-
   async acceptTip(talerTipUri: string): Promise<void> {
     try {
       return acceptTip(this.ws, talerTipUri);
diff --git a/src/webex/pages/withdraw.tsx b/src/webex/pages/withdraw.tsx
index c4e4ebbb..0216cdb4 100644
--- a/src/webex/pages/withdraw.tsx
+++ b/src/webex/pages/withdraw.tsx
@@ -35,7 +35,9 @@ import {
 } from "../wxApi";
 
 function WithdrawalDialog(props: { talerWithdrawUri: string }): JSX.Element {
-  const [details, setDetails] = useState<WithdrawalDetailsResponse | 
undefined>();
+  const [details, setDetails] = useState<
+    WithdrawalDetailsResponse | undefined
+  >();
   const [selectedExchange, setSelectedExchange] = useState<
     string | undefined
   >();
diff --git a/src/webex/wxBackend.ts b/src/webex/wxBackend.ts
index 12675616..d5a27216 100644
--- a/src/webex/wxBackend.ts
+++ b/src/webex/wxBackend.ts
@@ -29,10 +29,7 @@ import {
   openTalerDatabase,
   WALLET_DB_MINOR_VERSION,
 } from "../db";
-import {
-  ReturnCoinsRequest,
-  WalletDiagnostics,
-} from "../types/walletTypes";
+import { ReturnCoinsRequest, WalletDiagnostics } from "../types/walletTypes";
 import { BrowserHttpLib } from "../util/http";
 import { OpenedPromise, openPromise } from "../util/promiseUtils";
 import { classifyTalerUri, TalerUriType } from "../util/taleruri";

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

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