This commit is contained in:
Florian Dold 2016-02-18 22:50:17 +01:00
parent 0f607edbb2
commit 079e764ae6
9 changed files with 534 additions and 102 deletions

File diff suppressed because one or more lines are too long

View File

@ -89,6 +89,9 @@ var emsc = {
eddsa_sign: getEmsc('GNUNET_CRYPTO_eddsa_sign', eddsa_sign: getEmsc('GNUNET_CRYPTO_eddsa_sign',
'number', 'number',
['number', 'number', 'number']), ['number', 'number', 'number']),
eddsa_verify: getEmsc('GNUNET_CRYPTO_eddsa_verify',
'number',
['number', 'number', 'number', 'number']),
hash_create_random: getEmsc('GNUNET_CRYPTO_hash_create_random', hash_create_random: getEmsc('GNUNET_CRYPTO_hash_create_random',
'void', 'void',
['number', 'number']), ['number', 'number']),
@ -144,9 +147,10 @@ var emscAlloc = {
}; };
enum SignaturePurpose { export enum SignaturePurpose {
RESERVE_WITHDRAW = 1200, RESERVE_WITHDRAW = 1200,
WALLET_COIN_DEPOSIT = 1201, WALLET_COIN_DEPOSIT = 1201,
MASTER_DENOMINATION_KEY_VALIDITY = 1025,
} }
enum RandomQuality { enum RandomQuality {
@ -336,11 +340,11 @@ export class Amount extends ArenaObject {
return emsc.get_fraction(this.nativePtr); return emsc.get_fraction(this.nativePtr);
} }
get currency() { get currency(): String {
return emsc.get_currency(this.nativePtr); return emsc.get_currency(this.nativePtr);
} }
toJson() { toJson(): AmountJson {
return { return {
value: emsc.get_value(this.nativePtr), value: emsc.get_value(this.nativePtr),
fraction: emsc.get_fraction(this.nativePtr), fraction: emsc.get_fraction(this.nativePtr),
@ -351,7 +355,7 @@ export class Amount extends ArenaObject {
/** /**
* Add an amount to this amount. * Add an amount to this amount.
*/ */
add(a) { add(a: Amount) {
let res = emsc.amount_add(this.nativePtr, a.nativePtr, this.nativePtr); let res = emsc.amount_add(this.nativePtr, a.nativePtr, this.nativePtr);
if (res < 1) { if (res < 1) {
// Overflow // Overflow
@ -363,7 +367,7 @@ export class Amount extends ArenaObject {
/** /**
* Perform saturating subtraction on amounts. * Perform saturating subtraction on amounts.
*/ */
sub(a) { sub(a: Amount) {
// this = this - a // this = this - a
let res = emsc.amount_subtract(this.nativePtr, this.nativePtr, a.nativePtr); let res = emsc.amount_subtract(this.nativePtr, this.nativePtr, a.nativePtr);
if (res == 0) { if (res == 0) {
@ -376,7 +380,7 @@ export class Amount extends ArenaObject {
throw Error("Incompatible currencies"); throw Error("Incompatible currencies");
} }
cmp(a) { cmp(a: Amount) {
return emsc.amount_cmp(this.nativePtr, a.nativePtr); return emsc.amount_cmp(this.nativePtr, a.nativePtr);
} }
@ -848,6 +852,44 @@ export class DepositRequestPS extends SignatureStruct {
} }
} }
export interface DenominationKeyValidityPS_args {
master: EddsaPublicKey;
start: AbsoluteTimeNbo;
expire_withdraw: AbsoluteTimeNbo;
expire_spend: AbsoluteTimeNbo;
expire_legal: AbsoluteTimeNbo;
value: AmountNbo;
fee_withdraw: AmountNbo;
fee_deposit: AmountNbo;
fee_refresh: AmountNbo;
denom_hash: HashCode;
}
export class DenominationKeyValidityPS extends SignatureStruct {
constructor(w: DenominationKeyValidityPS_args) {
super(w);
}
purpose() {
return SignaturePurpose.MASTER_DENOMINATION_KEY_VALIDITY;
}
fieldTypes() {
return [
["master", EddsaPublicKey],
["start", AbsoluteTimeNbo],
["expire_withdraw", AbsoluteTimeNbo],
["expire_spend", AbsoluteTimeNbo],
["expire_legal", AbsoluteTimeNbo],
["value", AmountNbo],
["fee_withdraw", AmountNbo],
["fee_deposit", AmountNbo],
["fee_refresh", AmountNbo],
["denom_hash", HashCode]
];
}
}
interface Encodeable { interface Encodeable {
encode(arena?: Arena): ByteArray; encode(arena?: Arena): ByteArray;
@ -932,6 +974,22 @@ export function eddsaSign(purpose: EccSignaturePurpose,
} }
export function eddsaVerify(purposeNum: number,
verify: EccSignaturePurpose,
sig: EddsaSignature,
pub: EddsaPublicKey,
a?: Arena): boolean {
let r = emsc.eddsa_verify(purposeNum,
verify.nativePtr,
sig.nativePtr,
pub.nativePtr);
if (r === GNUNET_OK) {
return true;
}
return false;
}
export function rsaUnblind(sig: RsaSignature, export function rsaUnblind(sig: RsaSignature,
bk: RsaBlindingKey, bk: RsaBlindingKey,
pk: RsaPublicKey, pk: RsaPublicKey,

View File

@ -54,3 +54,55 @@ export class CreateReserveResponse {
static checked: (obj: any) => CreateReserveResponse; static checked: (obj: any) => CreateReserveResponse;
} }
@Checkable.Class
export class Denomination {
@Checkable.Value(AmountJson)
value: AmountJson;
@Checkable.String
denom_pub: string;
@Checkable.Value(AmountJson)
fee_withdraw: AmountJson;
@Checkable.Value(AmountJson)
fee_deposit: AmountJson;
@Checkable.Value(AmountJson)
fee_refresh: AmountJson;
@Checkable.String
stamp_start: string;
@Checkable.String
stamp_expire_withdraw: string;
@Checkable.String
stamp_expire_legal: string;
@Checkable.String
stamp_expire_deposit: string;
@Checkable.String
master_sig: string;
@Checkable.Optional(Checkable.String)
pub_hash: string;
static checked: (obj: any) => Denomination;
}
export interface IMintInfo {
baseUrl: string;
masterPublicKey: string;
denoms: Denomination[];
}
export interface ReserveCreationInfo {
mintInfo: IMintInfo;
selectedDenoms: Denomination[];
withdrawFee: AmountJson;
}

View File

@ -22,36 +22,48 @@
*/ */
import * as native from "./emscriptif"; import * as native from "./emscriptif";
import {AmountJson, CreateReserveResponse} from "./types"; import {AmountJson, CreateReserveResponse, IMintInfo, Denomination} from "./types";
import {HttpResponse, RequestException} from "./http"; import {HttpResponse, RequestException} from "./http";
import {Query} from "./query"; import {Query} from "./query";
import {Checkable} from "./checkable"; import {Checkable} from "./checkable";
import {canonicalizeBaseUrl} from "./helpers"; import {canonicalizeBaseUrl} from "./helpers";
import {ReserveCreationInfo} from "./types";
"use strict"; "use strict";
export interface Mint {
baseUrl: string;
keys: Keys
}
export interface CoinWithDenom { export interface CoinWithDenom {
coin: Coin; coin: Coin;
denom: Denomination; denom: Denomination;
} }
export interface Keys {
@Checkable.Class
export class KeysJson {
@Checkable.List(Checkable.Value(Denomination))
denoms: Denomination[]; denoms: Denomination[];
@Checkable.String
master_public_key: string;
@Checkable.Any
auditors: any[];
@Checkable.String
list_issue_date: string;
@Checkable.Any
signkeys: any;
@Checkable.String
eddsa_pub: string;
@Checkable.String
eddsa_sig: string;
static checked: (obj: any) => KeysJson;
} }
export interface Denomination {
value: AmountJson;
denom_pub: string;
fee_withdraw: AmountJson;
fee_deposit: AmountJson;
stamp_expire_withdraw: string;
}
export interface PreCoin { export interface PreCoin {
coinPub: string; coinPub: string;
@ -75,6 +87,115 @@ export interface Coin {
} }
function isValidDenom(denom: Denomination,
masterPub: string): boolean {
let p = new native.DenominationKeyValidityPS({
master: native.EddsaPublicKey.fromCrock(masterPub),
denom_hash: native.RsaPublicKey.fromCrock(denom.denom_pub).encode().hash(),
expire_legal: native.AbsoluteTimeNbo.fromTalerString(denom.stamp_expire_legal),
expire_spend: native.AbsoluteTimeNbo.fromTalerString(denom.stamp_expire_deposit),
expire_withdraw: native.AbsoluteTimeNbo.fromTalerString(denom.stamp_expire_withdraw),
start: native.AbsoluteTimeNbo.fromTalerString(denom.stamp_start),
value: (new native.Amount(denom.value)).toNbo(),
fee_deposit: (new native.Amount(denom.fee_deposit)).toNbo(),
fee_refresh: (new native.Amount(denom.fee_refresh)).toNbo(),
fee_withdraw: (new native.Amount(denom.fee_withdraw)).toNbo(),
});
let nativeSig = new native.EddsaSignature();
nativeSig.loadCrock(denom.master_sig);
let nativePub = native.EddsaPublicKey.fromCrock(masterPub);
return native.eddsaVerify(native.SignaturePurpose.MASTER_DENOMINATION_KEY_VALIDITY,
p.toPurpose(),
nativeSig,
nativePub);
}
class MintInfo implements IMintInfo {
baseUrl: string;
masterPublicKey: string;
denoms: Denomination[];
constructor(obj: {baseUrl: string} & any) {
this.baseUrl = obj.baseUrl;
if (obj.denoms) {
this.denoms = Array.from(<Denomination[]>obj.denoms);
} else {
this.denoms = [];
}
if (typeof obj.masterPublicKey === "string") {
this.masterPublicKey = obj.masterPublicKey;
}
}
static fresh(baseUrl: string): MintInfo {
return new MintInfo({baseUrl});
}
/**
* Merge new key information into the mint 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
* the first error.
*/
mergeKeys(newKeys: KeysJson) {
if (!this.masterPublicKey) {
this.masterPublicKey = newKeys.master_public_key;
}
if (this.masterPublicKey != newKeys.master_public_key) {
throw Error("public keys do not match");
}
for (let newDenom of newKeys.denoms) {
let found = false;
for (let oldDenom of this.denoms) {
if (oldDenom.denom_pub === newDenom.denom_pub) {
let a = Object.assign({}, oldDenom);
let b = Object.assign({}, newDenom);
// pub hash is only there for convenience in the wallet
delete a["pub_hash"];
delete b["pub_hash"];
if (!_.isEqual(a, b)) {
console.log("old/new:");
console.dir(a);
console.dir(b);
throw Error("denomination modified");
}
// TODO: check if info still matches
found = true;
break;
}
}
if (found) {
continue;
}
console.log("validating denomination");
if (!isValidDenom(newDenom, this.masterPublicKey)) {
throw Error("signature on denomination invalid");
}
let d: Denomination = Object.assign({}, newDenom);
d.pub_hash = native.RsaPublicKey.fromCrock(d.denom_pub)
.encode()
.hash()
.toCrock();
this.denoms.push(d);
}
}
}
@Checkable.Class @Checkable.Class
export class CreateReserveRequest { export class CreateReserveRequest {
/** /**
@ -107,14 +228,14 @@ export class ConfirmReserveRequest {
@Checkable.Class @Checkable.Class
export class MintInfo { export class MintHandle {
@Checkable.String @Checkable.String
master_pub: string; master_pub: string;
@Checkable.String @Checkable.String
url: string; url: string;
static checked: (obj: any) => MintInfo; static checked: (obj: any) => MintHandle;
} }
@ -144,8 +265,8 @@ export class Contract {
@Checkable.String @Checkable.String
merchant_pub: string; merchant_pub: string;
@Checkable.List(Checkable.Value(MintInfo)) @Checkable.List(Checkable.Value(MintHandle))
mints: MintInfo[]; mints: MintHandle[];
@Checkable.List(Checkable.AnyObject) @Checkable.List(Checkable.AnyObject)
products: any[]; products: any[];
@ -274,6 +395,10 @@ function rankDenom(denom1: any, denom2: any) {
} }
function mergeMintKeys(oldKeys: KeysJson, newKeys: KeysJson) {
}
/** /**
* Create a pre-coin of the given denomination to be withdrawn from then given * Create a pre-coin of the given denomination to be withdrawn from then given
* reserve. * reserve.
@ -441,7 +566,7 @@ export class Wallet {
*/ */
private getPossibleMintCoins(paymentAmount: AmountJson, private getPossibleMintCoins(paymentAmount: AmountJson,
depositFeeLimit: AmountJson, depositFeeLimit: AmountJson,
allowedMints: MintInfo[]): Promise<MintCoins> { allowedMints: MintHandle[]): Promise<MintCoins> {
// Mapping from mint base URL to list of coins together with their // Mapping from mint base URL to list of coins together with their
// denomination // denomination
let m: MintCoins = {}; let m: MintCoins = {};
@ -639,7 +764,8 @@ export class Wallet {
return Query(this.db).put("history", depleted).finish(); return Query(this.db).put("history", depleted).finish();
}) })
.catch((e) => { .catch((e) => {
console.error("Failed to deplete reserve", e.stack); console.error("Failed to deplete reserve");
console.error(e);
}); });
} }
@ -791,8 +917,8 @@ export class Wallet {
/** /**
* Withdraw coins from a reserve until it is empty. * Withdraw coins from a reserve until it is empty.
*/ */
private depleteReserve(reserve, mint: Mint): Promise<void> { private depleteReserve(reserve, mint: MintInfo): Promise<void> {
let denomsAvailable: Denomination[] = copy(mint.keys.denoms); let denomsAvailable: Denomination[] = copy(mint.denoms);
let denomsForWithdraw = getWithdrawDenomList(reserve.current_amount, let denomsForWithdraw = getWithdrawDenomList(reserve.current_amount,
denomsAvailable); denomsAvailable);
@ -811,7 +937,7 @@ export class Wallet {
* Update the information about a reserve that is stored in the wallet * Update the information about a reserve that is stored in the wallet
* by quering the reserve's mint. * by quering the reserve's mint.
*/ */
private updateReserve(reservePub: string, mint: Mint): Promise<Reserve> { private updateReserve(reservePub: string, mint: MintInfo): Promise<Reserve> {
return Query(this.db) return Query(this.db)
.get("reserves", reservePub) .get("reserves", reservePub)
.then((reserve) => { .then((reserve) => {
@ -846,27 +972,59 @@ export class Wallet {
} }
getReserveCreationInfo(baseUrl: string,
amount: AmountJson): Promise<ReserveCreationInfo> {
return this.updateMintFromUrl(baseUrl)
.then((mintInfo: IMintInfo) => {
let selectedDenoms = getWithdrawDenomList(amount,
mintInfo.denoms);
let acc = native.Amount.getZero(amount.currency);
for (let d of selectedDenoms) {
acc.add(new native.Amount(d.fee_withdraw));
}
let ret: ReserveCreationInfo = {
mintInfo,
selectedDenoms,
withdrawFee: acc.toJson(),
};
return ret;
});
}
/** /**
* Update or add mint DB entry by fetching the /keys information. * Update or add mint DB entry by fetching the /keys information.
* Optionally link the reserve entry to the new or existing * Optionally link the reserve entry to the new or existing
* mint entry in then DB. * mint entry in then DB.
*/ */
private updateMintFromUrl(baseUrl): Promise<Mint> { updateMintFromUrl(baseUrl): Promise<MintInfo> {
baseUrl = canonicalizeBaseUrl(baseUrl);
let reqUrl = URI("keys").absoluteTo(baseUrl); let reqUrl = URI("keys").absoluteTo(baseUrl);
return this.http.get(reqUrl).then((resp) => { return this.http.get(reqUrl).then((resp) => {
if (resp.status != 200) { if (resp.status != 200) {
throw Error("/keys request failed"); throw Error("/keys request failed");
} }
let mintKeysJson = JSON.parse(resp.responseText); let mintKeysJson = KeysJson.checked(JSON.parse(resp.responseText));
if (!mintKeysJson) {
throw new RequestException({url: reqUrl, hint: "keys invalid"}); return Query(this.db).get("mints", baseUrl).then((r) => {
let mint;
console.log("got mints result");
console.dir(r);
if (!r) {
mint = MintInfo.fresh(baseUrl);
console.log("making fresh mint");
} else {
mint = new MintInfo(r);
console.log("using old mint");
} }
let mint: Mint = {
baseUrl: baseUrl, mint.mergeKeys(mintKeysJson);
keys: mintKeysJson
};
return Query(this.db).put("mints", mint).finish().then(() => mint); return Query(this.db).put("mints", mint).finish().then(() => mint);
}); });
});
} }

View File

@ -19,6 +19,7 @@ import {Wallet, Offer, Badge, ConfirmReserveRequest, CreateReserveRequest} from
import {deleteDb, exportDb, openTalerDb} from "./db"; import {deleteDb, exportDb, openTalerDb} from "./db";
import {BrowserHttpLib} from "./http"; import {BrowserHttpLib} from "./http";
import {Checkable} from "./checkable"; import {Checkable} from "./checkable";
import {AmountJson} from "./types";
"use strict"; "use strict";
@ -77,7 +78,11 @@ function makeHandlers(db: IDBDatabase,
} catch (e) { } catch (e) {
if (e instanceof Checkable.SchemaError) { if (e instanceof Checkable.SchemaError) {
console.error("schema error:", e.message); console.error("schema error:", e.message);
return Promise.resolve({error: "invalid contract", hint: e.message, detail: detail}); return Promise.resolve({
error: "invalid contract",
hint: e.message,
detail: detail
});
} else { } else {
throw e; throw e;
} }
@ -88,6 +93,19 @@ function makeHandlers(db: IDBDatabase,
["execute-payment"]: function(detail) { ["execute-payment"]: function(detail) {
return wallet.executePayment(detail.H_contract); return wallet.executePayment(detail.H_contract);
}, },
["mint-info"]: function(detail) {
if (!detail.baseUrl) {
return Promise.resolve({error: "bad url"});
}
return wallet.updateMintFromUrl(detail.baseUrl);
},
["reserve-creation-info"]: function(detail) {
if (!detail.baseUrl || typeof detail.baseUrl !== "string") {
return Promise.resolve({error: "bad url"});
}
let amount = AmountJson.checked(detail.amount);
return wallet.getReserveCreationInfo(detail.baseUrl, amount);
},
["get-history"]: function(detail) { ["get-history"]: function(detail) {
// TODO: limit history length // TODO: limit history length
return wallet.getHistory(); return wallet.getHistory();
@ -119,7 +137,7 @@ function dispatch(handlers, req, sendResponse) {
}) })
}) })
.catch((e) => { .catch((e) => {
console.log("exception during wallet handler'"); console.log("exception during wallet handler");
console.error(e.stack); console.error(e.stack);
sendResponse({ sendResponse({
error: "exception", error: "exception",
@ -155,7 +173,18 @@ export function wxMain() {
let wallet = new Wallet(db, http, badge); let wallet = new Wallet(db, http, badge);
let handlers = makeHandlers(db, wallet); let handlers = makeHandlers(db, wallet);
chrome.runtime.onMessage.addListener((req, sender, sendResponse) => { chrome.runtime.onMessage.addListener((req, sender, sendResponse) => {
try {
return dispatch(handlers, req, sendResponse) return dispatch(handlers, req, sendResponse)
} catch (e) {
console.log("exception during wallet handler (dispatch)");
console.error(e.stack);
sendResponse({
error: "exception",
hint: e.message,
stack: e.stack.toString()
});
return false;
}
}); });
}) })
.catch((e) => { .catch((e) => {

View File

@ -42,6 +42,7 @@
"background": { "background": {
"scripts": [ "scripts": [
"lib/vendor/URI.js", "lib/vendor/URI.js",
"lib/vendor/lodash.core.min.js",
"lib/emscripten/libwrapper.js", "lib/emscripten/libwrapper.js",
"lib/vendor/system-csp-production.src.js", "lib/vendor/system-csp-production.src.js",
"background/main.js" "background/main.js"

View File

@ -45,9 +45,49 @@ System.register(["../lib/wallet/helpers", "../lib/wallet/types", "mithril"], fun
else { else {
mx("p", "Checking URL, please wait ..."); mx("p", "Checking URL, please wait ...");
} }
if (ctrl.reserveCreationInfo) {
var withdrawFeeStr = helpers_1.amountToPretty(ctrl.reserveCreationInfo.withdrawFee);
mx("p", "Fee for withdrawal: " + withdrawFeeStr);
if (ctrl.detailCollapsed()) {
mx("button.linky", {
onclick: function () {
ctrl.detailCollapsed(false);
}
}, "show more");
}
else {
mx("button.linky", {
onclick: function () {
ctrl.detailCollapsed(true);
}
}, "show less");
mx("div", {}, renderCoinTable(ctrl.reserveCreationInfo.selectedDenoms));
}
}
return mithril_1.default("div", controls); return mithril_1.default("div", controls);
var _a; var _a;
} }
function renderCoinTable(denoms) {
function row(denom) {
return mithril_1.default("tr", [
mithril_1.default("td", denom.pub_hash.substr(0, 5) + "..."),
mithril_1.default("td", helpers_1.amountToPretty(denom.value)),
mithril_1.default("td", helpers_1.amountToPretty(denom.fee_withdraw)),
mithril_1.default("td", helpers_1.amountToPretty(denom.fee_refresh)),
mithril_1.default("td", helpers_1.amountToPretty(denom.fee_deposit)),
]);
}
return mithril_1.default("table", [
mithril_1.default("tr", [
mithril_1.default("th", "Key Hash"),
mithril_1.default("th", "Value"),
mithril_1.default("th", "Withdraw Fee"),
mithril_1.default("th", "Refresh Fee"),
mithril_1.default("th", "Deposit Fee"),
]),
denoms.map(row)
]);
}
function probeMint(mintBaseUrl) { function probeMint(mintBaseUrl) {
throw Error("not implemented"); throw Error("not implemented");
} }
@ -64,6 +104,21 @@ System.register(["../lib/wallet/helpers", "../lib/wallet/types", "mithril"], fun
} }
return Promise.resolve(mint); return Promise.resolve(mint);
} }
function getReserveCreationInfo(baseUrl, amount) {
var m = { type: "reserve-creation-info", detail: { baseUrl: baseUrl, amount: amount } };
return new Promise(function (resolve, reject) {
chrome.runtime.sendMessage(m, function (resp) {
if (resp.error) {
console.error("error response", resp);
var e = Error("call to reserve-creation-info failed");
e.errorResponse = resp;
reject(e);
return;
}
resolve(resp);
});
});
}
function main() { function main() {
var url = URI(document.location.href); var url = URI(document.location.href);
var query = URI.parseQuery(url.query()); var query = URI.parseQuery(url.query());
@ -128,6 +183,9 @@ System.register(["../lib/wallet/helpers", "../lib/wallet/types", "mithril"], fun
this.url = mithril_1.default.prop(); this.url = mithril_1.default.prop();
this.statusString = null; this.statusString = null;
this.isValidMint = false; this.isValidMint = false;
this.reserveCreationInfo = null;
this.detailCollapsed = mithril_1.default.prop(true);
console.log("creating main controller");
this.amount = amount; this.amount = amount;
this.callbackUrl = callbackUrl; this.callbackUrl = callbackUrl;
this.timer = new DelayTimer(800, function () { return _this.update(); }); this.timer = new DelayTimer(800, function () { return _this.update(); });
@ -140,47 +198,44 @@ System.register(["../lib/wallet/helpers", "../lib/wallet/types", "mithril"], fun
var doUpdate = function () { var doUpdate = function () {
if (!_this.url()) { if (!_this.url()) {
_this.statusString = (_a = ["Please enter a URL"], _a.raw = ["Please enter a URL"], i18n(_a)); _this.statusString = (_a = ["Please enter a URL"], _a.raw = ["Please enter a URL"], i18n(_a));
mithril_1.default.endComputation();
return; return;
} }
_this.statusString = null; _this.statusString = null;
var parsedUrl = URI(_this.url()); var parsedUrl = URI(_this.url());
if (parsedUrl.is("relative")) { if (parsedUrl.is("relative")) {
_this.statusString = (_b = ["The URL you've entered is not valid (must be absolute)"], _b.raw = ["The URL you've entered is not valid (must be absolute)"], i18n(_b)); _this.statusString = (_b = ["The URL you've entered is not valid (must be absolute)"], _b.raw = ["The URL you've entered is not valid (must be absolute)"], i18n(_b));
mithril_1.default.endComputation();
return; return;
} }
var keysUrl = URI("/keys").absoluteTo(helpers_1.canonicalizeBaseUrl(_this.url())); mithril_1.default.redraw(true);
console.log("requesting keys from '" + keysUrl + "'"); console.log("doing get mint info");
_this.request = new XMLHttpRequest(); getReserveCreationInfo(_this.url(), _this.amount)
_this.request.onreadystatechange = function () { .then(function (r) {
if (_this.request.readyState == XMLHttpRequest.DONE) { console.log("get mint info resolved");
switch (_this.request.status) {
case 200:
_this.isValidMint = true; _this.isValidMint = true;
_this.reserveCreationInfo = r;
console.dir(r);
_this.statusString = "The mint base URL is valid!"; _this.statusString = "The mint base URL is valid!";
break; mithril_1.default.endComputation();
case 0: })
_this.statusString = "unknown request error"; .catch(function (e) {
break; console.log("get mint info rejected");
default: if (e.hasOwnProperty("httpStatus")) {
_this.statusString = "request failed with status " + _this.request.status; _this.statusString = "request failed with status " + _this.request.status;
break;
} }
else {
_this.statusString = "unknown request error";
} }
mithril_1.default.endComputation(); mithril_1.default.endComputation();
}; });
_this.request.open("get", keysUrl.href());
_this.request.send();
var _a, _b; var _a, _b;
}; };
mithril_1.default.startComputation();
doUpdate(); doUpdate();
console.log("got update"); console.log("got update");
}; };
Controller.prototype.reset = function () { Controller.prototype.reset = function () {
this.isValidMint = false; this.isValidMint = false;
this.statusString = null; this.statusString = null;
this.reserveCreationInfo = null;
if (this.request) { if (this.request) {
this.request.abort(); this.request.abort();
this.request = null; this.request = null;

View File

@ -19,6 +19,10 @@
import {amountToPretty, canonicalizeBaseUrl} from "../lib/wallet/helpers"; import {amountToPretty, canonicalizeBaseUrl} from "../lib/wallet/helpers";
import {AmountJson, CreateReserveResponse} from "../lib/wallet/types"; import {AmountJson, CreateReserveResponse} from "../lib/wallet/types";
import m from "mithril"; import m from "mithril";
import {IMintInfo} from "../lib/wallet/types";
import {ReserveCreationInfo} from "../lib/wallet/types";
import MithrilComponent = _mithril.MithrilComponent;
import {Denomination} from "../lib/wallet/types";
"use strict"; "use strict";
@ -56,12 +60,15 @@ class Controller {
url = m.prop<string>(); url = m.prop<string>();
statusString = null; statusString = null;
isValidMint = false; isValidMint = false;
reserveCreationInfo: ReserveCreationInfo = null;
private timer: DelayTimer; private timer: DelayTimer;
private request: XMLHttpRequest; private request: XMLHttpRequest;
amount: AmountJson; amount: AmountJson;
callbackUrl: string; callbackUrl: string;
detailCollapsed = m.prop<boolean>(true);
constructor(initialMintUrl: string, amount: AmountJson, callbackUrl: string) { constructor(initialMintUrl: string, amount: AmountJson, callbackUrl: string) {
console.log("creating main controller");
this.amount = amount; this.amount = amount;
this.callbackUrl = callbackUrl; this.callbackUrl = callbackUrl;
this.timer = new DelayTimer(800, () => this.update()); this.timer = new DelayTimer(800, () => this.update());
@ -74,44 +81,39 @@ class Controller {
const doUpdate = () => { const doUpdate = () => {
if (!this.url()) { if (!this.url()) {
this.statusString = i18n`Please enter a URL`; this.statusString = i18n`Please enter a URL`;
m.endComputation();
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`The URL you've entered is not valid (must be absolute)`;
m.endComputation();
return; return;
} }
const keysUrl = URI("/keys").absoluteTo(canonicalizeBaseUrl(this.url())); m.redraw(true);
console.log(`requesting keys from '${keysUrl}'`); console.log("doing get mint info");
this.request = new XMLHttpRequest(); getReserveCreationInfo(this.url(), this.amount)
this.request.onreadystatechange = () => { .then((r: ReserveCreationInfo) => {
if (this.request.readyState == XMLHttpRequest.DONE) { console.log("get mint info resolved");
switch (this.request.status) {
case 200:
this.isValidMint = true; this.isValidMint = true;
this.reserveCreationInfo = r;
console.dir(r);
this.statusString = "The mint base URL is valid!"; this.statusString = "The mint base URL is valid!";
break; m.endComputation();
case 0: })
this.statusString = `unknown request error`; .catch((e) => {
break; console.log("get mint info rejected");
default: if (e.hasOwnProperty("httpStatus")) {
this.statusString = `request failed with status ${this.request.status}`; this.statusString = `request failed with status ${this.request.status}`;
break; } else {
} this.statusString = `unknown request error`;
} }
m.endComputation(); m.endComputation();
}; });
this.request.open("get", keysUrl.href());
this.request.send();
}; };
m.startComputation();
doUpdate(); doUpdate();
@ -121,6 +123,7 @@ class Controller {
reset() { reset() {
this.isValidMint = false; this.isValidMint = false;
this.statusString = null; this.statusString = null;
this.reserveCreationInfo = null;
if (this.request) { if (this.request) {
this.request.abort(); this.request.abort();
this.request = null; this.request = null;
@ -168,7 +171,7 @@ class Controller {
function view(ctrl: Controller) { function view(ctrl: Controller) {
let controls = []; let controls = [];
let mx = (x: string, ...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`The bank wants to create a reserve over ${amountToPretty(
@ -196,10 +199,53 @@ function view(ctrl: Controller) {
mx("p", "Checking URL, please wait ..."); mx("p", "Checking URL, please wait ...");
} }
if (ctrl.reserveCreationInfo) {
let withdrawFeeStr = amountToPretty(ctrl.reserveCreationInfo.withdrawFee);
mx("p", `Fee for withdrawal: ${withdrawFeeStr}`);
if (ctrl.detailCollapsed()) {
mx("button.linky", {
onclick: () => {
ctrl.detailCollapsed(false);
}
}, "show more");
} else {
mx("button.linky", {
onclick: () => {
ctrl.detailCollapsed(true);
}
}, "show less");
mx("div", {}, renderCoinTable(ctrl.reserveCreationInfo.selectedDenoms))
}
}
return m("div", controls); return m("div", controls);
} }
function renderCoinTable(denoms: Denomination[]) {
function row(denom: Denomination) {
return m("tr", [
m("td", denom.pub_hash.substr(0, 5) + "..."),
m("td", amountToPretty(denom.value)),
m("td", amountToPretty(denom.fee_withdraw)),
m("td", amountToPretty(denom.fee_refresh)),
m("td", amountToPretty(denom.fee_deposit)),
]);
}
return m("table", [
m("tr", [
m("th", "Key Hash"),
m("th", "Value"),
m("th", "Withdraw Fee"),
m("th", "Refresh Fee"),
m("th", "Deposit Fee"),
]),
denoms.map(row)
]);
}
interface MintProbeResult { interface MintProbeResult {
keyInfo?: any; keyInfo?: any;
} }
@ -227,6 +273,24 @@ function getSuggestedMint(currency: string): Promise<string> {
} }
function getReserveCreationInfo(baseUrl: string,
amount: AmountJson): Promise<ReserveCreationInfo> {
let m = {type: "reserve-creation-info", detail: {baseUrl, amount}};
return new Promise((resolve, reject) => {
chrome.runtime.sendMessage(m, (resp) => {
if (resp.error) {
console.error("error response", resp);
let e = Error("call to reserve-creation-info failed");
(e as any).errorResponse = resp;
reject(e);
return;
}
resolve(resp);
});
});
}
export function main() { export function main() {
const url = URI(document.location.href); const url = URI(document.location.href);
const query: any = URI.parseQuery(url.query()); const query: any = URI.parseQuery(url.query());

View File

@ -121,3 +121,18 @@ button.confirm-pay {
from {opacity: 0} from {opacity: 0}
to {opacity: 1} to {opacity: 1}
} }
button.linky {
background:none!important;
border:none;
padding:0!important;
font-family:arial,sans-serif;
color:#069;
text-decoration:underline;
cursor:pointer;
}
table, th, td {
border: 1px solid black;
}