gnunet-svn
[Top][All Lists]
Advanced

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

[taler-wallet-core] branch master updated: new date format, replace chec


From: gnunet
Subject: [taler-wallet-core] branch master updated: new date format, replace checkable annotations with codecs
Date: Thu, 19 Dec 2019 20:42:52 +0100

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

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

The following commit(s) were added to refs/heads/master by this push:
     new 0c9358c1 new date format, replace checkable annotations with codecs
0c9358c1 is described below

commit 0c9358c1b2bd80e25940022e86bd8daef8184ad7
Author: Florian Dold <address@hidden>
AuthorDate: Thu Dec 19 20:42:49 2019 +0100

    new date format, replace checkable annotations with codecs
---
 src/crypto/workers/cryptoApi.ts            |   7 +-
 src/crypto/workers/cryptoImplementation.ts |  36 +-
 src/headless/merchant.ts                   |   9 +-
 src/headless/taler-wallet-cli.ts           |   4 +-
 src/operations/exchanges.ts                |  37 +-
 src/operations/history.ts                  |  15 +-
 src/operations/pay.ts                      | 212 ++++++------
 src/operations/payback.ts                  |   4 +-
 src/operations/pending.ts                  |  10 +-
 src/operations/refresh.ts                  |   2 +-
 src/operations/refund.ts                   |  15 +-
 src/operations/reserves.ts                 |   9 +-
 src/operations/return.ts                   | 271 ---------------
 src/operations/tip.ts                      |  15 +-
 src/operations/withdraw.ts                 |  92 ++---
 src/types/ReserveStatus.ts                 |  18 +-
 src/types/ReserveTransaction.ts            |  48 ++-
 src/types/dbTypes.ts                       | 132 +++-----
 src/types/history.ts                       |   3 +-
 src/types/pending.ts                       |   3 +-
 src/types/talerTypes.ts                    | 522 +++++++++++++++--------------
 src/types/types-test.ts                    |  17 +-
 src/types/walletTypes.ts                   |  97 ++----
 src/util/RequestThrottler.ts               |  14 +-
 src/util/amounts.ts                        |  34 +-
 src/util/checkable.ts                      | 417 -----------------------
 src/util/codec.ts                          |  54 ++-
 src/util/helpers.ts                        |  71 ----
 src/util/time.ts                           | 165 +++++++++
 src/util/timer.ts                          |  22 +-
 src/wallet.ts                              |  12 +-
 src/webex/pages/pay.tsx                    |   2 +-
 src/webex/renderHtml.tsx                   |   8 +-
 src/webex/wxBackend.ts                     |  10 +-
 tsconfig.json                              |   3 +-
 35 files changed, 907 insertions(+), 1483 deletions(-)

diff --git a/src/crypto/workers/cryptoApi.ts b/src/crypto/workers/cryptoApi.ts
index da807cce..1c54d286 100644
--- a/src/crypto/workers/cryptoApi.ts
+++ b/src/crypto/workers/cryptoApi.ts
@@ -30,6 +30,7 @@ import {
   RefreshSessionRecord,
   TipPlanchet,
   WireFee,
+  WalletContractData,
 } from "../../types/dbTypes";
 
 import { CryptoWorker } from "./cryptoWorker";
@@ -384,14 +385,16 @@ export class CryptoApi {
   }
 
   signDeposit(
-    contractTerms: ContractTerms,
+    contractTermsRaw: string,
+    contractData: WalletContractData,
     cds: CoinWithDenom[],
     totalAmount: AmountJson,
   ): Promise<PaySigInfo> {
     return this.doRpc<PaySigInfo>(
       "signDeposit",
       3,
-      contractTerms,
+      contractTermsRaw,
+      contractData,
       cds,
       totalAmount,
     );
diff --git a/src/crypto/workers/cryptoImplementation.ts 
b/src/crypto/workers/cryptoImplementation.ts
index d745f497..04371186 100644
--- a/src/crypto/workers/cryptoImplementation.ts
+++ b/src/crypto/workers/cryptoImplementation.ts
@@ -33,6 +33,7 @@ import {
   TipPlanchet,
   WireFee,
   initRetryInfo,
+  WalletContractData,
 } from "../../types/dbTypes";
 
 import { CoinPaySig, ContractTerms, PaybackRequest } from 
"../../types/talerTypes";
@@ -40,13 +41,11 @@ import {
   BenchmarkResult,
   CoinWithDenom,
   PaySigInfo,
-  Timestamp,
   PlanchetCreationResult,
   PlanchetCreationRequest,
-  getTimestampNow,
   CoinPayInfo,
 } from "../../types/walletTypes";
-import { canonicalJson, getTalerStampSec } from "../../util/helpers";
+import { canonicalJson } from "../../util/helpers";
 import { AmountJson } from "../../util/amounts";
 import * as Amounts from "../../util/amounts";
 import * as timer from "../../util/timer";
@@ -70,6 +69,7 @@ import {
 } from "../talerCrypto";
 import { randomBytes } from "../primitives/nacl-fast";
 import { kdf } from "../primitives/kdf";
+import { Timestamp, getTimestampNow } from "../../util/time";
 
 enum SignaturePurpose {
   RESERVE_WITHDRAW = 1200,
@@ -104,20 +104,6 @@ function timestampToBuffer(ts: Timestamp): Uint8Array {
   v.setBigUint64(0, s);
   return new Uint8Array(b);
 }
-
-function talerTimestampStringToBuffer(ts: string): Uint8Array {
-  const t_sec = getTalerStampSec(ts);
-  if (t_sec === null || t_sec === undefined) {
-    // Should have been validated before!
-    throw Error("invalid timestamp");
-  }
-  const buffer = new ArrayBuffer(8);
-  const dvbuf = new DataView(buffer);
-  const s = BigInt(t_sec) * BigInt(1000 * 1000);
-  dvbuf.setBigUint64(0, s);
-  return new Uint8Array(buffer);
-}
-
 class SignaturePurposeBuilder {
   private chunks: Uint8Array[] = [];
 
@@ -346,7 +332,8 @@ export class CryptoImplementation {
    * and deposit permissions for each given coin.
    */
   signDeposit(
-    contractTerms: ContractTerms,
+    contractTermsRaw: string,
+    contractData: WalletContractData,
     cds: CoinWithDenom[],
     totalAmount: AmountJson,
   ): PaySigInfo {
@@ -354,14 +341,13 @@ export class CryptoImplementation {
       coinInfo: [],
     };
 
-    const contractTermsHash = this.hashString(canonicalJson(contractTerms));
+    const contractTermsHash = 
this.hashString(canonicalJson(JSON.parse(contractTermsRaw)));
 
     const feeList: AmountJson[] = cds.map(x => x.denom.feeDeposit);
     let fees = Amounts.add(Amounts.getZero(feeList[0].currency), ...feeList)
       .amount;
     // okay if saturates
-    fees = Amounts.sub(fees, Amounts.parseOrThrow(contractTerms.max_fee))
-      .amount;
+    fees = Amounts.sub(fees, contractData.maxDepositFee).amount;
     const total = Amounts.add(fees, totalAmount).amount;
 
     let amountSpent = Amounts.getZero(cds[0].coin.currentAmount.currency);
@@ -395,12 +381,12 @@ export class CryptoImplementation {
 
       const d = buildSigPS(SignaturePurpose.WALLET_COIN_DEPOSIT)
         .put(decodeCrock(contractTermsHash))
-        .put(decodeCrock(contractTerms.H_wire))
-        .put(talerTimestampStringToBuffer(contractTerms.timestamp))
-        .put(talerTimestampStringToBuffer(contractTerms.refund_deadline))
+        .put(decodeCrock(contractData.wireInfoHash))
+        .put(timestampToBuffer(contractData.timestamp))
+        .put(timestampToBuffer(contractData.refundDeadline))
         .put(amountToBuffer(coinSpend))
         .put(amountToBuffer(cd.denom.feeDeposit))
-        .put(decodeCrock(contractTerms.merchant_pub))
+        .put(decodeCrock(contractData.merchantPub))
         .put(decodeCrock(cd.coin.coinPub))
         .build();
       const coinSig = eddsaSign(d, decodeCrock(cd.coin.coinPriv));
diff --git a/src/headless/merchant.ts b/src/headless/merchant.ts
index 6a2d0ad2..1da5d5f0 100644
--- a/src/headless/merchant.ts
+++ b/src/headless/merchant.ts
@@ -23,7 +23,7 @@
  * Imports.
  */
 import axios from "axios";
-import { CheckPaymentResponse } from "../types/talerTypes";
+import { CheckPaymentResponse, codecForCheckPaymentResponse } from 
"../types/talerTypes";
 
 /**
  * Connection to the *internal* merchant backend.
@@ -96,8 +96,8 @@ export class MerchantBackendConnection {
         amount,
         summary,
         fulfillment_url: fulfillmentUrl,
-        refund_deadline: `/Date(${t})/`,
-        wire_transfer_deadline: `/Date(${t})/`,
+        refund_deadline: { t_ms: t * 1000 },
+        wire_transfer_deadline: { t_ms: t * 1000 },
       },
     };
     const resp = await axios({
@@ -133,6 +133,7 @@ export class MerchantBackendConnection {
     if (resp.status != 200) {
       throw Error("failed to check payment");
     }
-    return CheckPaymentResponse.checked(resp.data);
+    
+    return codecForCheckPaymentResponse().decode(resp.data);
   }
 }
diff --git a/src/headless/taler-wallet-cli.ts b/src/headless/taler-wallet-cli.ts
index 610990ae..12f729be 100644
--- a/src/headless/taler-wallet-cli.ts
+++ b/src/headless/taler-wallet-cli.ts
@@ -50,7 +50,7 @@ async function doPay(
     return;
   }
   if (result.status === "insufficient-balance") {
-    console.log("contract", result.contractTerms!);
+    console.log("contract", result.contractTermsRaw);
     console.error("insufficient balance");
     process.exit(1);
     return;
@@ -65,7 +65,7 @@ async function doPay(
   } else {
     throw Error("not reached");
   }
-  console.log("contract", result.contractTerms!);
+  console.log("contract", result.contractTermsRaw);
   let pay;
   if (options.alwaysYes) {
     pay = true;
diff --git a/src/operations/exchanges.ts b/src/operations/exchanges.ts
index d9adc7c5..741be31b 100644
--- a/src/operations/exchanges.ts
+++ b/src/operations/exchanges.ts
@@ -15,8 +15,8 @@
  */
 
 import { InternalWalletState } from "./state";
-import { KeysJson, Denomination, ExchangeWireJson } from "../types/talerTypes";
-import { getTimestampNow, OperationError } from "../types/walletTypes";
+import { ExchangeKeysJson, Denomination, ExchangeWireJson, 
codecForExchangeKeysJson, codecForExchangeWireJson } from "../types/talerTypes";
+import { OperationError } from "../types/walletTypes";
 import {
   ExchangeRecord,
   ExchangeUpdateStatus,
@@ -29,8 +29,6 @@ import {
 } from "../types/dbTypes";
 import {
   canonicalizeBaseUrl,
-  extractTalerStamp,
-  extractTalerStampOrThrow,
 } from "../util/helpers";
 import { Database } from "../util/query";
 import * as Amounts from "../util/amounts";
@@ -40,6 +38,7 @@ import {
   guardOperationException,
 } from "./errors";
 import { WALLET_CACHE_BREAKER_CLIENT_VERSION } from "./versions";
+import { getTimestampNow } from "../util/time";
 
 async function denominationRecordFromKeys(
   ws: InternalWalletState,
@@ -57,12 +56,10 @@ async function denominationRecordFromKeys(
     feeWithdraw: Amounts.parseOrThrow(denomIn.fee_withdraw),
     isOffered: true,
     masterSig: denomIn.master_sig,
-    stampExpireDeposit: extractTalerStampOrThrow(denomIn.stamp_expire_deposit),
-    stampExpireLegal: extractTalerStampOrThrow(denomIn.stamp_expire_legal),
-    stampExpireWithdraw: extractTalerStampOrThrow(
-      denomIn.stamp_expire_withdraw,
-    ),
-    stampStart: extractTalerStampOrThrow(denomIn.stamp_start),
+    stampExpireDeposit: denomIn.stamp_expire_deposit,
+    stampExpireLegal: denomIn.stamp_expire_legal,
+    stampExpireWithdraw: denomIn.stamp_expire_withdraw,
+    stampStart: denomIn.stamp_start,
     status: DenominationStatus.Unverified,
     value: Amounts.parseOrThrow(denomIn.value),
   };
@@ -117,9 +114,9 @@ async function updateExchangeWithKeys(
     });
     throw new OperationFailedAndReportedError(m);
   }
-  let exchangeKeysJson: KeysJson;
+  let exchangeKeysJson: ExchangeKeysJson;
   try {
-    exchangeKeysJson = KeysJson.checked(keysResp);
+    exchangeKeysJson = codecForExchangeKeysJson().decode(keysResp);
   } catch (e) {
     const m = `Parsing /keys response failed: ${e.message}`;
     await setExchangeError(ws, baseUrl, {
@@ -130,9 +127,7 @@ async function updateExchangeWithKeys(
     throw new OperationFailedAndReportedError(m);
   }
 
-  const lastUpdateTimestamp = extractTalerStamp(
-    exchangeKeysJson.list_issue_date,
-  );
+  const lastUpdateTimestamp = exchangeKeysJson.list_issue_date
   if (!lastUpdateTimestamp) {
     const m = `Parsing /keys response failed: invalid list_issue_date.`;
     await setExchangeError(ws, baseUrl, {
@@ -329,7 +324,7 @@ async function updateExchangeWithWireInfo(
   if (!wiJson) {
     throw Error("/wire response malformed");
   }
-  const wireInfo = ExchangeWireJson.checked(wiJson);
+  const wireInfo = codecForExchangeWireJson().decode(wiJson);
   for (const a of wireInfo.accounts) {
     console.log("validating exchange acct");
     const isValid = await ws.cryptoApi.isValidWireAccount(
@@ -345,14 +340,8 @@ async function updateExchangeWithWireInfo(
   for (const wireMethod of Object.keys(wireInfo.fees)) {
     const feeList: WireFee[] = [];
     for (const x of wireInfo.fees[wireMethod]) {
-      const startStamp = extractTalerStamp(x.start_date);
-      if (!startStamp) {
-        throw Error("wrong date format");
-      }
-      const endStamp = extractTalerStamp(x.end_date);
-      if (!endStamp) {
-        throw Error("wrong date format");
-      }
+      const startStamp = x.start_date;
+      const endStamp = x.end_date;
       const fee: WireFee = {
         closingFee: Amounts.parseOrThrow(x.closing_fee),
         endStamp,
diff --git a/src/operations/history.ts b/src/operations/history.ts
index bb57a9c6..f02894b6 100644
--- a/src/operations/history.ts
+++ b/src/operations/history.ts
@@ -37,6 +37,7 @@ import {
 import { assertUnreachable } from "../util/assertUnreachable";
 import { TransactionHandle, Store } from "../util/query";
 import { ReserveTransactionType } from "../types/ReserveTransaction";
+import { timestampCmp } from "../util/time";
 
 /**
  * Create an event ID from the type and the primary key for the event.
@@ -53,11 +54,11 @@ function getOrderShortInfo(
     return undefined;
   }
   return {
-    amount: download.contractTerms.amount,
-    orderId: download.contractTerms.order_id,
-    merchantBaseUrl: download.contractTerms.merchant_base_url,
+    amount: Amounts.toString(download.contractData.amount),
+    orderId: download.contractData.orderId,
+    merchantBaseUrl: download.contractData.merchantBaseUrl,
     proposalId: proposal.proposalId,
-    summary: download.contractTerms.summary || "",
+    summary: download.contractData.summary,
   };
 }
 
@@ -356,9 +357,7 @@ export async function getHistory(
         if (!orderShortInfo) {
           return;
         }
-        const purchaseAmount = Amounts.parseOrThrow(
-          purchase.contractTerms.amount,
-        );
+        const purchaseAmount = purchase.contractData.amount;
         let amountRefundedRaw = Amounts.getZero(purchaseAmount.currency);
         let amountRefundedInvalid = Amounts.getZero(purchaseAmount.currency);
         let amountRefundedEffective = Amounts.getZero(purchaseAmount.currency);
@@ -408,7 +407,7 @@ export async function getHistory(
     },
   );
 
-  history.sort((h1, h2) => Math.sign(h1.timestamp.t_ms - h2.timestamp.t_ms));
+  history.sort((h1, h2) => timestampCmp(h1.timestamp, h2.timestamp));
 
   return { history };
 }
diff --git a/src/operations/pay.ts b/src/operations/pay.ts
index adbf6bb8..db2a2431 100644
--- a/src/operations/pay.ts
+++ b/src/operations/pay.ts
@@ -37,6 +37,7 @@ import {
   Stores,
   updateRetryInfoTimeout,
   PayEventRecord,
+  WalletContractData,
 } from "../types/dbTypes";
 import { NotificationType } from "../types/notifications";
 import {
@@ -46,33 +47,29 @@ import {
   MerchantRefundResponse,
   PayReq,
   Proposal,
+  codecForMerchantRefundResponse,
+  codecForProposal,
+  codecForContractTerms,
 } from "../types/talerTypes";
 import {
   CoinSelectionResult,
   CoinWithDenom,
   ConfirmPayResult,
-  getTimestampNow,
   OperationError,
   PaySigInfo,
   PreparePayResult,
   RefreshReason,
-  Timestamp,
 } from "../types/walletTypes";
 import * as Amounts from "../util/amounts";
 import { AmountJson } from "../util/amounts";
-import {
-  amountToPretty,
-  canonicalJson,
-  extractTalerDuration,
-  extractTalerStampOrThrow,
-  strcmp,
-} from "../util/helpers";
+import { amountToPretty, canonicalJson, strcmp } from "../util/helpers";
 import { Logger } from "../util/logging";
 import { getOrderDownloadUrl, parsePayUri } from "../util/taleruri";
 import { guardOperationException } from "./errors";
 import { createRefreshGroup, getTotalRefreshCost } from "./refresh";
 import { acceptRefundResponse } from "./refund";
 import { InternalWalletState } from "./state";
+import { Timestamp, getTimestampNow, timestampAddDuration } from 
"../util/time";
 
 interface CoinsForPaymentArgs {
   allowedAuditors: Auditor[];
@@ -177,20 +174,20 @@ export function selectPayCoins(
  */
 async function getCoinsForPayment(
   ws: InternalWalletState,
-  args: CoinsForPaymentArgs,
+  args: WalletContractData,
 ): Promise<CoinSelectionResult | undefined> {
   const {
     allowedAuditors,
     allowedExchanges,
-    depositFeeLimit,
-    paymentAmount,
+    maxDepositFee,
+    amount,
     wireFeeAmortization,
-    wireFeeLimit,
-    wireFeeTime,
+    maxWireFee,
+    timestamp,
     wireMethod,
   } = args;
 
-  let remainingAmount = paymentAmount;
+  let remainingAmount = amount;
 
   const exchanges = await ws.db.iter(Stores.exchanges).toArray();
 
@@ -207,7 +204,7 @@ async function getCoinsForPayment(
 
     // is the exchange explicitly allowed?
     for (const allowedExchange of allowedExchanges) {
-      if (allowedExchange.master_pub === exchangeDetails.masterPublicKey) {
+      if (allowedExchange.exchangePub === exchangeDetails.masterPublicKey) {
         isOkay = true;
         break;
       }
@@ -217,7 +214,7 @@ async function getCoinsForPayment(
     if (!isOkay) {
       for (const allowedAuditor of allowedAuditors) {
         for (const auditor of exchangeDetails.auditors) {
-          if (auditor.auditor_pub === allowedAuditor.auditor_pub) {
+          if (auditor.auditor_pub === allowedAuditor.auditorPub) {
             isOkay = true;
             break;
           }
@@ -281,7 +278,7 @@ async function getCoinsForPayment(
     let totalFees = Amounts.getZero(currency);
     let wireFee: AmountJson | undefined;
     for (const fee of exchangeFees.feesForType[wireMethod] || []) {
-      if (fee.startStamp <= wireFeeTime && fee.endStamp >= wireFeeTime) {
+      if (fee.startStamp <= timestamp && fee.endStamp >= timestamp) {
         wireFee = fee.wireFee;
         break;
       }
@@ -289,13 +286,13 @@ async function getCoinsForPayment(
 
     if (wireFee) {
       const amortizedWireFee = Amounts.divide(wireFee, wireFeeAmortization);
-      if (Amounts.cmp(wireFeeLimit, amortizedWireFee) < 0) {
+      if (Amounts.cmp(maxWireFee, amortizedWireFee) < 0) {
         totalFees = Amounts.add(amortizedWireFee, totalFees).amount;
         remainingAmount = Amounts.add(amortizedWireFee, 
remainingAmount).amount;
       }
     }
 
-    const res = selectPayCoins(denoms, cds, remainingAmount, depositFeeLimit);
+    const res = selectPayCoins(denoms, cds, remainingAmount, maxDepositFee);
 
     if (res) {
       totalFees = Amounts.add(totalFees, res.totalFees).amount;
@@ -332,18 +329,17 @@ async function recordConfirmPay(
   }
   logger.trace(`recording payment with session ID ${sessionId}`);
   const payReq: PayReq = {
-    coins: payCoinInfo.coinInfo.map((x) => x.sig),
-    merchant_pub: d.contractTerms.merchant_pub,
+    coins: payCoinInfo.coinInfo.map(x => x.sig),
+    merchant_pub: d.contractData.merchantPub,
     mode: "pay",
-    order_id: d.contractTerms.order_id,
+    order_id: d.contractData.orderId,
   };
   const t: PurchaseRecord = {
     abortDone: false,
     abortRequested: false,
-    contractTerms: d.contractTerms,
-    contractTermsHash: d.contractTermsHash,
+    contractTermsRaw: d.contractTermsRaw,
+    contractData: d.contractData,
     lastSessionId: sessionId,
-    merchantSig: d.merchantSig,
     payReq,
     timestampAccept: getTimestampNow(),
     timestampLastRefundStatus: undefined,
@@ -383,14 +379,19 @@ async function recordConfirmPay(
           throw Error("coin allocated for payment doesn't exist anymore");
         }
         coin.status = CoinStatus.Dormant;
-        const remaining = Amounts.sub(coin.currentAmount, 
coinInfo.subtractedAmount);
+        const remaining = Amounts.sub(
+          coin.currentAmount,
+          coinInfo.subtractedAmount,
+        );
         if (remaining.saturated) {
           throw Error("not enough remaining balance on coin for payment");
         }
         coin.currentAmount = remaining.amount;
         await tx.put(Stores.coins, coin);
       }
-      const refreshCoinPubs = payCoinInfo.coinInfo.map((x) => ({coinPub: 
x.coinPub}));
+      const refreshCoinPubs = payCoinInfo.coinInfo.map(x => ({
+        coinPub: x.coinPub,
+      }));
       await createRefreshGroup(tx, refreshCoinPubs, RefreshReason.Pay);
     },
   );
@@ -402,11 +403,11 @@ async function recordConfirmPay(
   return t;
 }
 
-function getNextUrl(contractTerms: ContractTerms): string {
-  const f = contractTerms.fulfillment_url;
+function getNextUrl(contractData: WalletContractData): string {
+  const f = contractData.fulfillmentUrl;
   if (f.startsWith("http://";) || f.startsWith("https://";)) {
-    const fu = new URL(contractTerms.fulfillment_url);
-    fu.searchParams.set("order_id", contractTerms.order_id);
+    const fu = new URL(contractData.fulfillmentUrl);
+    fu.searchParams.set("order_id", contractData.orderId);
     return fu.href;
   } else {
     return f;
@@ -440,7 +441,7 @@ export async function abortFailedPayment(
 
   const abortReq = { ...purchase.payReq, mode: "abort-refund" };
 
-  const payUrl = new URL("pay", purchase.contractTerms.merchant_base_url).href;
+  const payUrl = new URL("pay", purchase.contractData.merchantBaseUrl).href;
 
   try {
     resp = await ws.http.postJson(payUrl, abortReq);
@@ -454,7 +455,9 @@ export async function abortFailedPayment(
     throw Error(`unexpected status for /pay (${resp.status})`);
   }
 
-  const refundResponse = MerchantRefundResponse.checked(await resp.json());
+  const refundResponse = codecForMerchantRefundResponse().decode(
+    await resp.json(),
+  );
   await acceptRefundResponse(
     ws,
     purchase.proposalId,
@@ -574,13 +577,16 @@ async function processDownloadProposalImpl(
     throw Error(`contract download failed with status ${resp.status}`);
   }
 
-  const proposalResp = Proposal.checked(await resp.json());
+  const proposalResp = codecForProposal().decode(await resp.json());
 
   const contractTermsHash = await ws.cryptoApi.hashString(
     canonicalJson(proposalResp.contract_terms),
   );
 
-  const fulfillmentUrl = proposalResp.contract_terms.fulfillment_url;
+  const parsedContractTerms = codecForContractTerms().decode(
+    proposalResp.contract_terms,
+  );
+  const fulfillmentUrl = parsedContractTerms.fulfillment_url;
 
   await ws.db.runWithWriteTransaction(
     [Stores.proposals, Stores.purchases],
@@ -592,10 +598,42 @@ async function processDownloadProposalImpl(
       if (p.proposalStatus !== ProposalStatus.DOWNLOADING) {
         return;
       }
+      const amount = Amounts.parseOrThrow(parsedContractTerms.amount);
+      let maxWireFee: AmountJson;
+      if (parsedContractTerms.max_wire_fee) {
+        maxWireFee = Amounts.parseOrThrow(parsedContractTerms.max_wire_fee);
+      } else {
+        maxWireFee = Amounts.getZero(amount.currency);
+      }
       p.download = {
-        contractTerms: proposalResp.contract_terms,
-        merchantSig: proposalResp.sig,
-        contractTermsHash,
+        contractData: {
+          amount,
+          contractTermsHash: contractTermsHash,
+          fulfillmentUrl: parsedContractTerms.fulfillment_url,
+          merchantBaseUrl: parsedContractTerms.merchant_base_url,
+          merchantPub: parsedContractTerms.merchant_pub,
+          merchantSig: proposalResp.sig,
+          orderId: parsedContractTerms.order_id,
+          summary: parsedContractTerms.summary,
+          autoRefund: parsedContractTerms.auto_refund,
+          maxWireFee,
+          payDeadline: parsedContractTerms.pay_deadline,
+          refundDeadline: parsedContractTerms.refund_deadline,
+          wireFeeAmortization: parsedContractTerms.wire_fee_amortization || 1,
+          allowedAuditors: parsedContractTerms.auditors.map(x => ({
+            auditorBaseUrl: x.url,
+            auditorPub: x.master_pub,
+          })),
+          allowedExchanges: parsedContractTerms.exchanges.map(x => ({
+            exchangeBaseUrl: x.url,
+            exchangePub: x.master_pub,
+          })),
+          timestamp: parsedContractTerms.timestamp,
+          wireMethod: parsedContractTerms.wire_method,
+          wireInfoHash: parsedContractTerms.H_wire,
+          maxDepositFee: Amounts.parseOrThrow(parsedContractTerms.max_fee),
+        },
+        contractTermsRaw: JSON.stringify(proposalResp.contract_terms),
       };
       if (
         fulfillmentUrl.startsWith("http://";) ||
@@ -697,7 +735,7 @@ export async function submitPay(
 
   console.log("paying with session ID", sessionId);
 
-  const payUrl = new URL("pay", purchase.contractTerms.merchant_base_url).href;
+  const payUrl = new URL("pay", purchase.contractData.merchantBaseUrl).href;
 
   try {
     resp = await ws.http.postJson(payUrl, payReq);
@@ -714,10 +752,10 @@ export async function submitPay(
 
   const now = getTimestampNow();
 
-  const merchantPub = purchase.contractTerms.merchant_pub;
+  const merchantPub = purchase.contractData.merchantPub;
   const valid: boolean = await ws.cryptoApi.isValidPaymentSignature(
     merchantResp.sig,
-    purchase.contractTermsHash,
+    purchase.contractData.contractTermsHash,
     merchantPub,
   );
   if (!valid) {
@@ -731,19 +769,13 @@ export async function submitPay(
   purchase.lastPayError = undefined;
   purchase.payRetryInfo = initRetryInfo(false);
   if (isFirst) {
-    const ar = purchase.contractTerms.auto_refund;
+    const ar = purchase.contractData.autoRefund;
     if (ar) {
       console.log("auto_refund present");
-      const autoRefundDelay = extractTalerDuration(ar);
-      console.log("auto_refund valid", autoRefundDelay);
-      if (autoRefundDelay) {
-        purchase.refundStatusRequested = true;
-        purchase.refundStatusRetryInfo = initRetryInfo();
-        purchase.lastRefundStatusError = undefined;
-        purchase.autoRefundDeadline = {
-          t_ms: now.t_ms + autoRefundDelay.d_ms,
-        };
-      }
+      purchase.refundStatusRequested = true;
+      purchase.refundStatusRetryInfo = initRetryInfo();
+      purchase.lastRefundStatusError = undefined;
+      purchase.autoRefundDeadline = timestampAddDuration(now, ar);
     }
   }
 
@@ -761,8 +793,8 @@ export async function submitPay(
     },
   );
 
-  const nextUrl = getNextUrl(purchase.contractTerms);
-  ws.cachedNextUrl[purchase.contractTerms.fulfillment_url] = {
+  const nextUrl = getNextUrl(purchase.contractData);
+  ws.cachedNextUrl[purchase.contractData.fulfillmentUrl] = {
     nextUrl,
     lastSessionId: sessionId,
   };
@@ -816,9 +848,9 @@ export async function preparePay(
     console.error("bad proposal", proposal);
     throw Error("proposal is in invalid state");
   }
-  const contractTerms = d.contractTerms;
-  const merchantSig = d.merchantSig;
-  if (!contractTerms || !merchantSig) {
+  const contractData = d.contractData;
+  const merchantSig = d.contractData.merchantSig;
+  if (!merchantSig) {
     throw Error("BUG: proposal is in invalid state");
   }
 
@@ -828,45 +860,31 @@ export async function preparePay(
   const purchase = await ws.db.get(Stores.purchases, proposalId);
 
   if (!purchase) {
-    const paymentAmount = Amounts.parseOrThrow(contractTerms.amount);
-    let wireFeeLimit;
-    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: contractTerms.auditors,
-      allowedExchanges: contractTerms.exchanges,
-      depositFeeLimit: Amounts.parseOrThrow(contractTerms.max_fee),
-      paymentAmount,
-      wireFeeAmortization: contractTerms.wire_fee_amortization || 1,
-      wireFeeLimit,
-      wireFeeTime: extractTalerStampOrThrow(contractTerms.timestamp),
-      wireMethod: contractTerms.wire_method,
-    });
+    // If not already paid, check if we could pay for it.
+    const res = await getCoinsForPayment(ws, contractData);
 
     if (!res) {
       console.log("not confirming payment, insufficient coins");
       return {
         status: "insufficient-balance",
-        contractTerms: contractTerms,
+        contractTermsRaw: d.contractTermsRaw,
         proposalId: proposal.proposalId,
       };
     }
 
     return {
       status: "payment-possible",
-      contractTerms: contractTerms,
+      contractTermsRaw: d.contractTermsRaw,
       proposalId: proposal.proposalId,
       totalFees: res.totalFees,
     };
   }
 
   if (uriResult.sessionId && purchase.lastSessionId !== uriResult.sessionId) {
-    console.log("automatically re-submitting payment with different session 
ID")
-    await ws.db.runWithWriteTransaction([Stores.purchases], async (tx) => {
+    console.log(
+      "automatically re-submitting payment with different session ID",
+    );
+    await ws.db.runWithWriteTransaction([Stores.purchases], async tx => {
       const p = await tx.get(Stores.purchases, proposalId);
       if (!p) {
         return;
@@ -879,8 +897,8 @@ export async function preparePay(
 
   return {
     status: "paid",
-    contractTerms: purchase.contractTerms,
-    nextUrl: getNextUrl(purchase.contractTerms),
+    contractTermsRaw: purchase.contractTermsRaw,
+    nextUrl: getNextUrl(purchase.contractData),
   };
 }
 
@@ -906,7 +924,10 @@ export async function confirmPay(
     throw Error("proposal is in invalid state");
   }
 
-  let purchase = await ws.db.get(Stores.purchases, d.contractTermsHash);
+  let purchase = await ws.db.get(
+    Stores.purchases,
+    d.contractData.contractTermsHash,
+  );
 
   if (purchase) {
     if (
@@ -926,25 +947,7 @@ export async function confirmPay(
 
   logger.trace("confirmPay: purchase record does not exist yet");
 
-  const contractAmount = Amounts.parseOrThrow(d.contractTerms.amount);
-
-  let wireFeeLimit;
-  if (!d.contractTerms.max_wire_fee) {
-    wireFeeLimit = Amounts.getZero(contractAmount.currency);
-  } else {
-    wireFeeLimit = Amounts.parseOrThrow(d.contractTerms.max_wire_fee);
-  }
-
-  const res = await getCoinsForPayment(ws, {
-    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,
-    wireFeeTime: extractTalerStampOrThrow(d.contractTerms.timestamp),
-    wireMethod: d.contractTerms.wire_method,
-  });
+  const res = await getCoinsForPayment(ws, d.contractData);
 
   logger.trace("coin selection result", res);
 
@@ -956,7 +959,8 @@ export async function confirmPay(
 
   const { cds, totalAmount } = res;
   const payCoinInfo = await ws.cryptoApi.signDeposit(
-    d.contractTerms,
+    d.contractTermsRaw,
+    d.contractData,
     cds,
     totalAmount,
   );
@@ -964,7 +968,7 @@ export async function confirmPay(
     ws,
     proposal,
     payCoinInfo,
-    sessionIdOverride
+    sessionIdOverride,
   );
 
   logger.trace("confirmPay: submitting payment after creating purchase 
record");
diff --git a/src/operations/payback.ts b/src/operations/payback.ts
index 51adb6ad..18152769 100644
--- a/src/operations/payback.ts
+++ b/src/operations/payback.ts
@@ -24,7 +24,7 @@ import { InternalWalletState } from "./state";
 import { Stores, TipRecord, CoinStatus } from "../types/dbTypes";
 
 import { Logger } from "../util/logging";
-import { PaybackConfirmation } from "../types/talerTypes";
+import { RecoupConfirmation, codecForRecoupConfirmation } from 
"../types/talerTypes";
 import { updateExchangeFromUrl } from "./exchanges";
 import { NotificationType } from "../types/notifications";
 
@@ -72,7 +72,7 @@ export async function payback(
   if (resp.status !== 200) {
     throw Error();
   }
-  const paybackConfirmation = PaybackConfirmation.checked(await resp.json());
+  const paybackConfirmation = codecForRecoupConfirmation().decode(await 
resp.json());
   if (paybackConfirmation.reserve_pub !== coin.reservePub) {
     throw Error(`Coin's reserve doesn't match reserve on payback`);
   }
diff --git a/src/operations/pending.ts b/src/operations/pending.ts
index 36018085..ed3b59d7 100644
--- a/src/operations/pending.ts
+++ b/src/operations/pending.ts
@@ -27,7 +27,7 @@ import {
   PendingOperationsResponse,
   PendingOperationType,
 } from "../types/pending";
-import { Duration, getTimestampNow, Timestamp } from "../types/walletTypes";
+import { Duration, getTimestampNow, Timestamp, getDurationRemaining, 
durationMin } from "../util/time";
 import { TransactionHandle } from "../util/query";
 import { InternalWalletState } from "./state";
 
@@ -36,10 +36,8 @@ function updateRetryDelay(
   now: Timestamp,
   retryTimestamp: Timestamp,
 ): Duration {
-  if (retryTimestamp.t_ms <= now.t_ms) {
-    return { d_ms: 0 };
-  }
-  return { d_ms: Math.min(oldDelay.d_ms, retryTimestamp.t_ms - now.t_ms) };
+  const remaining = getDurationRemaining(retryTimestamp, now);
+  return durationMin(oldDelay, remaining);
 }
 
 async function gatherExchangePending(
@@ -278,7 +276,7 @@ async function gatherProposalPending(
       resp.pendingOperations.push({
         type: PendingOperationType.ProposalChoice,
         givesLifeness: false,
-        merchantBaseUrl: proposal.download!!.contractTerms.merchant_base_url,
+        merchantBaseUrl: proposal.download!!.contractData.merchantBaseUrl,
         proposalId: proposal.proposalId,
         proposalTimestamp: proposal.timestamp,
       });
diff --git a/src/operations/refresh.ts b/src/operations/refresh.ts
index 8390cac5..87d81cb2 100644
--- a/src/operations/refresh.ts
+++ b/src/operations/refresh.ts
@@ -34,7 +34,6 @@ import { Logger } from "../util/logging";
 import { getWithdrawDenomList } from "./withdraw";
 import { updateExchangeFromUrl } from "./exchanges";
 import {
-  getTimestampNow,
   OperationError,
   CoinPublicKey,
   RefreshReason,
@@ -43,6 +42,7 @@ import {
 import { guardOperationException } from "./errors";
 import { NotificationType } from "../types/notifications";
 import { getRandomBytes, encodeCrock } from "../crypto/talerCrypto";
+import { getTimestampNow } from "../util/time";
 
 const logger = new Logger("refresh.ts");
 
diff --git a/src/operations/refund.ts b/src/operations/refund.ts
index b4139c99..6a96868a 100644
--- a/src/operations/refund.ts
+++ b/src/operations/refund.ts
@@ -26,7 +26,6 @@
 import { InternalWalletState } from "./state";
 import {
   OperationError,
-  getTimestampNow,
   RefreshReason,
   CoinPublicKey,
 } from "../types/walletTypes";
@@ -47,12 +46,14 @@ import {
   MerchantRefundPermission,
   MerchantRefundResponse,
   RefundRequest,
+  codecForMerchantRefundResponse,
 } from "../types/talerTypes";
 import { AmountJson } from "../util/amounts";
 import { guardOperationException, OperationFailedError } from "./errors";
 import { randomBytes } from "../crypto/primitives/nacl-fast";
 import { encodeCrock } from "../crypto/talerCrypto";
 import { HttpResponseStatus } from "../util/http";
+import { getTimestampNow } from "../util/time";
 
 async function incrementPurchaseQueryRefundRetry(
   ws: InternalWalletState,
@@ -288,7 +289,7 @@ export async function applyRefund(
   console.log("processing purchase for refund");
   await startRefundQuery(ws, purchase.proposalId);
 
-  return purchase.contractTermsHash;
+  return purchase.contractData.contractTermsHash;
 }
 
 export async function processPurchaseQueryRefund(
@@ -334,9 +335,9 @@ async function processPurchaseQueryRefundImpl(
 
   const refundUrlObj = new URL(
     "refund",
-    purchase.contractTerms.merchant_base_url,
+    purchase.contractData.merchantBaseUrl,
   );
-  refundUrlObj.searchParams.set("order_id", purchase.contractTerms.order_id);
+  refundUrlObj.searchParams.set("order_id", purchase.contractData.orderId);
   const refundUrl = refundUrlObj.href;
   let resp;
   try {
@@ -349,7 +350,7 @@ async function processPurchaseQueryRefundImpl(
     throw Error(`unexpected status code (${resp.status}) for /refund`);
   }
 
-  const refundResponse = MerchantRefundResponse.checked(await resp.json());
+  const refundResponse = codecForMerchantRefundResponse().decode(await 
resp.json());
   await acceptRefundResponse(
     ws,
     proposalId,
@@ -409,8 +410,8 @@ async function processPurchaseApplyRefundImpl(
     const perm = info.perm;
     const req: RefundRequest = {
       coin_pub: perm.coin_pub,
-      h_contract_terms: purchase.contractTermsHash,
-      merchant_pub: purchase.contractTerms.merchant_pub,
+      h_contract_terms: purchase.contractData.contractTermsHash,
+      merchant_pub: purchase.contractData.merchantPub,
       merchant_sig: perm.merchant_sig,
       refund_amount: perm.refund_amount,
       refund_fee: perm.refund_fee,
diff --git a/src/operations/reserves.ts b/src/operations/reserves.ts
index 7be92782..2dedf17d 100644
--- a/src/operations/reserves.ts
+++ b/src/operations/reserves.ts
@@ -17,7 +17,6 @@
 import {
   CreateReserveRequest,
   CreateReserveResponse,
-  getTimestampNow,
   ConfirmReserveRequest,
   OperationError,
   AcceptWithdrawalResponse,
@@ -42,7 +41,7 @@ import {
   getExchangeTrust,
   getExchangePaytoUri,
 } from "./exchanges";
-import { WithdrawOperationStatusResponse } from "../types/talerTypes";
+import { WithdrawOperationStatusResponse, 
codecForWithdrawOperationStatusResponse } from "../types/talerTypes";
 import { assertUnreachable } from "../util/assertUnreachable";
 import { encodeCrock, getRandomBytes } from "../crypto/talerCrypto";
 import { randomBytes } from "../crypto/primitives/nacl-fast";
@@ -57,6 +56,7 @@ import {
 } from "./errors";
 import { NotificationType } from "../types/notifications";
 import { codecForReserveStatus } from "../types/ReserveStatus";
+import { getTimestampNow } from "../util/time";
 
 const logger = new Logger("reserves.ts");
 
@@ -289,7 +289,7 @@ async function processReserveBankStatusImpl(
         `unexpected status ${statusResp.status} for bank status query`,
       );
     }
-    status = WithdrawOperationStatusResponse.checked(await statusResp.json());
+    status = codecForWithdrawOperationStatusResponse().decode(await 
statusResp.json());
   } catch (e) {
     throw e;
   }
@@ -390,6 +390,7 @@ async function updateReserve(
   let resp;
   try {
     resp = await ws.http.get(reqUrl.href);
+    console.log("got reserve/status response", await resp.json());
     if (resp.status === 404) {
       const m = "The exchange does not know about this reserve (yet).";
       await incrementReserveRetry(ws, reservePub, undefined);
@@ -408,7 +409,7 @@ async function updateReserve(
     throw new OperationFailedAndReportedError(m);
   }
   const respJson = await resp.json();
-  const reserveInfo = codecForReserveStatus.decode(respJson);
+  const reserveInfo = codecForReserveStatus().decode(respJson);
   const balance = Amounts.parseOrThrow(reserveInfo.balance);
   await ws.db.runWithWriteTransaction(
     [Stores.reserves, Stores.reserveUpdatedEvents],
diff --git a/src/operations/return.ts b/src/operations/return.ts
deleted file mode 100644
index 4238f6cd..00000000
--- a/src/operations/return.ts
+++ /dev/null
@@ -1,271 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2019 GNUnet e.V.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- * Imports.
- */
-import {
-  ReturnCoinsRequest,
-  CoinWithDenom,
-} from "../types/walletTypes";
-import { Database } from "../util/query";
-import { InternalWalletState } from "./state";
-import { Stores, TipRecord, CoinStatus, CoinsReturnRecord, CoinRecord } from 
"../types/dbTypes";
-import * as Amounts from "../util/amounts";
-import { AmountJson } from "../util/amounts";
-import { Logger } from "../util/logging";
-import { canonicalJson } from "../util/helpers";
-import { ContractTerms } from "../types/talerTypes";
-import { selectPayCoins } from "./pay";
-
-const logger = new Logger("return.ts");
-
-async function getCoinsForReturn(
-  ws: InternalWalletState,
-  exchangeBaseUrl: string,
-  amount: AmountJson,
-): Promise<CoinWithDenom[] | undefined> {
-  const exchange = await ws.db.get(
-    Stores.exchanges,
-    exchangeBaseUrl,
-  );
-  if (!exchange) {
-    throw Error(`Exchange ${exchangeBaseUrl} not known to the wallet`);
-  }
-
-  const coins: CoinRecord[] = await ws.db.iterIndex(
-    Stores.coins.exchangeBaseUrlIndex,
-    exchange.baseUrl,
-  ).toArray();
-
-  if (!coins || !coins.length) {
-    return [];
-  }
-
-  const denoms = await ws.db.iterIndex(
-    Stores.denominations.exchangeBaseUrlIndex,
-    exchange.baseUrl,
-  ).toArray();
-
-  // Denomination of the first coin, we assume that all other
-  // coins have the same currency
-  const firstDenom = await ws.db.get(Stores.denominations, [
-    exchange.baseUrl,
-    coins[0].denomPub,
-  ]);
-  if (!firstDenom) {
-    throw Error("db inconsistent");
-  }
-  const currency = firstDenom.value.currency;
-
-  const cds: CoinWithDenom[] = [];
-  for (const coin of coins) {
-    const denom = await ws.db.get(Stores.denominations, [
-      exchange.baseUrl,
-      coin.denomPub,
-    ]);
-    if (!denom) {
-      throw Error("db inconsistent");
-    }
-    if (denom.value.currency !== currency) {
-      console.warn(
-        `same pubkey for different currencies at exchange ${exchange.baseUrl}`,
-      );
-      continue;
-    }
-    if (coin.suspended) {
-      continue;
-    }
-    if (coin.status !== CoinStatus.Fresh) {
-      continue;
-    }
-    cds.push({ coin, denom });
-  }
-
-  const res = selectPayCoins(denoms, cds, amount, amount);
-  if (res) {
-    return res.cds;
-  }
-  return undefined;
-}
-
-
-/**
- * Trigger paying coins back into the user's account.
- */
-export async function returnCoins(
-  ws: InternalWalletState,
-  req: ReturnCoinsRequest,
-): Promise<void> {
-  logger.trace("got returnCoins request", req);
-  const wireType = (req.senderWire as any).type;
-  logger.trace("wireType", wireType);
-  if (!wireType || typeof wireType !== "string") {
-    console.error(`wire type must be a non-empty string, not ${wireType}`);
-    return;
-  }
-  const stampSecNow = Math.floor(new Date().getTime() / 1000);
-  const exchange = await ws.db.get(Stores.exchanges, req.exchange);
-  if (!exchange) {
-    console.error(`Exchange ${req.exchange} not known to the wallet`);
-    return;
-  }
-  const exchangeDetails = exchange.details;
-  if (!exchangeDetails) {
-    throw Error("exchange information needs to be updated first.");
-  }
-  logger.trace("selecting coins for return:", req);
-  const cds = await getCoinsForReturn(ws, req.exchange, req.amount);
-  logger.trace(cds);
-
-  if (!cds) {
-    throw Error("coin return impossible, can't select coins");
-  }
-
-  const { priv, pub } = await ws.cryptoApi.createEddsaKeypair();
-
-  const wireHash = await ws.cryptoApi.hashString(
-    canonicalJson(req.senderWire),
-  );
-
-  const contractTerms: ContractTerms = {
-    H_wire: wireHash,
-    amount: Amounts.toString(req.amount),
-    auditors: [],
-    exchanges: [
-      { master_pub: exchangeDetails.masterPublicKey, url: exchange.baseUrl },
-    ],
-    extra: {},
-    fulfillment_url: "",
-    locations: [],
-    max_fee: Amounts.toString(req.amount),
-    merchant: {},
-    merchant_pub: pub,
-    order_id: "none",
-    pay_deadline: `/Date(${stampSecNow + 30 * 5})/`,
-    wire_transfer_deadline: `/Date(${stampSecNow + 60 * 5})/`,
-    merchant_base_url: "taler://return-to-account",
-    products: [],
-    refund_deadline: `/Date(${stampSecNow + 60 * 5})/`,
-    timestamp: `/Date(${stampSecNow})/`,
-    wire_method: wireType,
-  };
-
-  const contractTermsHash = await ws.cryptoApi.hashString(
-    canonicalJson(contractTerms),
-  );
-
-  const payCoinInfo = await ws.cryptoApi.signDeposit(
-    contractTerms,
-    cds,
-    Amounts.parseOrThrow(contractTerms.amount),
-  );
-
-  logger.trace("pci", payCoinInfo);
-
-  const coins = payCoinInfo.coinInfo.map(s => ({ coinPaySig: s.sig }));
-
-  const coinsReturnRecord: CoinsReturnRecord = {
-    coins,
-    contractTerms,
-    contractTermsHash,
-    exchange: exchange.baseUrl,
-    merchantPriv: priv,
-    wire: req.senderWire,
-  };
-
-  await ws.db.runWithWriteTransaction(
-    [Stores.coinsReturns, Stores.coins],
-    async tx => {
-      await tx.put(Stores.coinsReturns, coinsReturnRecord);
-      for (let coinInfo of payCoinInfo.coinInfo) {
-        const coin = await tx.get(Stores.coins, coinInfo.coinPub);
-        if (!coin) {
-          throw Error("coin allocated for deposit not in database anymore");
-        }
-        const remaining = Amounts.sub(coin.currentAmount, 
coinInfo.subtractedAmount);
-        if (remaining.saturated) {
-          throw Error("coin allocated for deposit does not have enough 
balance");
-        }
-        coin.currentAmount = remaining.amount;
-        await tx.put(Stores.coins, coin);
-      }
-    },
-  );
-
-  depositReturnedCoins(ws, coinsReturnRecord);
-}
-
-async function depositReturnedCoins(
-  ws: InternalWalletState,
-  coinsReturnRecord: CoinsReturnRecord,
-): Promise<void> {
-  for (const c of coinsReturnRecord.coins) {
-    if (c.depositedSig) {
-      continue;
-    }
-    const req = {
-      H_wire: coinsReturnRecord.contractTerms.H_wire,
-      coin_pub: c.coinPaySig.coin_pub,
-      coin_sig: c.coinPaySig.coin_sig,
-      contribution: c.coinPaySig.contribution,
-      denom_pub: c.coinPaySig.denom_pub,
-      h_contract_terms: coinsReturnRecord.contractTermsHash,
-      merchant_pub: coinsReturnRecord.contractTerms.merchant_pub,
-      pay_deadline: coinsReturnRecord.contractTerms.pay_deadline,
-      refund_deadline: coinsReturnRecord.contractTerms.refund_deadline,
-      timestamp: coinsReturnRecord.contractTerms.timestamp,
-      ub_sig: c.coinPaySig.ub_sig,
-      wire: coinsReturnRecord.wire,
-      wire_transfer_deadline: coinsReturnRecord.contractTerms.pay_deadline,
-    };
-    logger.trace("req", req);
-    const reqUrl = new URL("deposit", coinsReturnRecord.exchange);
-    const resp = await ws.http.postJson(reqUrl.href, req);
-    if (resp.status !== 200) {
-      console.error("deposit failed due to status code", resp);
-      continue;
-    }
-    const respJson = await resp.json();
-    if (respJson.status !== "DEPOSIT_OK") {
-      console.error("deposit failed", resp);
-      continue;
-    }
-
-    if (!respJson.sig) {
-      console.error("invalid 'sig' field", resp);
-      continue;
-    }
-
-    // FIXME: verify signature
-
-    // For every successful deposit, we replace the old record with an updated 
one
-    const currentCrr = await ws.db.get(
-      Stores.coinsReturns,
-      coinsReturnRecord.contractTermsHash,
-    );
-    if (!currentCrr) {
-      console.error("database inconsistent");
-      continue;
-    }
-    for (const nc of currentCrr.coins) {
-      if (nc.coinPaySig.coin_pub === c.coinPaySig.coin_pub) {
-        nc.depositedSig = respJson.sig;
-      }
-    }
-    await ws.db.put(Stores.coinsReturns, currentCrr);
-  }
-}
diff --git a/src/operations/tip.ts b/src/operations/tip.ts
index 76d0df22..fcdda076 100644
--- a/src/operations/tip.ts
+++ b/src/operations/tip.ts
@@ -18,13 +18,14 @@ import { InternalWalletState } from "./state";
 import { parseTipUri } from "../util/taleruri";
 import {
   TipStatus,
-  getTimestampNow,
   OperationError,
 } from "../types/walletTypes";
 import {
   TipPickupGetResponse,
   TipPlanchetDetail,
   TipResponse,
+  codecForTipPickupGetResponse,
+  codecForTipResponse,
 } from "../types/talerTypes";
 import * as Amounts from "../util/amounts";
 import {
@@ -39,11 +40,11 @@ import {
   getVerifiedWithdrawDenomList,
   processWithdrawSession,
 } from "./withdraw";
-import { getTalerStampSec, extractTalerStampOrThrow } from "../util/helpers";
 import { updateExchangeFromUrl } from "./exchanges";
 import { getRandomBytes, encodeCrock } from "../crypto/talerCrypto";
 import { guardOperationException } from "./errors";
 import { NotificationType } from "../types/notifications";
+import { getTimestampNow } from "../util/time";
 
 export async function getTipStatus(
   ws: InternalWalletState,
@@ -63,7 +64,7 @@ export async function getTipStatus(
   }
   const respJson = await merchantResp.json();
   console.log("resp:", respJson);
-  const tipPickupStatus = TipPickupGetResponse.checked(respJson);
+  const tipPickupStatus = codecForTipPickupGetResponse().decode(respJson);
 
   console.log("status", tipPickupStatus);
 
@@ -88,7 +89,7 @@ export async function getTipStatus(
       acceptedTimestamp: undefined,
       rejectedTimestamp: undefined,
       amount,
-      deadline: extractTalerStampOrThrow(tipPickupStatus.stamp_expire),
+      deadline: tipPickupStatus.stamp_expire,
       exchangeUrl: tipPickupStatus.exchange_url,
       merchantBaseUrl: res.merchantBaseUrl,
       nextUrl: undefined,
@@ -115,8 +116,8 @@ export async function getTipStatus(
     nextUrl: tipPickupStatus.extra.next_url,
     merchantOrigin: res.merchantOrigin,
     merchantTipId: res.merchantTipId,
-    expirationTimestamp: getTalerStampSec(tipPickupStatus.stamp_expire)!,
-    timestamp: getTalerStampSec(tipPickupStatus.stamp_created)!,
+    expirationTimestamp: tipPickupStatus.stamp_expire,
+    timestamp: tipPickupStatus.stamp_created,
     totalFees: tipRecord.totalFees,
     tipId: tipRecord.tipId,
   };
@@ -240,7 +241,7 @@ async function processTipImpl(
     throw e;
   }
 
-  const response = TipResponse.checked(await merchantResp.json());
+  const response = codecForTipResponse().decode(await merchantResp.json());
 
   if (response.reserve_sigs.length !== tipRecord.planchets.length) {
     throw Error("number of tip responses does not match requested planchets");
diff --git a/src/operations/withdraw.ts b/src/operations/withdraw.ts
index 38baffa8..27156015 100644
--- a/src/operations/withdraw.ts
+++ b/src/operations/withdraw.ts
@@ -27,33 +27,39 @@ import {
 } from "../types/dbTypes";
 import * as Amounts from "../util/amounts";
 import {
-  getTimestampNow,
-  AcceptWithdrawalResponse,
   BankWithdrawDetails,
   ExchangeWithdrawDetails,
   WithdrawDetails,
   OperationError,
 } from "../types/walletTypes";
-import { WithdrawOperationStatusResponse } from "../types/talerTypes";
+import { WithdrawOperationStatusResponse, 
codecForWithdrawOperationStatusResponse } from "../types/talerTypes";
 import { InternalWalletState } from "./state";
 import { parseWithdrawUri } from "../util/taleruri";
 import { Logger } from "../util/logging";
-import {
-  updateExchangeFromUrl,
-  getExchangeTrust,
-} from "./exchanges";
+import { updateExchangeFromUrl, getExchangeTrust } from "./exchanges";
 import { WALLET_EXCHANGE_PROTOCOL_VERSION } from "./versions";
 
 import * as LibtoolVersion from "../util/libtoolVersion";
 import { guardOperationException } from "./errors";
 import { NotificationType } from "../types/notifications";
+import {
+  getTimestampNow,
+  getDurationRemaining,
+  timestampCmp,
+  timestampSubtractDuraction,
+} from "../util/time";
 
 const logger = new Logger("withdraw.ts");
 
 function isWithdrawableDenom(d: DenominationRecord) {
   const now = getTimestampNow();
-  const started = now.t_ms >= d.stampStart.t_ms;
-  const stillOkay = d.stampExpireWithdraw.t_ms + 60 * 1000 > now.t_ms;
+  const started = timestampCmp(now, d.stampStart) >= 0;
+  const lastPossibleWithdraw = timestampSubtractDuraction(
+    d.stampExpireWithdraw,
+    { d_ms: 50 * 1000 },
+  );
+  const remaining = getDurationRemaining(lastPossibleWithdraw, now);
+  const stillOkay = remaining.d_ms !== 0;
   return started && stillOkay;
 }
 
@@ -108,11 +114,14 @@ export async function getBankWithdrawalInfo(
   }
   const resp = await ws.http.get(uriResult.statusUrl);
   if (resp.status !== 200) {
-    throw Error(`unexpected status (${resp.status}) from bank for 
${uriResult.statusUrl}`);
+    throw Error(
+      `unexpected status (${resp.status}) from bank for 
${uriResult.statusUrl}`,
+    );
   }
   const respJson = await resp.json();
   console.log("resp:", respJson);
-  const status = WithdrawOperationStatusResponse.checked(respJson);
+
+  const status = codecForWithdrawOperationStatusResponse().decode(respJson);
   return {
     amount: Amounts.parseOrThrow(status.amount),
     confirmTransferUrl: status.confirm_transfer_url,
@@ -125,20 +134,18 @@ export async function getBankWithdrawalInfo(
   };
 }
 
-
 async function getPossibleDenoms(
   ws: InternalWalletState,
   exchangeBaseUrl: string,
 ): Promise<DenominationRecord[]> {
-  return await ws.db.iterIndex(
-    Stores.denominations.exchangeBaseUrlIndex,
-    exchangeBaseUrl,
-  ).filter(d => {
-    return (
-      d.status === DenominationStatus.Unverified ||
-      d.status === DenominationStatus.VerifiedGood
-    );
-  });
+  return await ws.db
+    .iterIndex(Stores.denominations.exchangeBaseUrlIndex, exchangeBaseUrl)
+    .filter(d => {
+      return (
+        d.status === DenominationStatus.Unverified ||
+        d.status === DenominationStatus.VerifiedGood
+      );
+    });
 }
 
 /**
@@ -204,8 +211,11 @@ async function processPlanchet(
     planchet.denomPub,
   );
 
-
-  const isValid = await ws.cryptoApi.rsaVerify(planchet.coinPub, denomSig, 
planchet.denomPub);
+  const isValid = await ws.cryptoApi.rsaVerify(
+    planchet.coinPub,
+    denomSig,
+    planchet.denomPub,
+  );
   if (!isValid) {
     throw Error("invalid RSA signature by the exchange");
   }
@@ -261,7 +271,10 @@ async function processPlanchet(
             r.amountWithdrawCompleted,
             Amounts.add(denom.value, denom.feeWithdraw).amount,
           ).amount;
-          if (Amounts.cmp(r.amountWithdrawCompleted, 
r.amountWithdrawAllocated) == 0) {
+          if (
+            Amounts.cmp(r.amountWithdrawCompleted, r.amountWithdrawAllocated) 
==
+            0
+          ) {
             reserveDepleted = true;
           }
           await tx.put(Stores.reserves, r);
@@ -273,9 +286,9 @@ async function processPlanchet(
   );
 
   if (success) {
-    ws.notify( {
+    ws.notify({
       type: NotificationType.CoinWithdrawn,
-    } );
+    });
   }
 
   if (withdrawSessionFinished) {
@@ -436,10 +449,10 @@ async function processWithdrawCoin(
     return;
   }
 
-  const coin = await ws.db.getIndexed(
-    Stores.coins.byWithdrawalWithIdx,
-    [withdrawalSessionId, coinIndex],
-  );
+  const coin = await ws.db.getIndexed(Stores.coins.byWithdrawalWithIdx, [
+    withdrawalSessionId,
+    coinIndex,
+  ]);
 
   if (coin) {
     console.log("coin already exists");
@@ -494,7 +507,7 @@ async function resetWithdrawSessionRetry(
   ws: InternalWalletState,
   withdrawalSessionId: string,
 ) {
-  await ws.db.mutate(Stores.withdrawalSession, withdrawalSessionId, (x) => {
+  await ws.db.mutate(Stores.withdrawalSession, withdrawalSessionId, x => {
     if (x.retryInfo.active) {
       x.retryInfo = initRetryInfo();
     }
@@ -570,16 +583,12 @@ export async function getExchangeWithdrawalInfo(
     }
   }
 
-  const possibleDenoms = await ws.db.iterIndex(
-    Stores.denominations.exchangeBaseUrlIndex,
-    baseUrl,
-  ).filter(d => d.isOffered);
+  const possibleDenoms = await ws.db
+    .iterIndex(Stores.denominations.exchangeBaseUrlIndex, baseUrl)
+    .filter(d => d.isOffered);
 
   const trustedAuditorPubs = [];
-  const currencyRecord = await ws.db.get(
-    Stores.currencies,
-    amount.currency,
-  );
+  const currencyRecord = await ws.db.get(Stores.currencies, amount.currency);
   if (currencyRecord) {
     trustedAuditorPubs.push(...currencyRecord.auditors.map(a => a.auditorPub));
   }
@@ -606,7 +615,10 @@ export async function getExchangeWithdrawalInfo(
   let tosAccepted = false;
 
   if (exchangeInfo.termsOfServiceAcceptedTimestamp) {
-    if (exchangeInfo.termsOfServiceAcceptedEtag == 
exchangeInfo.termsOfServiceLastEtag) {
+    if (
+      exchangeInfo.termsOfServiceAcceptedEtag ==
+      exchangeInfo.termsOfServiceLastEtag
+    ) {
       tosAccepted = true;
     }
   }
diff --git a/src/types/ReserveStatus.ts b/src/types/ReserveStatus.ts
index d9b5d949..8ab7225e 100644
--- a/src/types/ReserveStatus.ts
+++ b/src/types/ReserveStatus.ts
@@ -29,14 +29,15 @@ import {
   makeCodecForUnion,
   makeCodecForList,
 } from "../util/codec";
-import { runBlock } from "../util/helpers";
 import { AmountString } from "./talerTypes";
-import { ReserveTransaction, codecForReserveTransaction } from 
"./ReserveTransaction";
-
+import {
+  ReserveTransaction,
+  codecForReserveTransaction,
+} from "./ReserveTransaction";
 
 /**
  * Status of a reserve.
- * 
+ *
  * Schema type for the exchange's response to "/reserve/status".
  */
 export interface ReserveStatus {
@@ -51,11 +52,10 @@ export interface ReserveStatus {
   history: ReserveTransaction[];
 }
 
-export const codecForReserveStatus = runBlock(() => (
+export const codecForReserveStatus = () =>
   typecheckedCodec<ReserveStatus>(
     makeCodecForObject<ReserveStatus>()
       .property("balance", codecForString)
-      .property("history", makeCodecForList(codecForReserveTransaction))
-      .build("ReserveStatus")
-  )
-));
\ No newline at end of file
+      .property("history", makeCodecForList(codecForReserveTransaction()))
+      .build("ReserveStatus"),
+  );
diff --git a/src/types/ReserveTransaction.ts b/src/types/ReserveTransaction.ts
index 2ec85949..e889f36a 100644
--- a/src/types/ReserveTransaction.ts
+++ b/src/types/ReserveTransaction.ts
@@ -28,15 +28,14 @@ import {
   makeCodecForConstString,
   makeCodecForUnion,
 } from "../util/codec";
-import { runBlock } from "../util/helpers";
 import {
   AmountString,
   Base32String,
   EddsaSignatureString,
-  TimestampString,
   EddsaPublicKeyString,
   CoinPublicKeyString,
 } from "./talerTypes";
+import { Timestamp, codecForTimestamp } from "../util/time";
 
 export const enum ReserveTransactionType {
   Withdraw = "WITHDRAW",
@@ -96,7 +95,7 @@ export interface ReserveDepositTransaction {
   /**
    * Timestamp of the incoming wire transfer.
    */
-  timestamp: TimestampString;
+  timestamp: Timestamp;
 }
 
 export interface ReserveClosingTransaction {
@@ -137,7 +136,7 @@ export interface ReserveClosingTransaction {
   /**
    * Time when the reserve was closed.
    */
-  timestamp: TimestampString;
+  timestamp: Timestamp;
 }
 
 export interface ReservePaybackTransaction {
@@ -173,7 +172,7 @@ export interface ReservePaybackTransaction {
   /**
    * Time when the funds were paid back into the reserve.
    */
-  timestamp: TimestampString;
+  timestamp: Timestamp;
 
   /**
    * Public key of the coin that was paid back.
@@ -190,7 +189,7 @@ export type ReserveTransaction =
   | ReserveClosingTransaction
   | ReservePaybackTransaction;
 
-export const codecForReserveWithdrawTransaction = runBlock(() =>
+export const codecForReserveWithdrawTransaction = () =>
   typecheckedCodec<ReserveWithdrawTransaction>(
     makeCodecForObject<ReserveWithdrawTransaction>()
       .property("amount", codecForString)
@@ -203,22 +202,20 @@ export const codecForReserveWithdrawTransaction = 
runBlock(() =>
       )
       .property("withdraw_fee", codecForString)
       .build("ReserveWithdrawTransaction"),
-  ),
-);
+  );
 
-export const codecForReserveDepositTransaction = runBlock(() =>
+export const codecForReserveDepositTransaction = () =>
   typecheckedCodec<ReserveDepositTransaction>(
     makeCodecForObject<ReserveDepositTransaction>()
       .property("amount", codecForString)
       .property("sender_account_url", codecForString)
-      .property("timestamp", codecForString)
+      .property("timestamp", codecForTimestamp)
       .property("wire_reference", codecForString)
       .property("type", 
makeCodecForConstString(ReserveTransactionType.Deposit))
       .build("ReserveDepositTransaction"),
-  ),
-);
+  );
 
-export const codecForReserveClosingTransaction = runBlock(() =>
+export const codecForReserveClosingTransaction = () =>
   typecheckedCodec<ReserveClosingTransaction>(
     makeCodecForObject<ReserveClosingTransaction>()
       .property("amount", codecForString)
@@ -226,14 +223,13 @@ export const codecForReserveClosingTransaction = 
runBlock(() =>
       .property("exchange_pub", codecForString)
       .property("exchange_sig", codecForString)
       .property("h_wire", codecForString)
-      .property("timestamp", codecForString)
+      .property("timestamp", codecForTimestamp)
       .property("type", 
makeCodecForConstString(ReserveTransactionType.Closing))
       .property("wtid", codecForString)
       .build("ReserveClosingTransaction"),
-  ),
-);
+  );
 
-export const codecForReservePaybackTransaction = runBlock(() =>
+export const codecForReservePaybackTransaction = () =>
   typecheckedCodec<ReservePaybackTransaction>(
     makeCodecForObject<ReservePaybackTransaction>()
       .property("amount", codecForString)
@@ -241,33 +237,31 @@ export const codecForReservePaybackTransaction = 
runBlock(() =>
       .property("exchange_pub", codecForString)
       .property("exchange_sig", codecForString)
       .property("receiver_account_details", codecForString)
-      .property("timestamp", codecForString)
+      .property("timestamp", codecForTimestamp)
       .property("type", 
makeCodecForConstString(ReserveTransactionType.Payback))
       .property("wire_transfer", codecForString)
       .build("ReservePaybackTransaction"),
-  ),
-);
+  );
 
-export const codecForReserveTransaction = runBlock(() =>
+export const codecForReserveTransaction = () =>
   typecheckedCodec<ReserveTransaction>(
     makeCodecForUnion<ReserveTransaction>()
       .discriminateOn("type")
       .alternative(
         ReserveTransactionType.Withdraw,
-        codecForReserveWithdrawTransaction,
+        codecForReserveWithdrawTransaction(),
       )
       .alternative(
         ReserveTransactionType.Closing,
-        codecForReserveClosingTransaction,
+        codecForReserveClosingTransaction(),
       )
       .alternative(
         ReserveTransactionType.Payback,
-        codecForReservePaybackTransaction,
+        codecForReservePaybackTransaction(),
       )
       .alternative(
         ReserveTransactionType.Deposit,
-        codecForReserveDepositTransaction,
+        codecForReserveDepositTransaction(),
       )
       .build<ReserveTransaction>("ReserveTransaction"),
-  ),
-);
+  );
diff --git a/src/types/dbTypes.ts b/src/types/dbTypes.ts
index f8f9880d..55559ab5 100644
--- a/src/types/dbTypes.ts
+++ b/src/types/dbTypes.ts
@@ -24,7 +24,6 @@
  * Imports.
  */
 import { AmountJson } from "../util/amounts";
-import { Checkable } from "../util/checkable";
 import {
   Auditor,
   CoinPaySig,
@@ -33,17 +32,16 @@ import {
   MerchantRefundPermission,
   PayReq,
   TipResponse,
+  ExchangeHandle,
 } from "./talerTypes";
 
 import { Index, Store } from "../util/query";
 import {
-  Timestamp,
   OperationError,
-  Duration,
-  getTimestampNow,
   RefreshReason,
 } from "./walletTypes";
 import { ReserveTransaction } from "./ReserveTransaction";
+import { Timestamp, Duration, getTimestampNow } from "../util/time";
 
 export enum ReserveRecordStatus {
   /**
@@ -104,6 +102,13 @@ export function updateRetryInfoTimeout(
   p: RetryPolicy = defaultRetryPolicy,
 ): void {
   const now = getTimestampNow();
+  if (now.t_ms === "never") {
+    throw Error("assertion failed");
+  }
+  if (p.backoffDelta.d_ms === "forever") {
+    r.nextRetry = { t_ms: "never" };
+    return;
+  }
   const t =
     now.t_ms + p.backoffDelta.d_ms * Math.pow(p.backoffBase, r.retryCounter);
   r.nextRetry = { t_ms: t };
@@ -319,86 +324,72 @@ export enum DenominationStatus {
 /**
  * Denomination record as stored in the wallet's database.
  */
-@Checkable.Class()
-export class DenominationRecord {
+export interface DenominationRecord {
   /**
    * Value of one coin of the denomination.
    */
-  @Checkable.Value(() => AmountJson)
   value: AmountJson;
 
   /**
    * The denomination public key.
    */
-  @Checkable.String()
   denomPub: string;
 
   /**
    * Hash of the denomination public key.
    * Stored in the database for faster lookups.
    */
-  @Checkable.String()
   denomPubHash: string;
 
   /**
    * Fee for withdrawing.
    */
-  @Checkable.Value(() => AmountJson)
   feeWithdraw: AmountJson;
 
   /**
    * Fee for depositing.
    */
-  @Checkable.Value(() => AmountJson)
   feeDeposit: AmountJson;
 
   /**
    * Fee for refreshing.
    */
-  @Checkable.Value(() => AmountJson)
   feeRefresh: AmountJson;
 
   /**
    * Fee for refunding.
    */
-  @Checkable.Value(() => AmountJson)
   feeRefund: AmountJson;
 
   /**
    * Validity start date of the denomination.
    */
-  @Checkable.Value(() => Timestamp)
   stampStart: Timestamp;
 
   /**
    * Date after which the currency can't be withdrawn anymore.
    */
-  @Checkable.Value(() => Timestamp)
   stampExpireWithdraw: Timestamp;
 
   /**
    * Date after the denomination officially doesn't exist anymore.
    */
-  @Checkable.Value(() => Timestamp)
   stampExpireLegal: Timestamp;
 
   /**
    * Data after which coins of this denomination can't be deposited anymore.
    */
-  @Checkable.Value(() => Timestamp)
   stampExpireDeposit: Timestamp;
 
   /**
    * Signature by the exchange's master key over the denomination
    * information.
    */
-  @Checkable.String()
   masterSig: string;
 
   /**
    * Did we verify the signature on the denomination?
    */
-  @Checkable.Number()
   status: DenominationStatus;
 
   /**
@@ -406,20 +397,12 @@ export class DenominationRecord {
    * we checked?
    * Only false when the exchange redacts a previously published denomination.
    */
-  @Checkable.Boolean()
   isOffered: boolean;
 
   /**
    * Base URL of the exchange.
    */
-  @Checkable.String()
   exchangeBaseUrl: string;
-
-  /**
-   * Verify that a value matches the schema of this class and convert it into a
-   * member.
-   */
-  static checked: (obj: any) => Denomination;
 }
 
 /**
@@ -713,36 +696,21 @@ export const enum ProposalStatus {
   REPURCHASE = "repurchase",
 }
 
-@Checkable.Class()
-export class ProposalDownload {
+export interface ProposalDownload {
   /**
    * The contract that was offered by the merchant.
    */
-  @Checkable.Value(() => ContractTerms)
-  contractTerms: ContractTerms;
+  contractTermsRaw: string;
 
-  /**
-   * Signature by the merchant over the contract details.
-   */
-  @Checkable.String()
-  merchantSig: string;
-
-  /**
-   * Signature by the merchant over the contract details.
-   */
-  @Checkable.String()
-  contractTermsHash: string;
+  contractData: WalletContractData;
 }
 
 /**
  * Record for a downloaded order, stored in the wallet's database.
  */
-@Checkable.Class()
-export class ProposalRecord {
-  @Checkable.String()
+export interface ProposalRecord {
   orderId: string;
 
-  @Checkable.String()
   merchantBaseUrl: string;
 
   /**
@@ -753,38 +721,31 @@ export class ProposalRecord {
   /**
    * Unique ID when the order is stored in the wallet DB.
    */
-  @Checkable.String()
   proposalId: string;
 
   /**
    * Timestamp (in ms) of when the record
    * was created.
    */
-  @Checkable.Number()
   timestamp: Timestamp;
 
   /**
    * Private key for the nonce.
    */
-  @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.
    */
-  @Checkable.Optional(Checkable.String())
   downloadSessionId?: string;
 
   /**
@@ -793,12 +754,6 @@ export class ProposalRecord {
    */
   retryInfo: RetryInfo;
 
-  /**
-   * Verify that a value matches the schema of this class and convert it into a
-   * member.
-   */
-  static checked: (obj: any) => ProposalRecord;
-
   lastError: OperationError | undefined;
 }
 
@@ -1120,6 +1075,38 @@ export interface ReserveUpdatedEventRecord {
   newHistoryTransactions: ReserveTransaction[];
 }
 
+export interface AllowedAuditorInfo {
+  auditorBaseUrl: string;
+  auditorPub: string;
+}
+
+export interface AllowedExchangeInfo {
+  exchangeBaseUrl: string;
+  exchangePub: string;
+}
+
+export interface WalletContractData {
+  fulfillmentUrl: string;
+  contractTermsHash: string;
+  merchantSig: string;
+  merchantPub: string;
+  amount: AmountJson;
+  orderId: string;
+  merchantBaseUrl: string;
+  summary: string;
+  autoRefund: Duration | undefined;
+  maxWireFee: AmountJson;
+  wireFeeAmortization: number;
+  payDeadline: Timestamp;
+  refundDeadline: Timestamp;
+  allowedAuditors: AllowedAuditorInfo[];
+  allowedExchanges: AllowedExchangeInfo[];
+  timestamp: Timestamp;
+  wireMethod: string;
+  wireInfoHash: string;
+  maxDepositFee: AmountJson;
+}
+
 /**
  * Record that stores status information about one purchase, starting from when
  * the customer accepts a proposal.  Includes refund status if applicable.
@@ -1131,15 +1118,12 @@ export interface PurchaseRecord {
    */
   proposalId: string;
 
-  /**
-   * Hash of the contract terms.
-   */
-  contractTermsHash: string;
-
   /**
    * Contract terms we got from the merchant.
    */
-  contractTerms: ContractTerms;
+  contractTermsRaw: string;
+
+  contractData: WalletContractData;
 
   /**
    * The payment request, ready to be send to the merchant's
@@ -1147,11 +1131,6 @@ export interface PurchaseRecord {
    */
   payReq: PayReq;
 
-  /**
-   * Signature from the merchant over the contract terms.
-   */
-  merchantSig: string;
-
   /**
    * Timestamp of the first time that sending a payment to the merchant
    * for this purchase was successful.
@@ -1266,12 +1245,9 @@ export interface DepositCoin {
  * the wallet itself, where the wallet acts as a "merchant" for the customer.
  */
 export interface CoinsReturnRecord {
-  /**
-   * Hash of the contract for sending coins to our own bank account.
-   */
-  contractTermsHash: string;
+  contractTermsRaw: string;
 
-  contractTerms: ContractTerms;
+  contractData: WalletContractData;
 
   /**
    * Private key where corresponding
@@ -1446,11 +1422,11 @@ export namespace Stores {
     fulfillmentUrlIndex = new Index<string, PurchaseRecord>(
       this,
       "fulfillmentUrlIndex",
-      "contractTerms.fulfillment_url",
+      "contractData.fulfillmentUrl",
     );
     orderIdIndex = new Index<string, PurchaseRecord>(this, "orderIdIndex", [
-      "contractTerms.merchant_base_url",
-      "contractTerms.order_id",
+      "contractData.merchantBaseUrl",
+      "contractData.orderId",
     ]);
   }
 
diff --git a/src/types/history.ts b/src/types/history.ts
index c49afd47..783b5591 100644
--- a/src/types/history.ts
+++ b/src/types/history.ts
@@ -18,9 +18,10 @@
  * Type and schema definitions for the wallet's history.
  */
 
-import { Timestamp, RefreshReason } from "./walletTypes";
+import { RefreshReason } from "./walletTypes";
 import { ReserveTransaction } from "./ReserveTransaction";
 import { WithdrawalSource } from "./dbTypes";
+import { Timestamp } from "../util/time";
 
 
 /**
diff --git a/src/types/pending.ts b/src/types/pending.ts
index efb97f53..f3979ac8 100644
--- a/src/types/pending.ts
+++ b/src/types/pending.ts
@@ -21,8 +21,9 @@
 /**
  * Imports.
  */
-import { OperationError, Timestamp, Duration } from "./walletTypes";
+import { OperationError } from "./walletTypes";
 import { WithdrawalSource, RetryInfo } from "./dbTypes";
+import { Timestamp, Duration } from "../util/time";
 
 export const enum PendingOperationType {
   Bug = "bug",
diff --git a/src/types/talerTypes.ts b/src/types/talerTypes.ts
index bb286b64..f8e2b1c6 100644
--- a/src/types/talerTypes.ts
+++ b/src/types/talerTypes.ts
@@ -26,132 +26,115 @@
 /**
  * Imports.
  */
-import { Checkable } from "../util/checkable";
 
-import * as Amounts from "../util/amounts";
-
-import { timestampCheck } from "../util/helpers";
+import {
+  typecheckedCodec,
+  makeCodecForObject,
+  codecForString,
+  makeCodecForList,
+  makeCodecOptional,
+  codecForAny,
+  codecForNumber,
+  codecForBoolean,
+  makeCodecForMap,
+} from "../util/codec";
+import { Timestamp, codecForTimestamp, Duration, codecForDuration } from 
"../util/time";
 
 /**
  * Denomination as found in the /keys response from the exchange.
  */
-@Checkable.Class()
 export class Denomination {
   /**
    * Value of one coin of the denomination.
    */
-  @Checkable.String(Amounts.check)
   value: string;
 
   /**
    * Public signing key of the denomination.
    */
-  @Checkable.String()
   denom_pub: string;
 
   /**
    * Fee for withdrawing.
    */
-  @Checkable.String(Amounts.check)
   fee_withdraw: string;
 
   /**
    * Fee for depositing.
    */
-  @Checkable.String(Amounts.check)
   fee_deposit: string;
 
   /**
    * Fee for refreshing.
    */
-  @Checkable.String(Amounts.check)
   fee_refresh: string;
 
   /**
    * Fee for refunding.
    */
-  @Checkable.String(Amounts.check)
   fee_refund: string;
 
   /**
    * Start date from which withdraw is allowed.
    */
-  @Checkable.String(timestampCheck)
-  stamp_start: string;
+  stamp_start: Timestamp;
 
   /**
    * End date for withdrawing.
    */
-  @Checkable.String(timestampCheck)
-  stamp_expire_withdraw: string;
+  stamp_expire_withdraw: Timestamp;
 
   /**
    * Expiration date after which the exchange can forget about
    * the currency.
    */
-  @Checkable.String(timestampCheck)
-  stamp_expire_legal: string;
+  stamp_expire_legal: Timestamp;
 
   /**
    * Date after which the coins of this denomination can't be
    * deposited anymore.
    */
-  @Checkable.String(timestampCheck)
-  stamp_expire_deposit: string;
+  stamp_expire_deposit: Timestamp;
 
   /**
    * Signature over the denomination information by the exchange's master
    * signing key.
    */
-  @Checkable.String()
   master_sig: string;
-
-  /**
-   * Verify that a value matches the schema of this class and convert it into a
-   * member.
-   */
-  static checked: (obj: any) => Denomination;
 }
 
 /**
  * Signature by the auditor that a particular denomination key is audited.
  */
-@Checkable.Class()
 export class AuditorDenomSig {
   /**
    * Denomination public key's hash.
    */
-  @Checkable.String()
   denom_pub_h: string;
 
   /**
    * The signature.
    */
-  @Checkable.String()
   auditor_sig: string;
 }
 
 /**
  * Auditor information as given by the exchange in /keys.
  */
-@Checkable.Class()
 export class Auditor {
   /**
    * Auditor's public key.
    */
-  @Checkable.String()
   auditor_pub: string;
 
   /**
    * Base URL of the auditor.
    */
-  @Checkable.String()
   auditor_url: string;
 
   /**
    * List of signatures for denominations by the auditor.
    */
-  @Checkable.List(Checkable.Value(() => AuditorDenomSig))
   denomination_keys: AuditorDenomSig[];
 }
 
@@ -190,26 +173,22 @@ export interface PaybackRequest {
 /**
  * Response that we get from the exchange for a payback request.
  */
-@Checkable.Class()
-export class PaybackConfirmation {
+export class RecoupConfirmation {
   /**
    * public key of the reserve that will receive the payback.
    */
-  @Checkable.String()
   reserve_pub: string;
 
   /**
    * How much will the exchange pay back (needed by wallet in
    * case coin was partially spent and wallet got restored from backup)
    */
-  @Checkable.String()
   amount: string;
 
   /**
    * Time by which the exchange received the /payback request.
    */
-  @Checkable.String()
-  timestamp: string;
+  timestamp: Timestamp;
 
   /**
    * the EdDSA signature of TALER_PaybackConfirmationPS using a current
@@ -218,7 +197,6 @@ export class PaybackConfirmation {
    * by the date specified (this allows the exchange delaying the transfer
    * a bit to aggregate additional payback requests into a larger one).
    */
-  @Checkable.String()
   exchange_sig: string;
 
   /**
@@ -227,14 +205,7 @@ export class PaybackConfirmation {
    * explicitly as the client might otherwise be confused by clock skew as to
    * which signing key was used.
    */
-  @Checkable.String()
   exchange_pub: string;
-
-  /**
-   * Verify that a value matches the schema of this class and convert it into a
-   * member.
-   */
-  static checked: (obj: any) => PaybackConfirmation;
 }
 
 /**
@@ -272,183 +243,155 @@ export interface CoinPaySig {
  * Information about an exchange as stored inside a
  * merchant's contract terms.
  */
-@Checkable.Class()
 export class ExchangeHandle {
   /**
    * Master public signing key of the exchange.
    */
-  @Checkable.String()
   master_pub: string;
 
   /**
    * Base URL of the exchange.
    */
-  @Checkable.String()
   url: string;
+}
 
+export class AuditorHandle {
   /**
-   * Verify that a value matches the schema of this class and convert it into a
-   * member.
+   * Official name of the auditor.
    */
-  static checked: (obj: any) => ExchangeHandle;
+  name: string;
+
+  /**
+   * Master public signing key of the auditor.
+   */
+  master_pub: string;
+
+  /**
+   * Base URL of the auditor.
+   */
+  url: string;
 }
 
 /**
  * Contract terms from a merchant.
  */
-@Checkable.Class({ validate: true })
 export class ContractTerms {
-  static validate(x: ContractTerms) {
-    if (x.exchanges.length === 0) {
-      throw Error("no exchanges in contract terms");
-    }
-  }
-
   /**
    * Hash of the merchant's wire details.
    */
-  @Checkable.String()
   H_wire: string;
 
   /**
    * Hash of the merchant's wire details.
    */
-  @Checkable.Optional(Checkable.String())
-  auto_refund?: string;
+  auto_refund?: Duration;
 
   /**
    * Wire method the merchant wants to use.
    */
-  @Checkable.String()
   wire_method: string;
 
   /**
    * Human-readable short summary of the contract.
    */
-  @Checkable.Optional(Checkable.String())
-  summary?: string;
+  summary: string;
 
   /**
    * Nonce used to ensure freshness.
    */
-  @Checkable.Optional(Checkable.String())
-  nonce?: string;
+  nonce: string;
 
   /**
    * Total amount payable.
    */
-  @Checkable.String(Amounts.check)
   amount: string;
 
   /**
    * Auditors accepted by the merchant.
    */
-  @Checkable.List(Checkable.AnyObject())
-  auditors: any[];
+  auditors: AuditorHandle[];
 
   /**
    * Deadline to pay for the contract.
    */
-  @Checkable.Optional(Checkable.String())
-  pay_deadline: string;
+  pay_deadline: Timestamp;
 
   /**
    * Delivery locations.
    */
-  @Checkable.Any()
   locations: any;
 
   /**
    * Maximum deposit fee covered by the merchant.
    */
-  @Checkable.String(Amounts.check)
   max_fee: string;
 
   /**
    * Information about the merchant.
    */
-  @Checkable.Any()
   merchant: any;
 
   /**
    * Public key of the merchant.
    */
-  @Checkable.String()
   merchant_pub: string;
 
   /**
    * List of accepted exchanges.
    */
-  @Checkable.List(Checkable.Value(() => ExchangeHandle))
   exchanges: ExchangeHandle[];
 
   /**
    * Products that are sold in this contract.
    */
-  @Checkable.List(Checkable.AnyObject())
-  products: any[];
+  products?: any[];
 
   /**
    * Deadline for refunds.
    */
-  @Checkable.String(timestampCheck)
-  refund_deadline: string;
+  refund_deadline: Timestamp;
 
   /**
    * Deadline for the wire transfer.
    */
-  @Checkable.String()
-  wire_transfer_deadline: string;
+  wire_transfer_deadline: Timestamp;
 
   /**
    * Time when the contract was generated by the merchant.
    */
-  @Checkable.String(timestampCheck)
-  timestamp: string;
+  timestamp: Timestamp;
 
   /**
    * Order id to uniquely identify the purchase within
    * one merchant instance.
    */
-  @Checkable.String()
   order_id: string;
 
   /**
    * Base URL of the merchant's backend.
    */
-  @Checkable.String()
   merchant_base_url: string;
 
   /**
    * Fulfillment URL to view the product or
    * delivery status.
    */
-  @Checkable.String()
   fulfillment_url: string;
 
   /**
    * Share of the wire fee that must be settled with one payment.
    */
-  @Checkable.Optional(Checkable.Number())
   wire_fee_amortization?: number;
 
   /**
    * Maximum wire fee that the merchant agrees to pay for.
    */
-  @Checkable.Optional(Checkable.String())
   max_wire_fee?: string;
 
   /**
    * Extra data, interpreted by the mechant only.
    */
-  @Checkable.Any()
   extra: any;
-
-  /**
-   * Verify that a value matches the schema of this class and convert it into a
-   * member.
-   */
-  static checked: (obj: any) => ContractTerms;
 }
 
 /**
@@ -480,42 +423,31 @@ export interface PayReq {
 /**
  * Refund permission in the format that the merchant gives it to us.
  */
-@Checkable.Class()
 export class MerchantRefundPermission {
   /**
    * Amount to be refunded.
    */
-  @Checkable.String(Amounts.check)
   refund_amount: string;
 
   /**
    * Fee for the refund.
    */
-  @Checkable.String(Amounts.check)
   refund_fee: string;
 
   /**
    * Public key of the coin being refunded.
    */
-  @Checkable.String()
   coin_pub: string;
 
   /**
    * Refund transaction ID between merchant and exchange.
    */
-  @Checkable.Number()
   rtransaction_id: number;
 
   /**
    * Signature made by the merchant over the refund permission.
    */
-  @Checkable.String()
   merchant_sig: string;
-
-  /**
-   * Create a MerchantRefundPermission from untyped JSON.
-   */
-  static checked: (obj: any) => MerchantRefundPermission;
 }
 
 /**
@@ -564,31 +496,22 @@ export interface RefundRequest {
 /**
  * Response for a refund pickup or a /pay in abort mode.
  */
-@Checkable.Class()
 export class MerchantRefundResponse {
   /**
    * Public key of the merchant
    */
-  @Checkable.String()
   merchant_pub: string;
 
   /**
    * Contract terms hash of the contract that
    * is being refunded.
    */
-  @Checkable.String()
   h_contract_terms: string;
 
   /**
    * The signed refund permissions, to be sent to the exchange.
    */
-  @Checkable.List(Checkable.Value(() => MerchantRefundPermission))
   refund_permissions: MerchantRefundPermission[];
-
-  /**
-   * Create a MerchantRefundReponse from untyped JSON.
-   */
-  static checked: (obj: any) => MerchantRefundResponse;
 }
 
 /**
@@ -625,306 +548,411 @@ export interface TipPickupRequest {
  * Reserve signature, defined as separate class to facilitate
  * schema validation with "@Checkable".
  */
-@Checkable.Class()
 export class ReserveSigSingleton {
   /**
    * Reserve signature.
    */
-  @Checkable.String()
   reserve_sig: string;
-
-  /**
-   * Create a ReserveSigSingleton from untyped JSON.
-   */
-  static checked: (obj: any) => ReserveSigSingleton;
 }
 
-
 /**
  * Response of the merchant
  * to the TipPickupRequest.
  */
-@Checkable.Class()
 export class TipResponse {
   /**
    * Public key of the reserve
    */
-  @Checkable.String()
   reserve_pub: string;
 
   /**
    * The order of the signatures matches the planchets list.
    */
-  @Checkable.List(Checkable.Value(() => ReserveSigSingleton))
   reserve_sigs: ReserveSigSingleton[];
-
-  /**
-   * Create a TipResponse from untyped JSON.
-   */
-  static checked: (obj: any) => TipResponse;
 }
 
 /**
  * Element of the payback list that the
  * exchange gives us in /keys.
  */
-@Checkable.Class()
 export class Payback {
   /**
    * The hash of the denomination public key for which the payback is offered.
    */
-  @Checkable.String()
   h_denom_pub: string;
 }
 
 /**
  * Structure that the exchange gives us in /keys.
  */
-@Checkable.Class({ extra: true })
-export class KeysJson {
+export class ExchangeKeysJson {
   /**
    * List of offered denominations.
    */
-  @Checkable.List(Checkable.Value(() => Denomination))
   denoms: Denomination[];
 
   /**
    * The exchange's master public key.
    */
-  @Checkable.String()
   master_public_key: string;
 
   /**
    * The list of auditors (partially) auditing the exchange.
    */
-  @Checkable.List(Checkable.Value(() => Auditor))
   auditors: Auditor[];
 
   /**
    * Timestamp when this response was issued.
    */
-  @Checkable.String(timestampCheck)
-  list_issue_date: string;
+  list_issue_date: Timestamp;
 
   /**
    * List of paybacks for compromised denominations.
    */
-  @Checkable.Optional(Checkable.List(Checkable.Value(() => Payback)))
   payback?: Payback[];
 
   /**
    * Short-lived signing keys used to sign online
    * responses.
    */
-  @Checkable.Any()
   signkeys: any;
 
   /**
    * Protocol version.
    */
-  @Checkable.Optional(Checkable.String())
-  version?: string;
-
-  /**
-   * Verify that a value matches the schema of this class and convert it into a
-   * member.
-   */
-  static checked: (obj: any) => KeysJson;
+  version: string;
 }
 
 /**
  * Wire fees as anounced by the exchange.
  */
-@Checkable.Class()
 export class WireFeesJson {
   /**
    * Cost of a wire transfer.
    */
-  @Checkable.String(Amounts.check)
   wire_fee: string;
 
   /**
    * Cost of clising a reserve.
    */
-  @Checkable.String(Amounts.check)
   closing_fee: string;
 
   /**
    * Signature made with the exchange's master key.
    */
-  @Checkable.String()
   sig: string;
 
   /**
    * Date from which the fee applies.
    */
-  @Checkable.String(timestampCheck)
-  start_date: string;
+  start_date: Timestamp;
 
   /**
    * Data after which the fee doesn't apply anymore.
    */
-  @Checkable.String(timestampCheck)
-  end_date: string;
-
-  /**
-   * Verify that a value matches the schema of this class and convert it into a
-   * member.
-   */
-  static checked: (obj: any) => WireFeesJson;
+  end_date: Timestamp;
 }
 
-@Checkable.Class({ extra: true })
 export class AccountInfo {
-  @Checkable.String()
   url: string;
-
-  @Checkable.String()
   master_sig: string;
 }
 
-@Checkable.Class({ extra: true })
 export class ExchangeWireJson {
-  @Checkable.Map(
-    Checkable.String(),
-    Checkable.List(Checkable.Value(() => WireFeesJson)),
-  )
-  fees: { [methodName: string]: WireFeesJson[] };
-
-  @Checkable.List(Checkable.Value(() => AccountInfo))
   accounts: AccountInfo[];
-
-  static checked: (obj: any) => ExchangeWireJson;
+  fees: { [methodName: string]: WireFeesJson[] };
 }
 
-/**
- * Wire detail, arbitrary object that must at least
- * contain a "type" key.
- */
-export type WireDetail = object & { type: string };
-
 /**
  * Proposal returned from the contract URL.
  */
-@Checkable.Class({ extra: true })
 export class Proposal {
   /**
    * Contract terms for the propoal.
+   * Raw, un-decoded JSON object.
    */
-  @Checkable.Value(() => ContractTerms)
-  contract_terms: ContractTerms;
+  contract_terms: any;
 
   /**
    * Signature over contract, made by the merchant.  The public key used for 
signing
    * must be contract_terms.merchant_pub.
    */
-  @Checkable.String()
   sig: string;
-
-  /**
-   * Verify that a value matches the schema of this class and convert it into a
-   * member.
-   */
-  static checked: (obj: any) => Proposal;
 }
 
 /**
  * Response from the internal merchant API.
  */
-@Checkable.Class({ extra: true })
 export class CheckPaymentResponse {
-  @Checkable.Boolean()
   paid: boolean;
-
-  @Checkable.Optional(Checkable.Boolean())
   refunded: boolean | undefined;
-
-  @Checkable.Optional(Checkable.String())
   refunded_amount: string | undefined;
-
-  @Checkable.Optional(Checkable.Value(() => ContractTerms))
-  contract_terms: ContractTerms | undefined;
-
-  @Checkable.Optional(Checkable.String())
+  contract_terms: any | undefined;
   taler_pay_uri: string | undefined;
-
-  @Checkable.Optional(Checkable.String())
   contract_url: string | undefined;
-
-  /**
-   * Verify that a value matches the schema of this class and convert it into a
-   * member.
-   */
-  static checked: (obj: any) => CheckPaymentResponse;
 }
 
 /**
  * Response from the bank.
  */
-@Checkable.Class({ extra: true })
 export class WithdrawOperationStatusResponse {
-  @Checkable.Boolean()
   selection_done: boolean;
 
-  @Checkable.Boolean()
   transfer_done: boolean;
 
-  @Checkable.String()
   amount: string;
 
-  @Checkable.Optional(Checkable.String())
   sender_wire?: string;
 
-  @Checkable.Optional(Checkable.String())
   suggested_exchange?: string;
 
-  @Checkable.Optional(Checkable.String())
   confirm_transfer_url?: string;
 
-  @Checkable.List(Checkable.String())
   wire_types: string[];
-
-  /**
-   * Verify that a value matches the schema of this class and convert it into a
-   * member.
-   */
-  static checked: (obj: any) => WithdrawOperationStatusResponse;
 }
 
 /**
  * Response from the merchant.
  */
-@Checkable.Class({ extra: true })
 export class TipPickupGetResponse {
-  @Checkable.AnyObject()
   extra: any;
 
-  @Checkable.String()
   amount: string;
 
-  @Checkable.String()
   amount_left: string;
 
-  @Checkable.String()
   exchange_url: string;
 
-  @Checkable.String()
-  stamp_expire: string;
-
-  @Checkable.String()
-  stamp_created: string;
+  stamp_expire: Timestamp;
 
-  /**
-   * Verify that a value matches the schema of this class and convert it into a
-   * member.
-   */
-  static checked: (obj: any) => TipPickupGetResponse;
+  stamp_created: Timestamp;
 }
 
-
 export type AmountString = string;
 export type Base32String = string;
 export type EddsaSignatureString = string;
 export type EddsaPublicKeyString = string;
 export type CoinPublicKeyString = string;
-export type TimestampString = string;
\ No newline at end of file
+
+export const codecForDenomination = () =>
+  typecheckedCodec<Denomination>(
+    makeCodecForObject<Denomination>()
+      .property("value", codecForString)
+      .property("denom_pub", codecForString)
+      .property("fee_withdraw", codecForString)
+      .property("fee_deposit", codecForString)
+      .property("fee_refresh", codecForString)
+      .property("fee_refund", codecForString)
+      .property("stamp_start", codecForTimestamp)
+      .property("stamp_expire_withdraw", codecForTimestamp)
+      .property("stamp_expire_legal", codecForTimestamp)
+      .property("stamp_expire_deposit", codecForTimestamp)
+      .property("master_sig", codecForString)
+      .build("Denomination"),
+  );
+
+export const codecForAuditorDenomSig = () =>
+  typecheckedCodec<AuditorDenomSig>(
+    makeCodecForObject<AuditorDenomSig>()
+      .property("denom_pub_h", codecForString)
+      .property("auditor_sig", codecForString)
+      .build("AuditorDenomSig"),
+  );
+
+export const codecForAuditor = () =>
+  typecheckedCodec<Auditor>(
+    makeCodecForObject<Auditor>()
+      .property("auditor_pub", codecForString)
+      .property("auditor_url", codecForString)
+      .property("denomination_keys", 
makeCodecForList(codecForAuditorDenomSig()))
+      .build("Auditor"),
+  );
+
+export const codecForExchangeHandle = () =>
+  typecheckedCodec<ExchangeHandle>(
+    makeCodecForObject<ExchangeHandle>()
+      .property("master_pub", codecForString)
+      .property("url", codecForString)
+      .build("ExchangeHandle"),
+  );
+
+export const codecForAuditorHandle = () =>
+  typecheckedCodec<AuditorHandle>(
+    makeCodecForObject<AuditorHandle>()
+    .property("name", codecForString)
+      .property("master_pub", codecForString)
+      .property("url", codecForString)
+      .build("AuditorHandle"),
+  );
+
+export const codecForContractTerms = () =>
+  typecheckedCodec<ContractTerms>(
+    makeCodecForObject<ContractTerms>()
+      .property("order_id", codecForString)
+      .property("fulfillment_url", codecForString)
+      .property("merchant_base_url", codecForString)
+      .property("H_wire", codecForString)
+      .property("auto_refund", makeCodecOptional(codecForDuration))
+      .property("wire_method", codecForString)
+      .property("summary", codecForString)
+      .property("nonce", codecForString)
+      .property("amount", codecForString)
+      .property("auditors", makeCodecForList(codecForAuditorHandle()))
+      .property("pay_deadline", codecForTimestamp)
+      .property("refund_deadline", codecForTimestamp)
+      .property("wire_transfer_deadline", codecForTimestamp)
+      .property("timestamp", codecForTimestamp)
+      .property("locations", codecForAny)
+      .property("max_fee", codecForString)
+      .property("max_wire_fee", makeCodecOptional(codecForString))
+      .property("merchant", codecForAny)
+      .property("merchant_pub", codecForString)
+      .property("exchanges", makeCodecForList(codecForExchangeHandle()))
+      .property("products", makeCodecOptional(makeCodecForList(codecForAny)))
+      .property("extra", codecForAny)
+      .build("ContractTerms"),
+  );
+
+export const codecForMerchantRefundPermission = () =>
+  typecheckedCodec<MerchantRefundPermission>(
+    makeCodecForObject<MerchantRefundPermission>()
+      .property("refund_amount", codecForString)
+      .property("refund_fee", codecForString)
+      .property("coin_pub", codecForString)
+      .property("rtransaction_id", codecForNumber)
+      .property("merchant_sig", codecForString)
+      .build("MerchantRefundPermission"),
+  );
+
+export const codecForMerchantRefundResponse = () =>
+  typecheckedCodec<MerchantRefundResponse>(
+    makeCodecForObject<MerchantRefundResponse>()
+      .property("merchant_pub", codecForString)
+      .property("h_contract_terms", codecForString)
+      .property(
+        "refund_permissions",
+        makeCodecForList(codecForMerchantRefundPermission()),
+      )
+      .build("MerchantRefundResponse"),
+  );
+
+export const codecForReserveSigSingleton = () =>
+  typecheckedCodec<ReserveSigSingleton>(
+    makeCodecForObject<ReserveSigSingleton>()
+      .property("reserve_sig", codecForString)
+      .build("ReserveSigSingleton"),
+  );
+
+export const codecForTipResponse = () =>
+  typecheckedCodec<TipResponse>(
+    makeCodecForObject<TipResponse>()
+      .property("reserve_pub", codecForString)
+      .property("reserve_sigs", 
makeCodecForList(codecForReserveSigSingleton()))
+      .build("TipResponse"),
+  );
+
+export const codecForPayback = () =>
+  typecheckedCodec<Payback>(
+    makeCodecForObject<Payback>()
+      .property("h_denom_pub", codecForString)
+      .build("Payback"),
+  );
+
+export const codecForExchangeKeysJson = () =>
+  typecheckedCodec<ExchangeKeysJson>(
+    makeCodecForObject<ExchangeKeysJson>()
+      .property("denoms", makeCodecForList(codecForDenomination()))
+      .property("master_public_key", codecForString)
+      .property("auditors", makeCodecForList(codecForAuditor()))
+      .property("list_issue_date", codecForTimestamp)
+      .property("payback", 
makeCodecOptional(makeCodecForList(codecForPayback())))
+      .property("signkeys", codecForAny)
+      .property("version", codecForString)
+      .build("KeysJson"),
+  );
+
+
+export const codecForWireFeesJson = () =>
+  typecheckedCodec<WireFeesJson>(
+    makeCodecForObject<WireFeesJson>()
+      .property("wire_fee", codecForString)
+      .property("closing_fee", codecForString)
+      .property("sig", codecForString)
+      .property("start_date", codecForTimestamp)
+      .property("end_date", codecForTimestamp)
+      .build("WireFeesJson"),
+  );
+
+export const codecForAccountInfo = () =>
+  typecheckedCodec<AccountInfo>(
+    makeCodecForObject<AccountInfo>()
+      .property("url", codecForString)
+      .property("master_sig", codecForString)
+      .build("AccountInfo"),
+  );
+
+export const codecForExchangeWireJson = () =>
+  typecheckedCodec<ExchangeWireJson>(
+    makeCodecForObject<ExchangeWireJson>()
+      .property("accounts", makeCodecForList(codecForAccountInfo()))
+      .property("fees", 
makeCodecForMap(makeCodecForList(codecForWireFeesJson())))
+      .build("ExchangeWireJson"),
+  );
+
+export const codecForProposal = () =>
+  typecheckedCodec<Proposal>(
+    makeCodecForObject<Proposal>()
+      .property("contract_terms", codecForAny)
+      .property("sig", codecForString)
+      .build("Proposal"),
+  );
+
+export const codecForCheckPaymentResponse = () =>
+  typecheckedCodec<CheckPaymentResponse>(
+    makeCodecForObject<CheckPaymentResponse>()
+      .property("paid", codecForBoolean)
+      .property("refunded", makeCodecOptional(codecForBoolean))
+      .property("refunded_amount", makeCodecOptional(codecForString))
+      .property("contract_terms", makeCodecOptional(codecForAny))
+      .property("taler_pay_uri", makeCodecOptional(codecForString))
+      .property("contract_url", makeCodecOptional(codecForString))
+      .build("CheckPaymentResponse"),
+  );
+
+
+export const codecForWithdrawOperationStatusResponse = () =>
+  typecheckedCodec<WithdrawOperationStatusResponse>(
+    makeCodecForObject<WithdrawOperationStatusResponse>()
+      .property("selection_done", codecForBoolean)
+      .property("transfer_done", codecForBoolean)
+      .property("amount",codecForString)
+      .property("sender_wire", makeCodecOptional(codecForString))
+      .property("suggested_exchange", makeCodecOptional(codecForString))
+      .property("confirm_transfer_url", makeCodecOptional(codecForString))
+      .property("wire_types", makeCodecForList(codecForString))
+      .build("WithdrawOperationStatusResponse"),
+  );
+
+export const codecForTipPickupGetResponse = () =>
+  typecheckedCodec<TipPickupGetResponse>(
+    makeCodecForObject<TipPickupGetResponse>()
+      .property("extra", codecForAny)
+      .property("amount", codecForString)
+      .property("amount_left", codecForString)
+      .property("exchange_url", codecForString)
+      .property("stamp_expire", codecForTimestamp)
+      .property("stamp_created", codecForTimestamp)
+      .build("TipPickupGetResponse"),
+  );
+
+
+export const codecForRecoupConfirmation = () =>
+  typecheckedCodec<RecoupConfirmation>(
+    makeCodecForObject<RecoupConfirmation>()
+      .property("reserve_pub", codecForString)
+      .property("amount", codecForString)
+      .property("timestamp", codecForTimestamp)
+      .property("exchange_sig", codecForString)
+      .property("exchange_pub", codecForString)
+      .build("RecoupConfirmation"),
+  );
diff --git a/src/types/types-test.ts b/src/types/types-test.ts
index a686fbe3..77ab2c4e 100644
--- a/src/types/types-test.ts
+++ b/src/types/types-test.ts
@@ -16,7 +16,7 @@
 
 import test from "ava";
 import * as Amounts from "../util/amounts";
-import { ContractTerms } from "./talerTypes";
+import { ContractTerms, codecForContractTerms } from "./talerTypes";
 
 const amt = (
   value: number,
@@ -130,6 +130,7 @@ test("amount stringification", t => {
 
 test("contract terms validation", t => {
   const c = {
+    nonce: "123123123",
     H_wire: "123",
     amount: "EUR:1.5",
     auditors: [],
@@ -138,23 +139,23 @@ test("contract terms validation", t => {
     max_fee: "EUR:1.5",
     merchant_pub: "12345",
     order_id: "test_order",
-    pay_deadline: "Date(12346)",
-    wire_transfer_deadline: "Date(12346)",
+    pay_deadline: { t_ms: 42 },
+    wire_transfer_deadline: { t_ms: 42 },
     merchant_base_url: "https://example.com/pay";,
     products: [],
-    refund_deadline: "Date(12345)",
+    refund_deadline: { t_ms: 42 },
     summary: "hello",
-    timestamp: "Date(12345)",
+    timestamp: { t_ms: 42 },
     wire_method: "test",
   };
 
-  ContractTerms.checked(c);
+  codecForContractTerms().decode(c);
 
   const c1 = JSON.parse(JSON.stringify(c));
-  c1.exchanges = [];
+  c1.pay_deadline = "foo";
 
   try {
-    ContractTerms.checked(c1);
+    codecForContractTerms().decode(c1);
   } catch (e) {
     t.pass();
     return;
diff --git a/src/types/walletTypes.ts b/src/types/walletTypes.ts
index df19d8dc..223ca432 100644
--- a/src/types/walletTypes.ts
+++ b/src/types/walletTypes.ts
@@ -25,8 +25,7 @@
 /**
  * Imports.
  */
-import { AmountJson } from "../util/amounts";
-import { Checkable } from "../util/checkable";
+import { AmountJson, codecForAmountJson } from "../util/amounts";
 import * as LibtoolVersion from "../util/libtoolVersion";
 import {
   CoinRecord,
@@ -35,30 +34,23 @@ import {
   ExchangeWireInfo,
 } from "./dbTypes";
 import { CoinPaySig, ContractTerms } from "./talerTypes";
+import { Timestamp } from "../util/time";
+import { typecheckedCodec, makeCodecForObject, codecForString, 
makeCodecOptional } from "../util/codec";
 
 /**
  * Response for the create reserve request to the wallet.
  */
-@Checkable.Class()
 export class CreateReserveResponse {
   /**
    * Exchange URL where the bank should create the reserve.
    * The URL is canonicalized in the response.
    */
-  @Checkable.String()
   exchange: string;
 
   /**
    * Reserve public key of the newly created reserve.
    */
-  @Checkable.String()
   reservePub: string;
-
-  /**
-   * Verify that a value matches the schema of this class and convert it into a
-   * member.
-   */
-  static checked: (obj: any) => CreateReserveResponse;
 }
 
 /**
@@ -259,88 +251,83 @@ export interface SenderWireInfos {
 /**
  * Request to mark a reserve as confirmed.
  */
-@Checkable.Class()
-export class CreateReserveRequest {
+export interface CreateReserveRequest {
   /**
    * The initial amount for the reserve.
    */
-  @Checkable.Value(() => AmountJson)
   amount: AmountJson;
 
   /**
    * Exchange URL where the bank should create the reserve.
    */
-  @Checkable.String()
   exchange: string;
 
   /**
    * Payto URI that identifies the exchange's account that the funds
    * for this reserve go into.
    */
-  @Checkable.String()
   exchangeWire: string;
 
   /**
    * Wire details (as a payto URI) for the bank account that sent the funds to
    * the exchange.
    */
-  @Checkable.Optional(Checkable.String())
   senderWire?: string;
 
   /**
    * URL to fetch the withdraw status from the bank.
    */
-  @Checkable.Optional(Checkable.String())
   bankWithdrawStatusUrl?: string;
-
-  /**
-   * Verify that a value matches the schema of this class and convert it into a
-   * member.
-   */
-  static checked: (obj: any) => CreateReserveRequest;
 }
 
+export const codecForCreateReserveRequest = () =>
+  typecheckedCodec<CreateReserveRequest>(
+    makeCodecForObject<CreateReserveRequest>()
+      .property("amount", codecForAmountJson())
+      .property("exchange", codecForString)
+      .property("exchangeWire", codecForString)
+      .property("senderWire", makeCodecOptional(codecForString))
+      .property("bankWithdrawStatusUrl", makeCodecOptional(codecForString))
+      .build("CreateReserveRequest"),
+  );
+
 /**
  * Request to mark a reserve as confirmed.
  */
-@Checkable.Class()
-export class ConfirmReserveRequest {
+export interface ConfirmReserveRequest {
   /**
    * Public key of then reserve that should be marked
    * as confirmed.
    */
-  @Checkable.String()
   reservePub: string;
-
-  /**
-   * Verify that a value matches the schema of this class and convert it into a
-   * member.
-   */
-  static checked: (obj: any) => ConfirmReserveRequest;
 }
 
+
+export const codecForConfirmReserveRequest = () =>
+  typecheckedCodec<ConfirmReserveRequest>(
+    makeCodecForObject<ConfirmReserveRequest>()
+      .property("reservePub", codecForString)
+      .build("ConfirmReserveRequest"),
+  );
+
 /**
  * Wire coins to the user's own bank account.
  */
-@Checkable.Class()
 export class ReturnCoinsRequest {
   /**
    * The amount to wire.
    */
-  @Checkable.Value(() => AmountJson)
   amount: AmountJson;
 
   /**
    * The exchange to take the coins from.
    */
-  @Checkable.String()
   exchange: string;
 
   /**
    * Wire details for the bank account of the customer that will
    * receive the funds.
    */
-  @Checkable.Any()
   senderWire?: object;
 
   /**
@@ -391,8 +378,8 @@ export interface TipStatus {
   tipId: string;
   merchantTipId: string;
   merchantOrigin: string;
-  expirationTimestamp: number;
-  timestamp: number;
+  expirationTimestamp: Timestamp;
+  timestamp: Timestamp;
   totalFees: AmountJson;
 }
 
@@ -418,14 +405,14 @@ export type PreparePayResult =
 export interface PreparePayResultPaymentPossible {
   status: "payment-possible";
   proposalId: string;
-  contractTerms: ContractTerms;
+  contractTermsRaw: string;
   totalFees: AmountJson;
 }
 
 export interface PreparePayResultInsufficientBalance {
   status: "insufficient-balance";
   proposalId: string;
-  contractTerms: ContractTerms;
+  contractTermsRaw: any;
 }
 
 export interface PreparePayResultError {
@@ -435,7 +422,7 @@ export interface PreparePayResultError {
 
 export interface PreparePayResultPaid {
   status: "paid";
-  contractTerms: ContractTerms;
+  contractTermsRaw: any;
   nextUrl: string;
 }
 
@@ -459,7 +446,7 @@ export interface AcceptWithdrawalResponse {
  * Details about a purchase, including refund status.
  */
 export interface PurchaseDetails {
-  contractTerms: ContractTerms;
+  contractTerms: any;
   hasRefund: boolean;
   totalRefundAmount: AmountJson;
   totalRefundAndRefreshFees: AmountJson;
@@ -479,30 +466,6 @@ export interface OperationError {
   details: any;
 }
 
-@Checkable.Class()
-export class Timestamp {
-  /**
-   * Timestamp in milliseconds.
-   */
-  @Checkable.Number()
-  readonly t_ms: number;
-
-  static checked: (obj: any) => Timestamp;
-}
-
-export interface Duration {
-  /**
-   * Duration in milliseconds.
-   */
-  readonly d_ms: number;
-}
-
-export function getTimestampNow(): Timestamp {
-  return {
-    t_ms: new Date().getTime(),
-  };
-}
-
 export interface PlanchetCreationResult {
   coinPub: string;
   coinPriv: string;
diff --git a/src/util/RequestThrottler.ts b/src/util/RequestThrottler.ts
index 01695ec3..0566306d 100644
--- a/src/util/RequestThrottler.ts
+++ b/src/util/RequestThrottler.ts
@@ -21,7 +21,7 @@
 /**
  * Imports.
  */
-import { getTimestampNow, Timestamp } from "../types/walletTypes";
+import { getTimestampNow, Timestamp, timestampSubtractDuraction, 
timestampDifference } from "../util/time";
 
 /**
  * Maximum request per second, per origin.
@@ -50,10 +50,14 @@ class OriginState {
 
   private refill(): void {
     const now = getTimestampNow();
-    const d = now.t_ms - this.lastUpdate.t_ms;
-    this.tokensSecond = Math.min(MAX_PER_SECOND, this.tokensSecond + (d / 
1000));
-    this.tokensMinute = Math.min(MAX_PER_MINUTE, this.tokensMinute + (d / 1000 
* 60));
-    this.tokensHour = Math.min(MAX_PER_HOUR, this.tokensHour + (d / 1000 * 60 
* 60));
+    const d = timestampDifference(now, this.lastUpdate);
+    if (d.d_ms === "forever") {
+      throw Error("assertion failed")
+    }
+    const d_s = d.d_ms / 1000;
+    this.tokensSecond = Math.min(MAX_PER_SECOND, this.tokensSecond + (d_s / 
1000));
+    this.tokensMinute = Math.min(MAX_PER_MINUTE, this.tokensMinute + (d_s / 
1000 * 60));
+    this.tokensHour = Math.min(MAX_PER_HOUR, this.tokensHour + (d_s / 1000 * 
60 * 60));
     this.lastUpdate = now;
   }
 
diff --git a/src/util/amounts.ts b/src/util/amounts.ts
index c8fb7679..c85c4839 100644
--- a/src/util/amounts.ts
+++ b/src/util/amounts.ts
@@ -1,17 +1,17 @@
 /*
- This file is part of TALER
- (C) 2018 GNUnet e.V. and INRIA
+ This file is part of GNU Taler
+ (C) 2019 Taler Systems S.A.
 
- TALER is free software; you can redistribute it and/or modify it under the
+ GNU Taler is free software; you can redistribute it and/or modify it under the
  terms of the GNU General Public License as published by the Free Software
  Foundation; either version 3, or (at your option) any later version.
 
- TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
  WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
  A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
 
  You should have received a copy of the GNU General Public License along with
- TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
 /**
@@ -21,7 +21,12 @@
 /**
  * Imports.
  */
-import { Checkable } from "./checkable";
+import {
+  typecheckedCodec,
+  makeCodecForObject,
+  codecForString,
+  codecForNumber,
+} from "./codec";
 
 /**
  * Number of fractional units that one value unit represents.
@@ -44,29 +49,32 @@ export const maxAmountValue = 2 ** 52;
  * Non-negative financial amount.  Fractional values are expressed as multiples
  * of 1e-8.
  */
-@Checkable.Class()
-export class AmountJson {
+export interface AmountJson {
   /**
    * Value, must be an integer.
    */
-  @Checkable.Number()
   readonly value: number;
 
   /**
    * Fraction, must be an integer.  Represent 1/1e8 of a unit.
    */
-  @Checkable.Number()
   readonly fraction: number;
 
   /**
    * Currency of the amount.
    */
-  @Checkable.String()
   readonly currency: string;
-
-  static checked: (obj: any) => AmountJson;
 }
 
+export const codecForAmountJson = () =>
+  typecheckedCodec<AmountJson>(
+    makeCodecForObject<AmountJson>()
+      .property("currency", codecForString)
+      .property("value", codecForNumber)
+      .property("fraction", codecForNumber)
+      .build("AmountJson"),
+  );
+
 /**
  * Result of a possibly overflowing operation.
  */
diff --git a/src/util/checkable.ts b/src/util/checkable.ts
deleted file mode 100644
index 3c9fe5bc..00000000
--- a/src/util/checkable.ts
+++ /dev/null
@@ -1,417 +0,0 @@
-/*
- This file is part of TALER
- (C) 2016 GNUnet e.V.
-
- TALER is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- TALER is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
- */
-
-
-/**
- * Decorators for validating JSON objects and converting them to a typed
- * object.
- *
- * The decorators are put onto classes, and the validation is done
- * via a static method that is filled in by the annotation.
- *
- * Example:
- * ```
- *  @Checkable.Class
- *  class Person {
- *    @Checkable.String
- *    name: string;
- *    @Checkable.Number
- *    age: number;
- *
- *    // Method will be implemented automatically
- *    static checked(obj: any): Person;
- *  }
- * ```
- */
-export namespace Checkable {
-
-  type Path = Array<number | string>;
-
-  interface SchemaErrorConstructor {
-    new (err: string): SchemaError;
-  }
-
-  interface SchemaError {
-    name: string;
-    message: string;
-  }
-
-  interface Prop {
-    propertyKey: any;
-    checker: any;
-    type?: any;
-    typeThunk?: () => any;
-    elementChecker?: any;
-    elementProp?: any;
-    keyProp?: any;
-    stringChecker?: (s: string) => boolean;
-    valueProp?: any;
-    optional?: boolean;
-  }
-
-  interface CheckableInfo {
-    extraAllowed: boolean;
-    props: Prop[];
-  }
-
-  // tslint:disable-next-line:no-shadowed-variable
-  export const SchemaError = (function SchemaError(this: any, message: string) 
{
-    const that: any = this as any;
-    that.name = "SchemaError";
-    that.message = message;
-    that.stack = (new Error() as any).stack;
-  }) as any as SchemaErrorConstructor;
-
-
-  SchemaError.prototype = new Error();
-
-  /**
-   * Classes that are checkable are annotated with this
-   * checkable info symbol, which contains the information necessary
-   * to check if they're valid.
-   */
-  const checkableInfoSym = Symbol("checkableInfo");
-
-  /**
-   * Get the current property list for a checkable type.
-   */
-  function getCheckableInfo(target: any): CheckableInfo {
-    let chk = target[checkableInfoSym] as CheckableInfo|undefined;
-    if (!chk) {
-      chk = { props: [], extraAllowed: false };
-      target[checkableInfoSym] = chk;
-    }
-    return chk;
-  }
-
-
-  function checkNumber(target: any, prop: Prop, path: Path): any {
-    if ((typeof target) !== "number") {
-      throw new SchemaError(`expected number for ${path}`);
-    }
-    return target;
-  }
-
-
-  function checkString(target: any, prop: Prop, path: Path): any {
-    if (typeof target !== "string") {
-      throw new SchemaError(`expected string for ${path}, got ${typeof target} 
instead`);
-    }
-    if (prop.stringChecker && !prop.stringChecker(target)) {
-      throw new SchemaError(`string property ${path} malformed`);
-    }
-    return target;
-  }
-
-  function checkBoolean(target: any, prop: Prop, path: Path): any {
-    if (typeof target !== "boolean") {
-      throw new SchemaError(`expected boolean for ${path}, got ${typeof 
target} instead`);
-    }
-    return target;
-  }
-
-
-  function checkAnyObject(target: any, prop: Prop, path: Path): any {
-    if (typeof target !== "object") {
-      throw new SchemaError(`expected (any) object for ${path}, got ${typeof 
target} instead`);
-    }
-    return target;
-  }
-
-
-  function checkAny(target: any, prop: Prop, path: Path): any {
-    return target;
-  }
-
-
-  function checkList(target: any, prop: Prop, path: Path): any {
-    if (!Array.isArray(target)) {
-      throw new SchemaError(`array expected for ${path}, got ${typeof target} 
instead`);
-    }
-    for (let i = 0; i < target.length; i++) {
-      const v = target[i];
-      prop.elementChecker(v, prop.elementProp, path.concat([i]));
-    }
-    return target;
-  }
-
-  function checkMap(target: any, prop: Prop, path: Path): any {
-    if (typeof target !== "object") {
-      throw new SchemaError(`expected object for ${path}, got ${typeof target} 
instead`);
-    }
-    for (const key in target) {
-      prop.keyProp.checker(key, prop.keyProp, path.concat([key]));
-      const value = target[key];
-      prop.valueProp.checker(value, prop.valueProp, path.concat([key]));
-    }
-    return target;
-  }
-
-
-  function checkOptional(target: any, prop: Prop, path: Path): any {
-    console.assert(prop.propertyKey);
-    prop.elementChecker(target,
-      prop.elementProp,
-      path.concat([prop.propertyKey]));
-    return target;
-  }
-
-
-  function checkValue(target: any, prop: Prop, path: Path): any {
-    let type;
-    if (prop.type) {
-     type = prop.type;
-    } else if (prop.typeThunk) {
-      type = prop.typeThunk();
-      if (!type) {
-        throw Error(`assertion failed: typeThunk returned null (prop is 
${JSON.stringify(prop)})`);
-      }
-    } else {
-      throw Error(`assertion failed: type/typeThunk missing (prop is 
${JSON.stringify(prop)})`);
-    }
-    const typeName = type.name || "??";
-    const v = target;
-    if (!v || typeof v !== "object") {
-      throw new SchemaError(
-        `expected object for ${path.join(".")}, got ${typeof v} instead`);
-    }
-    const chk = type.prototype[checkableInfoSym];
-    const props = chk.props;
-    const remainingPropNames = new Set(Object.getOwnPropertyNames(v));
-    const obj = new type();
-    for (const innerProp of props) {
-      if (!remainingPropNames.has(innerProp.propertyKey)) {
-        if (innerProp.optional) {
-          continue;
-        }
-        throw new SchemaError(`Property '${innerProp.propertyKey}' missing on 
'${path}' of '${typeName}'`);
-      }
-      if (!remainingPropNames.delete(innerProp.propertyKey)) {
-        throw new SchemaError("assertion failed");
-      }
-      const propVal = v[innerProp.propertyKey];
-      obj[innerProp.propertyKey] = innerProp.checker(propVal,
-        innerProp,
-        path.concat([innerProp.propertyKey]));
-    }
-
-    if (!chk.extraAllowed && remainingPropNames.size !== 0) {
-      const err = `superfluous properties 
${JSON.stringify(Array.from(remainingPropNames.values()))} of ${typeName}`;
-      throw new SchemaError(err);
-    }
-    return obj;
-  }
-
-
-  /**
-   * Class with checkable annotations on fields.
-   * This annotation adds the implementation of the `checked`
-   * static method.
-   */
-  export function Class(opts: {extra?: boolean, validate?: boolean} = {}) {
-    return (target: any) => {
-      const chk = getCheckableInfo(target.prototype);
-      chk.extraAllowed = !!opts.extra;
-      target.checked = (v: any) => {
-        const cv = checkValue(v, {
-          checker: checkValue,
-          propertyKey: "(root)",
-          type: target,
-        }, ["(root)"]);
-        if (opts.validate) {
-          if (typeof target.validate !== "function") {
-            throw Error("invalid Checkable annotion: validate method 
required");
-          }
-          // May throw exception
-          target.validate(cv);
-        }
-        return cv;
-      };
-      return target;
-    };
-  }
-
-
-  /**
-   * Target property must be a Checkable object of the given type.
-   */
-  export function Value(typeThunk: () => any) {
-    function deco(target: object, propertyKey: string | symbol): void {
-      const chk = getCheckableInfo(target);
-      chk.props.push({
-        checker: checkValue,
-        propertyKey,
-        typeThunk,
-      });
-    }
-
-    return deco;
-  }
-
-
-  /**
-   * List of values that match the given annotation.  For example, 
`@Checkable.List(Checkable.String)` is
-   * an annotation for a list of strings.
-   */
-  export function List(type: any) {
-    const stub = {};
-    type(stub, "(list-element)");
-    const elementProp = getCheckableInfo(stub).props[0];
-    const elementChecker = elementProp.checker;
-    if (!elementChecker) {
-      throw Error("assertion failed");
-    }
-    function deco(target: object, propertyKey: string | symbol): void {
-      const chk = getCheckableInfo(target);
-      chk.props.push({
-        checker: checkList,
-        elementChecker,
-        elementProp,
-        propertyKey,
-      });
-    }
-
-    return deco;
-  }
-
-
-  /**
-   * Map from the key type to value type.  Takes two annotations,
-   * one for the key type and one for the value type.
-   */
-  export function Map(keyType: any, valueType: any) {
-    const keyStub = {};
-    keyType(keyStub, "(map-key)");
-    const keyProp = getCheckableInfo(keyStub).props[0];
-    if (!keyProp) {
-      throw Error("assertion failed");
-    }
-    const valueStub = {};
-    valueType(valueStub, "(map-value)");
-    const valueProp = getCheckableInfo(valueStub).props[0];
-    if (!valueProp) {
-      throw Error("assertion failed");
-    }
-    function deco(target: object, propertyKey: string | symbol): void {
-      const chk = getCheckableInfo(target);
-      chk.props.push({
-        checker: checkMap,
-        keyProp,
-        propertyKey,
-        valueProp,
-      });
-    }
-
-    return deco;
-  }
-
-
-  /**
-   * Makes another annotation optional, for example 
`@Checkable.Optional(Checkable.Number)`.
-   */
-  export function Optional(type: (target: object, propertyKey: string | 
symbol) => void | any) {
-    const stub = {};
-    type(stub, "(optional-element)");
-    const elementProp = getCheckableInfo(stub).props[0];
-    const elementChecker = elementProp.checker;
-    if (!elementChecker) {
-      throw Error("assertion failed");
-    }
-    function deco(target: object, propertyKey: string | symbol): void {
-      const chk = getCheckableInfo(target);
-      chk.props.push({
-        checker: checkOptional,
-        elementChecker,
-        elementProp,
-        optional: true,
-        propertyKey,
-      });
-    }
-
-    return deco;
-  }
-
-
-  /**
-   * Target property must be a number.
-   */
-  export function Number(): (target: object, propertyKey: string | symbol) => 
void {
-    const deco = (target: object, propertyKey: string | symbol) => {
-      const chk = getCheckableInfo(target);
-      chk.props.push({checker: checkNumber, propertyKey});
-    };
-    return deco;
-  }
-
-
-  /**
-   * Target property must be an arbitary object.
-   */
-  export function AnyObject(): (target: object, propertyKey: string | symbol) 
=> void {
-    const deco = (target: object, propertyKey: string | symbol) => {
-      const chk = getCheckableInfo(target);
-      chk.props.push({
-        checker: checkAnyObject,
-        propertyKey,
-      });
-    };
-    return deco;
-  }
-
-
-  /**
-   * Target property can be anything.
-   *
-   * Not useful by itself, but in combination with higher-order annotations
-   * such as List or Map.
-   */
-  export function Any(): (target: object, propertyKey: string | symbol) => 
void {
-    const deco = (target: object, propertyKey: string | symbol) => {
-      const chk = getCheckableInfo(target);
-      chk.props.push({
-        checker: checkAny,
-        optional: true,
-        propertyKey,
-      });
-    };
-    return deco;
-  }
-
-
-  /**
-   * Target property must be a string.
-   */
-  export function String(
-    stringChecker?: (s: string) => boolean): (target: object, propertyKey: 
string | symbol,
-  ) => void {
-    const deco = (target: object, propertyKey: string | symbol) => {
-      const chk = getCheckableInfo(target);
-      chk.props.push({ checker: checkString, propertyKey, stringChecker });
-    };
-    return deco;
-  }
-
-  /**
-   * Target property must be a boolean value.
-   */
-  export function Boolean(): (target: object, propertyKey: string | symbol) => 
void {
-    const deco = (target: object, propertyKey: string | symbol) => {
-      const chk = getCheckableInfo(target);
-      chk.props.push({ checker: checkBoolean, propertyKey });
-    };
-    return deco;
-  }
-}
diff --git a/src/util/codec.ts b/src/util/codec.ts
index a13816c5..e18a5e74 100644
--- a/src/util/codec.ts
+++ b/src/util/codec.ts
@@ -32,11 +32,11 @@ export class DecodingError extends Error {
 /**
  * Context information to show nicer error messages when decoding fails.
  */
-interface Context {
+export interface Context {
   readonly path?: string[];
 }
 
-function renderContext(c?: Context): string {
+export function renderContext(c?: Context): string {
   const p = c?.path;
   if (p) {
     return p.join(".");
@@ -84,6 +84,9 @@ class ObjectCodecBuilder<OutputType, PartialOutputType> {
     x: K,
     codec: Codec<V>,
   ): ObjectCodecBuilder<OutputType, PartialOutputType & SingletonRecord<K, V>> 
{
+    if (!codec) {
+      throw Error("inner codec must be defined");
+    }
     this.propList.push({ name: x, codec: codec });
     return this as any;
   }
@@ -143,6 +146,9 @@ class UnionCodecBuilder<
     CommonBaseType,
     PartialTargetType | V
   > {
+    if (!codec) {
+      throw Error("inner codec must be defined");
+    }
     this.alternatives.set(tagValue, { codec, tagValue });
     return this as any;
   }
@@ -215,6 +221,9 @@ export function makeCodecForUnion<T>(): 
UnionCodecPreBuilder<T> {
 export function makeCodecForMap<T>(
   innerCodec: Codec<T>,
 ): Codec<{ [x: string]: T }> {
+  if (!innerCodec) {
+    throw Error("inner codec must be defined");
+  }
   return {
     decode(x: any, c?: Context): { [x: string]: T } {
       const map: { [x: string]: T } = {};
@@ -233,6 +242,9 @@ export function makeCodecForMap<T>(
  * Return a codec for a list, containing values described by the inner codec.
  */
 export function makeCodecForList<T>(innerCodec: Codec<T>): Codec<T[]> {
+  if (!innerCodec) {
+    throw Error("inner codec must be defined");
+  }
   return {
     decode(x: any, c?: Context): T[] {
       const arr: T[] = [];
@@ -255,7 +267,19 @@ export const codecForNumber: Codec<number> = {
     if (typeof x === "number") {
       return x;
     }
-    throw new DecodingError(`expected number at ${renderContext(c)}`);
+    throw new DecodingError(`expected number at ${renderContext(c)} but got 
${typeof x}`);
+  },
+};
+
+/**
+ * Return a codec for a value that must be a number.
+ */
+export const codecForBoolean: Codec<boolean> = {
+  decode(x: any, c?: Context): boolean {
+    if (typeof x === "boolean") {
+      return x;
+    }
+    throw new DecodingError(`expected boolean at ${renderContext(c)} but got 
${typeof x}`);
   },
 };
 
@@ -267,7 +291,16 @@ export const codecForString: Codec<string> = {
     if (typeof x === "string") {
       return x;
     }
-    throw new DecodingError(`expected string at ${renderContext(c)}`);
+    throw new DecodingError(`expected string at ${renderContext(c)} but got 
${typeof x}`);
+  },
+};
+
+/**
+ * Codec that allows any value.
+ */
+export const codecForAny: Codec<any> = {
+  decode(x: any, c?: Context): any {
+    return x;
   },
 };
 
@@ -281,12 +314,23 @@ export function makeCodecForConstString<V extends 
string>(s: V): Codec<V> {
         return x;
       }
       throw new DecodingError(
-        `expected string constant "${s}" at ${renderContext(c)}`,
+        `expected string constant "${s}" at ${renderContext(c)}  but got 
${typeof x}`,
       );
     },
   };
 }
 
+export function makeCodecOptional<V>(innerCodec: Codec<V>): Codec<V | 
undefined> {
+  return {
+    decode(x: any, c?: Context): V | undefined {
+      if (x === undefined || x === null) {
+        return undefined;
+      }
+      return innerCodec.decode(x, c);
+    }
+  }
+}
+
 export function typecheckedCodec<T = undefined>(c: Codec<T>): Codec<T> {
   return c;
 }
diff --git a/src/util/helpers.ts b/src/util/helpers.ts
index 8136f44f..722688d3 100644
--- a/src/util/helpers.ts
+++ b/src/util/helpers.ts
@@ -24,8 +24,6 @@
 import { AmountJson } from "./amounts";
 import * as Amounts from "./amounts";
 
-import { Timestamp, Duration } from "../types/walletTypes";
-
 /**
  * Show an amount in a form suitable for the user.
  * FIXME:  In the future, this should consider currency-specific
@@ -114,75 +112,6 @@ export function flatMap<T, U>(xs: T[], f: (x: T) => U[]): 
U[] {
   return xs.reduce((acc: U[], next: T) => [...f(next), ...acc], []);
 }
 
-
-/**
- * Extract a numeric timstamp (in seconds) from the Taler date format
- * ("/Date([n])/").  Returns null if input is not in the right format.
- */
-export function getTalerStampSec(stamp: string): number | null {
-  const m = stamp.match(/\/?Date\(([0-9]*)\)\/?/);
-  if (!m || !m[1]) {
-    return null;
-  }
-  return parseInt(m[1], 10);
-}
-
-/**
- * Extract a timestamp from a Taler timestamp string.
- */
-export function extractTalerStamp(stamp: string): Timestamp | undefined {
-  const m = stamp.match(/\/?Date\(([0-9]*)\)\/?/);
-  if (!m || !m[1]) {
-    return undefined;
-  }
-  return {
-    t_ms: parseInt(m[1], 10) * 1000,
-  };
-}
-
-/**
- * Extract a timestamp from a Taler timestamp string.
- */
-export function extractTalerStampOrThrow(stamp: string): Timestamp {
-  const r = extractTalerStamp(stamp);
-  if (!r) {
-    throw Error("invalid time stamp");
-  }
-  return r;
-}
-
-/**
- * Extract a duration from a Taler duration string.
- */
-export function extractTalerDuration(duration: string): Duration | undefined {
-  const m = duration.match(/\/?Delay\(([0-9]*)\)\/?/);
-  if (!m || !m[1]) {
-    return undefined;
-  }
-  return {
-    d_ms: parseInt(m[1], 10) * 1000,
-  };
-}
-
-/**
- * Extract a duration from a Taler duration string.
- */
-export function extractTalerDurationOrThrow(duration: string): Duration {
-  const r = extractTalerDuration(duration);
-  if (!r) {
-    throw Error("invalid duration");
-  }
-  return r;
-}
-
-/**
- * Check if a timestamp is in the right format.
- */
-export function timestampCheck(stamp: string): boolean {
-  return getTalerStampSec(stamp) !== null;
-}
-
-
 /**
  * Compute the hash function of a JSON object.
  */
diff --git a/src/util/time.ts b/src/util/time.ts
new file mode 100644
index 00000000..54d22bf8
--- /dev/null
+++ b/src/util/time.ts
@@ -0,0 +1,165 @@
+import { Codec, renderContext, Context } from "./codec";
+
+/*
+ This file is part of GNU Taler
+ (C) 2017-2019 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Helpers for relative and absolute time.
+ */
+
+export class Timestamp {
+  /**
+   * Timestamp in milliseconds.
+   */
+  readonly t_ms: number | "never";
+}
+
+export interface Duration {
+  /**
+   * Duration in milliseconds.
+   */
+  readonly d_ms: number | "forever";
+}
+
+export function getTimestampNow(): Timestamp {
+  return {
+    t_ms: new Date().getTime(),
+  };
+}
+
+export function getDurationRemaining(
+  deadline: Timestamp,
+  now = getTimestampNow(),
+): Duration {
+  if (deadline.t_ms === "never") {
+    return { d_ms: "forever" };
+  }
+  if (now.t_ms === "never") {
+    throw Error("invalid argument for 'now'");
+  }
+  if (deadline.t_ms < now.t_ms) {
+    return { d_ms: 0 };
+  }
+  return { d_ms: deadline.t_ms - now.t_ms };
+}
+
+export function timestampMin(t1: Timestamp, t2: Timestamp): Timestamp {
+  if (t1.t_ms === "never") {
+    return { t_ms: t2.t_ms };
+  }
+  if (t2.t_ms === "never") {
+    return { t_ms: t2.t_ms };
+  }
+  return { t_ms: Math.min(t1.t_ms, t2.t_ms) };
+}
+
+export function durationMin(d1: Duration, d2: Duration): Duration {
+  if (d1.d_ms === "forever") {
+    return { d_ms: d2.d_ms };
+  }
+  if (d2.d_ms === "forever") {
+    return { d_ms: d2.d_ms };
+  }
+  return { d_ms: Math.min(d1.d_ms, d2.d_ms) };
+}
+
+export function timestampCmp(t1: Timestamp, t2: Timestamp): number {
+  if (t1.t_ms === "never") {
+    if (t2.t_ms === "never") {
+      return 0;
+    }
+    return 1;
+  }
+  if (t2.t_ms === "never") {
+    return -1;
+  }
+  if (t1.t_ms == t2.t_ms) {
+    return 0;
+  }
+  if (t1.t_ms > t2.t_ms) {
+    return 1;
+  }
+  return -1;
+}
+
+export function timestampAddDuration(t1: Timestamp, d: Duration): Timestamp {
+  if (t1.t_ms === "never" || d.d_ms === "forever") {
+    return { t_ms: "never" };
+  }
+  return { t_ms: t1.t_ms + d.d_ms };
+}
+
+export function timestampSubtractDuraction(
+  t1: Timestamp,
+  d: Duration,
+): Timestamp {
+  if (t1.t_ms === "never") {
+    return { t_ms: "never" };
+  }
+  if (d.d_ms === "forever") {
+    return { t_ms: 0 };
+  }
+  return { t_ms: Math.max(0, t1.t_ms - d.d_ms) };
+}
+
+export function stringifyTimestamp(t: Timestamp) {
+  if (t.t_ms === "never") {
+    return "never";
+  }
+  return new Date(t.t_ms).toISOString();
+}
+
+export function timestampDifference(t1: Timestamp, t2: Timestamp): Duration {
+  if (t1.t_ms === "never") {
+    return { d_ms: "forever" };
+  }
+  if (t2.t_ms === "never") {
+    return { d_ms: "forever" };
+  }
+  return { d_ms: Math.abs(t1.t_ms - t2.t_ms) };
+}
+
+export const codecForTimestamp: Codec<Timestamp> = {
+  decode(x: any, c?: Context): Timestamp {
+    const t_ms = x.t_ms;
+    if (typeof t_ms === "string") {
+      if (t_ms === "never") {
+        return { t_ms: "never" };
+      }
+      throw Error(`expected timestamp at ${renderContext(c)}`);
+    }
+    if (typeof t_ms === "number") {
+      return { t_ms };
+    }
+    throw Error(`expected timestamp at ${renderContext(c)}`);
+  },
+};
+
+export const codecForDuration: Codec<Duration> = {
+  decode(x: any, c?: Context): Duration {
+    const d_ms = x.d_ms;
+    if (typeof d_ms === "string") {
+      if (d_ms === "forever") {
+        return { d_ms: "forever" };
+      }
+      throw Error(`expected duration at ${renderContext(c)}`);
+    }
+    if (typeof d_ms === "number") {
+      return { d_ms };
+    }
+    throw Error(`expected duration at ${renderContext(c)}`);
+  },
+};
diff --git a/src/util/timer.ts b/src/util/timer.ts
index 865c17fa..000f3660 100644
--- a/src/util/timer.ts
+++ b/src/util/timer.ts
@@ -1,17 +1,19 @@
+import { Duration } from "./time";
+
 /*
- This file is part of TALER
- (C) 2017 GNUnet e.V.
+ This file is part of GNU Taler
+ (C) 2017-2019 Taler Systems S.A.
 
- TALER is free software; you can redistribute it and/or modify it under the
+ GNU Taler is free software; you can redistribute it and/or modify it under the
  terms of the GNU General Public License as published by the Free Software
  Foundation; either version 3, or (at your option) any later version.
 
- TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
  WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
  A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
 
  You should have received a copy of the GNU General Public License along with
- TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
 /**
@@ -105,11 +107,13 @@ export class TimerGroup {
     }
   }
 
-  resolveAfter(delayMs: number): Promise<void> {
+  resolveAfter(delayMs: Duration): Promise<void> {
     return new Promise<void>((resolve, reject) => {
-      this.after(delayMs, () => {
-        resolve();
-      });
+      if (delayMs.d_ms !== "forever") {
+        this.after(delayMs.d_ms, () => {
+          resolve();
+        });
+      }
     });
   }
 
diff --git a/src/wallet.ts b/src/wallet.ts
index 407318aa..b08122b6 100644
--- a/src/wallet.ts
+++ b/src/wallet.ts
@@ -91,7 +91,6 @@ import { getHistory } from "./operations/history";
 import { getPendingOperations } from "./operations/pending";
 import { getBalances } from "./operations/balance";
 import { acceptTip, getTipStatus, processTip } from "./operations/tip";
-import { returnCoins } from "./operations/return";
 import { payback } from "./operations/payback";
 import { TimerGroup } from "./util/timer";
 import { AsyncCondition } from "./util/promiseUtils";
@@ -109,6 +108,7 @@ import {
   getFullRefundFees,
   applyRefund,
 } from "./operations/refund";
+import { durationMin, Duration } from "./util/time";
 
 
 const builtinCurrencies: CurrencyRecord[] = [
@@ -289,15 +289,15 @@ export class Wallet {
             numGivingLiveness++;
           }
         }
-        let dt;
+        let dt: Duration;
         if (
           allPending.pendingOperations.length === 0 ||
           allPending.nextRetryDelay.d_ms === Number.MAX_SAFE_INTEGER
         ) {
           // Wait for 5 seconds
-          dt = 5000;
+          dt = { d_ms: 5000 };
         } else {
-          dt = Math.min(5000, allPending.nextRetryDelay.d_ms);
+          dt = durationMin({ d_ms: 5000}, allPending.nextRetryDelay);
         }
         const timeout = this.timerGroup.resolveAfter(dt);
         this.ws.notify({
@@ -599,7 +599,7 @@ export class Wallet {
    * Trigger paying coins back into the user's account.
    */
   async returnCoins(req: ReturnCoinsRequest): Promise<void> {
-    return returnCoins(this.ws, req);
+    throw Error("not implemented");
   }
 
   /**
@@ -708,7 +708,7 @@ export class Wallet {
     ]).amount;
     const totalFees = totalRefundFees;
     return {
-      contractTerms: purchase.contractTerms,
+      contractTerms: purchase.contractTermsRaw,
       hasRefund: purchase.timestampLastRefundStatus !== undefined,
       totalRefundAmount: totalRefundAmount,
       totalRefundAndRefreshFees: totalFees,
diff --git a/src/webex/pages/pay.tsx b/src/webex/pages/pay.tsx
index eca115e7..b7f09b6f 100644
--- a/src/webex/pages/pay.tsx
+++ b/src/webex/pages/pay.tsx
@@ -74,7 +74,7 @@ function TalerPayDialog({ talerPayUri }: { talerPayUri: 
string }) {
     );
   }
 
-  const contractTerms = payStatus.contractTerms;
+  const contractTerms = payStatus.contractTermsRaw;
 
   if (!contractTerms) {
     return (
diff --git a/src/webex/renderHtml.tsx b/src/webex/renderHtml.tsx
index 767058eb..3204c410 100644
--- a/src/webex/renderHtml.tsx
+++ b/src/webex/renderHtml.tsx
@@ -31,6 +31,7 @@ import * as moment from "moment";
 import * as i18n from "./i18n";
 import React from "react";
 import ReactDOM from "react-dom";
+import { stringifyTimestamp } from "../util/time";
 
 /**
  * Render amount as HTML, which non-breaking space between
@@ -215,7 +216,7 @@ function FeeDetailsView(props: {
       <tbody>
         {rci!.wireFees.feesForType[s].map(f => (
           <tr>
-            <td>{moment.unix(Math.floor(f.endStamp.t_ms / 
1000)).format("llll")}</td>
+            <td>{stringifyTimestamp(f.endStamp)}</td>
             <td>{renderAmount(f.wireFee)}</td>
             <td>{renderAmount(f.closingFee)}</td>
           </tr>
@@ -239,9 +240,8 @@ function FeeDetailsView(props: {
       <p>
         {i18n.str`Rounding loss:`} {overhead}
       </p>
-      <p>{i18n.str`Earliest expiration (for deposit): ${moment
-        .unix(rci.earliestDepositExpiration.t_ms / 1000)
-        .fromNow()}`}</p>
+      <p>{i18n.str`Earliest expiration (for deposit): ${
+        stringifyTimestamp(rci.earliestDepositExpiration)}`}</p>
       <h3>Coin Fees</h3>
       <div style={{ overflow: "auto" }}>
         <table className="pure-table">
diff --git a/src/webex/wxBackend.ts b/src/webex/wxBackend.ts
index 97774a5c..ae12f9f9 100644
--- a/src/webex/wxBackend.ts
+++ b/src/webex/wxBackend.ts
@@ -25,8 +25,8 @@
  */
 import { BrowserCryptoWorkerFactory } from "../crypto/workers/cryptoApi";
 import { deleteTalerDatabase, openTalerDatabase, WALLET_DB_VERSION } from 
"../db";
-import { ConfirmReserveRequest, CreateReserveRequest, ReturnCoinsRequest, 
WalletDiagnostics } from "../types/walletTypes";
-import { AmountJson } from "../util/amounts";
+import { ConfirmReserveRequest, CreateReserveRequest, ReturnCoinsRequest, 
WalletDiagnostics, codecForCreateReserveRequest, codecForConfirmReserveRequest 
} from "../types/walletTypes";
+import { AmountJson, codecForAmountJson } from "../util/amounts";
 import { BrowserHttpLib } from "../util/http";
 import { OpenedPromise, openPromise } from "../util/promiseUtils";
 import { classifyTalerUri, TalerUriType } from "../util/taleruri";
@@ -91,14 +91,14 @@ async function handleMessage(
         exchange: detail.exchange,
         senderWire: detail.senderWire,
       };
-      const req = CreateReserveRequest.checked(d);
+      const req = codecForCreateReserveRequest().decode(d);
       return needsWallet().createReserve(req);
     }
     case "confirm-reserve": {
       const d = {
         reservePub: detail.reservePub,
       };
-      const req = ConfirmReserveRequest.checked(d);
+      const req = codecForConfirmReserveRequest().decode(d);
       return needsWallet().confirmReserve(req);
     }
     case "confirm-pay": {
@@ -117,7 +117,7 @@ async function handleMessage(
       if (!detail.baseUrl || typeof detail.baseUrl !== "string") {
         return Promise.resolve({ error: "bad url" });
       }
-      const amount = AmountJson.checked(detail.amount);
+      const amount = codecForAmountJson().decode(detail.amount);
       return needsWallet().getWithdrawDetailsForAmount(detail.baseUrl, amount);
     }
     case "get-history": {
diff --git a/tsconfig.json b/tsconfig.json
index 4156247a..ec15f8dd 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -56,7 +56,6 @@
     "src/operations/refresh.ts",
     "src/operations/refund.ts",
     "src/operations/reserves.ts",
-    "src/operations/return.ts",
     "src/operations/state.ts",
     "src/operations/tip.ts",
     "src/operations/versions.ts",
@@ -75,7 +74,6 @@
     "src/util/amounts.ts",
     "src/util/assertUnreachable.ts",
     "src/util/asyncMemo.ts",
-    "src/util/checkable.ts",
     "src/util/codec-test.ts",
     "src/util/codec.ts",
     "src/util/helpers-test.ts",
@@ -90,6 +88,7 @@
     "src/util/query.ts",
     "src/util/taleruri-test.ts",
     "src/util/taleruri.ts",
+    "src/util/time.ts",
     "src/util/timer.ts",
     "src/util/wire.ts",
     "src/wallet-test.ts",

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



reply via email to

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