Merge branch 'master' of git+ssh://taler.net/var/git/wallet-webex
This commit is contained in:
commit
2c90c1e903
@ -33,10 +33,6 @@ namespace TalerNotify {
|
|||||||
|
|
||||||
console.log("Taler injected", chrome.runtime.id);
|
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) {
|
function subst(url: string, H_contract) {
|
||||||
url = url.replace("${H_contract}", H_contract);
|
url = url.replace("${H_contract}", H_contract);
|
||||||
url = url.replace("${$}", "$");
|
url = url.replace("${$}", "$");
|
||||||
@ -45,17 +41,28 @@ namespace TalerNotify {
|
|||||||
|
|
||||||
const handlers = [];
|
const handlers = [];
|
||||||
|
|
||||||
// Hack to know when the extension is unloaded
|
function init() {
|
||||||
let port = chrome.runtime.connect();
|
chrome.runtime.sendMessage({type: "ping"}, () => {
|
||||||
|
if (chrome.runtime.lastError) {
|
||||||
|
console.log("extension not yet ready");
|
||||||
|
window.setTimeout(init, 200);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log("got pong");
|
||||||
|
registerHandlers();
|
||||||
|
// Hack to know when the extension is unloaded
|
||||||
|
let port = chrome.runtime.connect();
|
||||||
|
|
||||||
port.onDisconnect.addListener(() => {
|
port.onDisconnect.addListener(() => {
|
||||||
console.log("chrome runtime disconnected, removing handlers");
|
console.log("chrome runtime disconnected, removing handlers");
|
||||||
for (let handler of handlers) {
|
for (let handler of handlers) {
|
||||||
document.removeEventListener(handler.type, handler.listener);
|
document.removeEventListener(handler.type, handler.listener);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
registerHandlers();
|
init();
|
||||||
|
|
||||||
function registerHandlers() {
|
function registerHandlers() {
|
||||||
const $ = (x) => document.getElementById(x);
|
const $ = (x) => document.getElementById(x);
|
||||||
|
@ -125,6 +125,7 @@ namespace RpcFunctions {
|
|||||||
fee_deposit: (new native.Amount(denom.fee_deposit)).toNbo(),
|
fee_deposit: (new native.Amount(denom.fee_deposit)).toNbo(),
|
||||||
fee_refresh: (new native.Amount(denom.fee_refresh)).toNbo(),
|
fee_refresh: (new native.Amount(denom.fee_refresh)).toNbo(),
|
||||||
fee_withdraw: (new native.Amount(denom.fee_withdraw)).toNbo(),
|
fee_withdraw: (new native.Amount(denom.fee_withdraw)).toNbo(),
|
||||||
|
fee_refund: (new native.Amount(denom.fee_refund)).toNbo(),
|
||||||
});
|
});
|
||||||
|
|
||||||
let nativeSig = new native.EddsaSignature();
|
let nativeSig = new native.EddsaSignature();
|
||||||
|
@ -866,6 +866,7 @@ export interface DenominationKeyValidityPS_args {
|
|||||||
fee_withdraw: AmountNbo;
|
fee_withdraw: AmountNbo;
|
||||||
fee_deposit: AmountNbo;
|
fee_deposit: AmountNbo;
|
||||||
fee_refresh: AmountNbo;
|
fee_refresh: AmountNbo;
|
||||||
|
fee_refund: AmountNbo;
|
||||||
denom_hash: HashCode;
|
denom_hash: HashCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -889,6 +890,7 @@ export class DenominationKeyValidityPS extends SignatureStruct {
|
|||||||
["fee_withdraw", AmountNbo],
|
["fee_withdraw", AmountNbo],
|
||||||
["fee_deposit", AmountNbo],
|
["fee_deposit", AmountNbo],
|
||||||
["fee_refresh", AmountNbo],
|
["fee_refresh", AmountNbo],
|
||||||
|
["fee_refund", AmountNbo],
|
||||||
["denom_hash", HashCode]
|
["denom_hash", HashCode]
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -75,6 +75,9 @@ export class Denomination {
|
|||||||
@Checkable.Value(AmountJson)
|
@Checkable.Value(AmountJson)
|
||||||
fee_refresh: AmountJson;
|
fee_refresh: AmountJson;
|
||||||
|
|
||||||
|
@Checkable.Value(AmountJson)
|
||||||
|
fee_refund: AmountJson;
|
||||||
|
|
||||||
@Checkable.String
|
@Checkable.String
|
||||||
stamp_start: string;
|
stamp_start: string;
|
||||||
|
|
||||||
|
@ -521,6 +521,28 @@ export class Wallet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a contract to the wallet and sign coins,
|
||||||
|
* but do not send them yet.
|
||||||
|
*/
|
||||||
|
checkPay(offer: Offer): Promise<any> {
|
||||||
|
console.log("executing checkPay");
|
||||||
|
return Promise.resolve().then(() => {
|
||||||
|
return this.getPossibleExchangeCoins(offer.contract.amount,
|
||||||
|
offer.contract.max_fee,
|
||||||
|
offer.contract.exchanges)
|
||||||
|
}).then((mcs) => {
|
||||||
|
if (Object.keys(mcs).length == 0) {
|
||||||
|
console.log("not confirming payment, insufficient coins");
|
||||||
|
return {
|
||||||
|
error: "coins-insufficient",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve all necessary information for looking up the contract
|
* Retrieve all necessary information for looking up the contract
|
||||||
* with the given hash.
|
* with the given hash.
|
||||||
|
@ -99,6 +99,24 @@ function makeHandlers(db: IDBDatabase,
|
|||||||
|
|
||||||
return wallet.confirmPay(offer);
|
return wallet.confirmPay(offer);
|
||||||
},
|
},
|
||||||
|
["check-pay"]: function(detail, sender) {
|
||||||
|
let offer;
|
||||||
|
try {
|
||||||
|
offer = Offer.checked(detail.offer);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Checkable.SchemaError) {
|
||||||
|
console.error("schema error:", e.message);
|
||||||
|
return Promise.resolve({
|
||||||
|
error: "invalid contract",
|
||||||
|
hint: e.message,
|
||||||
|
detail: detail
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return wallet.checkPay(offer);
|
||||||
|
},
|
||||||
["execute-payment"]: function(detail, sender) {
|
["execute-payment"]: function(detail, sender) {
|
||||||
return wallet.executePayment(detail.H_contract);
|
return wallet.executePayment(detail.H_contract);
|
||||||
},
|
},
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"description": "Privacy preserving and transparent payments",
|
"description": "Privacy preserving and transparent payments",
|
||||||
"manifest_version": 2,
|
"manifest_version": 2,
|
||||||
"name": "GNU Taler Wallet (git)",
|
"name": "GNU Taler Wallet (git)",
|
||||||
"version": "0.5.19",
|
"version": "0.5.23",
|
||||||
|
|
||||||
"applications": {
|
"applications": {
|
||||||
"gecko": {
|
"gecko": {
|
||||||
|
@ -4,7 +4,8 @@
|
|||||||
"description": "",
|
"description": "",
|
||||||
"main": "wxwallet.js",
|
"main": "wxwallet.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "mocha --delay"
|
"test": "mocha --delay",
|
||||||
|
"test-cover": "istanbul cover -x libwrapper.js _mocha -- --delay"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@ -26,13 +27,15 @@
|
|||||||
"gulp-tar": "^1.8.0",
|
"gulp-tar": "^1.8.0",
|
||||||
"gulp-typescript": "^2.10.0",
|
"gulp-typescript": "^2.10.0",
|
||||||
"gulp-zip": "^3.1.0",
|
"gulp-zip": "^3.1.0",
|
||||||
|
"istanbul-lib-instrument": "^1.0.0-alpha.6",
|
||||||
"jed": "^1.1.0",
|
"jed": "^1.1.0",
|
||||||
"map-stream": "0.0.6",
|
"map-stream": "0.0.6",
|
||||||
"mocha": "^2.3.4",
|
"mocha": "^2.4.5",
|
||||||
"po2json": "git+https://github.com/mikeedwards/po2json",
|
"po2json": "git+https://github.com/mikeedwards/po2json",
|
||||||
"systemjs": "^0.19.14",
|
"systemjs": "^0.19.14",
|
||||||
"through2": "^2.0.1",
|
"through2": "^2.0.1",
|
||||||
"typescript": "^1.9.0-dev.20160225",
|
"typescript": "^1.9.0-dev.20160225",
|
||||||
|
"typhonjs-istanbul-instrument-jspm": "^0.1.0",
|
||||||
"vinyl": "^1.1.1"
|
"vinyl": "^1.1.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,6 @@
|
|||||||
<title>Taler Wallet: Confirm Reserve Creation</title>
|
<title>Taler Wallet: Confirm Reserve Creation</title>
|
||||||
|
|
||||||
<link rel="stylesheet" type="text/css" href="../style/lang.css">
|
<link rel="stylesheet" type="text/css" href="../style/lang.css">
|
||||||
<link rel="stylesheet" type="text/css" href="../style/wallet.css">
|
|
||||||
|
|
||||||
<script src="../lib/vendor/URI.js"></script>
|
<script src="../lib/vendor/URI.js"></script>
|
||||||
<script src="../lib/vendor/mithril.js"></script>
|
<script src="../lib/vendor/mithril.js"></script>
|
||||||
@ -15,18 +14,65 @@
|
|||||||
<script src="../i18n/strings.js"></script>
|
<script src="../i18n/strings.js"></script>
|
||||||
<script src="../lib/i18n.js"></script>
|
<script src="../lib/i18n.js"></script>
|
||||||
<script src="../lib/module-trampoline.js"></script>
|
<script src="../lib/module-trampoline.js"></script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
#main {
|
||||||
|
border: solid 1px black;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin: auto;
|
||||||
|
max-width: 50%;
|
||||||
|
padding: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.accept {
|
||||||
|
background-color: #5757D2;
|
||||||
|
border: 1px solid black;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin: 1em 0;
|
||||||
|
padding: 0.5em;
|
||||||
|
font-weight: bold;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
button.linky {
|
||||||
|
background:none!important;
|
||||||
|
border:none;
|
||||||
|
padding:0!important;
|
||||||
|
|
||||||
|
font-family:arial,sans-serif;
|
||||||
|
color:#069;
|
||||||
|
text-decoration:underline;
|
||||||
|
cursor:pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.url {
|
||||||
|
width: 25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
button.accept:disabled {
|
||||||
|
background-color: #dedbe8;
|
||||||
|
border: 1px solid white;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin: 1em 0;
|
||||||
|
padding: 0.5em;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #2C2C2C;
|
||||||
|
}
|
||||||
|
|
||||||
|
.errorbox {
|
||||||
|
border: 1px solid;
|
||||||
|
display: inline-block;
|
||||||
|
margin: 1em;
|
||||||
|
padding: 1em;
|
||||||
|
font-weight: bold;
|
||||||
|
background: #FF8A8A;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<header>
|
|
||||||
<div id="logo"></div>
|
|
||||||
<h1>Payment Confirmation</h1>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<aside class="sidebar" id="left">
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
<section id="main">
|
<section id="main">
|
||||||
|
<h1>GNU Taler Wallet</h1>
|
||||||
<article id="contract" class="fade"></article>
|
<article id="contract" class="fade"></article>
|
||||||
</section>
|
</section>
|
||||||
</body>
|
</body>
|
||||||
|
@ -72,6 +72,7 @@ export function main() {
|
|||||||
console.dir(offer);
|
console.dir(offer);
|
||||||
let contract = offer.contract;
|
let contract = offer.contract;
|
||||||
let error = null;
|
let error = null;
|
||||||
|
let payDisabled = true;
|
||||||
|
|
||||||
var Contract = {
|
var Contract = {
|
||||||
view(ctrl) {
|
view(ctrl) {
|
||||||
@ -87,8 +88,8 @@ export function main() {
|
|||||||
_.map(contract.products,
|
_.map(contract.products,
|
||||||
(p: any) => m("li",
|
(p: any) => m("li",
|
||||||
`${p.description}: ${prettyAmount(p.price)}`))),
|
`${p.description}: ${prettyAmount(p.price)}`))),
|
||||||
m("button.confirm-pay", {onclick: doPayment}, i18n`Confirm Payment`),
|
m("button.accept", {onclick: doPayment, disabled: payDisabled}, i18n`Confirm Payment`),
|
||||||
m("p", error ? error : []),
|
(error ? m("p.errorbox", error) : []),
|
||||||
m(Details, contract)
|
m(Details, contract)
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@ -96,6 +97,31 @@ export function main() {
|
|||||||
|
|
||||||
m.mount(document.getElementById("contract"), Contract);
|
m.mount(document.getElementById("contract"), Contract);
|
||||||
|
|
||||||
|
function checkPayment() {
|
||||||
|
chrome.runtime.sendMessage({type: 'check-pay', detail: {offer}}, (resp) => {
|
||||||
|
if (resp.error) {
|
||||||
|
console.log("check-pay error", JSON.stringify(resp));
|
||||||
|
switch (resp.error) {
|
||||||
|
case "coins-insufficient":
|
||||||
|
error = "You do not have enough coins of the requested currency.";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
error = `Error: ${resp.error}`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
payDisabled = true;
|
||||||
|
} else {
|
||||||
|
payDisabled = false;
|
||||||
|
error = null;
|
||||||
|
}
|
||||||
|
m.redraw();
|
||||||
|
window.setTimeout(checkPayment, 300);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
checkPayment();
|
||||||
|
|
||||||
|
|
||||||
function doPayment() {
|
function doPayment() {
|
||||||
let d = {offer};
|
let d = {offer};
|
||||||
chrome.runtime.sendMessage({type: 'confirm-pay', detail: d}, (resp) => {
|
chrome.runtime.sendMessage({type: 'confirm-pay', detail: d}, (resp) => {
|
||||||
|
@ -4,8 +4,6 @@
|
|||||||
<head>
|
<head>
|
||||||
<title>Taler Wallet: Select Taler Provider</title>
|
<title>Taler Wallet: Select Taler Provider</title>
|
||||||
|
|
||||||
<link rel="stylesheet" type="text/css" href="../style/wallet.css">
|
|
||||||
|
|
||||||
<script src="../lib/vendor/URI.js"></script>
|
<script src="../lib/vendor/URI.js"></script>
|
||||||
<script src="../lib/vendor/mithril.js"></script>
|
<script src="../lib/vendor/mithril.js"></script>
|
||||||
<script src="../lib/vendor/system-csp-production.src.js"></script>
|
<script src="../lib/vendor/system-csp-production.src.js"></script>
|
||||||
@ -13,21 +11,63 @@
|
|||||||
<script src="../i18n/strings.js"></script>
|
<script src="../i18n/strings.js"></script>
|
||||||
<script src="../lib/i18n.js"></script>
|
<script src="../lib/i18n.js"></script>
|
||||||
<script src="../lib/module-trampoline.js"></script>
|
<script src="../lib/module-trampoline.js"></script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
#main {
|
||||||
|
border: solid 1px black;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin: auto;
|
||||||
|
max-width: 50%;
|
||||||
|
padding: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.accept {
|
||||||
|
background-color: #5757D2;
|
||||||
|
border: 1px solid black;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin: 1em 0;
|
||||||
|
padding: 0.5em;
|
||||||
|
font-weight: bold;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
button.linky {
|
||||||
|
background:none!important;
|
||||||
|
border:none;
|
||||||
|
padding:0!important;
|
||||||
|
|
||||||
|
font-family:arial,sans-serif;
|
||||||
|
color:#069;
|
||||||
|
text-decoration:underline;
|
||||||
|
cursor:pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.url {
|
||||||
|
width: 25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
border-left: 1px solid black;
|
||||||
|
border-right: 1px solid black;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0.3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
span.spacer {
|
||||||
|
padding-left: 0.5em;
|
||||||
|
padding-right: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<header>
|
|
||||||
<div id="logo"></div>
|
|
||||||
<h1>Select Taler Provider</h1>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<aside class="sidebar" id="left">
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
<section id="main">
|
<section id="main">
|
||||||
<article>
|
<h1>GNU Taler Wallet</h1>
|
||||||
<div class="fade" id="exchange-selection"></div>
|
<div class="fade" id="exchange-selection"></div>
|
||||||
</article>
|
|
||||||
</section>
|
</section>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
@ -76,31 +76,38 @@ class Controller {
|
|||||||
callbackUrl: string;
|
callbackUrl: string;
|
||||||
wtTypes: string[];
|
wtTypes: string[];
|
||||||
detailCollapsed = m.prop<boolean>(true);
|
detailCollapsed = m.prop<boolean>(true);
|
||||||
|
suggestedExchangeUrl: string;
|
||||||
|
complexViewRequested = false;
|
||||||
|
urlOkay = false;
|
||||||
|
|
||||||
constructor(initialExchangeUrl: string,
|
constructor(suggestedExchangeUrl: string,
|
||||||
amount: AmountJson,
|
amount: AmountJson,
|
||||||
callbackUrl: string,
|
callbackUrl: string,
|
||||||
wt_types: string[]) {
|
wt_types: string[]) {
|
||||||
console.log("creating main controller");
|
console.log("creating main controller");
|
||||||
|
this.suggestedExchangeUrl = suggestedExchangeUrl;
|
||||||
this.amount = amount;
|
this.amount = amount;
|
||||||
this.callbackUrl = callbackUrl;
|
this.callbackUrl = callbackUrl;
|
||||||
this.wtTypes = wt_types;
|
this.wtTypes = wt_types;
|
||||||
this.timer = new DelayTimer(800, () => this.update());
|
this.timer = new DelayTimer(800, () => this.update());
|
||||||
this.url(initialExchangeUrl);
|
this.url(suggestedExchangeUrl);
|
||||||
this.update();
|
this.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
private update() {
|
private update() {
|
||||||
this.timer.stop();
|
this.timer.stop();
|
||||||
const doUpdate = () => {
|
const doUpdate = () => {
|
||||||
|
this.reserveCreationInfo = null;
|
||||||
if (!this.url()) {
|
if (!this.url()) {
|
||||||
this.statusString = i18n`Please enter a URL`;
|
this.statusString = i18n`Error: URL is empty`;
|
||||||
|
m.redraw(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.statusString = null;
|
this.statusString = null;
|
||||||
let parsedUrl = URI(this.url());
|
let parsedUrl = URI(this.url());
|
||||||
if (parsedUrl.is("relative")) {
|
if (parsedUrl.is("relative")) {
|
||||||
this.statusString = i18n`The URL you've entered is not valid (must be absolute)`;
|
this.statusString = i18n`Error: URL may not be relative`;
|
||||||
|
m.redraw(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -114,15 +121,15 @@ class Controller {
|
|||||||
this.isValidExchange = true;
|
this.isValidExchange = true;
|
||||||
this.reserveCreationInfo = r;
|
this.reserveCreationInfo = r;
|
||||||
console.dir(r);
|
console.dir(r);
|
||||||
this.statusString = "The exchange base URL is valid!";
|
|
||||||
m.endComputation();
|
m.endComputation();
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
console.log("get exchange info rejected");
|
console.log("get exchange info rejected");
|
||||||
if (e.hasOwnProperty("httpStatus")) {
|
if (e.hasOwnProperty("httpStatus")) {
|
||||||
this.statusString = `request failed with status ${this.request.status}`;
|
this.statusString = `Error: request failed with status ${this.request.status}`;
|
||||||
} else {
|
} else if (e.hasOwnProperty("errorResponse")) {
|
||||||
this.statusString = `unknown request error`;
|
let resp = e.errorResponse;
|
||||||
|
this.statusString = `Error: ${resp.error} (${resp.hint})`;
|
||||||
}
|
}
|
||||||
m.endComputation();
|
m.endComputation();
|
||||||
});
|
});
|
||||||
@ -130,7 +137,7 @@ class Controller {
|
|||||||
|
|
||||||
doUpdate();
|
doUpdate();
|
||||||
|
|
||||||
console.log("got update");
|
console.log("got update", this.url());
|
||||||
}
|
}
|
||||||
|
|
||||||
reset() {
|
reset() {
|
||||||
@ -187,14 +194,82 @@ class Controller {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function view(ctrl: Controller): any {
|
||||||
function view(ctrl: Controller) {
|
|
||||||
let controls = [];
|
let controls = [];
|
||||||
let mx = (x, ...args) => controls.push(m(x, ...args));
|
let mx = (x, ...args) => controls.push(m(x, ...args));
|
||||||
|
|
||||||
mx("p",
|
mx("p",
|
||||||
i18n`The bank wants to create a reserve over ${amountToPretty(
|
i18n.parts`You are about to withdraw ${m("strong", amountToPretty(
|
||||||
ctrl.amount)}.`);
|
ctrl.amount))} from your bank account into your wallet.`);
|
||||||
|
|
||||||
|
if (ctrl.complexViewRequested || !ctrl.suggestedExchangeUrl) {
|
||||||
|
return controls.concat(viewComplex(ctrl));
|
||||||
|
}
|
||||||
|
|
||||||
|
return controls.concat(viewSimple(ctrl));
|
||||||
|
}
|
||||||
|
|
||||||
|
function viewSimple(ctrl: Controller) {
|
||||||
|
let controls = [];
|
||||||
|
let mx = (x, ...args) => controls.push(m(x, ...args));
|
||||||
|
|
||||||
|
if (ctrl.statusString) {
|
||||||
|
mx("p", "Error: ", ctrl.statusString);
|
||||||
|
mx("button.linky", {
|
||||||
|
onclick: () => {
|
||||||
|
ctrl.complexViewRequested = true;
|
||||||
|
}
|
||||||
|
}, "advanced options");
|
||||||
|
}
|
||||||
|
else if (ctrl.reserveCreationInfo) {
|
||||||
|
mx("button.accept", {
|
||||||
|
onclick: () => ctrl.confirmReserve(ctrl.reserveCreationInfo,
|
||||||
|
ctrl.url(),
|
||||||
|
ctrl.amount,
|
||||||
|
ctrl.callbackUrl),
|
||||||
|
disabled: !ctrl.isValidExchange
|
||||||
|
},
|
||||||
|
"Accept fees and withdraw");
|
||||||
|
mx("span.spacer");
|
||||||
|
mx("button.linky", {
|
||||||
|
onclick: () => {
|
||||||
|
ctrl.complexViewRequested = true;
|
||||||
|
}
|
||||||
|
}, "advanced options");
|
||||||
|
let totalCost = Amounts.add(ctrl.reserveCreationInfo.overhead,
|
||||||
|
ctrl.reserveCreationInfo.withdrawFee).amount;
|
||||||
|
mx("p", `Withdraw cost: ${amountToPretty(totalCost)}`);
|
||||||
|
} else {
|
||||||
|
mx("p", "Please wait ...");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return controls;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function viewComplex(ctrl: Controller) {
|
||||||
|
let controls = [];
|
||||||
|
let mx = (x, ...args) => controls.push(m(x, ...args));
|
||||||
|
|
||||||
|
mx("button.accept", {
|
||||||
|
onclick: () => ctrl.confirmReserve(ctrl.reserveCreationInfo,
|
||||||
|
ctrl.url(),
|
||||||
|
ctrl.amount,
|
||||||
|
ctrl.callbackUrl),
|
||||||
|
disabled: !ctrl.isValidExchange
|
||||||
|
},
|
||||||
|
"Accept fees and withdraw");
|
||||||
|
mx("span.spacer");
|
||||||
|
mx("button.linky", {
|
||||||
|
onclick: () => {
|
||||||
|
ctrl.complexViewRequested = false;
|
||||||
|
}
|
||||||
|
}, "back to simple view");
|
||||||
|
|
||||||
|
mx("br");
|
||||||
|
|
||||||
|
|
||||||
mx("input",
|
mx("input",
|
||||||
{
|
{
|
||||||
className: "url",
|
className: "url",
|
||||||
@ -204,18 +279,11 @@ function view(ctrl: Controller) {
|
|||||||
oninput: m.withAttr("value", ctrl.onUrlChanged.bind(ctrl)),
|
oninput: m.withAttr("value", ctrl.onUrlChanged.bind(ctrl)),
|
||||||
});
|
});
|
||||||
|
|
||||||
mx("button", {
|
mx("br");
|
||||||
onclick: () => ctrl.confirmReserve(ctrl.reserveCreationInfo,
|
|
||||||
ctrl.url(),
|
|
||||||
ctrl.amount,
|
|
||||||
ctrl.callbackUrl),
|
|
||||||
disabled: !ctrl.isValidExchange
|
|
||||||
},
|
|
||||||
"Confirm exchange selection");
|
|
||||||
|
|
||||||
if (ctrl.statusString) {
|
if (ctrl.statusString) {
|
||||||
mx("p", ctrl.statusString);
|
mx("p", ctrl.statusString);
|
||||||
} else {
|
} else if (!ctrl.reserveCreationInfo) {
|
||||||
mx("p", "Checking URL, please wait ...");
|
mx("p", "Checking URL, please wait ...");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -246,9 +314,21 @@ function view(ctrl: Controller) {
|
|||||||
function renderReserveCreationDetails(rci: ReserveCreationInfo) {
|
function renderReserveCreationDetails(rci: ReserveCreationInfo) {
|
||||||
let denoms = rci.selectedDenoms;
|
let denoms = rci.selectedDenoms;
|
||||||
|
|
||||||
|
let countByPub = {};
|
||||||
|
let uniq = [];
|
||||||
|
|
||||||
|
denoms.forEach((x: Denomination) => {
|
||||||
|
let c = countByPub[x.denom_pub] || 0;
|
||||||
|
if (c == 0) {
|
||||||
|
uniq.push(x);
|
||||||
|
}
|
||||||
|
c += 1;
|
||||||
|
countByPub[x.denom_pub] = c;
|
||||||
|
});
|
||||||
|
|
||||||
function row(denom: Denomination) {
|
function row(denom: Denomination) {
|
||||||
return m("tr", [
|
return m("tr", [
|
||||||
m("td", denom.pub_hash.substr(0, 5) + "..."),
|
m("td", countByPub[denom.denom_pub] + "x"),
|
||||||
m("td", amountToPretty(denom.value)),
|
m("td", amountToPretty(denom.value)),
|
||||||
m("td", amountToPretty(denom.fee_withdraw)),
|
m("td", amountToPretty(denom.fee_withdraw)),
|
||||||
m("td", amountToPretty(denom.fee_refresh)),
|
m("td", amountToPretty(denom.fee_refresh)),
|
||||||
@ -263,13 +343,13 @@ function renderReserveCreationDetails(rci: ReserveCreationInfo) {
|
|||||||
m("p", `Overhead: ${overheadStr}`),
|
m("p", `Overhead: ${overheadStr}`),
|
||||||
m("table", [
|
m("table", [
|
||||||
m("tr", [
|
m("tr", [
|
||||||
m("th", "Key Hash"),
|
m("th", "Count"),
|
||||||
m("th", "Value"),
|
m("th", "Value"),
|
||||||
m("th", "Withdraw Fee"),
|
m("th", "Withdraw Fee"),
|
||||||
m("th", "Refresh Fee"),
|
m("th", "Refresh Fee"),
|
||||||
m("th", "Deposit Fee"),
|
m("th", "Deposit Fee"),
|
||||||
]),
|
]),
|
||||||
denoms.map(row)
|
uniq.map(row)
|
||||||
])
|
])
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
7
selenium/README
Normal file
7
selenium/README
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
Directory containing testscases for testing the wallet with Selenium ChromeDriver.
|
||||||
|
|
||||||
|
[1] Contains the ChromeDriver Pythonic documentation
|
||||||
|
[2] Tells which fields (and which values) the 'loggingPrefs' capability expects
|
||||||
|
|
||||||
|
[1] http://seleniumhq.github.io/selenium/docs/api/py/index.html
|
||||||
|
[2] https://github.com/SeleniumHQ/selenium/wiki/DesiredCapabilities#loggingpreferences-json-object
|
228
selenium/test.py
Normal file
228
selenium/test.py
Normal file
@ -0,0 +1,228 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
"""
|
||||||
|
Tests for the wallet. It looks for an env variable called TALER_BASEURL
|
||||||
|
where it appends "/banks" etc. in order to find bank and shops. If not
|
||||||
|
found, it defaults to https://test.taler.net/
|
||||||
|
"""
|
||||||
|
|
||||||
|
from selenium import webdriver
|
||||||
|
from selenium.webdriver.common.by import By
|
||||||
|
from selenium.webdriver.support import expected_conditions as EC
|
||||||
|
from selenium.webdriver.support.ui import WebDriverWait
|
||||||
|
from selenium.common.exceptions import NoSuchElementException, TimeoutException
|
||||||
|
from urllib import parse
|
||||||
|
import argparse
|
||||||
|
import time
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
taler_baseurl = os.environ.get('TALER_BASEURL', 'https://test.taler.net/')
|
||||||
|
|
||||||
|
def client_setup(args):
|
||||||
|
"""Return a dict containing the driver and the extension's id"""
|
||||||
|
co = webdriver.ChromeOptions()
|
||||||
|
co.add_argument("load-extension=" + args.extdir)
|
||||||
|
cap = webdriver.DesiredCapabilities.CHROME.copy()
|
||||||
|
cap['loggingPrefs'] = {'driver': 'INFO', 'browser': 'INFO'}
|
||||||
|
client = webdriver.Chrome(chrome_options=co, desired_capabilities=cap)
|
||||||
|
client.get('https://taler.net')
|
||||||
|
listener = """\
|
||||||
|
document.addEventListener('taler-id', function(evt){
|
||||||
|
var html = document.getElementsByTagName('html')[0];
|
||||||
|
html.setAttribute('data-taler-wallet-id', evt.detail.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
var evt = new CustomEvent('taler-query-id');
|
||||||
|
document.dispatchEvent(evt);
|
||||||
|
"""
|
||||||
|
client.execute_script(listener)
|
||||||
|
html = client.find_element(By.TAG_NAME, "html")
|
||||||
|
return {'client': client, 'ext_id': html.get_attribute('data-taler-wallet-id')}
|
||||||
|
|
||||||
|
def is_error(client):
|
||||||
|
"""Return True in case of errors in the browser, False otherwise"""
|
||||||
|
for log_type in ['browser']:
|
||||||
|
for log in client.get_log(log_type):
|
||||||
|
if log['level'] is 'error':
|
||||||
|
print(log['level'] + ': ' + log['message'])
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def switch_base():
|
||||||
|
"""If 'test' is in TALER_BASEURL, then make it be 'demo', and viceversa.
|
||||||
|
Used to trig currency mismatch errors. It assumes that the https://{test,demo}.taler.net
|
||||||
|
layout is being used"""
|
||||||
|
global taler_baseurl
|
||||||
|
url = parse.urlparse(taler_baseurl)
|
||||||
|
if url[1] == 'test.taler.net':
|
||||||
|
taler_baseurl = "https://demo.taler.net"
|
||||||
|
if url[1] == 'demo.taler.net':
|
||||||
|
taler_baseurl = "https://test.taler.net"
|
||||||
|
|
||||||
|
def make_donation(client, amount_value=None):
|
||||||
|
"""Make donation at shop.test.taler.net. Assume the wallet has coins"""
|
||||||
|
client.get(parse.urljoin(taler_baseurl, "shop"))
|
||||||
|
try:
|
||||||
|
form = client.find_element(By.TAG_NAME, "form")
|
||||||
|
except NoSuchElementException:
|
||||||
|
logger.error('No donation form found')
|
||||||
|
sys.exit(1)
|
||||||
|
if amount_value:
|
||||||
|
xpath = "//select[@id='taler-donation']/option[@value='" + str(amount_value) + "']"
|
||||||
|
try:
|
||||||
|
desired_amount = client.find_element(By.XPATH, xpath)
|
||||||
|
desired_amount.click()
|
||||||
|
except NoSuchElementException:
|
||||||
|
logger.error("value '" + str(amount_value) + "' is not offered by this shop to donate, please adapt it")
|
||||||
|
sys.exit(1)
|
||||||
|
form.submit() # amount and receiver chosen
|
||||||
|
try:
|
||||||
|
confirm_taler = client.find_element(By.XPATH, "//form//input[@type='button']")
|
||||||
|
except NoSuchElementException:
|
||||||
|
logger.error('Could not trigger contract on donation shop')
|
||||||
|
sys.exit(1)
|
||||||
|
confirm_taler.click() # Taler as payment option chosen
|
||||||
|
# explicit get() is needed, it hangs (sometimes) otherwise
|
||||||
|
time.sleep(1)
|
||||||
|
client.get(client.current_url)
|
||||||
|
wait = WebDriverWait(client, 10)
|
||||||
|
try:
|
||||||
|
confirm_pay = wait.until(EC.element_to_be_clickable((By.XPATH, "//button[@class='accept']")))
|
||||||
|
except TimeoutException:
|
||||||
|
logger.error('Could not confirm payment on donation shop')
|
||||||
|
sys.exit(1)
|
||||||
|
confirm_pay.click()
|
||||||
|
|
||||||
|
|
||||||
|
def buy_article(client):
|
||||||
|
"""Buy article at blog.test.taler.net. Assume the wallet has coins"""
|
||||||
|
client.get(parse.urljoin(taler_baseurl, "blog"))
|
||||||
|
try:
|
||||||
|
teaser = client.find_element(By.XPATH, "//ul/h3/a[1]") # Pick 'Foreword' chapter
|
||||||
|
except NoSuchElementException:
|
||||||
|
logger.error('Could not choose "Foreword" chapter on blog')
|
||||||
|
sys.exit(1)
|
||||||
|
teaser.click()
|
||||||
|
# explicit get() is needed, it hangs (sometimes) otherwise
|
||||||
|
time.sleep(1)
|
||||||
|
client.get(client.current_url)
|
||||||
|
wait = WebDriverWait(client, 10)
|
||||||
|
try:
|
||||||
|
confirm_pay = wait.until(EC.element_to_be_clickable((By.XPATH, "//button[@class='accept']")))
|
||||||
|
except TimeoutException:
|
||||||
|
logger.error('Could not confirm payment on blog')
|
||||||
|
sys.exit(1)
|
||||||
|
confirm_pay.click()
|
||||||
|
|
||||||
|
|
||||||
|
def register(client):
|
||||||
|
"""Register a new user to the bank delaying its execution until the
|
||||||
|
profile page is shown"""
|
||||||
|
client.get(parse.urljoin(taler_baseurl, "bank"))
|
||||||
|
try:
|
||||||
|
register_link = client.find_element(By.XPATH, "//a[@href='/accounts/register/']")
|
||||||
|
except NoSuchElementException:
|
||||||
|
logger.error("Could not find register link on bank's homepage")
|
||||||
|
sys.exit(1)
|
||||||
|
register_link.click()
|
||||||
|
try:
|
||||||
|
client.find_element(By.TAG_NAME, "form")
|
||||||
|
except NoSuchElementException:
|
||||||
|
logger.error("Register form not found")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
register = """\
|
||||||
|
var form = document.getElementsByTagName('form')[0];
|
||||||
|
form.username.value = '%s';
|
||||||
|
form.password.value = 'test';
|
||||||
|
form.submit();
|
||||||
|
""" % str(int(time.time())) # need fresh username
|
||||||
|
|
||||||
|
client.execute_script(register)
|
||||||
|
# need implicit wait to be set up
|
||||||
|
try:
|
||||||
|
button = client.find_element(By.ID, "select-exchange")
|
||||||
|
except NoSuchElementException:
|
||||||
|
logger.error("Selecting exchange impossible")
|
||||||
|
sys.exit(1)
|
||||||
|
# when button is gotten, the browser is in the profile page
|
||||||
|
# so the function can return
|
||||||
|
if not is_error(client):
|
||||||
|
logger.info('correctly registered at bank')
|
||||||
|
else:
|
||||||
|
logger.error('User not registered at bank')
|
||||||
|
|
||||||
|
|
||||||
|
def withdraw(client, amount_value=None):
|
||||||
|
"""Register and withdraw (1) KUDOS for a fresh user"""
|
||||||
|
register(client)
|
||||||
|
# trigger withdrawal button
|
||||||
|
try:
|
||||||
|
button = client.find_element(By.ID, "select-exchange")
|
||||||
|
except NoSuchElementException:
|
||||||
|
logger.error("Selecting exchange impossible")
|
||||||
|
sys.exit(1)
|
||||||
|
if amount_value:
|
||||||
|
xpath = "//select/option[@value='" + str(amount_value) + "']"
|
||||||
|
try:
|
||||||
|
desired_amount = client.find_element(By.XPATH, xpath)
|
||||||
|
desired_amount.click()
|
||||||
|
except NoSuchElementException:
|
||||||
|
logger.error("value '" + str(amount_value) + "' is not offered by this bank to withdraw, please adapt it")
|
||||||
|
sys.exit(1)
|
||||||
|
button.click()
|
||||||
|
location = client.execute_script("return document.location.href")
|
||||||
|
client.get(location)
|
||||||
|
# Confirm xchg
|
||||||
|
wait = WebDriverWait(client, 10)
|
||||||
|
try:
|
||||||
|
button = wait.until(EC.element_to_be_clickable((By.XPATH, "//button[1]")))
|
||||||
|
except TimeoutException:
|
||||||
|
logger.error("Could not confirm exchange (therefore provide withdrawal needed data)")
|
||||||
|
sys.exit(1)
|
||||||
|
# This click returns the captcha page (put wait?)
|
||||||
|
button.click()
|
||||||
|
try:
|
||||||
|
answer = client.find_element(By.XPATH, "//input[@name='pin_0']")
|
||||||
|
question = client.find_element(By.XPATH, "//span[@class='captcha-question']/div")
|
||||||
|
except NoSuchElementException:
|
||||||
|
logger.error("Captcha page not gotten or malformed")
|
||||||
|
sys.exit(1)
|
||||||
|
questionTok = question.text.split()
|
||||||
|
op1 = int(questionTok[2])
|
||||||
|
op2 = int(questionTok[4])
|
||||||
|
res = {'+': op1 + op2, '-': op1 - op2, u'\u00d7': op1 * op2}
|
||||||
|
answer.send_keys(res[questionTok[3]])
|
||||||
|
try:
|
||||||
|
form = client.find_element(By.TAG_NAME, "form")
|
||||||
|
except NoSuchElementException:
|
||||||
|
logger.error("Could not submit captcha answer (therefore trigger withdrawal)")
|
||||||
|
sys.exit(1)
|
||||||
|
form.submit()
|
||||||
|
# check outcome
|
||||||
|
try:
|
||||||
|
client.find_element(By.CLASS_NAME, "informational-ok")
|
||||||
|
except NoSuchElementException:
|
||||||
|
logger.error("Withdrawal not completed")
|
||||||
|
sys.exit(1)
|
||||||
|
logger.info("Withdrawal completed")
|
||||||
|
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument('--extdir', help="Folder containing the unpacked extension", metavar="EXTDIR", type=str, dest="extdir", required=True)
|
||||||
|
args = parser.parse_args()
|
||||||
|
ret = client_setup(args)
|
||||||
|
client = ret['client']
|
||||||
|
client.implicitly_wait(10)
|
||||||
|
withdraw(client, 10)
|
||||||
|
switch_base() # inducing error
|
||||||
|
make_donation(client, 6.0)
|
||||||
|
buy_article(client)
|
||||||
|
logger.info("Test passed")
|
||||||
|
client.close()
|
||||||
|
sys.exit(0)
|
@ -12,44 +12,42 @@
|
|||||||
let assert = require("better-assert");
|
let assert = require("better-assert");
|
||||||
let vm = require("vm");
|
let vm = require("vm");
|
||||||
let fs = require("fs");
|
let fs = require("fs");
|
||||||
|
let instrument = require("typhonjs-istanbul-instrument-jspm").default;
|
||||||
|
|
||||||
if ("function" !== typeof run) {
|
if ("function" !== typeof run) {
|
||||||
throw Error("test must be run with 'mocha --delay ...'");
|
throw Error("test must be run with 'mocha --delay ...'");
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("typeof require (here)", typeof require);
|
let emsc = require("../lib/emscripten/libwrapper.js");
|
||||||
|
|
||||||
// We might need thins in the future ...
|
// Do it here, since it breaks 'require'' for libwrapper
|
||||||
global.nodeRequire = function (modulePath) {
|
|
||||||
return require(modulePath);
|
|
||||||
};
|
|
||||||
|
|
||||||
global.require = global.nodeRequire;
|
|
||||||
|
|
||||||
let data = fs.readFileSync("lib/emscripten/libwrapper.js");
|
|
||||||
vm.runInThisContext(data);
|
|
||||||
|
|
||||||
// Do it here, since it breaks 'require''
|
|
||||||
let System = require("systemjs");
|
let System = require("systemjs");
|
||||||
|
|
||||||
|
|
||||||
System.config({
|
System.config({
|
||||||
defaultJSExtensions: true
|
defaultJSExtensions: true,
|
||||||
|
meta: {
|
||||||
|
'./test/tests/taler.js': {
|
||||||
|
format: 'register'
|
||||||
|
},
|
||||||
|
'./lib/wallet/*': {
|
||||||
|
format: 'register'
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let mod = System.newModule({Module: Module});
|
instrument(System);
|
||||||
|
|
||||||
|
let mod = System.newModule({Module: emsc});
|
||||||
let modName = System.normalizeSync(__dirname + "/../lib/emscripten/emsc");
|
let modName = System.normalizeSync(__dirname + "/../lib/emscripten/emsc");
|
||||||
console.log("registering", modName);
|
console.log("registering", modName);
|
||||||
System.set(modName, mod);
|
System.set(modName, mod);
|
||||||
|
|
||||||
|
|
||||||
System.import("./test/tests/taler.js")
|
System.import("./test/tests/taler.js")
|
||||||
.then((t) => {
|
.then((t) => {
|
||||||
t.declareTests(assert, context, it);
|
t.declareTests(assert, context, it);
|
||||||
run();
|
setTimeout(run, 1);
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
console.error("failed to load module", e.stack);
|
console.error("failed to load module", e.stack);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
@ -5,20 +5,9 @@ declare var HttpMockLib;
|
|||||||
|
|
||||||
export function declareTests(assert, context, it) {
|
export function declareTests(assert, context, it) {
|
||||||
|
|
||||||
it("works!", function() {
|
it("calls native emscripten code", function() {
|
||||||
let x = new Emsc.Amount({value: 42, fraction: 42, currency: "EUR"});
|
let x = new Emsc.Amount({value: 42, fraction: 42, currency: "EUR"});
|
||||||
let j = x.toJson();
|
let j = x.toJson();
|
||||||
assert("value" in j);
|
assert("value" in j);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
it("retries", function() {
|
|
||||||
let m = new HttpMockLib();
|
|
||||||
/*m.intercept()
|
|
||||||
.matchUrlContains()
|
|
||||||
.counterEquals(0)
|
|
||||||
.count()
|
|
||||||
.sen*/
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
|
@ -33,4 +33,4 @@
|
|||||||
"pages/confirm-create-reserve.tsx",
|
"pages/confirm-create-reserve.tsx",
|
||||||
"test/tests/taler.ts"
|
"test/tests/taler.ts"
|
||||||
]
|
]
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user