gnunet-svn
[Top][All Lists]
Advanced

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

[GNUnet-SVN] [taler-wallet-webex] branch master updated: implement payba


From: gnunet
Subject: [GNUnet-SVN] [taler-wallet-webex] branch master updated: implement payback (with rudimentary UI)
Date: Mon, 01 May 2017 04:05:37 +0200

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

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

The following commit(s) were added to refs/heads/master by this push:
     new 4c03a12  implement payback (with rudimentary UI)
4c03a12 is described below

commit 4c03a1200eb947a0ed13f78b46fd670601b8cb80
Author: Florian Dold <address@hidden>
AuthorDate: Mon May 1 04:05:16 2017 +0200

    implement payback (with rudimentary UI)
---
 src/cryptoApi-test.ts  |  35 +++++++------
 src/cryptoApi.ts       |  27 +++++++---
 src/cryptoWorker.ts    |  37 +++++++++++--
 src/emscriptif.ts      |  27 ++++++++++
 src/pages/payback.html |  37 +++++++++++++
 src/pages/payback.tsx  |  99 +++++++++++++++++++++++++++++++++++
 src/pages/popup.tsx    |   7 +++
 src/types.ts           |  94 ++++++++++++++++++++++++---------
 src/wallet.ts          | 137 +++++++++++++++++++++++++++++++++++++++++++++----
 src/wxApi.ts           |   8 +++
 src/wxBackend.ts       |  11 +++-
 tsconfig.json          |   1 +
 webpack.config.js      |   1 +
 13 files changed, 462 insertions(+), 59 deletions(-)

diff --git a/src/cryptoApi-test.ts b/src/cryptoApi-test.ts
index dde3ea8..8350def 100644
--- a/src/cryptoApi-test.ts
+++ b/src/cryptoApi-test.ts
@@ -1,42 +1,46 @@
 import {CryptoApi} from "./cryptoApi";
-import {ReserveRecord, DenominationRecord, denominationRecordFromKeys} from 
"./types";
+import {ReserveRecord, DenominationRecord, DenominationStatus} from "./types";
 import {test, TestLib} from "talertest";
 
 let masterPub1: string = 
"CQQZ9DY3MZ1ARMN5K1VKDETS04Y2QCKMMCFHZSWJWWVN82BTTH00";
 
-let denomValid1: DenominationRecord = 
denominationRecordFromKeys("https://example.com/exchange";, {
-  "master_sig": 
"CJFJCQ48Q45PSGJ5KY94N6M2TPARESM2E15BSPBD95YVVPEARAEQ6V6G4Z2XBMS0QM0F3Y9EYVP276FCS90EQ1578ZC8JHFBZ3NGP3G",
-  "stamp_start": "/Date(1473148381)/",
-  "stamp_expire_withdraw": "/Date(2482300381)/",
-  "stamp_expire_deposit": "/Date(1851580381)/",
-  "denom_pub": 
"51R7ARKCD5HJTTV5F4G0M818E9SP280A40G2GVH04CR30GHS84R3JHHP6GSM2D9Q6514CGT568R32C9J6CWM4DSH64TM4DSM851K0CA48CVKAC1P6H144C2160T46DHK8CVM4HJ274S38C1M6S338D9N6GWM8DT684T3JCT36S13EC9G88R3EGHQ8S0KJGSQ60SKGD216N33AGJ2651K2E9S60TMCD1N75244HHQ6X33EDJ570R3GGJ2651MACA38D130DA560VK4HHJ68WK2CA26GW3ECSH6D13EC9S88VK2GT66WVK8D9G750K0D9R8RRK4DHQ71332GHK8D23GE26710M2H9K6WVK8HJ38MVKEGA66N23AC9H88VKACT58MV3CCSJ6H1K4DT38GRK0C9M8N33CE1R60V4AHA38H1KECSH6S33JH9N8GRKGH1K68S36GH354520818CMG26C1H60R3
 [...]
-  "stamp_expire_legal": "/Date(1567756381)/",
-  "value": {
+let denomValid1: DenominationRecord = {
+  masterSig: 
"CJFJCQ48Q45PSGJ5KY94N6M2TPARESM2E15BSPBD95YVVPEARAEQ6V6G4Z2XBMS0QM0F3Y9EYVP276FCS90EQ1578ZC8JHFBZ3NGP3G",
+  stampStart: "/Date(1473148381)/",
+  stampExpireWithdraw: "/Date(2482300381)/",
+  stampExpireDeposit: "/Date(1851580381)/",
+  denomPub: 
"51R7ARKCD5HJTTV5F4G0M818E9SP280A40G2GVH04CR30GHS84R3JHHP6GSM2D9Q6514CGT568R32C9J6CWM4DSH64TM4DSM851K0CA48CVKAC1P6H144C2160T46DHK8CVM4HJ274S38C1M6S338D9N6GWM8DT684T3JCT36S13EC9G88R3EGHQ8S0KJGSQ60SKGD216N33AGJ2651K2E9S60TMCD1N75244HHQ6X33EDJ570R3GGJ2651MACA38D130DA560VK4HHJ68WK2CA26GW3ECSH6D13EC9S88VK2GT66WVK8D9G750K0D9R8RRK4DHQ71332GHK8D23GE26710M2H9K6WVK8HJ38MVKEGA66N23AC9H88VKACT58MV3CCSJ6H1K4DT38GRK0C9M8N33CE1R60V4AHA38H1KECSH6S33JH9N8GRKGH1K68S36GH354520818CMG26C1H60R30C9
 [...]
+  stampExpireLegal: "/Date(1567756381)/",
+  value: {
     "currency": "PUDOS",
     "value": 0,
     "fraction": 100000
   },
-  "fee_withdraw": {
+  feeWithdraw: {
     "currency": "PUDOS",
     "value": 0,
     "fraction": 10000
   },
-  "fee_deposit": {
+  feeDeposit: {
     "currency": "PUDOS",
     "value": 0,
     "fraction": 10000
   },
-  "fee_refresh": {
+  feeRefresh: {
     "currency": "PUDOS",
     "value": 0,
     "fraction": 10000
   },
-  "fee_refund": {
+  feeRefund: {
     "currency": "PUDOS",
     "value": 0,
     "fraction": 10000
-  }
-});
+  },
+  denomPubHash: "dummy",
+  status: DenominationStatus.Unverified,
+  isOffered: true,
+  exchangeBaseUrl: "https://exchange.example.com/";,
+};
 
 let denomInvalid1 = JSON.parse(JSON.stringify(denomValid1));
 denomInvalid1.value.value += 1;
@@ -55,6 +59,7 @@ test("precoin creation", async (t: TestLib) => {
   let r: ReserveRecord = {
     reserve_pub: pub,
     reserve_priv: priv,
+    hasPayback: false,
     exchange_base_url: "https://example.com/exchange";,
     created: 0,
     requested_amount: {currency: "PUDOS", value: 0, fraction: 0},
diff --git a/src/cryptoApi.ts b/src/cryptoApi.ts
index 5657d74..8dd1439 100644
--- a/src/cryptoApi.ts
+++ b/src/cryptoApi.ts
@@ -22,13 +22,20 @@
 
 
 import {
-  PreCoinRecord, CoinRecord, ReserveRecord, AmountJson,
-  DenominationRecord
+  PreCoinRecord,
+  CoinRecord,
+  ReserveRecord,
+  AmountJson,
+  DenominationRecord,
+  PaybackRequest,
+  RefreshSessionRecord,
+  WireFee,
+  PayCoinInfo,
 } from "./types";
-import {OfferRecord} from "./wallet";
-import {CoinWithDenom} from "./wallet";
-import {PayCoinInfo} from "./types";
-import {RefreshSessionRecord, WireFee} from "./types";
+import {
+  OfferRecord,
+  CoinWithDenom,
+} from "./wallet";
 
 
 interface WorkerState {
@@ -230,6 +237,10 @@ export class CryptoApi {
     return this.doRpc<string>("hashString", 1, str);
   }
 
+  hashDenomPub(denomPub: string): Promise<string> {
+    return this.doRpc<string>("hashDenomPub", 1, denomPub);
+  }
+
   isValidDenom(denom: DenominationRecord,
                masterPub: string): Promise<boolean> {
     return this.doRpc<boolean>("isValidDenom", 2, denom, masterPub);
@@ -256,6 +267,10 @@ export class CryptoApi {
     return this.doRpc<string>("rsaUnblind", 4, sig, bk, pk);
   }
 
+  createPaybackRequest(coin: CoinRecord, preCoin: PreCoinRecord): 
Promise<PaybackRequest> {
+    return this.doRpc<PaybackRequest>("createPaybackRequest", 1, coin, 
preCoin);
+  }
+
   createRefreshSession(exchangeBaseUrl: string,
                        kappa: number,
                        meltCoin: CoinRecord,
diff --git a/src/cryptoWorker.ts b/src/cryptoWorker.ts
index 55c08d4..a11a0d0 100644
--- a/src/cryptoWorker.ts
+++ b/src/cryptoWorker.ts
@@ -23,8 +23,14 @@
 
 import * as native from "./emscriptif";
 import {
-  PreCoinRecord, PayCoinInfo, AmountJson,
-  RefreshSessionRecord, RefreshPreCoinRecord, ReserveRecord, CoinStatus,
+  PreCoinRecord,
+  PayCoinInfo,
+  AmountJson,
+  RefreshSessionRecord,
+  RefreshPreCoinRecord,
+  ReserveRecord,
+  CoinStatus,
+  PaybackRequest,
 } from "./types";
 import create = chrome.alarms.create;
 import {OfferRecord} from "./wallet";
@@ -96,8 +102,29 @@ namespace RpcFunctions {
     return preCoin;
   }
 
+  export function createPaybackRequest(coin: CoinRecord, preCoin: 
PreCoinRecord): PaybackRequest {
+    if (coin.coinPub != preCoin.coinPub) {
+      throw Error("coin doesn't match precoin");
+    }
+    let p = new native.PaybackRequestPS({
+      coin_pub: native.EddsaPublicKey.fromCrock(coin.coinPub),
+      h_denom_pub: 
native.RsaPublicKey.fromCrock(coin.denomPub).encode().hash(),
+      coin_blind: native.RsaBlindingKeySecret.fromCrock(preCoin.blindingKey),
+    });
+    let coinPriv = native.EddsaPrivateKey.fromCrock(coin.coinPriv);
+    let coinSig = native.eddsaSign(p.toPurpose(), coinPriv);
+    let paybackRequest: PaybackRequest = {
+      denom_pub: coin.denomPub,
+      denom_sig: coin.denomSig,
+      coin_blind_key_secret: preCoin.blindingKey,
+      coin_pub: coin.coinPub,
+      coin_sig: coinSig.toCrock(),
+    };
+    return paybackRequest;
+  }
 
-  export function isValidPaymentSignature(sig: string, contractHash: string, 
merchantPub: string) {
+
+  export function isValidPaymentSignature(sig: string, contractHash: string, 
merchantPub: string): boolean {
     let p = new native.PaymentSignaturePS({
       contract_hash: native.HashCode.fromCrock(contractHash),
     });
@@ -366,6 +393,10 @@ namespace RpcFunctions {
     const b = native.ByteArray.fromStringWithNull(str);
     return b.hash().toCrock();
   }
+
+  export function hashDenomPub(denomPub: string): string {
+    return native.RsaPublicKey.fromCrock(denomPub).encode().hash().toCrock();
+  }
 }
 
 
diff --git a/src/emscriptif.ts b/src/emscriptif.ts
index 347ee54..caa0fb8 100644
--- a/src/emscriptif.ts
+++ b/src/emscriptif.ts
@@ -208,6 +208,7 @@ export enum SignaturePurpose {
   TEST = 4242,
   MERCHANT_PAYMENT_OK = 1104,
   MASTER_WIRE_FEES = 1028,
+  WALLET_COIN_PAYBACK = 1203,
 }
 
 
@@ -966,6 +967,32 @@ export class WithdrawRequestPS extends SignatureStruct {
 }
 
 
+export interface PaybackRequestPS_args {
+  coin_pub: EddsaPublicKey;
+  h_denom_pub: HashCode;
+  coin_blind: RsaBlindingKeySecret;
+}
+
+
+export class PaybackRequestPS extends SignatureStruct {
+  constructor(w: PaybackRequestPS_args) {
+    super(w);
+  }
+
+  purpose() {
+    return SignaturePurpose.WALLET_COIN_PAYBACK;
+  }
+
+  fieldTypes() {
+    return [
+      ["coin_pub", EddsaPublicKey],
+      ["h_denom_pub", HashCode],
+      ["coin_blind", RsaBlindingKeySecret],
+    ];
+  }
+}
+
+
 interface RefreshMeltCoinAffirmationPS_Args {
   session_hash: HashCode;
   amount_with_fee: AmountNbo;
diff --git a/src/pages/payback.html b/src/pages/payback.html
new file mode 100644
index 0000000..d7b913e
--- /dev/null
+++ b/src/pages/payback.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+  <meta charset="UTF-8">
+  <title>Taler Wallet: Payback</title>
+
+  <link rel="stylesheet" type="text/css" href="../style/lang.css">
+  <link rel="stylesheet" type="text/css" href="../style/wallet.css">
+
+  <link rel="icon" href="/img/icon.png">
+
+  <script src="/dist/page-common-bundle.js"></script>
+  <script src="/dist/payback-bundle.js"></script>
+
+  <style>
+    body {
+      font-size: 100%;
+    }
+    .tree-item {
+            margin: 2em;
+            border-radius: 5px;
+            border: 1px solid gray;
+            padding: 1em;
+    }
+    .button-linky {
+      background: none;
+      color: black;
+      text-decoration: underline;
+      border: none;
+    }
+  </style>
+
+  <body>
+    <div id="container"></div>
+  </body>
+</html>
diff --git a/src/pages/payback.tsx b/src/pages/payback.tsx
new file mode 100644
index 0000000..9e463d4
--- /dev/null
+++ b/src/pages/payback.tsx
@@ -0,0 +1,99 @@
+/*
+ This file is part of TALER
+ (C) 2017 Inria
+
+ 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/>
+ */
+
+/**
+ * View and edit auditors.
+ *
+ * @author Florian Dold
+ */
+
+
+import {
+  ExchangeRecord,
+  ExchangeForCurrencyRecord,
+  DenominationRecord,
+  AuditorRecord,
+  CurrencyRecord,
+  ReserveRecord,
+  CoinRecord,
+  PreCoinRecord,
+  Denomination,
+  WalletBalance,
+} from "../types";
+import { ImplicitStateComponent, StateHolder } from "../components";
+import {
+  getCurrencies,
+  updateCurrency,
+  getPaybackReserves,
+  withdrawPaybackReserve,
+} from "../wxApi";
+import { prettyAmount } from "../renderHtml";
+import { getTalerStampDate } from "../helpers";
+import * as React from "react";
+import * as ReactDOM from "react-dom";
+
+class Payback extends ImplicitStateComponent<any> {
+  reserves: StateHolder<ReserveRecord[]|null> = this.makeState(null);
+  constructor() {
+    super();
+    let port = chrome.runtime.connect();
+    port.onMessage.addListener((msg: any) => {
+      if (msg.notify) {
+        console.log("got notified");
+        this.update();
+      }
+    });
+    this.update();
+  }
+
+  async update() {
+    let reserves = await getPaybackReserves();
+    this.reserves(reserves);
+  }
+
+  withdrawPayback(pub: string) {
+    withdrawPaybackReserve(pub); 
+  }
+
+  render(): JSX.Element {
+    let reserves = this.reserves();
+    if (!reserves) {
+      return <span>loading ...</span>;
+    }
+    if (reserves.length == 0) {
+      return <span>No reserves with payback available.</span>;
+    }
+    return (
+      <div>
+        {reserves.map(r => (
+          <div>
+            <h2>Reserve for ${prettyAmount(r.current_amount!)}</h2>
+            <ul>
+              <li>Exchange: ${r.exchange_base_url}</li>
+            </ul>
+            <button onClick={() => 
this.withdrawPayback(r.reserve_pub)}>Withdraw again</button>
+          </div>
+        ))}
+      </div>
+    );
+  }
+}
+
+export function main() {
+  ReactDOM.render(<Payback />, document.getElementById("container")!);
+}
+
+document.addEventListener("DOMContentLoaded", main);
diff --git a/src/pages/popup.tsx b/src/pages/popup.tsx
index fc6d39a..9b37509 100644
--- a/src/pages/popup.tsx
+++ b/src/pages/popup.tsx
@@ -299,8 +299,12 @@ class WalletBalanceView extends React.Component<any, any> {
       return <span></span>;
     }
     console.log(wallet);
+    let paybackAvailable = false;
     let listing = Object.keys(wallet).map((key) => {
       let entry: WalletBalanceEntry = wallet[key];
+      if (entry.paybackAmount.value != 0 || entry.paybackAmount.fraction != 0) 
{
+        paybackAvailable = true;
+      }
       return (
         <p>
           {bigAmount(entry.available)}
@@ -311,9 +315,12 @@ class WalletBalanceView extends React.Component<any, any> {
     });
     let link = chrome.extension.getURL("/src/pages/auditors.html");
     let linkElem = <a className="actionLink" href={link} 
target="_blank">Trusted Auditors and Exchanges</a>;
+    let paybackLink = chrome.extension.getURL("/src/pages/payback.html");
+    let paybackLinkElem = <a className="actionLink" href={link} 
target="_blank">Trusted Auditors and Exchanges</a>;
     return (
       <div>
         {listing.length > 0 ? listing : this.renderEmpty()}
+        {paybackAvailable && paybackLinkElem}
         {linkElem}
       </div>
     );
diff --git a/src/types.ts b/src/types.ts
index e357dfa..4964d9f 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -73,7 +73,13 @@ export interface ReserveRecord {
   precoin_amount: AmountJson;
 
 
-  confirmed: boolean,
+  confirmed: boolean;
+
+  /**
+   * We got some payback to this reserve.  We'll cease to automatically
+   * withdraw money from it.
+   */
+  hasPayback: boolean;
 }
 
 export interface AuditorRecord {
@@ -127,6 +133,9 @@ export class DenominationRecord {
   @Checkable.String
   denomPub: string;
 
+  @Checkable.String
+  denomPubHash: string;
+
   @Checkable.Value(AmountJson)
   feeWithdraw: AmountJson;
 
@@ -276,27 +285,65 @@ export interface RefreshPreCoinRecord {
   publicKey: string;
   privateKey: string;
   coinEv: string;
-  blindingKey: string
-}
-
-export function denominationRecordFromKeys(exchangeBaseUrl: string, denomIn: 
Denomination): DenominationRecord {
-  let d: DenominationRecord = {
-    denomPub: denomIn.denom_pub,
-    exchangeBaseUrl: exchangeBaseUrl,
-    feeDeposit: denomIn.fee_deposit,
-    masterSig: denomIn.master_sig,
-    feeRefund: denomIn.fee_refund,
-    feeRefresh: denomIn.fee_refresh,
-    feeWithdraw: denomIn.fee_withdraw,
-    stampExpireDeposit: denomIn.stamp_expire_deposit,
-    stampExpireLegal: denomIn.stamp_expire_legal,
-    stampExpireWithdraw: denomIn.stamp_expire_withdraw,
-    stampStart: denomIn.stamp_start,
-    status: DenominationStatus.Unverified,
-    isOffered: true,
-    value: denomIn.value,
-  };
-  return d;
+  blindingKey: string;
+}
+
+export interface PaybackRequest {
+  denom_pub: string;
+
+  /**
+   * Signature over the coin public key by the denomination.
+   */
+  denom_sig: string;
+
+  coin_pub: string;
+
+  coin_blind_key_secret: string;
+
+  coin_sig: string;
+}
+
address@hidden
+export class PaybackConfirmation {
+  /**
+   * 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.Value(AmountJson)
+  amount: AmountJson;
+
+  /**
+   * Time by which the exchange received the /payback request.
+   */
+  @Checkable.String
+  timestamp: string;
+
+  /**
+   * the EdDSA signature of TALER_PaybackConfirmationPS using a current
+   * signing key of the exchange affirming the successful
+   * payback request, and that the exchange promises to transfer the funds
+   * 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;
+
+  /**
+   * Public EdDSA key of the exchange that was used to generate the signature.
+   * Should match one of the exchange's signing keys from /keys.  It is given
+   * explicitly as the client might otherwise be confused by clock skew as to
+   * which signing key was used.
+   */
+  @Checkable.String
+  exchange_pub: string;
+
+  static checked: (obj: any) => PaybackConfirmation;
 }
 
 /**
@@ -367,7 +414,7 @@ export interface CoinPaySig {
 
 
 export enum CoinStatus {
-  Fresh, TransactionPending, Dirty, Refreshed,
+  Fresh, TransactionPending, Dirty, Refreshed, PaybackPending, PaybackDone,
 }
 
 
@@ -440,6 +487,7 @@ export interface WalletBalanceEntry {
   available: AmountJson;
   pendingIncoming: AmountJson;
   pendingPayment: AmountJson;
+  paybackAmount: AmountJson;
 }
 
 
diff --git a/src/wallet.ts b/src/wallet.ts
index 4c44b5d..63cd597 100644
--- a/src/wallet.ts
+++ b/src/wallet.ts
@@ -44,8 +44,11 @@ import {
   WalletBalanceEntry,
   WireFee,
   ExchangeWireFeesRecord,
-  WireInfo, DenominationRecord, DenominationStatus, denominationRecordFromKeys,
+  WireInfo,
+  DenominationRecord,
+  DenominationStatus,
   CoinStatus,
+  PaybackConfirmation,
 } from "./types";
 import {
   HttpRequestLibrary,
@@ -410,6 +413,7 @@ export namespace Stores {
     }
 
     exchangeBaseUrlIndex = new Index<string,CoinRecord>(this, 
"exchangeBaseUrl", "exchangeBaseUrl");
+    denomPubIndex = new Index<string,CoinRecord>(this, "denomPub", "denomPub");
   }
 
   class HistoryStore extends Store<HistoryRecord> {
@@ -448,6 +452,7 @@ export namespace Stores {
             {keyPath: ["exchangeBaseUrl", "denomPub"] as any as IDBKeyPath});
     }
 
+    denomPubHashIndex = new Index<string,DenominationRecord>(this, 
"denomPubHash", "denomPubHash");
     exchangeBaseUrlIndex = new Index<string, DenominationRecord>(this, 
"exchangeBaseUrl", "exchangeBaseUrl");
     denomPubIndex = new Index<string, DenominationRecord>(this, "denomPub", 
"denomPub");
   }
@@ -894,9 +899,8 @@ export class Wallet {
 
     try {
       let exchange = await 
this.updateExchangeFromUrl(reserveRecord.exchange_base_url);
-      let reserve = await this.updateReserve(reserveRecord.reserve_pub,
-                                             exchange);
-      let n = await this.depleteReserve(reserve, exchange);
+      let reserve = await this.updateReserve(reserveRecord.reserve_pub);
+      let n = await this.depleteReserve(reserve);
 
       if (n != 0) {
         let depleted: HistoryRecord = {
@@ -1013,6 +1017,7 @@ export class Wallet {
     const canonExchange = canonicalizeBaseUrl(req.exchange);
 
     const reserveRecord: ReserveRecord = {
+      hasPayback: false,
       reserve_pub: keypair.pub,
       reserve_priv: keypair.priv,
       exchange_base_url: canonExchange,
@@ -1148,8 +1153,7 @@ export class Wallet {
   /**
    * Withdraw coins from a reserve until it is empty.
    */
-  private async depleteReserve(reserve: ReserveRecord,
-                               exchange: ExchangeRecord): Promise<number> {
+  private async depleteReserve(reserve: ReserveRecord): Promise<number> {
     console.log("depleting reserve");
     if (!reserve.current_amount) {
       throw Error("can't withdraw when amount is unknown");
@@ -1158,7 +1162,7 @@ export class Wallet {
     if (!currentAmount) {
       throw Error("can't withdraw when amount is unknown");
     }
-    let denomsForWithdraw = await 
this.getVerifiedWithdrawDenomList(exchange.baseUrl,
+    let denomsForWithdraw = await 
this.getVerifiedWithdrawDenomList(reserve.exchange_base_url,
                                                                     
currentAmount);
 
     console.log(`withdrawing ${denomsForWithdraw.length} coins`);
@@ -1204,14 +1208,13 @@ export class Wallet {
    * Update the information about a reserve that is stored in the wallet
    * by quering the reserve's exchange.
    */
-  private async updateReserve(reservePub: string,
-                              exchange: ExchangeRecord): 
Promise<ReserveRecord> {
+  private async updateReserve(reservePub: string): Promise<ReserveRecord> {
     let reserve = await this.q()
                             .get<ReserveRecord>(Stores.reserves, reservePub);
     if (!reserve) {
       throw Error("reserve not in db");
     }
-    let reqUrl = new URI("reserve/status").absoluteTo(exchange.baseUrl);
+    let reqUrl = new 
URI("reserve/status").absoluteTo(reserve.exchange_base_url);
     reqUrl.query({'reserve_pub': reservePub});
     let resp = await this.http.get(reqUrl.href());
     if (resp.status != 200) {
@@ -1549,6 +1552,20 @@ export class Wallet {
 
     await this.q().put(Stores.exchangeWireFees, oldWireFees);
 
+    if (exchangeKeysJson.payback) {
+      for (let payback of exchangeKeysJson.payback) {
+        let denom = await 
this.q().getIndexed(Stores.denominations.denomPubHashIndex, 
payback.h_denom_pub);
+        if (!denom) {
+          continue;
+        }
+        console.log(`cashing back denom`, denom);
+        let coins = await this.q().iterIndex(Stores.coins.denomPubIndex, 
denom.denomPub).toArray();
+        for (let coin of coins) {
+          this.payback(coin.coinPub);
+        }
+      }
+    }
+
     return updatedExchangeInfo;
   }
 
@@ -1571,7 +1588,7 @@ export class Wallet {
     const newAndUnseenDenoms: typeof existingDenoms = {};
 
     for (let d of newKeys.denoms) {
-      let dr = denominationRecordFromKeys(exchangeInfo.baseUrl, d);
+      let dr = await this.denominationRecordFromKeys(exchangeInfo.baseUrl, d);
       if (!(d.denom_pub in existingDenoms)) {
         newAndUnseenDenoms[dr.denomPub] = dr;
       }
@@ -1608,6 +1625,7 @@ export class Wallet {
           available: z,
           pendingIncoming: z,
           pendingPayment: z,
+          paybackAmount: z,
         };
       }
       return entry;
@@ -1643,6 +1661,17 @@ export class Wallet {
       return balance;
     }
 
+    function collectPaybacks(r: ReserveRecord, balance: WalletBalance) {
+      if (!r.hasPayback) {
+        return balance;
+      }
+      let entry = ensureEntry(balance, r.requested_amount.currency);
+      if (Amounts.cmp(smallestWithdraw[r.exchange_base_url], 
r.current_amount!) < 0) {
+        entry.paybackAmount = Amounts.add(entry.paybackAmount, 
r.current_amount!).amount;
+      }
+      return balance;
+    }
+
     function collectPendingRefresh(r: RefreshSessionRecord,
                                    balance: WalletBalance) {
       // Don't count finished refreshes, since the refresh already resulted
@@ -1699,6 +1728,8 @@ export class Wallet {
       .reduce(collectPendingRefresh, balance);
     tx.iter(Stores.reserves)
       .reduce(collectPendingWithdraw, balance);
+    tx.iter(Stores.reserves)
+      .reduce(collectPaybacks, balance);
     tx.iter(Stores.transactions)
       .reduce(collectPayments, balance);
     await tx.finish();
@@ -2085,4 +2116,88 @@ export class Wallet {
     doPaymentSucceeded();
     return;
   }
+
+  async payback(coinPub: string): Promise<void> {
+    let coin = await this.q().get(Stores.coins, coinPub);
+    if (!coin) {
+      throw Error(`Coin ${coinPub} not found, can't request payback`);
+    }
+    let preCoin = await this.q().get(Stores.precoins, coin.coinPub);
+    if (!preCoin) {
+      throw Error(`Precoin of coin ${coinPub} not found`);
+    }
+    let reserve = await this.q().get(Stores.reserves, preCoin.reservePub);
+    if (!reserve) {
+      throw Error(`Reserve of coin ${coinPub} not found`);
+    }
+    switch (coin.status) {
+      case CoinStatus.Refreshed:
+        throw Error(`Can't do payback for coin ${coinPub} since it's 
refreshed`);
+      case CoinStatus.PaybackDone:
+        console.log(`Coin ${coinPub} already payed back`);
+        return;
+    }
+    coin.status = CoinStatus.PaybackPending;
+    // Even if we didn't get the payback yet, we suspend withdrawal, since
+    // technically we might update reserve status before we get the response
+    // from the reserve for the payback request.
+    reserve.hasPayback = true;
+    await this.q().put(Stores.coins, coin).put(Stores.reserves, reserve);
+
+    let paybackRequest = await this.cryptoApi.createPaybackRequest(coin, 
preCoin);
+    let reqUrl = new URI("payback").absoluteTo(preCoin.exchangeBaseUrl);
+    let resp = await this.http.get(reqUrl.href());
+    if (resp.status != 200) {
+      throw Error();
+    }
+    let paybackConfirmation = 
PaybackConfirmation.checked(JSON.parse(resp.responseText));
+    if (paybackConfirmation.reserve_pub != preCoin.reservePub) {
+      throw Error(`Coin's reserve doesn't match reserve on payback`);
+    }
+    coin = await this.q().get(Stores.coins, coinPub);
+    if (!coin) {
+      throw Error(`Coin ${coinPub} not found, can't confirm payback`);
+    }
+    coin.status = CoinStatus.PaybackDone;
+    await this.q().put(Stores.coins, coin);
+    await this.updateReserve(preCoin.reservePub);
+  }
+
+
+  async denominationRecordFromKeys(exchangeBaseUrl: string, denomIn: 
Denomination): Promise<DenominationRecord> {
+    let denomPubHash = await this.cryptoApi.hashDenomPub(denomIn.denom_pub);
+    let d: DenominationRecord = {
+      denomPubHash,
+      denomPub: denomIn.denom_pub,
+      exchangeBaseUrl: exchangeBaseUrl,
+      feeDeposit: denomIn.fee_deposit,
+      masterSig: denomIn.master_sig,
+      feeRefund: denomIn.fee_refund,
+      feeRefresh: denomIn.fee_refresh,
+      feeWithdraw: denomIn.fee_withdraw,
+      stampExpireDeposit: denomIn.stamp_expire_deposit,
+      stampExpireLegal: denomIn.stamp_expire_legal,
+      stampExpireWithdraw: denomIn.stamp_expire_withdraw,
+      stampStart: denomIn.stamp_start,
+      status: DenominationStatus.Unverified,
+      isOffered: true,
+      value: denomIn.value,
+    };
+    return d;
+  }
+
+  async withdrawPaybackReserve(reservePub: string): Promise<void> {
+    let reserve = await this.q().get(Stores.reserves, reservePub);
+    if (!reserve) {
+      throw Error(`Reserve ${reservePub} does not exist`);
+    }
+    reserve.hasPayback = false;
+    await this.q().put(Stores.reserves, reserve);
+    this.depleteReserve(reserve);
+  }
+
+  async getPaybackReserves(): Promise<ReserveRecord[]> {
+    return await this.q().iter(Stores.reserves).filter(r => 
r.hasPayback).toArray()
+  }
+
 }
diff --git a/src/wxApi.ts b/src/wxApi.ts
index bdc02af..0f46008 100644
--- a/src/wxApi.ts
+++ b/src/wxApi.ts
@@ -84,6 +84,14 @@ export async function getReserves(exchangeBaseUrl: string): 
Promise<ReserveRecor
   return await callBackend("get-reserves", { exchangeBaseUrl });
 }
 
+export async function getPaybackReserves(): Promise<ReserveRecord[]> {
+  return await callBackend("get-payback-reserves");
+}
+
+export async function withdrawPaybackReserve(reservePub: string): 
Promise<ReserveRecord[]> {
+  return await callBackend("withdraw-payback-reserve", { reservePub });
+}
+
 export async function getCoins(exchangeBaseUrl: string): Promise<CoinRecord[]> 
{
   return await callBackend("get-coins", { exchangeBaseUrl });
 }
diff --git a/src/wxBackend.ts b/src/wxBackend.ts
index 716dc66..1588ec8 100644
--- a/src/wxBackend.ts
+++ b/src/wxBackend.ts
@@ -36,7 +36,7 @@ import URI = require("urijs");
 "use strict";
 
 const DB_NAME = "taler";
-const DB_VERSION = 16;
+const DB_VERSION = 17;
 
 import {Stores} from "./wallet";
 import {Store, Index} from "./query";
@@ -226,6 +226,15 @@ function makeHandlers(db: IDBDatabase,
       }
       return wallet.getReserves(detail.exchangeBaseUrl);
     },
+    ["get-payback-reserves"]: function (detail, sender) {
+      return wallet.getPaybackReserves();
+    },
+    ["withdraw-payback-reserve"]: function (detail, sender) {
+      if (typeof detail.reservePub !== "string") {
+        return Promise.reject(Error("reservePub missing"));
+      }
+      return wallet.withdrawPaybackReserve(detail.reservePub);
+    },
     ["get-coins"]: function (detail, sender) {
       if (typeof detail.exchangeBaseUrl !== "string") {
         return Promise.reject(Error("exchangBaseUrl missing"));
diff --git a/tsconfig.json b/tsconfig.json
index 8dc8cb7..67bb4f8 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -43,6 +43,7 @@
     "src/pages/confirm-create-reserve.tsx",
     "src/pages/error.tsx",
     "src/pages/logs.tsx",
+    "src/pages/payback.tsx",
     "src/pages/popup.tsx",
     "src/pages/show-db.ts",
     "src/pages/tree.tsx",
diff --git a/webpack.config.js b/webpack.config.js
index 2a1f13d..4295912 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -62,6 +62,7 @@ module.exports = function (env) {
       "popup": "./src/pages/popup.tsx",
       "show-db": "./src/pages/show-db.ts",
       "tree": "./src/pages/tree.tsx",
+      "payback": "./src/pages/payback.tsx",
     },
     plugins: [
       new webpack.optimize.CommonsChunkPlugin({

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



reply via email to

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