aboutsummaryrefslogtreecommitdiff
path: root/extension/lib/wallet
diff options
context:
space:
mode:
Diffstat (limited to 'extension/lib/wallet')
-rw-r--r--extension/lib/wallet/checkable.ts190
-rw-r--r--extension/lib/wallet/query.ts4
-rw-r--r--extension/lib/wallet/types.ts138
-rw-r--r--extension/lib/wallet/wallet.ts208
-rw-r--r--extension/lib/wallet/wxmessaging.js57
-rw-r--r--extension/lib/wallet/wxmessaging.ts45
6 files changed, 467 insertions, 175 deletions
diff --git a/extension/lib/wallet/checkable.ts b/extension/lib/wallet/checkable.ts
new file mode 100644
index 000000000..8f89c8669
--- /dev/null
+++ b/extension/lib/wallet/checkable.ts
@@ -0,0 +1,190 @@
+/*
+ This file is part of TALER
+ (C) 2016 GNUnet e.V.
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ TALER; see the file COPYING. If not, If not, see <http://www.gnu.org/licenses/>
+ */
+
+
+"use strict";
+
+/**
+ * Decorators for type-checking JSON into
+ * an object.
+ * @module Checkable
+ * @author Florian Dold
+ */
+
+export namespace Checkable {
+ let chkSym = Symbol("checkable");
+
+
+ function checkNumber(target, prop, path): any {
+ if ((typeof target) !== "number") {
+ throw Error(`expected number for ${path}`);
+ }
+ return target;
+ }
+
+
+ function checkString(target, prop, path): any {
+ if (typeof target !== "string") {
+ throw Error(`expected string for ${path}, got ${typeof target} instead`);
+ }
+ return target;
+ }
+
+
+ function checkAnyObject(target, prop, path): any {
+ if (typeof target !== "object") {
+ throw Error(`expected (any) object for ${path}, got ${typeof target} instead`);
+ }
+ return target;
+ }
+
+
+ function checkAny(target, prop, path): any {
+ return target;
+ }
+
+
+ function checkList(target, prop, path): any {
+ if (!Array.isArray(target)) {
+ throw Error(`array expected for ${path}, got ${typeof target} instead`);
+ }
+ for (let i = 0; i < target.length; i++) {
+ let v = target[i];
+ prop.elementChecker(v, prop.elementProp, path.concat([i]));
+ }
+ return target;
+ }
+
+
+ function checkValue(target, prop, path): any {
+ let type = prop.type;
+ if (!type) {
+ throw Error(`assertion failed (prop is ${JSON.stringify(prop)})`);
+ }
+ let v = target;
+ if (!v || typeof v !== "object") {
+ throw Error(`expected object for ${path}, got ${typeof v} instead`);
+ }
+ let props = type.prototype[chkSym].props;
+ let remainingPropNames = new Set(Object.getOwnPropertyNames(v));
+ let obj = new type();
+ for (let prop of props) {
+ if (!remainingPropNames.has(prop.propertyKey)) {
+ throw Error("Property missing: " + prop.propertyKey);
+ }
+ if (!remainingPropNames.delete(prop.propertyKey)) {
+ throw Error("assertion failed");
+ }
+ let propVal = v[prop.propertyKey];
+ obj[prop.propertyKey] = prop.checker(propVal,
+ prop,
+ path.concat([prop.propertyKey]));
+ }
+
+ if (remainingPropNames.size != 0) {
+ throw Error("superfluous properties " + JSON.stringify(Array.from(
+ remainingPropNames.values())));
+ }
+ return obj;
+ }
+
+
+ export function Class(target) {
+ target.checked = (v) => {
+ return checkValue(v, {
+ propertyKey: "(root)",
+ type: target,
+ checker: checkValue
+ }, []);
+ };
+ return target;
+ }
+
+
+ export function Value(type) {
+ if (!type) {
+ throw Error("Type does not exist yet (wrong order of definitions?)");
+ }
+ function deco(target: Object, propertyKey: string | symbol): void {
+ let chk = mkChk(target);
+ chk.props.push({
+ propertyKey: propertyKey,
+ checker: checkValue,
+ type: type
+ });
+ }
+
+ return deco;
+ }
+
+
+ export function List(type) {
+ let stub = {};
+ type(stub, "(list-element)");
+ let elementProp = mkChk(stub).props[0];
+ let elementChecker = elementProp.checker;
+ if (!elementChecker) {
+ throw Error("assertion failed");
+ }
+ function deco(target: Object, propertyKey: string | symbol): void {
+ let chk = mkChk(target);
+ chk.props.push({
+ elementChecker,
+ elementProp,
+ propertyKey: propertyKey,
+ checker: checkList,
+ });
+ }
+
+ return deco;
+ }
+
+
+ export function Number(target: Object, propertyKey: string | symbol): void {
+ let chk = mkChk(target);
+ chk.props.push({propertyKey: propertyKey, checker: checkNumber});
+ }
+
+
+ export function AnyObject(target: Object,
+ propertyKey: string | symbol): void {
+ let chk = mkChk(target);
+ chk.props.push({propertyKey: propertyKey, checker: checkAnyObject});
+ }
+
+
+ export function Any(target: Object,
+ propertyKey: string | symbol): void {
+ let chk = mkChk(target);
+ chk.props.push({propertyKey: propertyKey, checker: checkAny});
+ }
+
+
+ export function String(target: Object, propertyKey: string | symbol): void {
+ let chk = mkChk(target);
+ chk.props.push({propertyKey: propertyKey, checker: checkString});
+ }
+
+
+ function mkChk(target) {
+ let chk = target[chkSym];
+ if (!chk) {
+ chk = {props: []};
+ target[chkSym] = chk;
+ }
+ return chk;
+ }
+} \ No newline at end of file
diff --git a/extension/lib/wallet/query.ts b/extension/lib/wallet/query.ts
index 82053138f..b82c85189 100644
--- a/extension/lib/wallet/query.ts
+++ b/extension/lib/wallet/query.ts
@@ -268,6 +268,10 @@ class QueryRoot {
* Get one object from a store by its key.
*/
get(storeName, key): Promise<any> {
+ if (key === void 0) {
+ throw Error("key must not be undefined");
+ }
+
const {resolve, promise} = openPromise();
const doGet = (tx) => {
diff --git a/extension/lib/wallet/types.ts b/extension/lib/wallet/types.ts
index 478287a21..4f512800e 100644
--- a/extension/lib/wallet/types.ts
+++ b/extension/lib/wallet/types.ts
@@ -14,6 +14,8 @@
TALER; see the file COPYING. If not, If not, see <http://www.gnu.org/licenses/>
*/
+import {EddsaPublicKey} from "./emscriptif";
+import {Checkable} from "./checkable";
"use strict";
// TODO: factor into multiple files
@@ -61,48 +63,138 @@ export interface Coin {
}
-export interface AmountJson {
+@Checkable.Class
+export class AmountJson {
+ @Checkable.Number
value: number;
- fraction: number
+
+ @Checkable.Number
+ fraction: number;
+
+ @Checkable.String
currency: string;
+
+ static checked: (obj: any) => AmountJson;
}
-export interface ConfirmReserveRequest {
+@Checkable.Class
+export class CreateReserveRequest {
/**
- * Name of the form field for the amount.
+ * The initial amount for the reserve.
*/
- field_amount;
+ @Checkable.Value(AmountJson)
+ amount: AmountJson;
/**
- * Name of the form field for the reserve public key.
+ * Mint URL where the bank should create the reserve.
*/
- field_reserve_pub;
+ @Checkable.String
+ mint: string;
- /**
- * Name of the form field for the reserve public key.
- */
- field_mint;
+ static checked: (obj: any) => CreateReserveRequest;
+}
- /**
- * The actual amount in string form.
- * TODO: where is this format specified?
- */
- amount_str;
+@Checkable.Class
+export class CreateReserveResponse {
/**
- * Target URL for the reserve creation request.
+ * Mint URL where the bank should create the reserve.
+ * The URL is canonicalized in the response.
*/
- post_url;
+ @Checkable.String
+ mint: string;
+
+ @Checkable.String
+ reservePub: string;
+
+ static checked: (obj: any) => CreateReserveResponse;
+}
+
+@Checkable.Class
+export class ConfirmReserveRequest {
/**
- * Mint URL where the bank should create the reserve.
+ * Public key of then reserve that should be marked
+ * as confirmed.
*/
- mint;
+ @Checkable.String
+ reservePub: string;
+
+ static checked: (obj: any) => ConfirmReserveRequest;
}
-export interface ConfirmReserveResponse {
- backlink?: string;
- success: boolean;
+@Checkable.Class
+export class MintInfo {
+ @Checkable.String
+ master_pub: string;
+
+ @Checkable.String
+ url: string;
+
+ static checked: (obj: any) => MintInfo;
+}
+
+
+@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(MintInfo))
+ mints: MintInfo[];
+
+ @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;
+
+ static checked: (obj: any) => Contract;
+}
+
+
+@Checkable.Class
+export class Offer {
+ @Checkable.Value(Contract)
+ contract: Contract;
+
+ @Checkable.String
+ merchant_sig: string;
+
+ @Checkable.String
+ H_contract: string;
+
+ static checked: (obj: any) => Offer;
} \ No newline at end of file
diff --git a/extension/lib/wallet/wallet.ts b/extension/lib/wallet/wallet.ts
index cbc3e4b01..62d67b40c 100644
--- a/extension/lib/wallet/wallet.ts
+++ b/extension/lib/wallet/wallet.ts
@@ -32,8 +32,7 @@ import {UInt64} from "./emscriptif";
import {DepositRequestPS} from "./emscriptif";
import {eddsaSign} from "./emscriptif";
import {EddsaPrivateKey} from "./emscriptif";
-import {ConfirmReserveRequest} from "./types";
-import {ConfirmReserveResponse} from "./types";
+import {CreateReserveRequest} from "./types";
import {RsaPublicKey} from "./emscriptif";
import {Denomination} from "./types";
import {RsaBlindingKey} from "./emscriptif";
@@ -48,24 +47,15 @@ import {HttpResponse} from "./http";
import {RequestException} from "./http";
import {Query} from "./query";
import {AmountJson} from "./types";
+import {ConfirmReserveRequest} from "./types";
+import {Offer} from "./types";
+import {Contract} from "./types";
+import {MintInfo} from "./types";
+import {CreateReserveResponse} from "./types";
"use strict";
-
-class CoinPaySig {
- coin_sig: string;
-
- coin_pub: string;
-
- ub_sig: string;
-
- denom_pub: string;
-
- f: AmountJson;
-}
-
-
interface ConfirmPayRequest {
offer: Offer;
}
@@ -75,36 +65,7 @@ interface MintCoins {
}
-interface MintInfo {
- master_pub: string;
- url: string;
-}
-
-interface Offer {
- contract: Contract;
- merchant_sig: string;
- H_contract: string;
-}
-
-interface Contract {
- H_wire: string;
- amount: AmountJson;
- auditors: string[];
- expiry: string,
- locations: string[];
- max_fee: AmountJson;
- merchant: any;
- merchant_pub: string;
- mints: MintInfo[];
- products: string[];
- refund_deadline: string;
- timestamp: string;
- transaction_id: number;
- fulfillment_url: string;
-}
-
-
-interface CoinPaySig_interface {
+interface CoinPaySig {
coin_sig: string;
coin_pub: string;
ub_sig: string;
@@ -127,19 +88,13 @@ interface Reserve {
}
-interface PaymentResponse {
- payReq: any;
- contract: Contract;
-}
-
-
export interface Badge {
setText(s: string): void;
setColor(c: string): void;
}
-type PayCoinInfo = Array<{ updatedCoin: Coin, sig: CoinPaySig_interface }>;
+type PayCoinInfo = Array<{ updatedCoin: Coin, sig: CoinPaySig }>;
/**
@@ -250,7 +205,7 @@ export class Wallet {
EddsaPrivateKey.fromCrock(cd.coin.coinPriv))
.toCrock();
- let s: CoinPaySig_interface = {
+ let s: CoinPaySig = {
coin_sig: coinSig,
coin_pub: cd.coin.coinPub,
ub_sig: cd.coin.denomSig,
@@ -425,6 +380,11 @@ export class Wallet {
});
}
+
+ /**
+ * First fetch information requred to withdraw from the reserve,
+ * then deplete the reserve, withdrawing coins until it is empty.
+ */
initReserve(reserveRecord) {
this.updateMintFromUrl(reserveRecord.mint_base_url)
.then((mint) =>
@@ -447,72 +407,82 @@ export class Wallet {
}
- confirmReserve(req: ConfirmReserveRequest): Promise<ConfirmReserveResponse> {
- let reservePriv = EddsaPrivateKey.create();
- let reservePub = reservePriv.getPublicKey();
- let form = new FormData();
- let now: number = (new Date).getTime();
- form.append(req.field_amount, req.amount_str);
- form.append(req.field_reserve_pub, reservePub.toCrock());
- form.append(req.field_mint, req.mint);
- // TODO: set bank-specified fields.
- let mintBaseUrl = canonicalizeBaseUrl(req.mint);
- let requestedAmount = parsePrettyAmount(req.amount_str);
-
- if (!requestedAmount) {
- throw Error(`unrecognized amount ${req.amount_str}.`);
- }
+ /**
+ * Create a reserve, but do not flag it as confirmed yet.
+ */
+ createReserve(req: CreateReserveRequest): Promise<CreateReserveResponse> {
+ const reservePriv = EddsaPrivateKey.create();
+ const reservePub = reservePriv.getPublicKey();
+
+ const now = (new Date).getTime();
+ const canonMint = canonicalizeBaseUrl(req.mint);
+
+ const reserveRecord = {
+ reserve_pub: reservePub.toCrock(),
+ reserve_priv: reservePriv.toCrock(),
+ mint_base_url: canonMint,
+ created: now,
+ last_query: null,
+ current_amount: null,
+ requested_amount: req.amount,
+ confirmed: false,
+ };
+
+
+ const historyEntry = {
+ type: "create-reserve",
+ timestamp: now,
+ detail: {
+ requestedAmount: req.amount,
+ reservePub: reserveRecord.reserve_pub,
+ }
+ };
+
+ return Query(this.db)
+ .put("reserves", reserveRecord)
+ .put("history", historyEntry)
+ .finish()
+ .then(() => {
+ let r: CreateReserveResponse = {
+ mint: canonMint,
+ reservePub: reservePub.toCrock(),
+ };
+ return r;
+ });
+ }
- return this.http.postForm(req.post_url, form)
- .then((hresp) => {
- // TODO: look at response status code and handle errors appropriately
- let json = JSON.parse(hresp.responseText);
- if (!json) {
- return {
- success: false
- };
- }
- let resp: ConfirmReserveResponse = {
- success: undefined,
- backlink: json.redirect_url,
- };
- let reserveRecord = {
- reserve_pub: reservePub.toCrock(),
- reserve_priv: reservePriv.toCrock(),
- mint_base_url: mintBaseUrl,
- created: now,
- last_query: null,
- current_amount: null,
- // XXX: set to actual amount
- requested_amount: null
- };
-
- if (hresp.status != 200) {
- resp.success = false;
- return resp;
- }
-
- let historyEntry = {
- type: "create-reserve",
- timestamp: now,
- detail: {
- requestedAmount,
- reservePub: reserveRecord.reserve_pub,
- }
- };
-
- resp.success = true;
-
- return Query(this.db)
- .put("reserves", reserveRecord)
- .put("history", historyEntry)
- .finish()
- .then(() => {
- // Do this in the background
- this.initReserve(reserveRecord);
- return resp;
- });
- });
+
+ /**
+ * 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
+ * bank confirmed its creation.
+ *
+ * A confirmed reserve should be shown to the user in the UI, while
+ * an unconfirmed reserve should be hidden.
+ */
+ confirmReserve(req: ConfirmReserveRequest): Promise<void> {
+ const now = (new Date).getTime();
+ const historyEntry = {
+ type: "confirm-reserve",
+ timestamp: now,
+ detail: {
+ reservePub: req.reservePub,
+ }
+ };
+ return Query(this.db)
+ .get("reserves", req.reservePub)
+ .then((r) => {
+ r.confirmed = true;
+ return Query(this.db)
+ .put("reserves", r)
+ .put("history", historyEntry)
+ .finish()
+ .then(() => {
+ // Do this in the background
+ this.initReserve(r);
+ });
+ });
}
diff --git a/extension/lib/wallet/wxmessaging.js b/extension/lib/wallet/wxmessaging.js
index bd108276f..1e1029be0 100644
--- a/extension/lib/wallet/wxmessaging.js
+++ b/extension/lib/wallet/wxmessaging.js
@@ -13,10 +13,17 @@
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/>
*/
-System.register(["./wallet", "./db", "./http"], function(exports_1) {
+System.register(["./types", "./wallet", "./db", "./http"], function(exports_1) {
"use strict";
- var wallet_1, db_1, db_2, db_3, http_1;
+ var types_1, wallet_1, db_1, db_2, db_3, http_1, types_2, types_3;
var ChromeBadge;
+ /**
+ * Messaging for the WebExtensions wallet. Should contain
+ * parts that are specific for WebExtensions, but as little business
+ * logic as possible.
+ *
+ * @author Florian Dold
+ */
function makeHandlers(wallet) {
return (_a = {},
_a["balances"] = function (db, detail, sendResponse) {
@@ -43,29 +50,43 @@ System.register(["./wallet", "./db", "./http"], function(exports_1) {
// Response is synchronous
return false;
},
+ _a["create-reserve"] = function (db, detail, sendResponse) {
+ var d = {
+ mint: detail.mint,
+ amount: detail.amount,
+ };
+ var req = types_2.CreateReserveRequest.checked(d);
+ wallet.createReserve(req)
+ .then(function (resp) {
+ sendResponse(resp);
+ })
+ .catch(function (e) {
+ sendResponse({ error: "exception" });
+ console.error("exception during 'create-reserve'");
+ console.error(e.stack);
+ });
+ return true;
+ },
_a["confirm-reserve"] = function (db, detail, sendResponse) {
// TODO: make it a checkable
- var req = {
- field_amount: detail.field_amount,
- field_mint: detail.field_mint,
- field_reserve_pub: detail.field_reserve_pub,
- post_url: detail.post_url,
- mint: detail.mint,
- amount_str: detail.amount_str
+ var d = {
+ reservePub: detail.reservePub
};
+ var req = types_1.ConfirmReserveRequest.checked(d);
wallet.confirmReserve(req)
.then(function (resp) {
sendResponse(resp);
})
.catch(function (e) {
- sendResponse({ success: false });
+ sendResponse({ error: "exception" });
console.error("exception during 'confirm-reserve'");
console.error(e.stack);
});
return true;
},
_a["confirm-pay"] = function (db, detail, sendResponse) {
- wallet.confirmPay(detail.offer, detail.merchantPageUrl)
+ var offer = types_3.Offer.checked(detail.offer);
+ wallet.confirmPay(offer)
.then(function (r) {
sendResponse(r);
})
@@ -84,7 +105,7 @@ System.register(["./wallet", "./db", "./http"], function(exports_1) {
.catch(function (e) {
console.error("exception during 'execute-payment'");
console.error(e.stack);
- sendResponse({ success: false, error: e.message });
+ sendResponse({ error: e.message });
});
// async sendResponse
return true;
@@ -133,6 +154,11 @@ System.register(["./wallet", "./db", "./http"], function(exports_1) {
exports_1("wxMain", wxMain);
return {
setters:[
+ function (types_1_1) {
+ types_1 = types_1_1;
+ types_2 = types_1_1;
+ types_3 = types_1_1;
+ },
function (wallet_1_1) {
wallet_1 = wallet_1_1;
},
@@ -145,13 +171,6 @@ System.register(["./wallet", "./db", "./http"], function(exports_1) {
http_1 = http_1_1;
}],
execute: function() {
- /**
- * Messaging for the WebExtensions wallet. Should contain
- * parts that are specific for WebExtensions, but as little business
- * logic as possible.
- * @module Messaging
- * @author Florian Dold
- */
"use strict";
ChromeBadge = (function () {
function ChromeBadge() {
diff --git a/extension/lib/wallet/wxmessaging.ts b/extension/lib/wallet/wxmessaging.ts
index 63310270f..9af63eb2f 100644
--- a/extension/lib/wallet/wxmessaging.ts
+++ b/extension/lib/wallet/wxmessaging.ts
@@ -22,18 +22,20 @@ import {deleteDb} from "./db";
import {openTalerDb} from "./db";
import {BrowserHttpLib} from "./http";
import {Badge} from "./wallet";
+import {CreateReserveRequest} from "./types";
+import {Offer} from "./types";
+
+"use strict";
/**
* Messaging for the WebExtensions wallet. Should contain
* parts that are specific for WebExtensions, but as little business
* logic as possible.
- * @module Messaging
+ *
* @author Florian Dold
*/
-"use strict";
-
-function makeHandlers(wallet) {
+function makeHandlers(wallet: Wallet) {
return {
["balances"]: function(db, detail, sendResponse) {
wallet.getBalances()
@@ -60,29 +62,43 @@ function makeHandlers(wallet) {
// Response is synchronous
return false;
},
+ ["create-reserve"]: function(db, detail, sendResponse) {
+ const d = {
+ mint: detail.mint,
+ amount: detail.amount,
+ };
+ const req = CreateReserveRequest.checked(d);
+ wallet.createReserve(req)
+ .then((resp) => {
+ sendResponse(resp);
+ })
+ .catch((e) => {
+ sendResponse({error: "exception"});
+ console.error("exception during 'create-reserve'");
+ console.error(e.stack);
+ });
+ return true;
+ },
["confirm-reserve"]: function(db, detail, sendResponse) {
// TODO: make it a checkable
- let req: ConfirmReserveRequest = {
- field_amount: detail.field_amount,
- field_mint: detail.field_mint,
- field_reserve_pub: detail.field_reserve_pub,
- post_url: detail.post_url,
- mint: detail.mint,
- amount_str: detail.amount_str
+ const d = {
+ reservePub: detail.reservePub
};
+ const req = ConfirmReserveRequest.checked(d);
wallet.confirmReserve(req)
.then((resp) => {
sendResponse(resp);
})
.catch((e) => {
- sendResponse({success: false});
+ sendResponse({error: "exception"});
console.error("exception during 'confirm-reserve'");
console.error(e.stack);
});
return true;
},
["confirm-pay"]: function(db, detail, sendResponse) {
- wallet.confirmPay(detail.offer, detail.merchantPageUrl)
+ const offer = Offer.checked(detail.offer);
+ wallet.confirmPay(offer)
.then((r) => {
sendResponse(r)
})
@@ -101,7 +117,7 @@ function makeHandlers(wallet) {
.catch((e) => {
console.error("exception during 'execute-payment'");
console.error(e.stack);
- sendResponse({success: false, error: e.message});
+ sendResponse({error: e.message});
});
// async sendResponse
return true;
@@ -124,6 +140,7 @@ function makeHandlers(wallet) {
};
}
+
class ChromeBadge implements Badge {
setText(s: string) {
chrome.browserAction.setBadgeText({text: s});