Merge branch 'master' of taler.net:/var/git/wallet-webex

This commit is contained in:
tg(x) 2016-03-02 16:53:14 +01:00
commit 51d6f9fd94
26 changed files with 569 additions and 229 deletions

View File

@ -16,6 +16,8 @@
/**
* Entry point for the background page.
*
* @author Florian Dold
*/
"use strict";
@ -34,4 +36,4 @@ System.import("../lib/wallet/wxMessaging")
.catch((e) => {
console.log("wallet failed");
console.error(e.stack);
});
});

View File

@ -13,24 +13,50 @@
You should have received a copy of the GNU General Public License along with
TALER; see the file COPYING. If not, If not, see <http://www.gnu.org/licenses/>
*/
/// <reference path="../lib/decl/chrome/chrome.d.ts" />
"use strict";
/**
* Script that is injected into (all!) pages to allow them
* to interact with the GNU Taler wallet via DOM Events.
*
* @author Florian Dold
*/
/// <reference path="../lib/decl/chrome/chrome.d.ts" />
"use strict";
// Make sure we don't pollute the namespace too much.
var TalerNotify;
(function (TalerNotify) {
var PROTOCOL_VERSION = 1;
console.log("Taler injected");
console.log("Taler injected", chrome.runtime.id);
// FIXME: only do this for test wallets?
// This is no security risk, since the extension ID for published
// extension is publicly known.
function subst(url, H_contract) {
url = url.replace("${H_contract}", H_contract);
url = url.replace("${$}", "$");
return url;
}
var handlers = [];
var port = chrome.runtime.connect();
port.onDisconnect.addListener(function () {
console.log("chrome runtime disconnected");
for (var _i = 0, handlers_1 = handlers; _i < handlers_1.length; _i++) {
var handler = handlers_1[_i];
document.removeEventListener(handler.type, handler.listener);
}
});
var $ = function (x) { return document.getElementById(x); };
document.addEventListener("taler-probe", function (e) {
function addHandler(type, listener) {
document.addEventListener(type, listener);
handlers.push({ type: type, listener: listener });
}
addHandler("taler-query-id", function (e) {
var evt = new CustomEvent("taler-id", {
detail: {
id: chrome.runtime.id
}
});
document.dispatchEvent(evt);
});
addHandler("taler-probe", function (e) {
var evt = new CustomEvent("taler-wallet-present", {
detail: {
walletProtocolVersion: PROTOCOL_VERSION
@ -39,18 +65,18 @@ var TalerNotify;
document.dispatchEvent(evt);
console.log("handshake done");
});
document.addEventListener("taler-create-reserve", function (e) {
addHandler("taler-create-reserve", function (e) {
console.log("taler-create-reserve with " + JSON.stringify(e.detail));
var params = {
amount: JSON.stringify(e.detail.amount),
callback_url: URI(e.detail.callback_url).absoluteTo(document.location.href),
callback_url: URI(e.detail.callback_url)
.absoluteTo(document.location.href),
bank_url: document.location.href,
suggested_mint: e.detail.suggested_mint,
};
var uri = URI(chrome.extension.getURL("pages/confirm-create-reserve.html"));
document.location.href = uri.query(params).href();
});
document.addEventListener("taler-confirm-reserve", function (e) {
addHandler("taler-confirm-reserve", function (e) {
console.log("taler-confirm-reserve with " + JSON.stringify(e.detail));
var msg = {
type: "confirm-reserve",
@ -62,7 +88,8 @@ var TalerNotify;
console.log("confirm reserve done");
});
});
document.addEventListener("taler-contract", function (e) {
// XXX: remove in a bit, just here for compatibility ...
addHandler("taler-contract", function (e) {
// XXX: the merchant should just give us the parsed data ...
var offer = JSON.parse(e.detail);
if (!offer.contract) {
@ -96,7 +123,50 @@ var TalerNotify;
}
});
});
document.addEventListener('taler-execute-payment', function (e) {
addHandler("taler-confirm-contract", function (e) {
if (!e.detail.contract_wrapper) {
console.error("contract wrapper missing");
return;
}
var offer = e.detail.contract_wrapper;
if (!offer.contract) {
console.error("contract field missing");
return;
}
var msg = {
type: "check-repurchase",
detail: {
contract: offer.contract
},
};
chrome.runtime.sendMessage(msg, function (resp) {
if (resp.error) {
console.error("wallet backend error", resp);
return;
}
if (resp.isRepurchase) {
console.log("doing repurchase");
console.assert(resp.existingFulfillmentUrl);
console.assert(resp.existingContractHash);
window.location.href = subst(resp.existingFulfillmentUrl, resp.existingContractHash);
}
else {
var uri = URI(chrome.extension.getURL("pages/confirm-contract.html"));
var params = {
offer: JSON.stringify(offer),
merchantPageUrl: document.location.href,
};
var target = uri.query(params).href();
if (e.detail.replace_navigation === true) {
document.location.replace(target);
}
else {
document.location.href = target;
}
}
});
});
addHandler('taler-execute-payment', function (e) {
console.log("got taler-execute-payment in content page");
if (!e.detail.pay_url) {
console.log("field 'pay_url' missing in taler-execute-payment event");

View File

@ -14,21 +14,28 @@
TALER; see the file COPYING. If not, If not, see <http://www.gnu.org/licenses/>
*/
/// <reference path="../lib/decl/chrome/chrome.d.ts" />
"use strict";
/**
* Script that is injected into (all!) pages to allow them
* to interact with the GNU Taler wallet via DOM Events.
*
* @author Florian Dold
*/
/// <reference path="../lib/decl/chrome/chrome.d.ts" />
"use strict";
// Make sure we don't pollute the namespace too much.
namespace TalerNotify {
const PROTOCOL_VERSION = 1;
console.log("Taler injected");
console.log("Taler injected", chrome.runtime.id);
// FIXME: only do this for test wallets?
// This is no security risk, since the extension ID for published
// extension is publicly known.
function subst(url: string, H_contract) {
url = url.replace("${H_contract}", H_contract);
@ -36,9 +43,34 @@ namespace TalerNotify {
return url;
}
let handlers = [];
let port = chrome.runtime.connect();
port.onDisconnect.addListener(() => {
console.log("chrome runtime disconnected");
for (let handler of handlers) {
document.removeEventListener(handler.type, handler.listener);
}
});
let $ = (x) => document.getElementById(x);
document.addEventListener("taler-probe", function(e) {
function addHandler(type, listener) {
document.addEventListener(type, listener);
handlers.push({type, listener});
}
addHandler("taler-query-id", function(e) {
let evt = new CustomEvent("taler-id", {
detail: {
id: chrome.runtime.id
}
});
document.dispatchEvent(evt);
});
addHandler("taler-probe", function(e) {
let evt = new CustomEvent("taler-wallet-present", {
detail: {
walletProtocolVersion: PROTOCOL_VERSION
@ -48,19 +80,19 @@ namespace TalerNotify {
console.log("handshake done");
});
document.addEventListener("taler-create-reserve", function(e: CustomEvent) {
addHandler("taler-create-reserve", function(e: CustomEvent) {
console.log("taler-create-reserve with " + JSON.stringify(e.detail));
let params = {
amount: JSON.stringify(e.detail.amount),
callback_url: URI(e.detail.callback_url).absoluteTo(document.location.href),
callback_url: URI(e.detail.callback_url)
.absoluteTo(document.location.href),
bank_url: document.location.href,
suggested_mint: e.detail.suggested_mint,
};
let uri = URI(chrome.extension.getURL("pages/confirm-create-reserve.html"));
document.location.href = uri.query(params).href();
});
document.addEventListener("taler-confirm-reserve", function(e: CustomEvent) {
addHandler("taler-confirm-reserve", function(e: CustomEvent) {
console.log("taler-confirm-reserve with " + JSON.stringify(e.detail));
let msg = {
type: "confirm-reserve",
@ -74,7 +106,8 @@ namespace TalerNotify {
});
document.addEventListener("taler-contract", function(e: CustomEvent) {
// XXX: remove in a bit, just here for compatibility ...
addHandler("taler-contract", function(e: CustomEvent) {
// XXX: the merchant should just give us the parsed data ...
let offer = JSON.parse(e.detail);
@ -114,7 +147,56 @@ namespace TalerNotify {
});
document.addEventListener('taler-execute-payment', function(e: CustomEvent) {
addHandler("taler-confirm-contract", function(e: CustomEvent) {
if (!e.detail.contract_wrapper) {
console.error("contract wrapper missing");
return;
}
let offer = e.detail.contract_wrapper;
if (!offer.contract) {
console.error("contract field missing");
return;
}
let msg = {
type: "check-repurchase",
detail: {
contract: offer.contract
},
};
chrome.runtime.sendMessage(msg, (resp) => {
if (resp.error) {
console.error("wallet backend error", resp);
return;
}
if (resp.isRepurchase) {
console.log("doing repurchase");
console.assert(resp.existingFulfillmentUrl);
console.assert(resp.existingContractHash);
window.location.href = subst(resp.existingFulfillmentUrl,
resp.existingContractHash);
} else {
let uri = URI(chrome.extension.getURL("pages/confirm-contract.html"));
let params = {
offer: JSON.stringify(offer),
merchantPageUrl: document.location.href,
};
let target = uri.query(params).href();
if (e.detail.replace_navigation === true) {
document.location.replace(target);
} else {
document.location.href = target;
}
}
});
});
addHandler('taler-execute-payment', function(e: CustomEvent) {
console.log("got taler-execute-payment in content page");
if (!e.detail.pay_url) {
console.log("field 'pay_url' missing in taler-execute-payment event");

View File

@ -25,6 +25,8 @@
* development
* - package: create Chrome extension zip file in
* build/.
*
* @author Florian Dold
*/
const gulp = require("gulp");
@ -56,6 +58,7 @@ const paths = {
"img/*",
"style/*.css",
"lib/vendor/*",
"lib/i18n-strings.js",
"lib/emscripten/libwrapper.js",
"lib/module-trampoline.js",
"popup/**/*.{html,css}",

View File

@ -608,7 +608,7 @@ declare module _mithril {
*
* @see m.component
*/
controller: MithrilControllerFunction<T> |
controller?: MithrilControllerFunction<T> |
MithrilControllerConstructor<T>;
/**

View File

@ -8472,7 +8472,7 @@ function _TALER_WRALL_sign_deposit_permission($h_contract,$h_wire,$timestamp,$re
}
return (0)|0;
}
function _TALER_WR_verify_confirmation($h_contract,$h_wire,$timestamp,$refund,$0,$1,$amount_minus_fee,$coin_pub,$merchant_pub,$sig,$mint_pub) {
function _TALER_WR_verify_confirmation($h_contract,$h_wire,$timestamp,$refund,$0,$1,$amount_minus_fee,$coin_pub,$merchant_pub,$sig,$exchange_pub) {
$h_contract = $h_contract|0;
$h_wire = $h_wire|0;
$timestamp = $timestamp|0;
@ -8483,7 +8483,7 @@ function _TALER_WR_verify_confirmation($h_contract,$h_wire,$timestamp,$refund,$0
$coin_pub = $coin_pub|0;
$merchant_pub = $merchant_pub|0;
$sig = $sig|0;
$mint_pub = $mint_pub|0;
$exchange_pub = $exchange_pub|0;
var $10 = 0, $11 = 0, $12 = 0, $13 = 0, $14 = 0, $15 = 0, $16 = 0, $17 = 0, $18 = 0, $19 = 0, $2 = 0, $20 = 0, $21 = 0, $22 = 0, $23 = 0, $24 = 0, $25 = 0, $26 = 0, $27 = 0, $28 = 0;
var $29 = 0, $3 = 0, $30 = 0, $31 = 0, $32 = 0, $33 = 0, $34 = 0, $35 = 0, $36 = 0, $37 = 0, $38 = 0, $39 = 0, $4 = 0, $40 = 0, $41 = 0, $42 = 0, $43 = 0, $44 = 0, $45 = 0, $46 = 0;
var $47 = 0, $48 = 0, $49 = 0, $5 = 0, $50 = 0, $51 = 0, $52 = 0, $53 = 0, $54 = 0, $55 = 0, $56 = 0, $57 = 0, $58 = 0, $59 = 0, $6 = 0, $60 = 0, $61 = 0, $62 = 0, $63 = 0, $64 = 0;
@ -8514,7 +8514,7 @@ function _TALER_WR_verify_confirmation($h_contract,$h_wire,$timestamp,$refund,$0
$9 = $coin_pub;
$10 = $merchant_pub;
$11 = $sig;
$12 = $mint_pub;
$12 = $exchange_pub;
$19 = ((($dc)) + 8|0);
$20 = $3;
dest=$19; src=$20; stop=dest+64|0; do { HEAP8[dest>>0]=HEAP8[src>>0]|0; dest=dest+1|0; src=src+1|0; } while ((dest|0) < (stop|0));

View File

@ -17,6 +17,8 @@
/**
* Boilerplate to initialize the module system and call main()
*
* @author Florian Dold
*/
"use strict";
@ -75,4 +77,4 @@ System.import(me)
.catch((e) => {
console.log("trampoline failed");
console.error(e.stack);
});
});

25
lib/vendor/mithril.js vendored
View File

@ -1,3 +1,28 @@
/*
The MIT License (MIT)
Copyright (c) 2014 Leo Horie
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
;(function (global, factory) { // eslint-disable-line
"use strict"
/* eslint-disable no-undef */

View File

@ -15,6 +15,12 @@
*/
/**
* API to access the Taler crypto worker thread.
* @author Florian Dold
*/
import {PreCoin} from "./types";
import {Reserve} from "./types";
import {Denomination} from "./types";
@ -90,4 +96,4 @@ export class CryptoApi {
rsaUnblind(sig: string, bk: string, pk: string): Promise<string> {
return this.doRpc("rsaUnblind", sig, bk, pk);
}
}
}

View File

@ -14,7 +14,6 @@
TALER; see the file COPYING. If not, If not, see <http://www.gnu.org/licenses/>
*/
import {Denomination} from "./types";
/**
* Web worker for crypto operations.
* @author Florian Dold
@ -28,6 +27,7 @@ import create = chrome.alarms.create;
import {Offer} from "./wallet";
import {CoinWithDenom} from "./wallet";
import {CoinPaySig} from "./types";
import {Denomination} from "./types";
export function main(worker: Worker) {
@ -101,7 +101,7 @@ namespace RpcFunctions {
coinPub: coinPub.toCrock(),
coinPriv: coinPriv.toCrock(),
denomPub: denomPub.encode().toCrock(),
mintBaseUrl: reserve.mint_base_url,
exchangeBaseUrl: reserve.exchange_base_url,
withdrawSig: sig.toCrock(),
coinEv: ev.toCrock(),
coinValue: denom.value

View File

@ -25,7 +25,7 @@
*/
const DB_NAME = "taler";
const DB_VERSION = 1;
const DB_VERSION = 5;
/**
* Return a promise that resolves
@ -45,12 +45,13 @@ export function openTalerDb(): Promise<IDBDatabase> {
console.log("DB: upgrade needed: oldVersion = " + e.oldVersion);
switch (e.oldVersion) {
case 0: // DB does not exist yet
const mints = db.createObjectStore("mints", {keyPath: "baseUrl"});
mints.createIndex("pubKey", "masterPublicKey");
const exchanges = db.createObjectStore("exchanges",
{keyPath: "baseUrl"});
exchanges.createIndex("pubKey", "masterPublicKey");
db.createObjectStore("reserves", {keyPath: "reserve_pub"});
db.createObjectStore("denoms", {keyPath: "denomPub"});
const coins = db.createObjectStore("coins", {keyPath: "coinPub"});
coins.createIndex("mintBaseUrl", "mintBaseUrl");
coins.createIndex("exchangeBaseUrl", "exchangeBaseUrl");
const transactions = db.createObjectStore("transactions",
{keyPath: "contractHash"});
transactions.createIndex("repurchase",
@ -68,6 +69,15 @@ export function openTalerDb(): Promise<IDBDatabase> {
});
history.createIndex("timestamp", "timestamp");
break;
default:
if (e.oldVersion != DB_VERSION) {
window.alert("Incompatible wallet dababase version, please reset" +
" db.");
chrome.browserAction.setBadgeText({text: "R!"});
chrome.browserAction.setBadgeBackgroundColor({color: "#F00"});
throw Error("incompatible DB");
}
break;
}
};
});

View File

@ -18,6 +18,8 @@
/**
* Smaller helper functions that do not depend
* on the emscripten machinery.
*
* @author Florian Dold
*/
import {AmountJson} from "./types";
@ -36,7 +38,7 @@ export function amountToPretty(amount: AmountJson): string {
/**
* Canonicalize a base url, typically for the mint.
* Canonicalize a base url, typically for the exchange.
*
* See http://api.taler.net/wallet.html#general
*/
@ -62,4 +64,4 @@ export function parsePrettyAmount(pretty: string): AmountJson {
fraction: res[2] ? (parseFloat(`0.${res[2]}`) * 1e-6) : 0,
currency: res[3]
}
}
}

View File

@ -81,4 +81,4 @@ export class RequestException {
constructor(detail) {
}
}
}

View File

@ -21,6 +21,8 @@
* are defined in types.ts are intended to be used by components
* that do not depend on the whole wallet implementation (which depends on
* emscripten).
*
* @author Florian Dold
*/
import {Checkable} from "./checkable";
@ -43,11 +45,11 @@ export class AmountJson {
@Checkable.Class
export class CreateReserveResponse {
/**
* Mint URL where the bank should create the reserve.
* Exchange URL where the bank should create the reserve.
* The URL is canonicalized in the response.
*/
@Checkable.String
mint: string;
exchange: string;
@Checkable.String
reservePub: string;
@ -95,14 +97,14 @@ export class Denomination {
}
export interface IMintInfo {
export interface IExchangeInfo {
baseUrl: string;
masterPublicKey: string;
denoms: Denomination[];
}
export interface ReserveCreationInfo {
mintInfo: IMintInfo;
exchangeInfo: IExchangeInfo;
selectedDenoms: Denomination[];
withdrawFee: AmountJson;
overhead: AmountJson;
@ -117,13 +119,13 @@ export interface PreCoin {
blindingKey: string;
withdrawSig: string;
coinEv: string;
mintBaseUrl: string;
exchangeBaseUrl: string;
coinValue: AmountJson;
}
export interface Reserve {
mint_base_url: string
exchange_base_url: string
reserve_priv: string;
reserve_pub: string;
}
@ -144,7 +146,70 @@ export interface Coin {
denomPub: string;
denomSig: string;
currentAmount: AmountJson;
mintBaseUrl: string;
exchangeBaseUrl: string;
}
@Checkable.Class
export class ExchangeHandle {
@Checkable.String
master_pub: string;
@Checkable.String
url: string;
static checked: (obj: any) => ExchangeHandle;
}
@Checkable.Class
export class Contract {
@Checkable.String
H_wire: string;
@Checkable.Value(AmountJson)
amount: AmountJson;
@Checkable.List(Checkable.AnyObject)
auditors: any[];
@Checkable.String
expiry: string;
@Checkable.Any
locations: any;
@Checkable.Value(AmountJson)
max_fee: AmountJson;
@Checkable.Any
merchant: any;
@Checkable.String
merchant_pub: string;
@Checkable.List(Checkable.Value(ExchangeHandle))
exchanges: ExchangeHandle[];
@Checkable.List(Checkable.AnyObject)
products: any[];
@Checkable.String
refund_deadline: string;
@Checkable.String
timestamp: string;
@Checkable.Number
transaction_id: number;
@Checkable.String
fulfillment_url: string;
@Checkable.Optional(Checkable.String)
repurchase_correlation_id: string;
static checked: (obj: any) => Contract;
}
@ -266,4 +331,4 @@ export interface CheckRepurchaseResult {
export interface Notifier {
notify();
}
}

View File

@ -21,7 +21,7 @@
* @author Florian Dold
*/
import {AmountJson, CreateReserveResponse, IMintInfo, Denomination, Notifier} from "./types";
import {AmountJson, CreateReserveResponse, IExchangeInfo, Denomination, Notifier} from "./types";
import {HttpResponse, RequestException} from "./http";
import {Query} from "./query";
import {Checkable} from "./checkable";
@ -33,6 +33,8 @@ import {CryptoApi} from "./cryptoApi";
import {Coin} from "./types";
import {PayCoinInfo} from "./types";
import {CheckRepurchaseResult} from "./types";
import {Contract} from "./types";
import {ExchangeHandle} from "./types";
"use strict";
@ -70,7 +72,7 @@ export class KeysJson {
}
class MintInfo implements IMintInfo {
class ExchangeInfo implements IExchangeInfo {
baseUrl: string;
masterPublicKey: string;
denoms: Denomination[];
@ -89,15 +91,15 @@ class MintInfo implements IMintInfo {
}
}
static fresh(baseUrl: string): MintInfo {
return new MintInfo({baseUrl});
static fresh(baseUrl: string): ExchangeInfo {
return new ExchangeInfo({baseUrl});
}
/**
* Merge new key information into the mint info.
* Merge new key information into the exchange info.
* If the new key information is invalid (missing fields,
* invalid signatures), an exception is thrown, but the
* mint info is updated with the new information up until
* exchange info is updated with the new information up until
* the first error.
*/
mergeKeys(newKeys: KeysJson, cryptoApi: CryptoApi): Promise<void> {
@ -160,10 +162,10 @@ export class CreateReserveRequest {
amount: AmountJson;
/**
* Mint URL where the bank should create the reserve.
* Exchange URL where the bank should create the reserve.
*/
@Checkable.String
mint: string;
exchange: string;
static checked: (obj: any) => CreateReserveRequest;
}
@ -182,68 +184,6 @@ export class ConfirmReserveRequest {
}
@Checkable.Class
export class MintHandle {
@Checkable.String
master_pub: string;
@Checkable.String
url: string;
static checked: (obj: any) => MintHandle;
}
@Checkable.Class
export class Contract {
@Checkable.String
H_wire: string;
@Checkable.Value(AmountJson)
amount: AmountJson;
@Checkable.List(Checkable.AnyObject)
auditors: any[];
@Checkable.String
expiry: string;
@Checkable.Any
locations: any;
@Checkable.Value(AmountJson)
max_fee: AmountJson;
@Checkable.Any
merchant: any;
@Checkable.String
merchant_pub: string;
@Checkable.List(Checkable.Value(MintHandle))
mints: MintHandle[];
@Checkable.List(Checkable.AnyObject)
products: any[];
@Checkable.String
refund_deadline: string;
@Checkable.String
timestamp: string;
@Checkable.Number
transaction_id: number;
@Checkable.String
fulfillment_url: string;
@Checkable.Optional(Checkable.String)
repurchase_correlation_id: string;
static checked: (obj: any) => Contract;
}
@Checkable.Class
export class Offer {
@ -264,8 +204,8 @@ interface ConfirmPayRequest {
offer: Offer;
}
interface MintCoins {
[mintUrl: string]: CoinWithDenom[];
interface ExchangeCoins {
[exchangeUrl: string]: CoinWithDenom[];
}
@ -402,22 +342,22 @@ export class Wallet {
/**
* Get mints and associated coins that are still spendable,
* Get exchanges and associated coins that are still spendable,
* but only if the sum the coins' remaining value exceeds the payment amount.
*/
private getPossibleMintCoins(paymentAmount: AmountJson,
private getPossibleExchangeCoins(paymentAmount: AmountJson,
depositFeeLimit: AmountJson,
allowedMints: MintHandle[]): Promise<MintCoins> {
// Mapping from mint base URL to list of coins together with their
allowedExchanges: ExchangeHandle[]): Promise<ExchangeCoins> {
// Mapping from exchange base URL to list of coins together with their
// denomination
let m: MintCoins = {};
let m: ExchangeCoins = {};
function storeMintCoin(mc) {
let mint: IMintInfo = mc[0];
function storeExchangeCoin(mc) {
let exchange: IExchangeInfo = mc[0];
let coin: Coin = mc[1];
let cd = {
coin: coin,
denom: mint.denoms.find((e) => e.denom_pub === coin.denomPub)
denom: exchange.denoms.find((e) => e.denom_pub === coin.denomPub)
};
if (!cd.denom) {
throw Error("denom not found (database inconsistent)");
@ -426,36 +366,36 @@ export class Wallet {
console.warn("same pubkey for different currencies");
return;
}
let x = m[mint.baseUrl];
let x = m[exchange.baseUrl];
if (!x) {
m[mint.baseUrl] = [cd];
m[exchange.baseUrl] = [cd];
} else {
x.push(cd);
}
}
let ps = allowedMints.map((info) => {
console.log("Checking for merchant's mint", JSON.stringify(info));
let ps = allowedExchanges.map((info) => {
console.log("Checking for merchant's exchange", JSON.stringify(info));
return Query(this.db)
.iter("mints", {indexName: "pubKey", only: info.master_pub})
.indexJoin("coins", "mintBaseUrl", (mint) => mint.baseUrl)
.reduce(storeMintCoin);
.iter("exchanges", {indexName: "pubKey", only: info.master_pub})
.indexJoin("coins", "exchangeBaseUrl", (exchange) => exchange.baseUrl)
.reduce(storeExchangeCoin);
});
return Promise.all(ps).then(() => {
let ret: MintCoins = {};
let ret: ExchangeCoins = {};
if (Object.keys(m).length == 0) {
console.log("not suitable mints found");
console.log("not suitable exchanges found");
}
console.dir(m);
// We try to find the first mint where we have
// We try to find the first exchange where we have
// enough coins to cover the paymentAmount with fees
// under depositFeeLimit
nextMint:
nextExchange:
for (let key in m) {
let coins = m[key];
// Sort by ascending deposit fee
@ -479,12 +419,12 @@ export class Wallet {
// FIXME: if the fees are too high, we have
// to cover them ourselves ....
console.log("too much fees");
continue nextMint;
continue nextExchange;
}
usableCoins.push(coins[i]);
if (Amounts.cmp(accAmount, minAmount) >= 0) {
ret[key] = usableCoins;
continue nextMint;
continue nextExchange;
}
}
}
@ -499,14 +439,14 @@ export class Wallet {
*/
private recordConfirmPay(offer: Offer,
payCoinInfo: PayCoinInfo,
chosenMint: string): Promise<void> {
chosenExchange: string): Promise<void> {
let payReq = {};
payReq["amount"] = offer.contract.amount;
payReq["coins"] = payCoinInfo.map((x) => x.sig);
payReq["H_contract"] = offer.H_contract;
payReq["max_fee"] = offer.contract.max_fee;
payReq["merchant_sig"] = offer.merchant_sig;
payReq["mint"] = URI(chosenMint).href();
payReq["exchange"] = URI(chosenExchange).href();
payReq["refund_deadline"] = offer.contract.refund_deadline;
payReq["timestamp"] = offer.contract.timestamp;
payReq["transaction_id"] = offer.contract.transaction_id;
@ -549,9 +489,9 @@ export class Wallet {
confirmPay(offer: Offer): Promise<any> {
console.log("executing confirmPay");
return Promise.resolve().then(() => {
return this.getPossibleMintCoins(offer.contract.amount,
return this.getPossibleExchangeCoins(offer.contract.amount,
offer.contract.max_fee,
offer.contract.mints)
offer.contract.exchanges)
}).then((mcs) => {
if (Object.keys(mcs).length == 0) {
console.log("not confirming payment, insufficient coins");
@ -559,10 +499,10 @@ export class Wallet {
error: "coins-insufficient",
};
}
let mintUrl = Object.keys(mcs)[0];
let exchangeUrl = Object.keys(mcs)[0];
return this.cryptoApi.signDeposit(offer, mcs[mintUrl])
.then((ds) => this.recordConfirmPay(offer, ds, mintUrl))
return this.cryptoApi.signDeposit(offer, mcs[exchangeUrl])
.then((ds) => this.recordConfirmPay(offer, ds, exchangeUrl))
.then(() => ({}));
});
}
@ -599,11 +539,11 @@ export class Wallet {
* then deplete the reserve, withdrawing coins until it is empty.
*/
private initReserve(reserveRecord) {
this.updateMintFromUrl(reserveRecord.mint_base_url)
.then((mint) =>
this.updateReserve(reserveRecord.reserve_pub, mint)
this.updateExchangeFromUrl(reserveRecord.exchange_base_url)
.then((exchange) =>
this.updateReserve(reserveRecord.reserve_pub, exchange)
.then((reserve) => this.depleteReserve(reserve,
mint)))
exchange)))
.then(() => {
let depleted = {
type: "depleted-reserve",
@ -627,12 +567,12 @@ export class Wallet {
createReserve(req: CreateReserveRequest): Promise<CreateReserveResponse> {
return this.cryptoApi.createEddsaKeypair().then((keypair) => {
const now = (new Date).getTime();
const canonMint = canonicalizeBaseUrl(req.mint);
const canonExchange = canonicalizeBaseUrl(req.exchange);
const reserveRecord = {
reserve_pub: keypair.pub,
reserve_priv: keypair.priv,
mint_base_url: canonMint,
exchange_base_url: canonExchange,
created: now,
last_query: null,
current_amount: null,
@ -656,7 +596,7 @@ export class Wallet {
.finish()
.then(() => {
let r: CreateReserveResponse = {
mint: canonMint,
exchange: canonExchange,
reservePub: keypair.pub,
};
return r;
@ -668,7 +608,7 @@ export class Wallet {
/**
* Mark an existing reserve as confirmed. The wallet will start trying
* to withdraw from that reserve. This may not immediately succeed,
* since the mint might not know about the reserve yet, even though the
* since the exchange might not know about the reserve yet, even though the
* bank confirmed its creation.
*
* A confirmed reserve should be shown to the user in the UI, while
@ -708,7 +648,7 @@ export class Wallet {
wd.reserve_pub = pc.reservePub;
wd.reserve_sig = pc.withdrawSig;
wd.coin_ev = pc.coinEv;
let reqUrl = URI("reserve/withdraw").absoluteTo(r.mint_base_url);
let reqUrl = URI("reserve/withdraw").absoluteTo(r.exchange_base_url);
return this.http.postJson(reqUrl, wd);
})
.then(resp => {
@ -727,7 +667,7 @@ export class Wallet {
denomPub: pc.denomPub,
denomSig: denomSig,
currentAmount: pc.coinValue,
mintBaseUrl: pc.mintBaseUrl,
exchangeBaseUrl: pc.exchangeBaseUrl,
};
return coin;
@ -775,8 +715,8 @@ export class Wallet {
/**
* Withdraw coins from a reserve until it is empty.
*/
private depleteReserve(reserve, mint: MintInfo): Promise<void> {
let denomsAvailable: Denomination[] = copy(mint.denoms);
private depleteReserve(reserve, exchange: ExchangeInfo): Promise<void> {
let denomsAvailable: Denomination[] = copy(exchange.denoms);
let denomsForWithdraw = getWithdrawDenomList(reserve.current_amount,
denomsAvailable);
@ -793,13 +733,13 @@ export class Wallet {
/**
* Update the information about a reserve that is stored in the wallet
* by quering the reserve's mint.
* by quering the reserve's exchange.
*/
private updateReserve(reservePub: string, mint: MintInfo): Promise<Reserve> {
private updateReserve(reservePub: string, exchange: ExchangeInfo): Promise<Reserve> {
return Query(this.db)
.get("reserves", reservePub)
.then((reserve) => {
let reqUrl = URI("reserve/status").absoluteTo(mint.baseUrl);
let reqUrl = URI("reserve/status").absoluteTo(exchange.baseUrl);
reqUrl.query({'reserve_pub': reservePub});
return this.http.get(reqUrl).then(resp => {
if (resp.status != 200) {
@ -832,10 +772,10 @@ export class Wallet {
getReserveCreationInfo(baseUrl: string,
amount: AmountJson): Promise<ReserveCreationInfo> {
return this.updateMintFromUrl(baseUrl)
.then((mintInfo: IMintInfo) => {
return this.updateExchangeFromUrl(baseUrl)
.then((exchangeInfo: IExchangeInfo) => {
let selectedDenoms = getWithdrawDenomList(amount,
mintInfo.denoms);
exchangeInfo.denoms);
let acc = Amounts.getZero(amount.currency);
for (let d of selectedDenoms) {
@ -846,7 +786,7 @@ export class Wallet {
d.fee_withdraw).amount)
.reduce((a, b) => Amounts.add(a, b).amount);
let ret: ReserveCreationInfo = {
mintInfo,
exchangeInfo,
selectedDenoms,
withdrawFee: acc,
overhead: Amounts.sub(amount, actualCoinCost).amount,
@ -857,37 +797,37 @@ export class Wallet {
/**
* Update or add mint DB entry by fetching the /keys information.
* Update or add exchange DB entry by fetching the /keys information.
* Optionally link the reserve entry to the new or existing
* mint entry in then DB.
* exchange entry in then DB.
*/
updateMintFromUrl(baseUrl): Promise<MintInfo> {
updateExchangeFromUrl(baseUrl): Promise<ExchangeInfo> {
baseUrl = canonicalizeBaseUrl(baseUrl);
let reqUrl = URI("keys").absoluteTo(baseUrl);
return this.http.get(reqUrl).then((resp) => {
if (resp.status != 200) {
throw Error("/keys request failed");
}
let mintKeysJson = KeysJson.checked(JSON.parse(resp.responseText));
let exchangeKeysJson = KeysJson.checked(JSON.parse(resp.responseText));
return Query(this.db).get("mints", baseUrl).then((r) => {
let mintInfo;
return Query(this.db).get("exchanges", baseUrl).then((r) => {
let exchangeInfo;
console.dir(r);
if (!r) {
mintInfo = MintInfo.fresh(baseUrl);
console.log("making fresh mint");
exchangeInfo = ExchangeInfo.fresh(baseUrl);
console.log("making fresh exchange");
} else {
mintInfo = new MintInfo(r);
console.log("using old mint");
exchangeInfo = new ExchangeInfo(r);
console.log("using old exchange");
}
return mintInfo.mergeKeys(mintKeysJson, this.cryptoApi)
return exchangeInfo.mergeKeys(exchangeKeysJson, this.cryptoApi)
.then(() => {
return Query(this.db)
.put("mints", mintInfo)
.put("exchanges", exchangeInfo)
.finish()
.then(() => mintInfo);
.then(() => exchangeInfo);
});
});
@ -912,14 +852,17 @@ export class Wallet {
return Query(this.db)
.iter("coins")
.reduce(collectBalances, {});
.reduce(collectBalances, {})
.then(byCurrency => {
return {balances: byCurrency};
});
}
/**
* Retrive the full event history for this wallet.
*/
getHistory(): Promise<any[]> {
getHistory(): Promise<any> {
function collect(x, acc) {
acc.push(x);
return acc;
@ -928,6 +871,7 @@ export class Wallet {
return Query(this.db)
.iter("history", {indexName: "timestamp"})
.reduce(collect, [])
.then(acc => ({history: acc}));
}
checkRepurchase(contract: Contract): Promise<CheckRepurchaseResult> {
@ -954,4 +898,4 @@ export class Wallet {
}
});
}
}
}

View File

@ -19,6 +19,7 @@ import {ReserveCreationInfo} from "./types";
/**
* Interface to the wallet through WebExtension messaging.
* @author Florian Dold
*/
@ -37,4 +38,4 @@ export function getReserveCreationInfo(baseUrl: string,
resolve(resp);
});
});
}
}

View File

@ -22,7 +22,7 @@ import {Checkable} from "./checkable";
import {AmountJson} from "./types";
import Port = chrome.runtime.Port;
import {Notifier} from "./types";
import {Contract} from "./wallet";
import {Contract} from "./types";
"use strict";
@ -47,9 +47,11 @@ function makeHandlers(db: IDBDatabase,
return exportDb(db);
},
["reset"]: function(detail) {
let tx = db.transaction(db.objectStoreNames, 'readwrite');
for (let i = 0; i < db.objectStoreNames.length; i++) {
tx.objectStore(db.objectStoreNames[i]).clear();
if (db) {
let tx = db.transaction(db.objectStoreNames, 'readwrite');
for (let i = 0; i < db.objectStoreNames.length; i++) {
tx.objectStore(db.objectStoreNames[i]).clear();
}
}
deleteDb();
@ -60,7 +62,7 @@ function makeHandlers(db: IDBDatabase,
},
["create-reserve"]: function(detail) {
const d = {
mint: detail.mint,
exchange: detail.exchange,
amount: detail.amount,
};
const req = CreateReserveRequest.checked(d);
@ -96,11 +98,11 @@ function makeHandlers(db: IDBDatabase,
["execute-payment"]: function(detail) {
return wallet.executePayment(detail.H_contract);
},
["mint-info"]: function(detail) {
["exchange-info"]: function(detail) {
if (!detail.baseUrl) {
return Promise.resolve({error: "bad url"});
}
return wallet.updateMintFromUrl(detail.baseUrl);
return wallet.updateExchangeFromUrl(detail.baseUrl);
},
["reserve-creation-info"]: function(detail) {
if (!detail.baseUrl || typeof detail.baseUrl !== "string") {
@ -193,6 +195,19 @@ class ChromeNotifier implements Notifier {
export function wxMain() {
chrome.browserAction.setBadgeText({text: ""});
chrome.tabs.query({}, function(tabs) {
for (let tab of tabs) {
if (!tab.url) {
return;
}
let uri = URI(tab.url);
if (uri.protocol() == "http" || uri.protocol() == "https") {
console.log("injecting into existing tab", tab.id);
chrome.tabs.executeScript(tab.id, {file: "content_scripts/notify.js"});
}
}
});
Promise.resolve()
.then(() => {
return openTalerDb();

View File

@ -2,7 +2,7 @@
"description": "Privacy preserving and transparent payments",
"manifest_version": 2,
"name": "GNU Taler Wallet (git)",
"version": "0.5.11",
"version": "0.5.15",
"applications": {
"gecko": {
@ -12,6 +12,7 @@
"permissions": [
"storage",
"tabs",
"http://*/*",
"https://*/*"
],

View File

@ -14,12 +14,21 @@
TALER; see the file COPYING. If not, If not, see <http://www.gnu.org/licenses/>
*/
/**
* Page shown to the user to confirm entering
* a contract.
*
* @author Florian Dold
*/
/// <reference path="../lib/decl/handlebars/handlebars.d.ts" />
import MithrilComponent = _mithril.MithrilComponent;
import {substituteFulfillmentUrl} from "../lib/wallet/helpers";
import m from "mithril";
import {Contract} from "../lib/wallet/types";
"use strict";
import {substituteFulfillmentUrl} from "../lib/wallet/helpers";
declare var m: any;
function prettyAmount(amount) {
let v = amount.value + amount.fraction / 1e6;
@ -27,6 +36,35 @@ function prettyAmount(amount) {
}
const Details = {
controller() {
return {collapsed: m.prop(true)};
},
view(ctrl, contract: Contract) {
if (ctrl.collapsed()) {
return m("div", [
m("button.linky", {
onclick: () => {
ctrl.collapsed(false);
}
}, "show more details")
]);
} else {
return m("div", [
m("button.linky", {
onclick: () => {
ctrl.collapsed(true);
}
}, "show less details"),
m("div", [
"Accepted exchanges:",
m("ul", contract.exchanges.map(e => m("li", `${e.url}: ${e.master_pub}`)))
])
]);
}
}
};
export function main() {
let url = URI(document.location.href);
let query: any = URI.parseQuery(url.query());
@ -51,6 +89,7 @@ export function main() {
`${p.description}: ${prettyAmount(p.price)}`))),
m("button.confirm-pay", {onclick: doPayment}, i18n`Confirm Payment`),
m("p", error ? error : []),
m(Details, contract)
];
}
};
@ -79,4 +118,4 @@ export function main() {
offer);
});
}
}
}

View File

@ -4,8 +4,7 @@
<head>
<title>Taler Wallet: Select Taler Provider</title>
<link rel="stylesheet" type="text/css" href="../style/lang.css">
<link rel="stylesheet" type="text/css" href="popup.css">
<link rel="stylesheet" type="text/css" href="../style/wallet.css">
<script src="../lib/vendor/URI.js"></script>
<script src="../lib/vendor/mithril.js"></script>
@ -27,7 +26,7 @@
<section id="main">
<article>
<div class="fade" id="mint-selection"></div>
<div class="fade" id="exchange-selection"></div>
</article>
</section>
</body>

View File

@ -14,12 +14,20 @@
TALER; see the file COPYING. If not, If not, see <http://www.gnu.org/licenses/>
*/
/**
* Page shown to the user to confirm creation
* of a reserve, usually requested by the bank.
*
* @author Florian Dold
*/
/// <reference path="../lib/decl/mithril.d.ts" />
import {amountToPretty, canonicalizeBaseUrl} from "../lib/wallet/helpers";
import {AmountJson, CreateReserveResponse} from "../lib/wallet/types";
import m from "mithril";
import {IMintInfo} from "../lib/wallet/types";
import {IExchangeInfo} from "../lib/wallet/types";
import {ReserveCreationInfo, Amounts} from "../lib/wallet/types";
import MithrilComponent = _mithril.MithrilComponent;
import {Denomination} from "../lib/wallet/types";
@ -60,7 +68,7 @@ class DelayTimer {
class Controller {
url = m.prop<string>();
statusString = null;
isValidMint = false;
isValidExchange = false;
reserveCreationInfo: ReserveCreationInfo = null;
private timer: DelayTimer;
private request: XMLHttpRequest;
@ -68,12 +76,12 @@ class Controller {
callbackUrl: string;
detailCollapsed = m.prop<boolean>(true);
constructor(initialMintUrl: string, amount: AmountJson, callbackUrl: string) {
constructor(initialExchangeUrl: string, amount: AmountJson, callbackUrl: string) {
console.log("creating main controller");
this.amount = amount;
this.callbackUrl = callbackUrl;
this.timer = new DelayTimer(800, () => this.update());
this.url(initialMintUrl);
this.url(initialExchangeUrl);
this.update();
}
@ -93,19 +101,19 @@ class Controller {
m.redraw(true);
console.log("doing get mint info");
console.log("doing get exchange info");
getReserveCreationInfo(this.url(), this.amount)
.then((r: ReserveCreationInfo) => {
console.log("get mint info resolved");
this.isValidMint = true;
console.log("get exchange info resolved");
this.isValidExchange = true;
this.reserveCreationInfo = r;
console.dir(r);
this.statusString = "The mint base URL is valid!";
this.statusString = "The exchange base URL is valid!";
m.endComputation();
})
.catch((e) => {
console.log("get mint info rejected");
console.log("get exchange info rejected");
if (e.hasOwnProperty("httpStatus")) {
this.statusString = `request failed with status ${this.request.status}`;
} else {
@ -122,7 +130,7 @@ class Controller {
}
reset() {
this.isValidMint = false;
this.isValidExchange = false;
this.statusString = null;
this.reserveCreationInfo = null;
if (this.request) {
@ -131,8 +139,8 @@ class Controller {
}
}
confirmReserve(mint: string, amount: AmountJson, callback_url: string) {
const d = {mint, amount};
confirmReserve(exchange: string, amount: AmountJson, callback_url: string) {
const d = {exchange, amount};
const cb = (rawResp) => {
if (!rawResp) {
throw Error("empty response");
@ -140,7 +148,7 @@ class Controller {
if (!rawResp.error) {
const resp = CreateReserveResponse.checked(rawResp);
let q = {
mint: resp.mint,
exchange: resp.exchange,
reserve_pub: resp.reservePub,
amount_value: amount.value,
amount_fraction: amount.fraction,
@ -190,7 +198,7 @@ function view(ctrl: Controller) {
onclick: () => ctrl.confirmReserve(ctrl.url(),
ctrl.amount,
ctrl.callbackUrl),
disabled: !ctrl.isValidMint
disabled: !ctrl.isValidExchange
},
"Confirm exchange selection");
@ -256,30 +264,30 @@ function renderReserveCreationDetails(rci: ReserveCreationInfo) {
}
interface MintProbeResult {
interface ExchangeProbeResult {
keyInfo?: any;
}
function probeMint(mintBaseUrl: string): Promise<MintProbeResult> {
function probeExchange(exchangeBaseUrl: string): Promise<ExchangeProbeResult> {
throw Error("not implemented");
}
function getSuggestedMint(currency: string): Promise<string> {
function getSuggestedExchange(currency: string): Promise<string> {
// TODO: make this request go to the wallet backend
// Right now, this is a stub.
const defaultMint = {
const defaultExchange = {
"KUDOS": "http://exchange.demo.taler.net",
"PUDOS": "http://exchange.test.taler.net",
};
let mint = defaultMint[currency];
let exchange = defaultExchange[currency];
if (!mint) {
mint = ""
if (!exchange) {
exchange = ""
}
return Promise.resolve(mint);
return Promise.resolve(exchange);
}
@ -290,11 +298,11 @@ export function main() {
const callback_url = query.callback_url;
const bank_url = query.bank_url;
getSuggestedMint(amount.currency)
.then((suggestedMintUrl) => {
const controller = () => new Controller(suggestedMintUrl, amount, callback_url);
var MintSelection = {controller, view};
m.mount(document.getElementById("mint-selection"), MintSelection);
getSuggestedExchange(amount.currency)
.then((suggestedExchangeUrl) => {
const controller = () => new Controller(suggestedExchangeUrl, amount, callback_url);
var ExchangeSelection = {controller, view};
m.mount(document.getElementById("exchange-selection"), ExchangeSelection);
})
.catch((e) => {
// TODO: provide more context information, maybe factor it out into a

View File

@ -15,6 +15,12 @@
*/
/**
* Wallet database dump for debugging.
*
* @author Florian Dold
*/
function replacer(match, pIndent, pKey, pVal, pEnd) {
var key = '<span class=json-key>';
var val = '<span class=json-value>';

View File

@ -20,6 +20,8 @@
*
* Note that duplicate message IDs are NOT merged, to get the same output as
* you would from xgettext, just run msguniq.
*
* @author Florian Dold
*/
/// <reference path="../lib/decl/node.d.ts" />

View File

@ -1,3 +1,10 @@
/**
* @author Gabor X. Toth
* @author Marcello Stanisci
* @author Florian Dold
*/
body {
min-height: 20em;
width: 30em;

View File

@ -15,6 +15,14 @@
*/
/**
* Popup shown to the user when they click
* the Taler browser action button.
*
* @author Florian Dold
*/
/// <reference path="../lib/decl/mithril.d.ts" />
/// <reference path="../lib/decl/lodash.d.ts" />
@ -87,6 +95,7 @@ namespace WalletBalance {
class Controller {
myWallet;
gotError = false;
constructor() {
this.updateBalance();
@ -96,9 +105,16 @@ namespace WalletBalance {
updateBalance() {
m.startComputation();
chrome.runtime.sendMessage({type: "balances"}, (wallet) => {
console.log("got wallet", wallet);
this.myWallet = wallet;
chrome.runtime.sendMessage({type: "balances"}, (resp) => {
if (resp.error) {
this.gotError = true;
console.error("could not retrieve balances", resp);
m.endComputation();
return;
}
this.gotError = false;
console.log("got wallet", resp);
this.myWallet = resp.balances;
m.endComputation();
});
}
@ -106,6 +122,9 @@ namespace WalletBalance {
export function view(ctrl: Controller) {
let wallet = ctrl.myWallet;
if (ctrl.gotError) {
return i18n`Error: could not retrieve balance information.`;
}
if (!wallet) {
throw Error("Could not retrieve wallet");
}
@ -192,6 +211,7 @@ namespace WalletHistory {
class Controller {
myHistory;
gotError = false;
constructor() {
this.update();
@ -201,8 +221,15 @@ namespace WalletHistory {
update() {
m.startComputation();
chrome.runtime.sendMessage({type: "get-history"}, (resp) => {
console.log("got history", history);
this.myHistory = resp;
if (resp.error) {
this.gotError = true;
console.error("could not retrieve history", resp);
m.endComputation();
return;
}
this.gotError = false;
console.log("got history", resp.history);
this.myHistory = resp.history;
m.endComputation();
});
}
@ -210,6 +237,9 @@ namespace WalletHistory {
export function view(ctrl: Controller) {
let history = ctrl.myHistory;
if (ctrl.gotError) {
return i18n`Error: could not retrieve event history`;
}
if (!history) {
throw Error("Could not retrieve history");
}

21
test/integration/tests.py Executable file
View File

@ -0,0 +1,21 @@
#!/usr/bin/python3
import unittest
from selenium import webdriver
from selenium.webdriver.common.keys import Keys
class PythonOrgSearch(unittest.TestCase):
def setUp(self):
self.driver = webdriver.Chrome()
def test_taler_reachable(self):
driver = self.driver
driver.get("https://bank.demo.taler.net")
def tearDown(self):
self.driver.close()
if __name__ == "__main__":
unittest.main()