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 new r


From: gnunet
Subject: [GNUnet-SVN] [taler-wallet-webex] branch master updated: implement new reserve creation dialog and auditor management
Date: Fri, 28 Apr 2017 23:28:32 +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 d6bf249  implement new reserve creation dialog and auditor management
d6bf249 is described below

commit d6bf24902a34f2094363121c8d9f4d54db6f7b6c
Author: Florian Dold <address@hidden>
AuthorDate: Fri Apr 28 23:28:27 2017 +0200

    implement new reserve creation dialog and auditor management
---
 node_modules/.yarn-integrity          |   2 +-
 package.json                          |   6 +-
 src/components.ts                     |  13 +-
 src/i18n.tsx                          |   4 +-
 src/pages/auditors.html               |   3 +
 src/pages/auditors.tsx                |  32 ++-
 src/pages/confirm-create-reserve.html |  29 +++
 src/pages/confirm-create-reserve.tsx  | 401 +++++++++++++++++++++++++---------
 src/pages/popup.html                  |   1 +
 src/pages/popup.tsx                   |  22 +-
 src/style/wallet.css                  |   4 +
 src/types.ts                          |   6 +-
 src/wallet.ts                         |  79 ++++++-
 src/wxApi.ts                          |  17 +-
 src/wxBackend.ts                      |  15 +-
 yarn.lock                             |  21 +-
 16 files changed, 520 insertions(+), 135 deletions(-)

diff --git a/node_modules/.yarn-integrity b/node_modules/.yarn-integrity
index 6d4c42d..d454402 100644
--- a/node_modules/.yarn-integrity
+++ b/node_modules/.yarn-integrity
@@ -1 +1 @@
-751d3ff225403bea12799f2c0ad32d26a0ff81a4f88821c8f1615d3ddc5a9533
\ No newline at end of file
+f57c90a35fd7bae0b594a5c9114779b9b7c1629f6977a421d3e666087dc7ed0f
\ No newline at end of file
diff --git a/package.json b/package.json
index f667583..0709bc2 100644
--- a/package.json
+++ b/package.json
@@ -14,6 +14,7 @@
   "author": "",
   "license": "GPL-3.0",
   "devDependencies": {
+    "@types/moment": "^2.13.0",
     "@types/react": "^15.0.22",
     "@types/react-dom": "^15.5.0",
     "async": "^2.1.2",
@@ -39,6 +40,7 @@
     "map-stream": "0.0.6",
     "minimist": "^1.2.0",
     "mocha": "^2.4.5",
+    "moment": "^2.18.1",
     "po2json": "git+https://github.com/mikeedwards/po2json";,
     "react": "^15.5.4",
     "react-dom": "^15.5.4",
@@ -50,11 +52,11 @@
     "ts-loader": "^2.0.3",
     "typescript": "next",
     "typhonjs-istanbul-instrument-jspm": "^0.1.0",
+    "uglify-js": "^2.8.22",
     "urijs": "^1.18.10",
     "vinyl": "^2.0.0",
     "vinyl-fs": "^2.4.3",
     "webpack": "^2.4.1",
-    "webpack-merge": "^4.1.0",
-    "uglify-js": "^2.8.22"
+    "webpack-merge": "^4.1.0"
   }
 }
diff --git a/src/components.ts b/src/components.ts
index 4ed746f..569810f 100644
--- a/src/components.ts
+++ b/src/components.ts
@@ -33,12 +33,23 @@ export interface StateHolder<T> {
  * but has multiple state holders.
  */
 export abstract class ImplicitStateComponent<PropType> extends 
React.Component<PropType, any> {
+  _implicit = {needsUpdate: false, didMount: false};
+  componentDidMount() {
+    this._implicit.didMount = true;
+    if (this._implicit.needsUpdate) {
+      this.setState({} as any);
+    }
+  }
   makeState<StateType>(initial: StateType): StateHolder<StateType> {
     let state: StateType = initial;
     return (s?: StateType): StateType => {
       if (s !== undefined) {
         state = s;
-        this.setState({} as any);
+        if (this._implicit.didMount) {
+          this.setState({} as any);
+        } else {
+          this._implicit.needsUpdate = true;
+        }
       }
       return state;
     };
diff --git a/src/i18n.tsx b/src/i18n.tsx
index ff32e62..aa26407 100644
--- a/src/i18n.tsx
+++ b/src/i18n.tsx
@@ -177,11 +177,13 @@ interface TranslateProps {
 export class Translate extends React.Component<TranslateProps,void> {
   render(): JSX.Element {
     let s = stringifyChildren(this.props.children);
+    console.log(`string "${s}"`);
     let tr = jed.ngettext(s, s, 1).split(/%(\d+)\$s/).filter((e: any, i: 
number) => i % 2 == 0);
+    console.log(`tr "${JSON.stringify(tr)}"`);
     let childArray = React.Children.toArray(this.props.children!);
     for (let i = 0; i < childArray.length - 1; ++i) {
       if ((typeof childArray[i]) == "string" && (typeof childArray[i+1]) == 
"string") {
-        childArray[i+i] = childArray[i] as string + childArray[i+1] as string;
+        childArray[i+1] = (childArray[i] as string).concat(childArray[i+1] as 
string);
         childArray.splice(i,1);
       }
     }
diff --git a/src/pages/auditors.html b/src/pages/auditors.html
index 7e01f4e..2f50b28 100644
--- a/src/pages/auditors.html
+++ b/src/pages/auditors.html
@@ -14,6 +14,9 @@
   <script src="/dist/auditors-bundle.js"></script>
 
   <style>
+    body {
+      font-size: 100%;
+    }
     .tree-item {
             margin: 2em;
             border-radius: 5px;
diff --git a/src/pages/auditors.tsx b/src/pages/auditors.tsx
index 41339b0..762d22a 100644
--- a/src/pages/auditors.tsx
+++ b/src/pages/auditors.tsx
@@ -23,6 +23,7 @@
 
 import {
   ExchangeRecord,
+  ExchangeForCurrencyRecord,
   DenominationRecord,
   AuditorRecord,
   CurrencyRecord,
@@ -65,13 +66,20 @@ class CurrencyList extends React.Component<any, 
CurrencyListState> {
     this.setState({ currencies });
   }
 
-  async confirmRemove(c: CurrencyRecord, a: AuditorRecord) {
+  async confirmRemoveAuditor(c: CurrencyRecord, a: AuditorRecord) {
     if (window.confirm(`Do you really want to remove auditor ${a.baseUrl} for 
currency ${c.name}?`)) {
       c.auditors = c.auditors.filter((x) => x.auditorPub != a.auditorPub);
       await updateCurrency(c);
     }
   }
 
+  async confirmRemoveExchange(c: CurrencyRecord, e: ExchangeForCurrencyRecord) 
{
+    if (window.confirm(`Do you really want to remove exchange ${e.baseUrl} for 
currency ${c.name}?`)) {
+      c.exchanges = c.exchanges.filter((x) => x.baseUrl != e.baseUrl);
+      await updateCurrency(c);
+    }
+  }
+
   renderAuditors(c: CurrencyRecord): any {
     if (c.auditors.length == 0) {
       return <p>No trusted auditors for this currency.</p>
@@ -81,7 +89,7 @@ class CurrencyList extends React.Component<any, 
CurrencyListState> {
         <p>Trusted Auditors:</p>
         <ul>
         {c.auditors.map(a => (
-          <li>{a.baseUrl} <button className="pure-button button-destructive" 
onClick={() => this.confirmRemove(c, a)}>Remove</button>
+          <li>{a.baseUrl} <button className="pure-button button-destructive" 
onClick={() => this.confirmRemoveAuditor(c, a)}>Remove</button>
             <ul>
               <li>valid until {new Date(a.expirationStamp).toString()}</li>
               <li>public key {a.auditorPub}</li>
@@ -93,6 +101,23 @@ class CurrencyList extends React.Component<any, 
CurrencyListState> {
     );
   }
 
+  renderExchanges(c: CurrencyRecord): any {
+    if (c.exchanges.length == 0) {
+      return <p>No trusted exchanges for this currency.</p>
+    }
+    return (
+      <div>
+        <p>Trusted Exchanges:</p>
+        <ul>
+        {c.exchanges.map(e => (
+          <li>{e.baseUrl} <button className="pure-button button-destructive" 
onClick={() => this.confirmRemoveExchange(c, e)}>Remove</button>
+          </li>
+        ))}
+        </ul>
+      </div>
+    );
+  }
+
   render(): JSX.Element {
     let currencies = this.state.currencies;
     if (!currencies) {
@@ -104,7 +129,10 @@ class CurrencyList extends React.Component<any, 
CurrencyListState> {
         <div>
           <h1>Currency {c.name}</h1>
           <p>Displayed with {c.fractionalDigits} fractional digits.</p>
+          <h2>Auditors</h2>
           <div>{this.renderAuditors(c)}</div>
+          <h2>Exchanges</h2>
+          <div>{this.renderExchanges(c)}</div>
         </div>
       ))}
       </div>
diff --git a/src/pages/confirm-create-reserve.html 
b/src/pages/confirm-create-reserve.html
index c1e4b7c..16ab12a 100644
--- a/src/pages/confirm-create-reserve.html
+++ b/src/pages/confirm-create-reserve.html
@@ -7,10 +7,39 @@
 
   <link rel="icon" href="/img/icon.png">
   <link rel="stylesheet" type="text/css" href="/src/style/wallet.css">
+  <link rel="stylesheet" type="text/css" href="/src/style/pure.css">
 
   <script src="/dist/page-common-bundle.js"></script>
   <script src="/dist/confirm-create-reserve-bundle.js"></script>
 
+  <style>
+    body {
+      font-size: 100%;
+      overflow-y: scroll;
+    }
+    .button-success {
+      background: rgb(28, 184, 65); /* this is a green */
+      color: white;
+      border-radius: 4px;
+      text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2);
+    }
+    .button-secondary {
+      background: rgb(66, 184, 221); /* this is a light blue */
+      color: white;
+      border-radius: 4px;
+      text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2);
+    }
+    a.opener  {
+      color: black;
+    }
+    .opener-open::before {
+      content: "\25bc"
+    }
+    .opener-collapsed::before {
+      content: "\25b6 "
+    }
+  </style>
+
 </head>
 
 <body>
diff --git a/src/pages/confirm-create-reserve.tsx 
b/src/pages/confirm-create-reserve.tsx
index a7fd7b0..6b618c2 100644
--- a/src/pages/confirm-create-reserve.tsx
+++ b/src/pages/confirm-create-reserve.tsx
@@ -26,14 +26,15 @@ import {amountToPretty, canonicalizeBaseUrl} from 
"../helpers";
 import {
   AmountJson, CreateReserveResponse,
   ReserveCreationInfo, Amounts,
-  Denomination, DenominationRecord,
+  Denomination, DenominationRecord, CurrencyRecord
 } from "../types";
-import {getReserveCreationInfo} from "../wxApi";
+import {getReserveCreationInfo, getCurrency, getExchangeInfo} from "../wxApi";
 import {ImplicitStateComponent, StateHolder} from "../components";
 import * as i18n from "../i18n";
 import * as React from "react";
 import * as ReactDOM from "react-dom";
 import URI = require("urijs");
+import * as moment from "moment";
 
 
 function delay<T>(delayMs: number, value: T): Promise<T> {
@@ -67,10 +68,72 @@ class EventTrigger {
 }
 
 
+interface CollapsibleState {
+  collapsed: boolean;
+}
+
+interface CollapsibleProps {
+  initiallyCollapsed: boolean;
+  title: string;
+}
+
+class Collapsible extends React.Component<CollapsibleProps, CollapsibleState> {
+  constructor(props: CollapsibleProps) {
+    super(props);
+    this.state = { collapsed: props.initiallyCollapsed };
+  }
+  render() {
+    const doOpen = (e: any) => {
+      this.setState({collapsed: false})
+      e.preventDefault()
+    };
+    const doClose = (e: any) => {
+      this.setState({collapsed: true})
+      e.preventDefault();
+    };
+    if (this.state.collapsed) {
+      return <h2><a className="opener opener-collapsed" href="#" 
onClick={doOpen}>{this.props.title}</a></h2>;
+    }
+    return (
+      <div>
+        <h2><a className="opener opener-open" href="#" 
onClick={doClose}>{this.props.title}</a></h2>
+        {this.props.children}
+      </div>
+    );
+  }
+}
+
+function renderAuditorDetails(rci: ReserveCreationInfo|null) {
+  if (!rci) {
+    return (
+      <p>
+        Details will be displayed when a valid exchange provider URL is 
entered.
+      </p>
+    );
+  }
+  if (rci.exchangeInfo.auditors.length == 0) {
+    return (
+      <p>
+        The exchange is not audited by any auditors.
+      </p>
+    );
+  }
+  return (
+    <div>
+      {rci.exchangeInfo.auditors.map(a => (
+        <h3>Auditor {a.url}</h3>
+      ))}
+    </div>
+  );
+}
+
 function renderReserveCreationDetails(rci: ReserveCreationInfo|null) {
   if (!rci) {
-    return <p>
-      Details will be displayed when a valid exchange provider URL is 
entered.</p>
+    return (
+      <p>
+        Details will be displayed when a valid exchange provider URL is 
entered.
+      </p>
+    );
   }
 
   let denoms = rci.selectedDenoms;
@@ -99,25 +162,57 @@ function renderReserveCreationDetails(rci: 
ReserveCreationInfo|null) {
     );
   }
 
+  function wireFee(s: string) {
+    return [
+      <thead>
+        <tr>
+        <th colSpan={3}>Wire Method {s}</th>
+        </tr>
+        <tr>
+        <th>Applies Until</th>
+        <th>Wire Fee</th>
+        <th>Closing Fee</th>
+        </tr>
+      </thead>,
+      <tbody>
+      {rci!.wireFees.feesForType[s].map(f => (
+        <tr>
+          <td>{moment.unix(f.endStamp).format("llll")}</td>
+          <td>{amountToPretty(f.wireFee)}</td>
+          <td>{amountToPretty(f.closingFee)}</td>
+        </tr>
+      ))}
+      </tbody>
+    ];
+  }
+
   let withdrawFeeStr = amountToPretty(rci.withdrawFee);
   let overheadStr = amountToPretty(rci.overhead);
 
   return (
     <div>
+      <h3>Overview</h3>
       <p>{i18n.str`Withdrawal fees: ${withdrawFeeStr}`}</p>
       <p>{i18n.str`Rounding loss: ${overheadStr}`}</p>
-      <table>
+      <h3>Coin Fees</h3>
+      <table className="pure-table">
         <thead>
-        <th>{i18n.str`# Coins`}</th>
-        <th>{i18n.str`Value`}</th>
-        <th>{i18n.str`Withdraw Fee`}</th>
-        <th>{i18n.str`Refresh Fee`}</th>
-        <th>{i18n.str`Deposit Fee`}</th>
+        <tr>
+          <th>{i18n.str`# Coins`}</th>
+          <th>{i18n.str`Value`}</th>
+          <th>{i18n.str`Withdraw Fee`}</th>
+          <th>{i18n.str`Refresh Fee`}</th>
+          <th>{i18n.str`Deposit Fee`}</th>
+        </tr>
         </thead>
         <tbody>
         {uniq.map(row)}
         </tbody>
       </table>
+      <h3>Wire Fees</h3>
+      <table className="pure-table">
+      {Object.keys(rci.wireFees.feesForType).map(wireFee)}
+      </table>
     </div>
   );
 }
@@ -156,6 +251,87 @@ interface ExchangeSelectionProps {
   amount: AmountJson;
   callback_url: string;
   wt_types: string[];
+  currencyRecord: CurrencyRecord|null;
+}
+
+interface ManualSelectionProps {
+  onSelect(url: string): void;
+  initialUrl: string;
+}
+
+class ManualSelection extends ImplicitStateComponent<ManualSelectionProps> {
+  url: StateHolder<string> = this.makeState("");
+  errorMessage: StateHolder<string|null> = this.makeState(null);
+  isOkay: StateHolder<boolean> = this.makeState(false);
+  updateEvent = new EventTrigger();
+  constructor(p: ManualSelectionProps) {
+    super(p);
+    this.url(p.initialUrl);
+    this.update();
+  }
+  render() {
+    return (
+      <div className="pure-g pure-form pure-form-stacked">
+        <div className="pure-u-1">
+          <label>URL</label>
+          <input className="url" type="text" spellCheck={false}
+                 value={this.url()}
+                 key="exchange-url-input"
+                 onInput={(e) => this.onUrlChanged((e.target as 
HTMLInputElement).value)} />
+        </div>
+        <div className="pure-u-1">
+          <button className="pure-button button-success"
+                  disabled={!this.isOkay()}
+                  onClick={() => this.props.onSelect(this.url())}>
+            {i18n.str`Select`}
+          </button>
+          {this.errorMessage()}
+        </div>
+      </div>
+    );
+  }
+
+  async update() {
+    this.errorMessage(null);
+    this.isOkay(false);
+    if (!this.url()) {
+      return;
+    }
+    let parsedUrl = new URI(this.url()!);
+    if (parsedUrl.is("relative")) {
+      this.errorMessage(i18n.str`Error: URL may not be relative`);
+      this.isOkay(false);
+      return;
+    }
+    try {
+      let url = canonicalizeBaseUrl(this.url()!);
+      let r = await getExchangeInfo(url)
+      console.log("getExchangeInfo returned")
+      this.isOkay(true);
+    } catch (e) {
+      console.log("got error", e);
+      if (e.hasOwnProperty("httpStatus")) {
+        this.errorMessage(`Error: request failed with status ${e.httpStatus}`);
+      } else if (e.hasOwnProperty("errorResponse")) {
+        let resp = e.errorResponse;
+        this.errorMessage(`Error: ${resp.error} (${resp.hint})`);
+      } else {
+        this.errorMessage("invalid exchange URL");
+      }
+    }
+  }
+
+  async onUrlChanged(s: string) {
+    this.url(s);
+    this.errorMessage(null);
+    this.isOkay(false);
+    this.updateEvent.trigger();
+    let waited = await this.updateEvent.wait(200);
+    if (waited) {
+      // Run the actual update if nobody else preempted us.
+      this.update();
+    }
+  }
 }
 
 
@@ -164,60 +340,64 @@ class ExchangeSelection extends 
ImplicitStateComponent<ExchangeSelectionProps> {
   reserveCreationInfo: StateHolder<ReserveCreationInfo|null> = this.makeState(
     null);
   url: StateHolder<string|null> = this.makeState(null);
-  detailCollapsed: StateHolder<boolean> = this.makeState(true);
 
-  updateEvent = new EventTrigger();
+  selectingExchange: StateHolder<boolean> = this.makeState(false);
 
   constructor(props: ExchangeSelectionProps) {
     super(props);
-    this.onUrlChanged(props.suggestedExchangeUrl || null);
-    this.forceReserveUpdate();
-  }
-
-
-  renderAdvanced(): JSX.Element {
-    if (this.detailCollapsed() && this.url() !== null && !this.statusString()) 
{
-      return (
-        <button className="linky"
-                onClick={() => this.detailCollapsed(false)}>
-          {i18n.str`view fee structure / select different exchange provider`}
-        </button>
-      );
+    let prefilledExchangesUrls = [];
+    if (props.currencyRecord) {
+      let exchanges = props.currencyRecord.exchanges.map((x) => x.baseUrl);
+      prefilledExchangesUrls.push(...exchanges);
     }
-    return (
-      <div>
-        <h2>Provider Selection</h2>
-        <label>URL: </label>
-        <input className="url" type="text" spellCheck={false}
-               value={this.url()!}
-               key="exchange-url-input"
-               onInput={(e) => this.onUrlChanged((e.target as 
HTMLInputElement).value)}/>
-        <br />
-        {this.renderStatus()}
-        <h2>{i18n.str`Detailed Fee Structure`}</h2>
-        {renderReserveCreationDetails(this.reserveCreationInfo())}
-      </div>)
-  }
-
-  renderFee() {
-    if (!this.reserveCreationInfo()) {
-      return "??";
+    if (props.suggestedExchangeUrl) {
+      prefilledExchangesUrls.push(props.suggestedExchangeUrl);
+    }
+    if (prefilledExchangesUrls.length != 0) {
+      this.url(prefilledExchangesUrls[0]);
+      this.forceReserveUpdate();
+    } else {
+      this.selectingExchange(true);
     }
-    let rci = this.reserveCreationInfo()!;
-    let totalCost = Amounts.add(rci.overhead, rci.withdrawFee).amount;
-    return `${amountToPretty(totalCost)}`;
   }
 
   renderFeeStatus() {
-    if (this.reserveCreationInfo()) {
+    let rci = this.reserveCreationInfo();
+    if (rci) {
+      let totalCost = Amounts.add(rci.overhead, rci.withdrawFee).amount;
+      let trustMessage;
+      if (rci.isTrusted) {
+        trustMessage = (
+          <i18n.Translate wrap="p">
+            The exchange is trusted by the wallet.
+          </i18n.Translate>
+        );
+      } else if (rci.isAudited) {
+        trustMessage = (
+          <i18n.Translate wrap="p">
+            The exchange is audited by a trusted auditor.
+          </i18n.Translate>
+        );
+      } else {
+        trustMessage = (
+          <i18n.Translate wrap="p">
+            Warning:  The exchange is neither directly trusted nor audited by 
a trusted auditor.
+            If you withdraw from this exchange, it will be trusted in the 
future.
+          </i18n.Translate>
+        );
+      }
       return (
+        <div>
         <i18n.Translate wrap="p">
+          Using exchange provider <strong>{this.url()}</strong>.
           The exchange provider will charge
           {" "}
-          <span>{this.renderFee()}</span>
+          <span>{amountToPretty(totalCost)}</span>
           {" "}
           in fees.
         </i18n.Translate>
+        {trustMessage}
+        </div>
       );
     }
     if (this.url() && !this.statusString()) {
@@ -233,7 +413,7 @@ class ExchangeSelection extends 
ImplicitStateComponent<ExchangeSelectionProps> {
     if (this.statusString()) {
       return (
         <p>
-          <strong style={{color: "red"}}>{i18n.str`A problem occured, see 
below.`}</strong>
+          <strong style={{color: "red"}}>{i18n.str`A problem occured, see 
below. ${this.statusString()}`}</strong>
         </p>
       );
     }
@@ -244,22 +424,80 @@ class ExchangeSelection extends 
ImplicitStateComponent<ExchangeSelectionProps> {
     );
   }
 
-  render(): JSX.Element {
+  renderConfirm() {
     return (
       <div>
-        <i18n.Translate wrap="p">
-          {"You are about to withdraw "}
-          <strong>{amountToPretty(this.props.amount)}</strong>
-          {" from your bank account into your wallet."}
-        </i18n.Translate>
         {this.renderFeeStatus()}
-        <button className="accept"
+        <button className="pure-button button-success"
                 disabled={this.reserveCreationInfo() == null}
                 onClick={() => this.confirmReserve()}>
           {i18n.str`Accept fees and withdraw`}
         </button>
+        { " " }
+        <button className="pure-button button-secondary"
+                onClick={() => this.selectingExchange(true)}>
+          {i18n.str`Change Exchange Provider`}
+        </button>
         <br/>
-        {this.renderAdvanced()}
+        <Collapsible initiallyCollapsed={true} title="Fee Details">
+          {renderReserveCreationDetails(this.reserveCreationInfo())}
+        </Collapsible>
+        <Collapsible initiallyCollapsed={true} title="Auditor Details">
+          {renderAuditorDetails(this.reserveCreationInfo())}
+        </Collapsible>
+      </div>
+    );
+  }
+
+  select(url: string) {
+    this.reserveCreationInfo(null);
+    this.url(url);
+    this.selectingExchange(false);
+    this.forceReserveUpdate();
+  }
+
+  renderSelect() {
+    let exchanges = (this.props.currencyRecord && 
this.props.currencyRecord.exchanges) || [];
+    console.log(exchanges);
+    return (
+      <div>
+        Please select an exchange.  You can review the details before after 
your selection.
+
+        {this.props.suggestedExchangeUrl && (
+          <div>
+            <h2>Bank Suggestion</h2>
+            <button className="pure-button button-success" onClick={() => 
this.select(this.props.suggestedExchangeUrl)}>
+              Select <strong>{this.props.suggestedExchangeUrl}</strong>
+            </button>
+          </div>
+        )}
+
+        {exchanges.length > 0 && (
+          <div>
+            <h2>Known Exchanges</h2>
+            {exchanges.map(e => (
+              <button className="pure-button button-success" onClick={() => 
this.select(e.baseUrl)}>
+              Select <strong>{e.baseUrl}</strong>
+              </button>
+            ))}
+          </div>
+        )}
+
+        <h2>Manual Selection</h2>
+        <ManualSelection initialUrl={this.url() || ""} onSelect={(url: string) 
=> this.select(url)} />
+      </div>
+    );
+  }
+
+  render(): JSX.Element {
+    return (
+      <div>
+        <i18n.Translate wrap="p">
+          {"You are about to withdraw "}
+          <strong>{amountToPretty(this.props.amount)}</strong>
+          {" from your bank account into your wallet."}
+        </i18n.Translate>
+        {this.selectingExchange() ? this.renderSelect() : this.renderConfirm()}
       </div>
     );
   }
@@ -277,20 +515,6 @@ class ExchangeSelection extends 
ImplicitStateComponent<ExchangeSelectionProps> {
    */
   async forceReserveUpdate() {
     this.reserveCreationInfo(null);
-    if (!this.url()) {
-      this.statusString(i18n.str`Error: URL is empty`);
-      this.detailCollapsed(false);
-      return;
-    }
-
-    this.statusString(null);
-    let parsedUrl = new URI(this.url()!);
-    if (parsedUrl.is("relative")) {
-      this.statusString(i18n.str`Error: URL may not be relative`);
-      this.detailCollapsed(false);
-      return;
-    }
-
     try {
       let url = canonicalizeBaseUrl(this.url()!);
       let r = await getReserveCreationInfo(url,
@@ -299,23 +523,16 @@ class ExchangeSelection extends 
ImplicitStateComponent<ExchangeSelectionProps> {
       this.reserveCreationInfo(r);
       console.dir(r);
     } catch (e) {
-      console.log("get exchange info rejected");
+      console.log("get exchange info rejected", e);
       if (e.hasOwnProperty("httpStatus")) {
         this.statusString(`Error: request failed with status ${e.httpStatus}`);
-        this.detailCollapsed(false);
       } else if (e.hasOwnProperty("errorResponse")) {
         let resp = e.errorResponse;
         this.statusString(`Error: ${resp.error} (${resp.hint})`);
-        this.detailCollapsed(false);
       }
     }
   }
 
-  reset() {
-    this.statusString(null);
-    this.reserveCreationInfo(null);
-  }
-
   confirmReserveImpl(rci: ReserveCreationInfo,
                      exchange: string,
                      amount: AmountJson,
@@ -358,30 +575,13 @@ class ExchangeSelection extends 
ImplicitStateComponent<ExchangeSelectionProps> {
         console.log("going to", url.href());
         document.location.href = url.href();
       } else {
-        this.reset();
         this.statusString(
           i18n.str`Oops, something went wrong. The wallet responded with error 
status (${rawResp.error}).`);
-        this.detailCollapsed(false);
       }
     };
     chrome.runtime.sendMessage({type: 'create-reserve', detail: d}, cb);
   }
 
-  async onUrlChanged(url: string|null) {
-    this.reset();
-    this.url(url);
-    if (url == undefined) {
-      return;
-    }
-    this.updateEvent.trigger();
-    let waited = await this.updateEvent.wait(200);
-    if (waited) {
-      // Run the actual update if nobody else preempted us.
-      this.forceReserveUpdate();
-      this.forceUpdate();
-    }
-  }
-
   renderStatus(): any {
     if (this.statusString()) {
       return <p><strong style={{color: 
"red"}}>{this.statusString()}</strong></p>;
@@ -411,16 +611,15 @@ export async function main() {
       throw Error(i18n.str`Can't parse wire_types: ${e.message}`);
     }
 
-    let suggestedExchangeUrl = await getSuggestedExchange(amount.currency);
-    if (!suggestedExchangeUrl && query.suggested_exchange_url) {
-      suggestedExchangeUrl = query.suggested_exchange_url;
-    }
+    let suggestedExchangeUrl = query.suggested_exchange_url;
+    let currencyRecord = await getCurrency(amount.currency);
 
     let args = {
       wt_types,
       suggestedExchangeUrl,
       callback_url,
-      amount
+      amount,
+      currencyRecord,
     };
 
     ReactDOM.render(<ExchangeSelection {...args} />, document.getElementById(
diff --git a/src/pages/popup.html b/src/pages/popup.html
index 7ff5cff..702f43c 100644
--- a/src/pages/popup.html
+++ b/src/pages/popup.html
@@ -5,6 +5,7 @@
   <meta charset="utf-8">
 
   <link rel="stylesheet" type="text/css" href="../style/lang.css">
+  <link rel="stylesheet" type="text/css" href="../style/wallet.css">
   <link rel="stylesheet" type="text/css" href="popup.css">
 
   <script src="/dist/page-common-bundle.js"></script>
diff --git a/src/pages/popup.tsx b/src/pages/popup.tsx
index c8d52b4..7f17936 100644
--- a/src/pages/popup.tsx
+++ b/src/pages/popup.tsx
@@ -309,18 +309,16 @@ class WalletBalanceView extends React.Component<any, any> 
{
         </p>
       );
     });
-    if (listing.length > 0) {
-      let link = chrome.extension.getURL("/src/pages/auditors.html");
-      let linkElem = <a href={link} target="_blank">auditors</a>;
-      return (
-        <div>
-          {listing}
-          {linkElem}
-        </div>
-      );
-    }
-
-    return this.renderEmpty();
+    let link = chrome.extension.getURL("/src/pages/auditors.html");
+    let linkElem = <a className="actionLink" href={link} 
target="_blank">Trusted Auditors and Exchanges</a>;
+    return (
+      <div>
+        <h2>Available Balance</h2>
+        {listing.length > 0 ? listing : this.renderEmpty()}
+        <h2>Settings</h2>
+        {linkElem}
+      </div>
+    );
   }
 }
 
diff --git a/src/style/wallet.css b/src/style/wallet.css
index 7fe5e37..752fc6d 100644
--- a/src/style/wallet.css
+++ b/src/style/wallet.css
@@ -216,3 +216,7 @@ span.spacer {
 .button-secondary {
     background: rgb(66, 184, 221);
 }
+
+a.actionLink {
+  color: black;
+}
diff --git a/src/types.ts b/src/types.ts
index 5d53f8d..4707edd 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -87,7 +87,7 @@ export interface ExchangeForCurrencyRecord {
    * Priority for automatic selection when withdrawing.
    */
   priority: number;
-  pinnedPub: string;
+  pinnedPub?: string;
   baseUrl: string;
 }
 
@@ -232,6 +232,7 @@ export interface ExchangeRecord {
   baseUrl: string;
   masterPublicKey: string;
   auditors: Auditor[];
+  currency: string;
 
   /**
    * Timestamp for last update.
@@ -249,6 +250,9 @@ export interface ReserveCreationInfo {
   selectedDenoms: DenominationRecord[];
   withdrawFee: AmountJson;
   overhead: AmountJson;
+  wireFees: ExchangeWireFeesRecord;
+  isAudited: boolean;
+  isTrusted: boolean;
 }
 
 
diff --git a/src/wallet.ts b/src/wallet.ts
index 982801f..bc3cd59 100644
--- a/src/wallet.ts
+++ b/src/wallet.ts
@@ -267,13 +267,22 @@ const builtinCurrencies: CurrencyRecord[] = [
     fractionalDigits: 2,
     auditors: [
       {
-        baseUrl: "https://auditor.demo.taler.net";,
+        baseUrl: "https://auditor.demo.taler.net/";,
         expirationStamp: (new Date(2027, 1)).getTime(),
         auditorPub: "XN9KMN5G2KGPCAN0E89MM5HE8FV4WBWA9KDTMTDR817MWBCYA7H0",
       },
     ],
     exchanges: [],
   },
+  {
+    name: "PUDOS",
+    fractionalDigits: 2,
+    auditors: [
+    ],
+    exchanges: [
+      { baseUrl: "https://exchange.test.taler.net/";, priority: 0 },
+    ],
+  },
 ];
 
 
@@ -994,6 +1003,9 @@ export class Wallet {
 
   /**
    * Create a reserve, but do not flag it as confirmed yet.
+   *
+   * Adds the corresponding exchange as a trusted exchange if it is neither
+   * audited nor trusted already.
    */
   async createReserve(req: CreateReserveRequest): 
Promise<CreateReserveResponse> {
     let keypair = await this.cryptoApi.createEddsaKeypair();
@@ -1023,7 +1035,24 @@ export class Wallet {
       }
     };
 
+    let exchangeInfo = await this.updateExchangeFromUrl(req.exchange);
+    let {isAudited, isTrusted} = await this.getExchangeTrust(exchangeInfo);
+    let currencyRecord = await this.q().get(Stores.currencies, 
exchangeInfo.currency);
+    if (!currencyRecord) {
+      currencyRecord = {
+        name: exchangeInfo.currency,
+        fractionalDigits: 2,
+        exchanges: [],
+        auditors: [],
+      }
+    }
+
+    if (!isAudited && !isTrusted) {
+      currencyRecord.exchanges.push({baseUrl: req.exchange, priority: 0});
+    }
+
     await this.q()
+              .put(Stores.currencies, currencyRecord)
               .put(Stores.reserves, reserveRecord)
               .put(Stores.history, historyEntry)
               .finish();
@@ -1295,6 +1324,34 @@ export class Wallet {
     return selectedDenoms;
   }
 
+
+
+  /**
+   * Check if and how an exchange is trusted and/or audited.
+   */
+  async getExchangeTrust(exchangeInfo: ExchangeRecord): Promise<{isTrusted: 
boolean, isAudited: boolean}> {
+    let isTrusted = false;
+    let isAudited = false;
+    let currencyRecord = await this.q().get(Stores.currencies, 
exchangeInfo.currency);
+    if (currencyRecord) {
+      for (let trustedExchange of currencyRecord.exchanges) {
+        if (trustedExchange.baseUrl == exchangeInfo.baseUrl) {
+          isTrusted = true;
+          break;
+        }
+      }
+      for (let trustedAuditor of currencyRecord.auditors) {
+        for (let exchangeAuditor of exchangeInfo.auditors) {
+          if (trustedAuditor.baseUrl == exchangeAuditor.url) {
+            isAudited = true;
+            break;
+          }
+        }
+      }
+    }
+    return {isTrusted, isAudited};
+  }
+
   async getReserveCreationInfo(baseUrl: string,
                                amount: AmountJson): 
Promise<ReserveCreationInfo> {
     let exchangeInfo = await this.updateExchangeFromUrl(baseUrl);
@@ -1312,10 +1369,21 @@ export class Wallet {
 
     let wireInfo = await this.getWireInfo(baseUrl);
 
+    let wireFees = await this.q().get(Stores.exchangeWireFees, baseUrl);
+    if (!wireFees) {
+      // should never happen unless DB is inconsistent
+      throw Error(`no wire fees found for exchange ${baseUrl}`);
+    }
+
+    let {isTrusted, isAudited} = await this.getExchangeTrust(exchangeInfo);
+
     let ret: ReserveCreationInfo = {
       exchangeInfo,
       selectedDenoms,
       wireInfo,
+      wireFees,
+      isAudited,
+      isTrusted,
       withdrawFee: acc,
       overhead: Amounts.sub(amount, actualCoinCost).amount,
     };
@@ -1388,6 +1456,10 @@ export class Wallet {
       throw Error("invalid update time");
     }
 
+    if (exchangeKeysJson.denoms.length == 0) {
+      throw Error("exchange doesn't offer any denominations");
+    }
+
     const r = await this.q().get<ExchangeRecord>(Stores.exchanges, baseUrl);
 
     let exchangeInfo: ExchangeRecord;
@@ -1398,6 +1470,7 @@ export class Wallet {
         lastUpdateTime: updateTimeSec,
         masterPublicKey: exchangeKeysJson.master_public_key,
         auditors: exchangeKeysJson.auditors,
+        currency: exchangeKeysJson.denoms[0].value.currency,
       };
       console.log("making fresh exchange");
     } else {
@@ -1960,6 +2033,10 @@ export class Wallet {
     return pub;
   }
 
+  async getCurrencyRecord(currency: string): Promise<CurrencyRecord|undefined> 
{
+    return this.q().get(Stores.currencies, currency);
+  }
+
 
   async paymentSucceeded(contractHash: string, merchantSig: string): 
Promise<any> {
     const doPaymentSucceeded = async() => {
diff --git a/src/wxApi.ts b/src/wxApi.ts
index de59914..bdc02af 100644
--- a/src/wxApi.ts
+++ b/src/wxApi.ts
@@ -48,9 +48,13 @@ export function getReserveCreationInfo(baseUrl: string,
 }
 
 export async function callBackend(type: string, detail?: any): Promise<any> {
-  return new Promise<ExchangeRecord[]>((resolve, reject) => {
+  return new Promise<any>((resolve, reject) => {
     chrome.runtime.sendMessage({ type, detail }, (resp) => {
-      resolve(resp);
+      if (resp.error) {
+        reject(resp);
+      } else {
+        resolve(resp);
+      }
     });
   });
 }
@@ -63,6 +67,15 @@ export async function getCurrencies(): 
Promise<CurrencyRecord[]> {
   return await callBackend("get-currencies");
 }
 
+
+export async function getCurrency(name: string): Promise<CurrencyRecord|null> {
+  return await callBackend("currency-info", {name});
+}
+
+export async function getExchangeInfo(baseUrl: string): 
Promise<ExchangeRecord> {
+  return await callBackend("exchange-info", {baseUrl});
+}
+
 export async function updateCurrency(currencyRecord: CurrencyRecord): 
Promise<void> {
   return await callBackend("update-currency", { currencyRecord });
 }
diff --git a/src/wxBackend.ts b/src/wxBackend.ts
index 984cad2..716dc66 100644
--- a/src/wxBackend.ts
+++ b/src/wxBackend.ts
@@ -167,6 +167,12 @@ function makeHandlers(db: IDBDatabase,
       }
       return wallet.updateExchangeFromUrl(detail.baseUrl);
     },
+    ["currency-info"]: function (detail) {
+      if (!detail.name) {
+        return Promise.resolve({ error: "name missing" });
+      }
+      return wallet.getCurrencyRecord(detail.name);
+    },
     ["hash-contract"]: function (detail) {
       if (!detail.contract) {
         return Promise.resolve({ error: "contract missing" });
@@ -289,13 +295,20 @@ async function dispatch(handlers: any, req: any, sender: 
any, sendResponse: any)
     console.log(`exception during wallet handler for '${req.type}'`);
     console.log("request", req);
     console.error(e);
+    let stack = undefined;
+    try {
+      stack = e.stack.toString();
+    } catch (e) {
+      // might fail
+    }
     try {
       sendResponse({
+        stack,
         error: "exception",
         hint: e.message,
-        stack: e.stack.toString()
       });
     } catch (e) {
+      console.log(e);
       // might fail if tab disconnected
     }
   }
diff --git a/yarn.lock b/yarn.lock
index b334da6..1b21aba 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2,6 +2,12 @@
 # yarn lockfile v1
 
 
+"@types/address@hidden":
+  version "2.13.0"
+  resolved 
"https://registry.yarnpkg.com/@types/moment/-/moment-2.13.0.tgz#604ebd189bc3bc34a1548689404e61a2a4aac896";
+  dependencies:
+    moment "*"
+
 "@types/address@hidden":
   version "15.5.0"
   resolved 
"https://registry.yarnpkg.com/@types/react-dom/-/react-dom-15.5.0.tgz#7f4fb9613d4051141773242f7b6b5f1a46b34bd9";
@@ -2554,6 +2560,10 @@ address@hidden:
     supports-color "1.2.0"
     to-iso-string "0.0.2"
 
address@hidden, address@hidden:
+  version "2.18.1"
+  resolved 
"https://registry.yarnpkg.com/moment/-/moment-2.18.1.tgz#c36193dd3ce1c2eed2adb7c802dbbc77a81b1c0f";
+
 address@hidden:
   version "0.7.1"
   resolved 
"https://registry.yarnpkg.com/ms/-/ms-0.7.1.tgz#9cd13c03adbff25b65effde7ce864ee952017098";
@@ -3673,16 +3683,7 @@ address@hidden:
   version "0.7.12"
   resolved 
"https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.12.tgz#04c81a99bdd5dc52263ea29d24c6bf8d4818a4bb";
 
address@hidden, address@hidden, address@hidden:
-  version "2.8.22"
-  resolved 
"git://github.com/mishoo/UglifyJS2#278577f3cb75e72320564805ee91be63e5f9c806"
-  dependencies:
-    source-map "~0.5.1"
-    yargs "~3.10.0"
-  optionalDependencies:
-    uglify-to-browserify "~1.0.0"
-
address@hidden:
address@hidden, address@hidden, address@hidden, address@hidden:
   version "2.8.22"
   resolved 
"https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.22.tgz#d54934778a8da14903fa29a326fb24c0ab51a1a0";
   dependencies:

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



reply via email to

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