diff options
author | Florian Dold <florian.dold@gmail.com> | 2016-02-09 21:56:06 +0100 |
---|---|---|
committer | Florian Dold <florian.dold@gmail.com> | 2016-02-09 21:56:06 +0100 |
commit | 5e85cd8b8fa25ed3fbfc260b48bcad098978407a (patch) | |
tree | 3c875261e2c3fa2176911caefc4dcf225a04df74 /extension/lib | |
parent | 42a0076f5951d303635b2e544aa66112cdb9abfe (diff) |
new reserve creation protocol
Diffstat (limited to 'extension/lib')
-rw-r--r-- | extension/lib/wallet/checkable.ts | 190 | ||||
-rw-r--r-- | extension/lib/wallet/query.ts | 4 | ||||
-rw-r--r-- | extension/lib/wallet/types.ts | 138 | ||||
-rw-r--r-- | extension/lib/wallet/wallet.ts | 208 | ||||
-rw-r--r-- | extension/lib/wallet/wxmessaging.js | 57 | ||||
-rw-r--r-- | extension/lib/wallet/wxmessaging.ts | 45 | ||||
-rw-r--r-- | extension/lib/web-common.ts | 6 |
7 files changed, 473 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}); diff --git a/extension/lib/web-common.ts b/extension/lib/web-common.ts index 34ae2a1a8..96f9b61b7 100644 --- a/extension/lib/web-common.ts +++ b/extension/lib/web-common.ts @@ -14,8 +14,14 @@ TALER; see the file COPYING. If not, If not, see <http://www.gnu.org/licenses/> */ +import {AmountJson} from "./wallet/types"; export function substituteFulfillmentUrl(url: string, vars) { url = url.replace("${H_contract}", vars.H_contract); url = url.replace("${$}", "$"); return url; +} + +export function amountToPretty(amount: AmountJson): string { + let x = amount.value + amount.fraction / 1e6; + return `${x} ${amount.currency}`; }
\ No newline at end of file |