From 613a14c14f969bf21ff7569f93cde3a7a35ce96a Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Wed, 31 May 2017 16:04:14 +0200 Subject: fix messaging, small issues and safer types --- src/webex/messages.ts | 28 +++-- src/webex/notify.ts | 10 +- src/webex/pages/confirm-contract.html | 2 +- src/webex/pages/confirm-contract.tsx | 41 +++---- src/webex/wxApi.ts | 44 ++++---- src/webex/wxBackend.ts | 202 ++++++++++++---------------------- 6 files changed, 135 insertions(+), 192 deletions(-) (limited to 'src/webex') diff --git a/src/webex/messages.ts b/src/webex/messages.ts index 21acfc1d5..27ff9a5b6 100644 --- a/src/webex/messages.ts +++ b/src/webex/messages.ts @@ -69,11 +69,11 @@ export interface MessageMap { response: string; }; "confirm-pay": { - request: { offer: types.OfferRecord; }; + request: { proposalId: number; }; response: types.ConfirmPayResult; }; "check-pay": { - request: { offer: types.OfferRecord; }; + request: { proposalId: number; }; response: types.CheckPayResult; }; "query-payment": { @@ -96,21 +96,29 @@ export interface MessageMap { request: { historyEntry: types.HistoryRecord }; response: void; }; - "safe-offer": { - request: { offer: types.OfferRecord }; + "save-proposal": { + request: { proposal: types.ProposalRecord }; response: void; }; "reserve-creation-info": { - request: { baseUrl: string }; + request: { baseUrl: string, amount: types.AmountJson }; response: types.ReserveCreationInfo; } "get-history": { request: { }; response: types.HistoryRecord[]; }; - "get-offer": { - request: { offerId: number }; - response: types.OfferRecord | undefined; + "get-proposal": { + request: { proposalId: number }; + response: types.ProposalRecord | undefined; + }; + "get-coins": { + request: { exchangeBaseUrl: string }; + response: any; + }; + "refresh-coin": { + request: { coinPub: string }; + response: any; }; "get-currencies": { request: { }; @@ -120,6 +128,10 @@ export interface MessageMap { request: { currencyRecord: types.CurrencyRecord }; response: void; }; + "get-exchanges": { + request: { }; + response: types.ExchangeRecord[]; + }; "get-reserves": { request: { exchangeBaseUrl: string }; response: types.ReserveRecord[]; diff --git a/src/webex/notify.ts b/src/webex/notify.ts index 81bc6808d..2f38658bd 100644 --- a/src/webex/notify.ts +++ b/src/webex/notify.ts @@ -280,7 +280,8 @@ async function processProposal(proposal: any) { const contractHash = await wxApi.hashContract(proposal.data); if (contractHash !== proposal.hash) { - console.error("merchant-supplied contract hash is wrong"); + console.error(`merchant-supplied contract hash is wrong (us: ${contractHash}, merchant: ${proposal.hash})`); + console.dir(proposal.data); return; } @@ -301,12 +302,11 @@ async function processProposal(proposal: any) { type: "offer-contract", }; await wxApi.putHistory(historyEntry); - const offerId = await wxApi.saveOffer(proposal); + let proposalId = await wxApi.saveProposal(proposal); - const uri = new URI(chrome.extension.getURL( - "/src/webex/pages/confirm-contract.html")); + const uri = new URI(chrome.extension.getURL("/src/webex/pages/confirm-contract.html")); const params = { - offerId: offerId.toString(), + proposalId: proposalId.toString(), }; const target = uri.query(params).href(); document.location.replace(target); diff --git a/src/webex/pages/confirm-contract.html b/src/webex/pages/confirm-contract.html index 6713b2e2c..e5ba68404 100644 --- a/src/webex/pages/confirm-contract.html +++ b/src/webex/pages/confirm-contract.html @@ -5,7 +5,7 @@ Taler Wallet: Confirm Reserve Creation - + diff --git a/src/webex/pages/confirm-contract.tsx b/src/webex/pages/confirm-contract.tsx index 9b4c93334..c5513f7c6 100644 --- a/src/webex/pages/confirm-contract.tsx +++ b/src/webex/pages/confirm-contract.tsx @@ -27,7 +27,7 @@ import * as i18n from "../../i18n"; import { Contract, ExchangeRecord, - OfferRecord, + ProposalRecord, } from "../../types"; import { renderContract } from "../renderHtml"; @@ -98,11 +98,11 @@ class Details extends React.Component { } interface ContractPromptProps { - offerId: number; + proposalId: number; } interface ContractPromptState { - offer: OfferRecord|null; + proposal: ProposalRecord|null; error: string|null; payDisabled: boolean; exchanges: null|ExchangeRecord[]; @@ -114,7 +114,7 @@ class ContractPrompt extends React.Component e.master_pub); + if (this.state.exchanges && this.state.proposal) { + const acceptedExchangePubs = this.state.proposal.contractTerms.exchanges.map((e) => e.master_pub); const ex = this.state.exchanges.find((e) => acceptedExchangePubs.indexOf(e.masterPublicKey) >= 0); if (ex) { this.setState({error: msgInsufficient}); @@ -165,28 +160,28 @@ class ContractPrompt extends React.Component...; } - const c = this.state.offer.contract; + const c = this.state.proposal.contractTerms; return (
@@ -210,8 +205,8 @@ class ContractPrompt extends React.Component { const url = new URI(document.location.href); const query: any = URI.parseQuery(url.query()); - const offerId = JSON.parse(query.offerId); + const proposalId = JSON.parse(query.proposalId); - ReactDOM.render(, document.getElementById( + ReactDOM.render(, document.getElementById( "contract")!); }); diff --git a/src/webex/wxApi.ts b/src/webex/wxApi.ts index ff601b6f7..4babb2a79 100644 --- a/src/webex/wxApi.ts +++ b/src/webex/wxApi.ts @@ -30,15 +30,16 @@ import { CurrencyRecord, DenominationRecord, ExchangeRecord, - OfferRecord, PreCoinRecord, ReserveCreationInfo, ReserveRecord, } from "../types"; +import { MessageType, MessageMap } from "./messages"; -async function callBackend(type: string, detail?: any): Promise { + +async function callBackend(type: T, detail: MessageMap[T]["request"]): Promise { return new Promise((resolve, reject) => { chrome.runtime.sendMessage({ type, detail }, (resp) => { if (resp && resp.error) { @@ -65,7 +66,7 @@ export function getReserveCreationInfo(baseUrl: string, * Get all exchanges the wallet knows about. */ export function getExchanges(): Promise { - return callBackend("get-exchanges"); + return callBackend("get-exchanges", { }); } @@ -73,7 +74,7 @@ export function getExchanges(): Promise { * Get all currencies the exchange knows about. */ export function getCurrencies(): Promise { - return callBackend("get-currencies"); + return callBackend("get-currencies", { }); } @@ -114,7 +115,7 @@ export function getReserves(exchangeBaseUrl: string): Promise { * Get all reserves for which a payback is available. */ export function getPaybackReserves(): Promise { - return callBackend("get-payback-reserves"); + return callBackend("get-payback-reserves", { }); } @@ -166,41 +167,40 @@ export function payback(coinPub: string): Promise { } /** - * Get an offer stored in the wallet by its offer id. - * Note that the numeric offer id is not to be confused with - * the string order_id from the contract terms. + * Get a proposal stored in the wallet by its proposal id. */ -export function getOffer(offerId: number) { - return callBackend("get-offer", { offerId }); +export function getProposal(proposalId: number) { + return callBackend("get-proposal", { proposalId }); } /** * Check if payment is possible or already done. */ -export function checkPay(offer: OfferRecord): Promise { - return callBackend("check-pay", { offer }); +export function checkPay(proposalId: number): Promise { + return callBackend("check-pay", { proposalId }); } /** - * Pay for an offer. + * Pay for a proposal. */ -export function confirmPay(offer: OfferRecord): Promise { - return callBackend("confirm-pay", { offer }); +export function confirmPay(proposalId: number): Promise { + return callBackend("confirm-pay", { proposalId }); } /** * Hash a contract. Throws if its not a valid contract. */ export function hashContract(contract: object): Promise { - return callBackend("confirm-pay", { contract }); + return callBackend("hash-contract", { contract }); } + /** - * Save an offer in the wallet. Returns the offer id that - * the offer is stored under. + * Save a proposal in the wallet. Returns the proposal id that + * the proposal is stored under. */ -export function saveOffer(offer: object): Promise { - return callBackend("save-offer", { offer }); +export function saveProposal(proposal: any): Promise { + return callBackend("save-proposal", proposal); } /** @@ -243,7 +243,7 @@ export function paymentFailed(contractTermsHash: string): Promise { * cookie was set. */ export function getTabCookie(contractTermsHash: string, merchantSig: string): Promise { - return callBackend("get-tab-cookie"); + return callBackend("get-tab-cookie", { }); } /** @@ -251,5 +251,5 @@ export function getTabCookie(contractTermsHash: string, merchantSig: string): Pr * database and return the public key. */ export function generateNonce(): Promise { - return callBackend("generate-nonce"); + return callBackend("generate-nonce", { }); } diff --git a/src/webex/wxBackend.ts b/src/webex/wxBackend.ts index 816ce0251..356b2af6e 100644 --- a/src/webex/wxBackend.ts +++ b/src/webex/wxBackend.ts @@ -24,7 +24,6 @@ /** * Imports. */ -import { Checkable } from "../checkable"; import { BrowserHttpLib } from "../http"; import * as logging from "../logging"; import { @@ -34,7 +33,7 @@ import { import { AmountJson, Notifier, - OfferRecord, + ProposalRecord, } from "../types"; import { ConfirmReserveRequest, @@ -44,6 +43,7 @@ import { } from "../wallet"; import { ChromeBadge } from "./chromeBadge"; +import { MessageType } from "./messages"; import URI = require("urijs"); import Port = chrome.runtime.Port; @@ -60,21 +60,23 @@ const DB_NAME = "taler"; */ const DB_VERSION = 17; -type Handler = (detail: any, sender: MessageSender) => Promise; - -function makeHandlers(db: IDBDatabase, - wallet: Wallet): { [msg: string]: Handler } { - return { - ["balances"]: (detail, sender) => { +function handleMessage(db: IDBDatabase, + wallet: Wallet, + sender: MessageSender, + type: MessageType, detail: any): any { + function assertNotFound(t: never): never { + console.error(`Request type ${t as string} unknown`); + console.error(`Request detail was ${detail}`); + return { error: "request unknown", requestType: type } as never; + } + switch (type) { + case "balances": return wallet.getBalances(); - }, - ["dump-db"]: (detail, sender) => { + case "dump-db": return exportDb(db); - }, - ["import-db"]: (detail, sender) => { + case "import-db": return importDb(db, detail.dump); - }, - ["get-tab-cookie"]: (detail, sender) => { + case "get-tab-cookie": if (!sender || !sender.tab || !sender.tab.id) { return Promise.resolve(); } @@ -82,11 +84,9 @@ function makeHandlers(db: IDBDatabase, const info: any = paymentRequestCookies[id] as any; delete paymentRequestCookies[id]; return Promise.resolve(info); - }, - ["ping"]: (detail, sender) => { + case "ping": return Promise.resolve(); - }, - ["reset"]: (detail, sender) => { + case "reset": if (db) { const tx = db.transaction(Array.from(db.objectStoreNames), "readwrite"); // tslint:disable-next-line:prefer-for-of @@ -95,69 +95,36 @@ function makeHandlers(db: IDBDatabase, } } deleteDb(); - chrome.browserAction.setBadgeText({ text: "" }); console.log("reset done"); - // Response is synchronous return Promise.resolve({}); - }, - ["create-reserve"]: (detail, sender) => { + case "create-reserve": { const d = { amount: detail.amount, exchange: detail.exchange, }; const req = CreateReserveRequest.checked(d); return wallet.createReserve(req); - }, - ["confirm-reserve"]: (detail, sender) => { - // TODO: make it a checkable + } + case "confirm-reserve": const d = { reservePub: detail.reservePub, }; const req = ConfirmReserveRequest.checked(d); return wallet.confirmReserve(req); - }, - ["generate-nonce"]: (detail, sender) => { + case "generate-nonce": return wallet.generateNonce(); - }, - ["confirm-pay"]: (detail, sender) => { - let offer: OfferRecord; - try { - offer = OfferRecord.checked(detail.offer); - } catch (e) { - if (e instanceof Checkable.SchemaError) { - console.error("schema error:", e.message); - return Promise.resolve({ - detail, - error: "invalid contract", - hint: e.message, - }); - } else { - throw e; - } + case "confirm-pay": + if (typeof detail.proposalId !== "number") { + throw Error("proposalId must be number"); } - - return wallet.confirmPay(offer); - }, - ["check-pay"]: (detail, sender) => { - let offer: OfferRecord; - try { - offer = OfferRecord.checked(detail.offer); - } catch (e) { - if (e instanceof Checkable.SchemaError) { - console.error("schema error:", e.message); - return Promise.resolve({ - detail, - error: "invalid contract", - hint: e.message, - }); - } else { - throw e; - } + return wallet.confirmPay(detail.proposalId); + case "check-pay": + if (typeof detail.proposalId !== "number") { + throw Error("proposalId must be number"); } - return wallet.checkPay(offer); - }, - ["query-payment"]: (detail: any, sender: MessageSender) => { + return wallet.checkPay(detail.proposalId); + case "query-payment": if (sender.tab && sender.tab.id) { rateLimitCache[sender.tab.id]++; if (rateLimitCache[sender.tab.id] > 10) { @@ -171,120 +138,98 @@ function makeHandlers(db: IDBDatabase, } } return wallet.queryPayment(detail.url); - }, - ["exchange-info"]: (detail) => { + case "exchange-info": if (!detail.baseUrl) { return Promise.resolve({ error: "bad url" }); } return wallet.updateExchangeFromUrl(detail.baseUrl); - }, - ["currency-info"]: (detail) => { + case "currency-info": if (!detail.name) { return Promise.resolve({ error: "name missing" }); } return wallet.getCurrencyRecord(detail.name); - }, - ["hash-contract"]: (detail) => { + case "hash-contract": if (!detail.contract) { return Promise.resolve({ error: "contract missing" }); } return wallet.hashContract(detail.contract).then((hash) => { - return { hash }; + return hash; }); - }, - ["put-history-entry"]: (detail: any) => { + case "put-history-entry": if (!detail.historyEntry) { return Promise.resolve({ error: "historyEntry missing" }); } return wallet.putHistory(detail.historyEntry); - }, - ["save-offer"]: (detail: any) => { - const offer = detail.offer; - if (!offer) { - return Promise.resolve({ error: "offer missing" }); - } - console.log("handling safe-offer", detail); - // FIXME: fully migrate to new terminology - const checkedOffer = OfferRecord.checked(offer); - return wallet.saveOffer(checkedOffer); - }, - ["reserve-creation-info"]: (detail, sender) => { + case "save-proposal": + console.log("handling save-proposal", detail); + const checkedRecord = ProposalRecord.checked({ + contractTerms: detail.data, + contractTermsHash: detail.hash, + merchantSig: detail.sig, + }); + return wallet.saveProposal(checkedRecord); + case "reserve-creation-info": if (!detail.baseUrl || typeof detail.baseUrl !== "string") { return Promise.resolve({ error: "bad url" }); } const amount = AmountJson.checked(detail.amount); return wallet.getReserveCreationInfo(detail.baseUrl, amount); - }, - ["get-history"]: (detail, sender) => { + case "get-history": // TODO: limit history length return wallet.getHistory(); - }, - ["get-offer"]: (detail, sender) => { - return wallet.getOffer(detail.offerId); - }, - ["get-exchanges"]: (detail, sender) => { + case "get-proposal": + return wallet.getProposal(detail.proposalId); + case "get-exchanges": return wallet.getExchanges(); - }, - ["get-currencies"]: (detail, sender) => { + case "get-currencies": return wallet.getCurrencies(); - }, - ["update-currency"]: (detail, sender) => { + case "update-currency": return wallet.updateCurrency(detail.currencyRecord); - }, - ["get-reserves"]: (detail, sender) => { + case "get-reserves": if (typeof detail.exchangeBaseUrl !== "string") { return Promise.reject(Error("exchangeBaseUrl missing")); } return wallet.getReserves(detail.exchangeBaseUrl); - }, - ["get-payback-reserves"]: (detail, sender) => { + case "get-payback-reserves": return wallet.getPaybackReserves(); - }, - ["withdraw-payback-reserve"]: (detail, sender) => { + case "withdraw-payback-reserve": if (typeof detail.reservePub !== "string") { return Promise.reject(Error("reservePub missing")); } return wallet.withdrawPaybackReserve(detail.reservePub); - }, - ["get-coins"]: (detail, sender) => { + case "get-coins": if (typeof detail.exchangeBaseUrl !== "string") { return Promise.reject(Error("exchangBaseUrl missing")); } return wallet.getCoins(detail.exchangeBaseUrl); - }, - ["get-precoins"]: (detail, sender) => { + case "get-precoins": if (typeof detail.exchangeBaseUrl !== "string") { return Promise.reject(Error("exchangBaseUrl missing")); } return wallet.getPreCoins(detail.exchangeBaseUrl); - }, - ["get-denoms"]: (detail, sender) => { + case "get-denoms": if (typeof detail.exchangeBaseUrl !== "string") { return Promise.reject(Error("exchangBaseUrl missing")); } return wallet.getDenoms(detail.exchangeBaseUrl); - }, - ["refresh-coin"]: (detail, sender) => { + case "refresh-coin": if (typeof detail.coinPub !== "string") { return Promise.reject(Error("coinPub missing")); } return wallet.refresh(detail.coinPub); - }, - ["payback-coin"]: (detail, sender) => { + case "payback-coin": if (typeof detail.coinPub !== "string") { return Promise.reject(Error("coinPub missing")); } return wallet.payback(detail.coinPub); - }, - ["payment-failed"]: (detail, sender) => { + case "payment-failed": // For now we just update exchanges (maybe the exchange did something // wrong and the keys were messed up). // FIXME: in the future we should look at what actually went wrong. console.error("payment reported as failed"); wallet.updateExchanges(); return Promise.resolve(); - }, - ["payment-succeeded"]: (detail, sender) => { + case "payment-succeeded": const contractTermsHash = detail.contractTermsHash; const merchantSig = detail.merchantSig; if (!contractTermsHash) { @@ -294,24 +239,16 @@ function makeHandlers(db: IDBDatabase, return Promise.reject(Error("merchantSig missing")); } return wallet.paymentSucceeded(contractTermsHash, merchantSig); - }, - }; -} - - -async function dispatch(handlers: any, req: any, sender: any, sendResponse: any): Promise { - if (!(req.type in handlers)) { - console.error(`Request type ${req.type} unknown`); - console.error(`Request was ${req}`); - try { - sendResponse({ error: "request unknown", requestType: req.type }); - } catch (e) { - // might fail if tab disconnected - } + default: + // Exhaustiveness check. + // See https://www.typescriptlang.org/docs/handbook/advanced-types.html + return assertNotFound(type); } +} +async function dispatch(db: IDBDatabase, wallet: Wallet, req: any, sender: any, sendResponse: any): Promise { try { - const p = handlers[req.type](req.detail, sender); + const p = handleMessage(db, wallet, sender, req.type, req.detail); const r = await p; try { sendResponse(r); @@ -587,9 +524,8 @@ export async function wxMain() { // Handlers for messages coming directly from the content // script on the page - const handlers = makeHandlers(db, wallet!); chrome.runtime.onMessage.addListener((req, sender, sendResponse) => { - dispatch(handlers, req, sender, sendResponse); + dispatch(db, wallet, req, sender, sendResponse); return true; }); -- cgit v1.2.3