gnunet-svn
[Top][All Lists]
Advanced

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

[taler-wallet-core] branch master updated: backup import WIP


From: gnunet
Subject: [taler-wallet-core] branch master updated: backup import WIP
Date: Mon, 21 Dec 2020 13:23:23 +0100

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

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

The following commit(s) were added to refs/heads/master by this push:
     new 95568395 backup import WIP
95568395 is described below

commit 95568395ce5817028046a96d95bd3399995154d5
Author: Florian Dold <florian@dold.me>
AuthorDate: Mon Dec 21 13:23:07 2020 +0100

    backup import WIP
---
 .../taler-wallet-core/src/operations/backup.ts     | 502 ++++++++++++++++++++-
 packages/taler-wallet-core/src/operations/pay.ts   |  47 +-
 .../taler-wallet-core/src/types/backupTypes.ts     |  61 ++-
 packages/taler-wallet-core/src/types/dbTypes.ts    |  17 +-
 .../taler-wallet-core/src/types/pendingTypes.ts    | 276 +++++++++++
 .../src/types/transactionsTypes.ts                 | 337 ++++++++++++++
 6 files changed, 1183 insertions(+), 57 deletions(-)

diff --git a/packages/taler-wallet-core/src/operations/backup.ts 
b/packages/taler-wallet-core/src/operations/backup.ts
index f071b6d0..fdccd23c 100644
--- a/packages/taler-wallet-core/src/operations/backup.ts
+++ b/packages/taler-wallet-core/src/operations/backup.ts
@@ -31,6 +31,7 @@ import {
   BackupCoinSource,
   BackupCoinSourceType,
   BackupDenomination,
+  BackupDenomSel,
   BackupExchange,
   BackupExchangeWireFee,
   BackupProposal,
@@ -39,6 +40,7 @@ import {
   BackupRecoupGroup,
   BackupRefreshGroup,
   BackupRefreshOldCoin,
+  BackupRefreshReason,
   BackupRefreshSession,
   BackupRefundItem,
   BackupRefundState,
@@ -50,15 +52,24 @@ import {
 import { TransactionHandle } from "../util/query";
 import {
   AbortStatus,
+  CoinSource,
   CoinSourceType,
   CoinStatus,
   ConfigRecord,
+  DenominationStatus,
+  DenomSelectionState,
+  ExchangeUpdateStatus,
+  ExchangeWireInfo,
+  ProposalDownload,
   ProposalStatus,
+  RefreshSessionRecord,
   RefundState,
+  ReserveBankInfo,
+  ReserveRecordStatus,
   Stores,
 } from "../types/dbTypes";
-import { checkDbInvariant } from "../util/invariants";
-import { Amounts, codecForAmountString } from "../util/amounts";
+import { checkDbInvariant, checkLogicInvariant } from "../util/invariants";
+import { AmountJson, Amounts, codecForAmountString } from "../util/amounts";
 import {
   decodeCrock,
   eddsaGetPublic,
@@ -71,7 +82,11 @@ import {
 import { canonicalizeBaseUrl, canonicalJson, j2s } from "../util/helpers";
 import { getTimestampNow, Timestamp } from "../util/time";
 import { URL } from "../util/url";
-import { AmountString, TipResponse } from "../types/talerTypes";
+import {
+  AmountString,
+  codecForContractTerms,
+  ContractTerms,
+} from "../types/talerTypes";
 import {
   buildCodecForObject,
   Codec,
@@ -85,6 +100,8 @@ import {
 import { Logger } from "../util/logging";
 import { gzipSync } from "fflate";
 import { kdf } from "../crypto/primitives/kdf";
+import { initRetryInfo } from "../util/retries";
+import { RefreshReason } from "../types/walletTypes";
 
 interface WalletBackupConfState {
   deviceId: string;
@@ -207,7 +224,7 @@ export async function exportBackup(
           timestamp_start: wg.timestampStart,
           timestamp_finish: wg.timestampFinish,
           withdrawal_group_id: wg.withdrawalGroupId,
-          secret_seed: wg.secretSeed
+          secret_seed: wg.secretSeed,
         });
       });
 
@@ -425,7 +442,7 @@ export async function exportBackup(
 
         backupPurchases.push({
           clock_created: 1,
-          contract_terms_raw: purch.contractTermsRaw,
+          contract_terms_raw: purch.download.contractTermsRaw,
           auto_refund_deadline: purch.autoRefundDeadline,
           merchant_pay_sig: purch.merchantPaySig,
           pay_coins: purch.payCoinSelection.coinPubs.map((x, i) => ({
@@ -478,6 +495,9 @@ export async function exportBackup(
           timestamp: prop.timestamp,
           contract_terms_raw: prop.download?.contractTermsRaw,
           download_session_id: prop.downloadSessionId,
+          merchant_base_url: prop.merchantBaseUrl,
+          order_id: prop.orderId,
+          merchant_sig: prop.download?.contractData.merchantSig,
         });
       });
 
@@ -572,9 +592,47 @@ export async function encryptBackup(
   throw Error("not implemented");
 }
 
+interface CompletedCoin {
+  coinPub: string;
+  coinEvHash: string;
+}
+
+/**
+ * Precomputed cryptographic material for a backup import.
+ *
+ * We separate this data from the backup blob as we want the backup
+ * blob to be small, and we can't compute it during the database transaction,
+ * as the async crypto worker communication would auto-close the database 
transaction.
+ */
+interface BackupCryptoPrecomputedData {
+  denomPubToHash: Record<string, string>;
+  coinPrivToCompletedCoin: Record<string, CompletedCoin>;
+  proposalNoncePrivToProposalPub: { [priv: string]: string };
+  proposalIdToContractTermsHash: { [proposalId: string]: string };
+  reservePrivToPub: Record<string, string>;
+}
+
+function checkBackupInvariant(b: boolean, m?: string): asserts b {
+  if (!b) {
+    if (m) {
+      throw Error(`BUG: backup invariant failed (${m})`);
+    } else {
+      throw Error("BUG: backup invariant failed");
+    }
+  }
+}
+
+function getDenomSelStateFromBackup(
+  tx: TransactionHandle<typeof Stores.denominations>,
+  sel: BackupDenomSel,
+): Promise<DenomSelectionState> {
+  throw Error("not implemented");
+}
+
 export async function importBackup(
   ws: InternalWalletState,
   backupRequest: BackupRequest,
+  cryptoComp: BackupCryptoPrecomputedData,
 ): Promise<void> {
   await provideBackupState(ws);
   return ws.db.runWithWriteTransaction(
@@ -593,8 +651,439 @@ export async function importBackup(
       Stores.withdrawalGroups,
     ],
     async (tx) => {
+      // FIXME: validate schema!
+      const backupBlob = backupRequest.backupBlob as WalletBackupContentV1;
 
-    });
+      // FIXME: validate version
+
+      for (const backupExchange of backupBlob.exchanges) {
+        const existingExchange = await tx.get(
+          Stores.exchanges,
+          backupExchange.base_url,
+        );
+
+        if (!existingExchange) {
+          const wireInfo: ExchangeWireInfo = {
+            accounts: backupExchange.accounts.map((x) => ({
+              master_sig: x.master_sig,
+              payto_uri: x.payto_uri,
+            })),
+            feesForType: {},
+          };
+          for (const fee of backupExchange.wire_fees) {
+            const w = (wireInfo.feesForType[fee.wire_type] ??= []);
+            w.push({
+              closingFee: Amounts.parseOrThrow(fee.closing_fee),
+              endStamp: fee.end_stamp,
+              sig: fee.sig,
+              startStamp: fee.start_stamp,
+              wireFee: Amounts.parseOrThrow(fee.wire_fee),
+            });
+          }
+          await tx.put(Stores.exchanges, {
+            addComplete: true,
+            baseUrl: backupExchange.base_url,
+            builtIn: false,
+            updateReason: undefined,
+            permanent: true,
+            retryInfo: initRetryInfo(),
+            termsOfServiceAcceptedEtag: backupExchange.tos_etag_accepted,
+            termsOfServiceText: undefined,
+            termsOfServiceLastEtag: backupExchange.tos_etag_last,
+            updateStarted: getTimestampNow(),
+            updateStatus: ExchangeUpdateStatus.FetchKeys,
+            wireInfo,
+            details: {
+              currency: backupExchange.currency,
+              auditors: backupExchange.auditors.map((x) => ({
+                auditor_pub: x.auditor_pub,
+                auditor_url: x.auditor_url,
+                denomination_keys: x.denomination_keys,
+              })),
+              lastUpdateTime: { t_ms: "never" },
+              masterPublicKey: backupExchange.master_public_key,
+              nextUpdateTime: { t_ms: "never" },
+              protocolVersion: backupExchange.protocol_version,
+              signingKeys: backupExchange.signing_keys.map((x) => ({
+                key: x.key,
+                master_sig: x.master_sig,
+                stamp_end: x.stamp_end,
+                stamp_expire: x.stamp_expire,
+                stamp_start: x.stamp_start,
+              })),
+            },
+          });
+        }
+
+        for (const backupDenomination of backupExchange.denominations) {
+          const denomPubHash =
+            cryptoComp.denomPubToHash[backupDenomination.denom_pub];
+          checkLogicInvariant(!!denomPubHash);
+          const existingDenom = await tx.get(Stores.denominations, [
+            backupExchange.base_url,
+            denomPubHash,
+          ]);
+          if (!existingDenom) {
+            await tx.put(Stores.denominations, {
+              denomPub: backupDenomination.denom_pub,
+              denomPubHash: denomPubHash,
+              exchangeBaseUrl: backupExchange.base_url,
+              feeDeposit: Amounts.parseOrThrow(backupDenomination.fee_deposit),
+              feeRefresh: Amounts.parseOrThrow(backupDenomination.fee_refresh),
+              feeRefund: Amounts.parseOrThrow(backupDenomination.fee_refund),
+              feeWithdraw: Amounts.parseOrThrow(
+                backupDenomination.fee_withdraw,
+              ),
+              isOffered: backupDenomination.is_offered,
+              isRevoked: backupDenomination.is_revoked,
+              masterSig: backupDenomination.master_sig,
+              stampExpireDeposit: backupDenomination.stamp_expire_deposit,
+              stampExpireLegal: backupDenomination.stamp_expire_legal,
+              stampExpireWithdraw: backupDenomination.stamp_expire_withdraw,
+              stampStart: backupDenomination.stamp_start,
+              status: DenominationStatus.VerifiedGood,
+              value: Amounts.parseOrThrow(backupDenomination.value),
+            });
+          }
+          for (const backupCoin of backupDenomination.coins) {
+            const compCoin =
+              cryptoComp.coinPrivToCompletedCoin[backupCoin.coin_priv];
+            checkLogicInvariant(!!compCoin);
+            const existingCoin = await tx.get(Stores.coins, compCoin.coinPub);
+            if (!existingCoin) {
+              let coinSource: CoinSource;
+              switch (backupCoin.coin_source.type) {
+                case BackupCoinSourceType.Refresh:
+                  coinSource = {
+                    type: CoinSourceType.Refresh,
+                    oldCoinPub: backupCoin.coin_source.old_coin_pub,
+                  };
+                  break;
+                case BackupCoinSourceType.Tip:
+                  coinSource = {
+                    type: CoinSourceType.Tip,
+                    coinIndex: backupCoin.coin_source.coin_index,
+                    walletTipId: backupCoin.coin_source.wallet_tip_id,
+                  };
+                  break;
+                case BackupCoinSourceType.Withdraw:
+                  coinSource = {
+                    type: CoinSourceType.Withdraw,
+                    coinIndex: backupCoin.coin_source.coin_index,
+                    reservePub: backupCoin.coin_source.reserve_pub,
+                    withdrawalGroupId:
+                      backupCoin.coin_source.withdrawal_group_id,
+                  };
+                  break;
+              }
+              await tx.put(Stores.coins, {
+                blindingKey: backupCoin.blinding_key,
+                coinEvHash: compCoin.coinEvHash,
+                coinPriv: backupCoin.coin_priv,
+                currentAmount: Amounts.parseOrThrow(backupCoin.current_amount),
+                denomSig: backupCoin.denom_sig,
+                coinPub: compCoin.coinPub,
+                suspended: false,
+                exchangeBaseUrl: backupExchange.base_url,
+                denomPub: backupDenomination.denom_pub,
+                denomPubHash,
+                status: backupCoin.fresh
+                  ? CoinStatus.Fresh
+                  : CoinStatus.Dormant,
+                coinSource,
+              });
+            }
+          }
+        }
+
+        for (const backupReserve of backupExchange.reserves) {
+          const reservePub =
+            cryptoComp.reservePrivToPub[backupReserve.reserve_priv];
+          checkLogicInvariant(!!reservePub);
+          const existingReserve = await tx.get(Stores.reserves, reservePub);
+          const instructedAmount = Amounts.parseOrThrow(
+            backupReserve.instructed_amount,
+          );
+          if (!existingReserve) {
+            let bankInfo: ReserveBankInfo | undefined;
+            if (backupReserve.bank_info) {
+              bankInfo = {
+                exchangePaytoUri: backupReserve.bank_info.exchange_payto_uri,
+                statusUrl: backupReserve.bank_info.status_url,
+                confirmUrl: backupReserve.bank_info.confirm_url,
+              };
+            }
+            await tx.put(Stores.reserves, {
+              currency: instructedAmount.currency,
+              instructedAmount,
+              exchangeBaseUrl: backupExchange.base_url,
+              reservePub,
+              reservePriv: backupReserve.reserve_priv,
+              requestedQuery: false,
+              bankInfo,
+              timestampCreated: backupReserve.timestamp_created,
+              timestampBankConfirmed:
+                backupReserve.bank_info?.timestamp_bank_confirmed,
+              timestampReserveInfoPosted:
+                backupReserve.bank_info?.timestamp_reserve_info_posted,
+              senderWire: backupReserve.sender_wire,
+              retryInfo: initRetryInfo(false),
+              lastError: undefined,
+              lastSuccessfulStatusQuery: { t_ms: "never" },
+              initialWithdrawalGroupId:
+                backupReserve.initial_withdrawal_group_id,
+              initialWithdrawalStarted:
+                backupReserve.withdrawal_groups.length > 0,
+              // FIXME!
+              reserveStatus: ReserveRecordStatus.QUERYING_STATUS,
+              initialDenomSel: await getDenomSelStateFromBackup(
+                tx,
+                backupReserve.initial_selected_denoms,
+              ),
+            });
+          }
+          for (const backupWg of backupReserve.withdrawal_groups) {
+            const existingWg = await tx.get(
+              Stores.withdrawalGroups,
+              backupWg.withdrawal_group_id,
+            );
+            if (!existingWg) {
+              await tx.put(Stores.withdrawalGroups, {
+                denomsSel: await getDenomSelStateFromBackup(
+                  tx,
+                  backupWg.selected_denoms,
+                ),
+                exchangeBaseUrl: backupExchange.base_url,
+                lastError: undefined,
+                rawWithdrawalAmount: Amounts.parseOrThrow(
+                  backupWg.raw_withdrawal_amount,
+                ),
+                reservePub,
+                retryInfo: initRetryInfo(false),
+                secretSeed: backupWg.secret_seed,
+                timestampStart: backupWg.timestamp_start,
+                timestampFinish: backupWg.timestamp_finish,
+                withdrawalGroupId: backupWg.withdrawal_group_id,
+              });
+            }
+          }
+        }
+      }
+
+      for (const backupProposal of backupBlob.proposals) {
+        const existingProposal = await tx.get(
+          Stores.proposals,
+          backupProposal.proposal_id,
+        );
+        if (!existingProposal) {
+          let download: ProposalDownload | undefined;
+          let proposalStatus: ProposalStatus;
+          switch (backupProposal.proposal_status) {
+            case BackupProposalStatus.Proposed:
+              if (backupProposal.contract_terms_raw) {
+                proposalStatus = ProposalStatus.PROPOSED;
+              } else {
+                proposalStatus = ProposalStatus.DOWNLOADING;
+              }
+              break;
+            case BackupProposalStatus.Refused:
+              proposalStatus = ProposalStatus.REFUSED;
+              break;
+            case BackupProposalStatus.Repurchase:
+              proposalStatus = ProposalStatus.REPURCHASE;
+              break;
+            case BackupProposalStatus.PermanentlyFailed:
+              proposalStatus = ProposalStatus.PERMANENTLY_FAILED;
+              break;
+          }
+          if (backupProposal.contract_terms_raw) {
+            checkDbInvariant(!!backupProposal.merchant_sig);
+            const parsedContractTerms = codecForContractTerms().decode(
+              backupProposal.contract_terms_raw,
+            );
+            const amount = Amounts.parseOrThrow(parsedContractTerms.amount);
+            const contractTermsHash =
+              cryptoComp.proposalIdToContractTermsHash[
+                backupProposal.proposal_id
+              ];
+            let maxWireFee: AmountJson;
+            if (parsedContractTerms.max_wire_fee) {
+              maxWireFee = Amounts.parseOrThrow(
+                parsedContractTerms.max_wire_fee,
+              );
+            } else {
+              maxWireFee = Amounts.getZero(amount.currency);
+            }
+            download = {
+              contractData: {
+                amount,
+                contractTermsHash: contractTermsHash,
+                fulfillmentUrl: parsedContractTerms.fulfillment_url ?? "",
+                merchantBaseUrl: parsedContractTerms.merchant_base_url,
+                merchantPub: parsedContractTerms.merchant_pub,
+                merchantSig: backupProposal.merchant_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,
+                ),
+                merchant: parsedContractTerms.merchant,
+                products: parsedContractTerms.products,
+                summaryI18n: parsedContractTerms.summary_i18n,
+              },
+              contractTermsRaw: backupProposal.contract_terms_raw,
+            };
+          }
+          await tx.put(Stores.proposals, {
+            claimToken: backupProposal.claim_token,
+            lastError: undefined,
+            merchantBaseUrl: backupProposal.merchant_base_url,
+            timestamp: backupProposal.timestamp,
+            orderId: backupProposal.order_id,
+            noncePriv: backupProposal.nonce_priv,
+            noncePub:
+              cryptoComp.proposalNoncePrivToProposalPub[
+                backupProposal.nonce_priv
+              ],
+            proposalId: backupProposal.proposal_id,
+            repurchaseProposalId: backupProposal.repurchase_proposal_id,
+            retryInfo: initRetryInfo(false),
+            download,
+            proposalStatus,
+          });
+        }
+      }
+
+      for (const backupPurchase of backupBlob.purchases) {
+        const existingPurchase = await tx.get(
+          Stores.purchases,
+          backupPurchase.proposal_id,
+        );
+        if (!existingPurchase) {
+          await tx.put(Stores.purchases, {});
+        }
+      }
+
+      for (const backupRefreshGroup of backupBlob.refresh_groups) {
+        const existingRg = await tx.get(
+          Stores.refreshGroups,
+          backupRefreshGroup.refresh_group_id,
+        );
+        if (!existingRg) {
+          let reason: RefreshReason;
+          switch (backupRefreshGroup.reason) {
+            case BackupRefreshReason.AbortPay:
+              reason = RefreshReason.AbortPay;
+              break;
+            case BackupRefreshReason.BackupRestored:
+              reason = RefreshReason.BackupRestored;
+              break;
+            case BackupRefreshReason.Manual:
+              reason = RefreshReason.Manual;
+              break;
+            case BackupRefreshReason.Pay:
+              reason = RefreshReason.Pay;
+              break;
+            case BackupRefreshReason.Recoup:
+              reason = RefreshReason.Recoup;
+              break;
+            case BackupRefreshReason.Refund:
+              reason = RefreshReason.Refund;
+              break;
+            case BackupRefreshReason.Scheduled:
+              reason = RefreshReason.Scheduled;
+              break;
+          }
+          const refreshSessionPerCoin: (
+            | RefreshSessionRecord
+            | undefined
+          )[] = [];
+          for (const oldCoin of backupRefreshGroup.old_coins) {
+            if (oldCoin.refresh_session) {
+              const denomSel = await getDenomSelStateFromBackup(
+                tx,
+                oldCoin.refresh_session.new_denoms,
+              );
+              refreshSessionPerCoin.push({
+                sessionSecretSeed: oldCoin.refresh_session.session_secret_seed,
+                norevealIndex: oldCoin.refresh_session.noreveal_index,
+                newDenoms: oldCoin.refresh_session.new_denoms.map((x) => ({
+                  count: x.count,
+                  denomPubHash: x.denom_pub_hash,
+                })),
+                amountRefreshOutput: denomSel.totalCoinValue,
+              });
+            } else {
+              refreshSessionPerCoin.push(undefined);
+            }
+          }
+          await tx.put(Stores.refreshGroups, {
+            timestampFinished: backupRefreshGroup.timestamp_finished,
+            timestampCreated: backupRefreshGroup.timestamp_started,
+            refreshGroupId: backupRefreshGroup.refresh_group_id,
+            reason,
+            lastError: undefined,
+            lastErrorPerCoin: {},
+            oldCoinPubs: backupRefreshGroup.old_coins.map((x) => x.coin_pub),
+            finishedPerCoin: backupRefreshGroup.old_coins.map(
+              (x) => x.finished,
+            ),
+            inputPerCoin: backupRefreshGroup.old_coins.map((x) =>
+              Amounts.parseOrThrow(x.input_amount),
+            ),
+            estimatedOutputPerCoin: backupRefreshGroup.old_coins.map((x) =>
+              Amounts.parseOrThrow(x.estimated_output_amount),
+            ),
+            refreshSessionPerCoin,
+            retryInfo: initRetryInfo(false),
+          });
+        }
+      }
+
+      for (const backupTip of backupBlob.tips) {
+        const existingTip = await tx.get(Stores.tips, backupTip.wallet_tip_id);
+        if (!existingTip) {
+          const denomsSel = await getDenomSelStateFromBackup(
+            tx,
+            backupTip.selected_denoms,
+          );
+          await tx.put(Stores.tips, {
+            acceptedTimestamp: backupTip.timestamp_accepted,
+            createdTimestamp: backupTip.timestamp_created,
+            denomsSel,
+            exchangeBaseUrl: backupTip.exchange_base_url,
+            lastError: undefined,
+            merchantBaseUrl: backupTip.exchange_base_url,
+            merchantTipId: backupTip.merchant_tip_id,
+            pickedUpTimestamp: backupTip.timestam_picked_up,
+            retryInfo: initRetryInfo(false),
+            secretSeed: backupTip.secret_seed,
+            tipAmountEffective: denomsSel.totalCoinValue,
+            tipAmountRaw: Amounts.parseOrThrow(backupTip.tip_amount_raw),
+            tipExpiration: backupTip.timestamp_expiration,
+            walletTipId: backupTip.wallet_tip_id,
+          });
+        }
+      }
+    },
+  );
 }
 
 function deriveAccountKeyPair(
@@ -607,7 +1096,6 @@ function deriveAccountKeyPair(
     stringToBytes("taler-sync-account-key-salt"),
     stringToBytes(providerUrl),
   );
-
   return {
     eddsaPriv: privateKey,
     eddsaPub: eddsaGetPublic(privateKey),
diff --git a/packages/taler-wallet-core/src/operations/pay.ts 
b/packages/taler-wallet-core/src/operations/pay.ts
index c374cfe4..ecbe37a6 100644
--- a/packages/taler-wallet-core/src/operations/pay.ts
+++ b/packages/taler-wallet-core/src/operations/pay.ts
@@ -441,8 +441,7 @@ async function recordConfirmPay(
   const payCostInfo = await getTotalPaymentCost(ws, coinSelection);
   const t: PurchaseRecord = {
     abortStatus: AbortStatus.None,
-    contractTermsRaw: d.contractTermsRaw,
-    contractData: d.contractData,
+    download: d,
     lastSessionId: sessionId,
     payCoinSelection: coinSelection,
     totalPayCost: payCostInfo,
@@ -763,7 +762,7 @@ async function processDownloadProposalImpl(
           products: parsedContractTerms.products,
           summaryI18n: parsedContractTerms.summary_i18n,
         },
-        contractTermsRaw: JSON.stringify(proposalResp.contract_terms),
+        contractTermsRaw: proposalResp.contract_terms,
       };
       if (
         fulfillmentUrl &&
@@ -877,7 +876,7 @@ async function storeFirstPaySuccess(
     purchase.payRetryInfo = initRetryInfo(false);
     purchase.merchantPaySig = paySig;
     if (isFirst) {
-      const ar = purchase.contractData.autoRefund;
+      const ar = purchase.download.contractData.autoRefund;
       if (ar) {
         logger.info("auto_refund present");
         purchase.refundQueryRequested = true;
@@ -938,8 +937,8 @@ async function submitPay(
 
   if (!purchase.merchantPaySig) {
     const payUrl = new URL(
-      `orders/${purchase.contractData.orderId}/pay`,
-      purchase.contractData.merchantBaseUrl,
+      `orders/${purchase.download.contractData.orderId}/pay`,
+      purchase.download.contractData.merchantBaseUrl,
     ).href;
 
     const reqBody = {
@@ -986,10 +985,10 @@ async function submitPay(
 
     logger.trace("got success from pay URL", merchantResp);
 
-    const merchantPub = purchase.contractData.merchantPub;
+    const merchantPub = purchase.download.contractData.merchantPub;
     const valid: boolean = await ws.cryptoApi.isValidPaymentSignature(
       merchantResp.sig,
-      purchase.contractData.contractTermsHash,
+      purchase.download.contractData.contractTermsHash,
       merchantPub,
     );
 
@@ -1002,12 +1001,12 @@ async function submitPay(
     await storeFirstPaySuccess(ws, proposalId, sessionId, merchantResp.sig);
   } else {
     const payAgainUrl = new URL(
-      `orders/${purchase.contractData.orderId}/paid`,
-      purchase.contractData.merchantBaseUrl,
+      `orders/${purchase.download.contractData.orderId}/paid`,
+      purchase.download.contractData.merchantBaseUrl,
     ).href;
     const reqBody = {
       sig: purchase.merchantPaySig,
-      h_contract: purchase.contractData.contractTermsHash,
+      h_contract: purchase.download.contractData.contractTermsHash,
       session_id: sessionId ?? "",
     };
     const resp = await ws.runSequentialized([EXCHANGE_COINS_LOCK], () =>
@@ -1047,7 +1046,7 @@ async function submitPay(
 
   return {
     type: ConfirmPayResultType.Done,
-    contractTerms: JSON.parse(purchase.contractTermsRaw),
+    contractTerms: purchase.download.contractTermsRaw,
   };
 }
 
@@ -1120,7 +1119,7 @@ export async function preparePayForUri(
       logger.info("not confirming payment, insufficient coins");
       return {
         status: PreparePayResultType.InsufficientBalance,
-        contractTerms: JSON.parse(d.contractTermsRaw),
+        contractTerms: d.contractTermsRaw,
         proposalId: proposal.proposalId,
         amountRaw: Amounts.stringify(d.contractData.amount),
       };
@@ -1132,7 +1131,7 @@ export async function preparePayForUri(
 
     return {
       status: PreparePayResultType.PaymentPossible,
-      contractTerms: JSON.parse(d.contractTermsRaw),
+      contractTerms: d.contractTermsRaw,
       proposalId: proposal.proposalId,
       amountEffective: Amounts.stringify(totalCost),
       amountRaw: Amounts.stringify(res.paymentAmount),
@@ -1161,20 +1160,20 @@ export async function preparePayForUri(
     }
     return {
       status: PreparePayResultType.AlreadyConfirmed,
-      contractTerms: JSON.parse(purchase.contractTermsRaw),
-      contractTermsHash: purchase.contractData.contractTermsHash,
+      contractTerms: purchase.download.contractTermsRaw,
+      contractTermsHash: purchase.download.contractData.contractTermsHash,
       paid: true,
-      amountRaw: Amounts.stringify(purchase.contractData.amount),
+      amountRaw: Amounts.stringify(purchase.download.contractData.amount),
       amountEffective: Amounts.stringify(purchase.totalPayCost),
       proposalId,
     };
   } else if (!purchase.timestampFirstSuccessfulPay) {
     return {
       status: PreparePayResultType.AlreadyConfirmed,
-      contractTerms: JSON.parse(purchase.contractTermsRaw),
-      contractTermsHash: purchase.contractData.contractTermsHash,
+      contractTerms: purchase.download.contractTermsRaw,
+      contractTermsHash: purchase.download.contractData.contractTermsHash,
       paid: false,
-      amountRaw: Amounts.stringify(purchase.contractData.amount),
+      amountRaw: Amounts.stringify(purchase.download.contractData.amount),
       amountEffective: Amounts.stringify(purchase.totalPayCost),
       proposalId,
     };
@@ -1182,12 +1181,12 @@ export async function preparePayForUri(
     const paid = !purchase.paymentSubmitPending;
     return {
       status: PreparePayResultType.AlreadyConfirmed,
-      contractTerms: JSON.parse(purchase.contractTermsRaw),
-      contractTermsHash: purchase.contractData.contractTermsHash,
+      contractTerms: purchase.download.contractTermsRaw,
+      contractTermsHash: purchase.download.contractData.contractTermsHash,
       paid,
-      amountRaw: Amounts.stringify(purchase.contractData.amount),
+      amountRaw: Amounts.stringify(purchase.download.contractData.amount),
       amountEffective: Amounts.stringify(purchase.totalPayCost),
-      ...(paid ? { nextUrl: purchase.contractData.orderId } : {}),
+      ...(paid ? { nextUrl: purchase.download.contractData.orderId } : {}),
       proposalId,
     };
   }
diff --git a/packages/taler-wallet-core/src/types/backupTypes.ts 
b/packages/taler-wallet-core/src/types/backupTypes.ts
index d40d4fa6..0b7f93c6 100644
--- a/packages/taler-wallet-core/src/types/backupTypes.ts
+++ b/packages/taler-wallet-core/src/types/backupTypes.ts
@@ -33,11 +33,15 @@
  *    aren't exported yet (and not even implemented in wallet-core).
  * 6. Returning money to own bank account isn't supported/exported yet.
  * 7. Peer-to-peer payments aren't supported yet.
+ * 8. Next update time / next refresh time isn't backed up yet.
  *
  * Questions:
  * 1. What happens when two backups are merged that have
  *    the same coin in different refresh groups?
  *    => Both are added, one will eventually fail
+ * 2. Should we make more information forgettable?  I.e. is
+ *    the coin selection still relevant for a purchase after the coins
+ *    are legally expired?
  *
  * General considerations / decisions:
  * 1. Information about previously occurring errors and
@@ -74,6 +78,8 @@ type DeviceIdString = string;
  */
 type ClockValue = number;
 
+type RawContractTerms = any;
+
 /**
  * Content of the backup.
  *
@@ -544,10 +550,7 @@ export interface BackupRefreshSession {
   /**
    * Hased denominations of the newly requested coins.
    */
-  new_denoms: {
-    count: number;
-    denom_pub_hash: string;
-  }[];
+  new_denoms: BackupDenomSel;
 
   /**
    * Seed used to derive the planchets and
@@ -654,10 +657,7 @@ export interface BackupWithdrawalGroup {
   /**
    * Multiset of denominations selected for withdrawal.
    */
-  selected_denoms: {
-    denom_pub_hash: string;
-    count: number;
-  }[];
+  selected_denoms: BackupDenomSel;
 }
 
 export enum BackupRefundState {
@@ -747,7 +747,14 @@ export interface BackupPurchase {
   /**
    * Contract terms we got from the merchant.
    */
-  contract_terms_raw: string;
+  contract_terms_raw: RawContractTerms;
+
+  /**
+   * Signature on the contract terms.
+   *
+   * Must be present if contract_terms_raw is present.
+   */
+  merchant_sig?: string;
 
   /**
    * Private key for the nonce.  Might eventually be used
@@ -889,6 +896,14 @@ export interface BackupDenomination {
   coins: BackupCoin[];
 }
 
+/**
+ * Denomination selection.
+ */
+export type BackupDenomSel = {
+  denom_pub_hash: string;
+  count: number;
+}[];
+
 export interface BackupReserve {
   /**
    * The reserve private key.
@@ -961,10 +976,7 @@ export interface BackupReserve {
    * Denominations selected for the initial withdrawal.
    * Stored here to show costs before withdrawal has begun.
    */
-  initial_selected_denoms: {
-    denom_pub_hash: string;
-    count: number;
-  }[];
+  initial_selected_denoms: BackupDenomSel;
 
   /**
    * Groups of withdrawal operations for this reserve.  Typically just one.
@@ -1126,10 +1138,6 @@ export enum BackupProposalStatus {
    * but the user needs to accept/reject it.
    */
   Proposed = "proposed",
-  /**
-   * The user has accepted the proposal.
-   */
-  Accepted = "accepted",
   /**
    * The user has rejected the proposal.
    */
@@ -1150,16 +1158,33 @@ export enum BackupProposalStatus {
  * Proposal by a merchant.
  */
 export interface BackupProposal {
+  /**
+   * Base URL of the merchant that proposed the purchase.
+   */
+  merchant_base_url: string;
+
   /**
    * Downloaded data from the merchant.
    */
-  contract_terms_raw?: string;
+  contract_terms_raw?: RawContractTerms;
+
+  /**
+   * Signature on the contract terms.
+   *
+   * Must be present if contract_terms_raw is present.
+   */
+  merchant_sig?: string;
 
   /**
    * Unique ID when the order is stored in the wallet DB.
    */
   proposal_id: string;
 
+  /**
+   * Merchant-assigned order ID of the proposal.
+   */
+  order_id: string;
+
   /**
    * Timestamp of when the record
    * was created.
diff --git a/packages/taler-wallet-core/src/types/dbTypes.ts 
b/packages/taler-wallet-core/src/types/dbTypes.ts
index 7ba3b860..5b05e287 100644
--- a/packages/taler-wallet-core/src/types/dbTypes.ts
+++ b/packages/taler-wallet-core/src/types/dbTypes.ts
@@ -753,7 +753,7 @@ export interface ProposalDownload {
   /**
    * The contract that was offered by the merchant.
    */
-  contractTermsRaw: string;
+  contractTermsRaw: any;
 
   contractData: WalletContractData;
 }
@@ -1200,14 +1200,9 @@ export interface PurchaseRecord {
   noncePub: string;
 
   /**
-   * Contract terms we got from the merchant.
+   * Downloaded and parsed proposal data.
    */
-  contractTermsRaw: string;
-
-  /**
-   * Parsed contract terms.
-   */
-  contractData: WalletContractData;
+  download: ProposalDownload;
 
   /**
    * Deposit permissions, available once the user has accepted the payment.
@@ -1291,6 +1286,9 @@ export interface ConfigRecord<T> {
   value: T;
 }
 
+/**
+ * FIXME: Eliminate this in favor of DenomSelectionState.
+ */
 export interface DenominationSelectionInfo {
   totalCoinValue: AmountJson;
   totalWithdrawCost: AmountJson;
@@ -1303,6 +1301,9 @@ export interface DenominationSelectionInfo {
   }[];
 }
 
+/**
+ * Selected denominations withn some extra info.
+ */
 export interface DenomSelectionState {
   totalCoinValue: AmountJson;
   totalWithdrawCost: AmountJson;
diff --git a/packages/taler-wallet-core/src/types/pendingTypes.ts 
b/packages/taler-wallet-core/src/types/pendingTypes.ts
new file mode 100644
index 00000000..18d9a2fa
--- /dev/null
+++ b/packages/taler-wallet-core/src/types/pendingTypes.ts
@@ -0,0 +1,276 @@
+/*
+ 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/>
+ */
+
+/**
+ * Type and schema definitions for pending operations in the wallet.
+ */
+
+/**
+ * Imports.
+ */
+import { TalerErrorDetails, BalancesResponse } from "./walletTypes";
+import { ReserveRecordStatus } from "./dbTypes";
+import { Timestamp, Duration } from "../util/time";
+import { RetryInfo } from "../util/retries";
+
+export enum PendingOperationType {
+  Bug = "bug",
+  ExchangeUpdate = "exchange-update",
+  ExchangeCheckRefresh = "exchange-check-refresh",
+  Pay = "pay",
+  ProposalChoice = "proposal-choice",
+  ProposalDownload = "proposal-download",
+  Refresh = "refresh",
+  Reserve = "reserve",
+  Recoup = "recoup",
+  RefundQuery = "refund-query",
+  TipChoice = "tip-choice",
+  TipPickup = "tip-pickup",
+  Withdraw = "withdraw",
+}
+
+/**
+ * Information about a pending operation.
+ */
+export type PendingOperationInfo = PendingOperationInfoCommon &
+  (
+    | PendingBugOperation
+    | PendingExchangeUpdateOperation
+    | PendingExchangeCheckRefreshOperation
+    | PendingPayOperation
+    | PendingProposalChoiceOperation
+    | PendingProposalDownloadOperation
+    | PendingRefreshOperation
+    | PendingRefundQueryOperation
+    | PendingReserveOperation
+    | PendingTipChoiceOperation
+    | PendingTipPickupOperation
+    | PendingWithdrawOperation
+    | PendingRecoupOperation
+  );
+
+/**
+ * The wallet is currently updating information about an exchange.
+ */
+export interface PendingExchangeUpdateOperation {
+  type: PendingOperationType.ExchangeUpdate;
+  stage: ExchangeUpdateOperationStage;
+  reason: string;
+  exchangeBaseUrl: string;
+  lastError: TalerErrorDetails | undefined;
+}
+
+/**
+ * The wallet should check whether coins from this exchange
+ * need to be auto-refreshed.
+ */
+export interface PendingExchangeCheckRefreshOperation {
+  type: PendingOperationType.ExchangeCheckRefresh;
+  exchangeBaseUrl: string;
+}
+
+/**
+ * Some interal error happened in the wallet.  This pending operation
+ * should *only* be reported for problems in the wallet, not when
+ * a problem with a merchant/exchange/etc. occurs.
+ */
+export interface PendingBugOperation {
+  type: PendingOperationType.Bug;
+  message: string;
+  details: any;
+}
+
+/**
+ * Current state of an exchange update operation.
+ */
+export enum ExchangeUpdateOperationStage {
+  FetchKeys = "fetch-keys",
+  FetchWire = "fetch-wire",
+  FinalizeUpdate = "finalize-update",
+}
+
+export enum ReserveType {
+  /**
+   * Manually created.
+   */
+  Manual = "manual",
+  /**
+   * Withdrawn from a bank that has "tight" Taler integration
+   */
+  TalerBankWithdraw = "taler-bank-withdraw",
+}
+
+/**
+ * Status of processing a reserve.
+ *
+ * Does *not* include the withdrawal operation that might result
+ * from this.
+ */
+export interface PendingReserveOperation {
+  type: PendingOperationType.Reserve;
+  retryInfo: RetryInfo | undefined;
+  stage: ReserveRecordStatus;
+  timestampCreated: Timestamp;
+  reserveType: ReserveType;
+  reservePub: string;
+  bankWithdrawConfirmUrl?: string;
+}
+
+/**
+ * Status of an ongoing withdrawal operation.
+ */
+export interface PendingRefreshOperation {
+  type: PendingOperationType.Refresh;
+  lastError?: TalerErrorDetails;
+  refreshGroupId: string;
+  finishedPerCoin: boolean[];
+  retryInfo: RetryInfo;
+}
+
+/**
+ * Status of downloading signed contract terms from a merchant.
+ */
+export interface PendingProposalDownloadOperation {
+  type: PendingOperationType.ProposalDownload;
+  merchantBaseUrl: string;
+  proposalTimestamp: Timestamp;
+  proposalId: string;
+  orderId: string;
+  lastError?: TalerErrorDetails;
+  retryInfo: RetryInfo;
+}
+
+/**
+ * User must choose whether to accept or reject the merchant's
+ * proposed contract terms.
+ */
+export interface PendingProposalChoiceOperation {
+  type: PendingOperationType.ProposalChoice;
+  merchantBaseUrl: string;
+  proposalTimestamp: Timestamp;
+  proposalId: string;
+}
+
+/**
+ * The wallet is picking up a tip that the user has accepted.
+ */
+export interface PendingTipPickupOperation {
+  type: PendingOperationType.TipPickup;
+  tipId: string;
+  merchantBaseUrl: string;
+  merchantTipId: string;
+}
+
+/**
+ * The wallet has been offered a tip, and the user now needs to
+ * decide whether to accept or reject the tip.
+ */
+export interface PendingTipChoiceOperation {
+  type: PendingOperationType.TipChoice;
+  tipId: string;
+  merchantBaseUrl: string;
+  merchantTipId: string;
+}
+
+/**
+ * The wallet is signing coins and then sending them to
+ * the merchant.
+ */
+export interface PendingPayOperation {
+  type: PendingOperationType.Pay;
+  proposalId: string;
+  isReplay: boolean;
+  retryInfo: RetryInfo;
+  lastError: TalerErrorDetails | undefined;
+}
+
+/**
+ * The wallet is querying the merchant about whether any refund
+ * permissions are available for a purchase.
+ */
+export interface PendingRefundQueryOperation {
+  type: PendingOperationType.RefundQuery;
+  proposalId: string;
+  retryInfo: RetryInfo;
+  lastError: TalerErrorDetails | undefined;
+}
+
+export interface PendingRecoupOperation {
+  type: PendingOperationType.Recoup;
+  recoupGroupId: string;
+  retryInfo: RetryInfo;
+  lastError: TalerErrorDetails | undefined;
+}
+
+/**
+ * Status of an ongoing withdrawal operation.
+ */
+export interface PendingWithdrawOperation {
+  type: PendingOperationType.Withdraw;
+  lastError: TalerErrorDetails | undefined;
+  retryInfo: RetryInfo;
+  withdrawalGroupId: string;
+  numCoinsWithdrawn: number;
+  numCoinsTotal: number;
+}
+
+/**
+ * Fields that are present in every pending operation.
+ */
+export interface PendingOperationInfoCommon {
+  /**
+   * Type of the pending operation.
+   */
+  type: PendingOperationType;
+
+  /**
+   * Set to true if the operation indicates that something is really in 
progress,
+   * as opposed to some regular scheduled operation or a permanent failure.
+   */
+  givesLifeness: boolean;
+
+  /**
+   * Retry info, not available on all pending operations.
+   * If it is available, it must have the same name.
+   */
+  retryInfo?: RetryInfo;
+}
+
+/**
+ * Response returned from the pending operations API.
+ */
+export interface PendingOperationsResponse {
+  /**
+   * List of pending operations.
+   */
+  pendingOperations: PendingOperationInfo[];
+
+  /**
+   * Current wallet balance, including pending balances.
+   */
+  walletBalance: BalancesResponse;
+
+  /**
+   * When is the next pending operation due to be re-tried?
+   */
+  nextRetryDelay: Duration;
+
+  /**
+   * Does this response only include pending operations that
+   * are due to be executed right now?
+   */
+  onlyDue: boolean;
+}
diff --git a/packages/taler-wallet-core/src/types/transactionsTypes.ts 
b/packages/taler-wallet-core/src/types/transactionsTypes.ts
new file mode 100644
index 00000000..0a683f29
--- /dev/null
+++ b/packages/taler-wallet-core/src/types/transactionsTypes.ts
@@ -0,0 +1,337 @@
+/*
+ This file is part of GNU Taler
+ (C) 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/>
+ */
+
+/**
+ * Type and schema definitions for the wallet's transaction list.
+ *
+ * @author Florian Dold
+ * @author Torsten Grote
+ */
+
+/**
+ * Imports.
+ */
+import { Timestamp } from "../util/time";
+import {
+  AmountString,
+  Product,
+  InternationalizedString,
+  MerchantInfo,
+  codecForInternationalizedString,
+  codecForMerchantInfo,
+  codecForProduct,
+} from "./talerTypes";
+import {
+  Codec,
+  buildCodecForObject,
+  codecOptional,
+  codecForString,
+  codecForList,
+  codecForAny,
+} from "../util/codec";
+import { TalerErrorDetails } from "./walletTypes";
+
+export interface TransactionsRequest {
+  /**
+   * return only transactions in the given currency
+   */
+  currency?: string;
+
+  /**
+   * if present, results will be limited to transactions related to the given 
search string
+   */
+  search?: string;
+}
+
+export interface TransactionsResponse {
+  // a list of past and pending transactions sorted by pending, timestamp and 
transactionId.
+  // In case two events are both pending and have the same timestamp,
+  // they are sorted by the transactionId
+  // (lexically ascending and locale-independent comparison).
+  transactions: Transaction[];
+}
+
+export interface TransactionCommon {
+  // opaque unique ID for the transaction, used as a starting point for 
paginating queries
+  // and for invoking actions on the transaction (e.g. deleting/hiding it from 
the history)
+  transactionId: string;
+
+  // the type of the transaction; different types might provide additional 
information
+  type: TransactionType;
+
+  // main timestamp of the transaction
+  timestamp: Timestamp;
+
+  // true if the transaction is still pending, false otherwise
+  // If a transaction is not longer pending, its timestamp will be updated,
+  // but its transactionId will remain unchanged
+  pending: boolean;
+
+  // Raw amount of the transaction (exclusive of fees or other extra costs)
+  amountRaw: AmountString;
+
+  // Amount added or removed from the wallet's balance (including all fees and 
other costs)
+  amountEffective: AmountString;
+
+  error?: TalerErrorDetails;
+}
+
+export type Transaction =
+  | TransactionWithdrawal
+  | TransactionPayment
+  | TransactionRefund
+  | TransactionTip
+  | TransactionRefresh;
+
+export enum TransactionType {
+  Withdrawal = "withdrawal",
+  Payment = "payment",
+  Refund = "refund",
+  Refresh = "refresh",
+  Tip = "tip",
+}
+
+export enum WithdrawalType {
+  TalerBankIntegrationApi = "taler-bank-integration-api",
+  ManualTransfer = "manual-transfer",
+}
+
+export type WithdrawalDetails =
+  | WithdrawalDetailsForManualTransfer
+  | WithdrawalDetailsForTalerBankIntegrationApi;
+
+interface WithdrawalDetailsForManualTransfer {
+  type: WithdrawalType.ManualTransfer;
+
+  /**
+   * Payto URIs that the exchange supports.
+   *
+   * Already contains the amount and message.
+   */
+  exchangePaytoUris: string[];
+}
+
+interface WithdrawalDetailsForTalerBankIntegrationApi {
+  type: WithdrawalType.TalerBankIntegrationApi;
+
+  /**
+   * Set to true if the bank has confirmed the withdrawal, false if not.
+   * An unconfirmed withdrawal usually requires user-input and should be 
highlighted in the UI.
+   * See also bankConfirmationUrl below.
+   */
+  confirmed: boolean;
+
+  /**
+   * If the withdrawal is unconfirmed, this can include a URL for user
+   * initiated confirmation.
+   */
+  bankConfirmationUrl?: string;
+}
+
+// This should only be used for actual withdrawals
+// and not for tips that have their own transactions type.
+interface TransactionWithdrawal extends TransactionCommon {
+  type: TransactionType.Withdrawal;
+
+  /**
+   * Exchange of the withdrawal.
+   */
+  exchangeBaseUrl: string;
+
+  /**
+   * Amount that got subtracted from the reserve balance.
+   */
+  amountRaw: AmountString;
+
+  /**
+   * Amount that actually was (or will be) added to the wallet's balance.
+   */
+  amountEffective: AmountString;
+
+  withdrawalDetails: WithdrawalDetails;
+}
+
+export enum PaymentStatus {
+  /**
+   * Explicitly aborted after timeout / failure
+   */
+  Aborted = "aborted",
+
+  /**
+   * Payment failed, wallet will auto-retry.
+   * User should be given the option to retry now / abort.
+   */
+  Failed = "failed",
+
+  /**
+   * Paid successfully
+   */
+  Paid = "paid",
+
+  /**
+   * User accepted, payment is processing.
+   */
+  Accepted = "accepted",
+}
+
+export interface TransactionPayment extends TransactionCommon {
+  type: TransactionType.Payment;
+
+  /**
+   * Additional information about the payment.
+   */
+  info: OrderShortInfo;
+
+  /**
+   * Wallet-internal end-to-end identifier for the payment.
+   */
+  proposalId: string;
+
+  /**
+   * How far did the wallet get with processing the payment?
+   */
+  status: PaymentStatus;
+
+  /**
+   * Amount that must be paid for the contract
+   */
+  amountRaw: AmountString;
+
+  /**
+   * Amount that was paid, including deposit, wire and refresh fees.
+   */
+  amountEffective: AmountString;
+}
+
+export interface OrderShortInfo {
+  /**
+   * Order ID, uniquely identifies the order within a merchant instance
+   */
+  orderId: string;
+
+  /**
+   * Hash of the contract terms.
+   */
+  contractTermsHash: string;
+
+  /**
+   * More information about the merchant
+   */
+  merchant: MerchantInfo;
+
+  /**
+   * Summary of the order, given by the merchant
+   */
+  summary: string;
+
+  /**
+   * Map from IETF BCP 47 language tags to localized summaries
+   */
+  summary_i18n?: InternationalizedString;
+
+  /**
+   * List of products that are part of the order
+   */
+  products: Product[] | undefined;
+
+  /**
+   * URL of the fulfillment, given by the merchant
+   */
+  fulfillmentUrl?: string;
+
+  /**
+   * Plain text message that should be shown to the user
+   * when the payment is complete.
+   */
+  fulfillmentMessage?: string;
+
+  /**
+   * Translations of fulfillmentMessage.
+   */
+  fulfillmentMessage_i18n?: InternationalizedString;
+}
+
+interface TransactionRefund extends TransactionCommon {
+  type: TransactionType.Refund;
+
+  // ID for the transaction that is refunded
+  refundedTransactionId: string;
+
+  // Additional information about the refunded payment
+  info: OrderShortInfo;
+
+  // Amount that has been refunded by the merchant
+  amountRaw: AmountString;
+
+  // Amount will be added to the wallet's balance after fees and refreshing
+  amountEffective: AmountString;
+}
+
+interface TransactionTip extends TransactionCommon {
+  type: TransactionType.Tip;
+
+  // Raw amount of the tip, without extra fees that apply
+  amountRaw: AmountString;
+
+  // Amount will be (or was) added to the wallet's balance after fees and 
refreshing
+  amountEffective: AmountString;
+
+  merchantBaseUrl: string;
+}
+
+// A transaction shown for refreshes that are not associated to other 
transactions
+// such as a refresh necessary before coin expiration.
+// It should only be returned by the API if the effective amount is different 
from zero.
+interface TransactionRefresh extends TransactionCommon {
+  type: TransactionType.Refresh;
+
+  // Exchange that the coins are refreshed with
+  exchangeBaseUrl: string;
+
+  // Raw amount that is refreshed
+  amountRaw: AmountString;
+
+  // Amount that will be paid as fees for the refresh
+  amountEffective: AmountString;
+}
+
+export const codecForTransactionsRequest = (): Codec<TransactionsRequest> =>
+  buildCodecForObject<TransactionsRequest>()
+    .property("currency", codecOptional(codecForString()))
+    .property("search", codecOptional(codecForString()))
+    .build("TransactionsRequest");
+
+// FIXME: do full validation here!
+export const codecForTransactionsResponse = (): Codec<TransactionsResponse> =>
+  buildCodecForObject<TransactionsResponse>()
+    .property("transactions", codecForList(codecForAny()))
+    .build("TransactionsResponse");
+
+export const codecForOrderShortInfo = (): Codec<OrderShortInfo> =>
+  buildCodecForObject<OrderShortInfo>()
+    .property("contractTermsHash", codecForString())
+    .property("fulfillmentMessage", codecOptional(codecForString()))
+    .property(
+      "fulfillmentMessage_i18n",
+      codecOptional(codecForInternationalizedString()),
+    )
+    .property("fulfillmentUrl", codecOptional(codecForString()))
+    .property("merchant", codecForMerchantInfo())
+    .property("orderId", codecForString())
+    .property("products", codecOptional(codecForList(codecForProduct())))
+    .property("summary", codecForString())
+    .property("summary_i18n", codecOptional(codecForInternationalizedString()))
+    .build("OrderShortInfo");

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



reply via email to

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