implement new protocol / naming
This commit is contained in:
parent
dd5b679791
commit
08d4a5b625
@ -90,6 +90,19 @@ namespace TalerNotify {
|
||||
});
|
||||
}
|
||||
|
||||
function queryPayment(query: any): Promise<any> {
|
||||
// current URL without fragment
|
||||
const walletMsg = {
|
||||
type: "query-payment",
|
||||
detail: query,
|
||||
};
|
||||
return new Promise((resolve, reject) => {
|
||||
chrome.runtime.sendMessage(walletMsg, (resp: any) => {
|
||||
resolve(resp);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function putHistory(historyEntry: any): Promise<void> {
|
||||
const walletMsg = {
|
||||
type: "put-history-entry",
|
||||
@ -109,16 +122,20 @@ namespace TalerNotify {
|
||||
type: "save-offer",
|
||||
detail: {
|
||||
offer: {
|
||||
contract: offer.contract,
|
||||
merchant_sig: offer.merchant_sig,
|
||||
H_contract: offer.H_contract,
|
||||
contract: offer.data,
|
||||
merchant_sig: offer.sig,
|
||||
H_contract: offer.hash,
|
||||
offer_time: new Date().getTime() / 1000
|
||||
},
|
||||
},
|
||||
};
|
||||
return new Promise((resolve, reject) => {
|
||||
chrome.runtime.sendMessage(walletMsg, (resp: any) => {
|
||||
resolve(resp);
|
||||
if (resp && resp.error) {
|
||||
reject(resp);
|
||||
} else {
|
||||
resolve(resp);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -141,17 +158,10 @@ namespace TalerNotify {
|
||||
}
|
||||
});
|
||||
|
||||
if (resp && resp.type === "fetch") {
|
||||
logVerbose && console.log("it's fetch");
|
||||
taler.internalOfferContractFrom(resp.contractUrl);
|
||||
if (resp && resp.type == "pay") {
|
||||
logVerbose && console.log("doing taler.pay with", resp.payDetail);
|
||||
taler.internalPay(resp.payDetail);
|
||||
document.documentElement.style.visibility = "hidden";
|
||||
|
||||
} else if (resp && resp.type === "execute") {
|
||||
logVerbose && console.log("it's execute");
|
||||
document.documentElement.style.visibility = "hidden";
|
||||
taler.internalExecutePayment(resp.contractHash,
|
||||
resp.payUrl,
|
||||
resp.offerUrl);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -163,6 +173,104 @@ namespace TalerNotify {
|
||||
(detail: any, sendResponse: (msg: any) => void): void;
|
||||
}
|
||||
|
||||
function downloadContract(url: string): Promise<any> {
|
||||
// FIXME: include and check nonce!
|
||||
return new Promise((resolve, reject) => {
|
||||
const contract_request = new XMLHttpRequest();
|
||||
console.log("downloading contract from '" + url + "'")
|
||||
contract_request.open("GET", url, true);
|
||||
contract_request.onload = function (e) {
|
||||
if (contract_request.readyState == 4) {
|
||||
if (contract_request.status == 200) {
|
||||
console.log("response text:",
|
||||
contract_request.responseText);
|
||||
var contract_wrapper = JSON.parse(contract_request.responseText);
|
||||
if (!contract_wrapper) {
|
||||
console.error("response text was invalid json");
|
||||
let detail = {hint: "invalid json", status: contract_request.status, body: contract_request.responseText};
|
||||
reject(detail);
|
||||
return;
|
||||
}
|
||||
resolve(contract_wrapper);
|
||||
} else {
|
||||
let detail = {hint: "contract download failed", status: contract_request.status, body: contract_request.responseText};
|
||||
reject(detail);
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
contract_request.onerror = function (e) {
|
||||
let detail = {hint: "contract download failed", status: contract_request.status, body: contract_request.responseText};
|
||||
reject(detail);
|
||||
return;
|
||||
};
|
||||
contract_request.send();
|
||||
});
|
||||
}
|
||||
|
||||
async function processProposal(proposal: any) {
|
||||
if (!proposal.data) {
|
||||
console.error("field proposal.data field missing");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!proposal.hash) {
|
||||
console.error("proposal.hash field missing");
|
||||
return;
|
||||
}
|
||||
|
||||
let contractHash = await hashContract(proposal.data);
|
||||
|
||||
if (contractHash != proposal.hash) {
|
||||
console.error("merchant-supplied contract hash is wrong");
|
||||
return;
|
||||
}
|
||||
|
||||
let resp = await checkRepurchase(proposal.data);
|
||||
|
||||
if (resp.error) {
|
||||
console.error("wallet backend error", resp);
|
||||
return;
|
||||
}
|
||||
|
||||
if (resp.isRepurchase) {
|
||||
logVerbose && console.log("doing repurchase");
|
||||
console.assert(resp.existingFulfillmentUrl);
|
||||
console.assert(resp.existingContractHash);
|
||||
window.location.href = subst(resp.existingFulfillmentUrl,
|
||||
resp.existingContractHash);
|
||||
|
||||
} else {
|
||||
|
||||
let merchantName = "(unknown)";
|
||||
try {
|
||||
merchantName = proposal.data.merchant.name;
|
||||
} catch (e) {
|
||||
// bad contract / name not included
|
||||
}
|
||||
|
||||
let historyEntry = {
|
||||
timestamp: (new Date).getTime(),
|
||||
subjectId: `contract-${contractHash}`,
|
||||
type: "offer-contract",
|
||||
detail: {
|
||||
contractHash,
|
||||
merchantName,
|
||||
}
|
||||
};
|
||||
await putHistory(historyEntry);
|
||||
let offerId = await saveOffer(proposal);
|
||||
|
||||
const uri = URI(chrome.extension.getURL(
|
||||
"/src/pages/confirm-contract.html"));
|
||||
const params = {
|
||||
offerId: offerId.toString(),
|
||||
};
|
||||
const target = uri.query(params).href();
|
||||
document.location.replace(target);
|
||||
}
|
||||
}
|
||||
|
||||
function registerHandlers() {
|
||||
/**
|
||||
* Add a handler for a DOM event, which automatically
|
||||
@ -237,70 +345,28 @@ namespace TalerNotify {
|
||||
|
||||
const proposal = msg.contract_wrapper;
|
||||
|
||||
if (!proposal.data) {
|
||||
console.error("field proposal.data field missing");
|
||||
processProposal(proposal);
|
||||
});
|
||||
|
||||
addHandler("taler-pay", async(msg: any, sendResponse: any) => {
|
||||
let res = await queryPayment(msg.contract_query);
|
||||
logVerbose && console.log("taler-pay: got response", res);
|
||||
if (res && res.payReq) {
|
||||
sendResponse(res);
|
||||
return;
|
||||
}
|
||||
if (msg.contract_url) {
|
||||
let proposal = await downloadContract(msg.contract_url);
|
||||
await processProposal(proposal);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!proposal.hash) {
|
||||
console.error("proposal.hash field missing");
|
||||
if (msg.offer_url) {
|
||||
document.location.href = msg.offer_url;
|
||||
return;
|
||||
}
|
||||
|
||||
let contractHash = await hashContract(proposal.data);
|
||||
|
||||
if (contractHash != proposal.hash) {
|
||||
console.error("merchant-supplied contract hash is wrong");
|
||||
return;
|
||||
}
|
||||
|
||||
let resp = await checkRepurchase(proposal.data);
|
||||
|
||||
if (resp.error) {
|
||||
console.error("wallet backend error", resp);
|
||||
return;
|
||||
}
|
||||
|
||||
if (resp.isRepurchase) {
|
||||
logVerbose && console.log("doing repurchase");
|
||||
console.assert(resp.existingFulfillmentUrl);
|
||||
console.assert(resp.existingContractHash);
|
||||
window.location.href = subst(resp.existingFulfillmentUrl,
|
||||
resp.existingContractHash);
|
||||
|
||||
} else {
|
||||
|
||||
let merchantName = "(unknown)";
|
||||
try {
|
||||
merchantName = proposal.data.merchant.name;
|
||||
} catch (e) {
|
||||
// bad contract / name not included
|
||||
}
|
||||
|
||||
let historyEntry = {
|
||||
timestamp: (new Date).getTime(),
|
||||
subjectId: `contract-${contractHash}`,
|
||||
type: "offer-contract",
|
||||
detail: {
|
||||
contractHash,
|
||||
merchantName,
|
||||
}
|
||||
};
|
||||
await putHistory(historyEntry);
|
||||
let offerId = await saveOffer(proposal);
|
||||
|
||||
const uri = URI(chrome.extension.getURL(
|
||||
"/src/pages/confirm-contract.html"));
|
||||
const params = {
|
||||
offerId: offerId.toString(),
|
||||
};
|
||||
const target = uri.query(params).href();
|
||||
if (msg.replace_navigation === true) {
|
||||
document.location.replace(target);
|
||||
} else {
|
||||
document.location.href = target;
|
||||
}
|
||||
}
|
||||
console.log("can't proceed with payment, no way to get contract specified");
|
||||
});
|
||||
|
||||
addHandler("taler-payment-failed", (msg: any, sendResponse: any) => {
|
||||
@ -331,41 +397,5 @@ namespace TalerNotify {
|
||||
sendResponse();
|
||||
})
|
||||
});
|
||||
|
||||
addHandler("taler-get-payment", (msg: any, sendResponse: any) => {
|
||||
const walletMsg = {
|
||||
type: "execute-payment",
|
||||
detail: {
|
||||
H_contract: msg.H_contract,
|
||||
},
|
||||
};
|
||||
|
||||
chrome.runtime.sendMessage(walletMsg, (resp) => {
|
||||
if (resp.rateLimitExceeded) {
|
||||
console.error("rate limit exceeded, check for redirect loops");
|
||||
}
|
||||
|
||||
if (!resp.success) {
|
||||
if (msg.offering_url) {
|
||||
window.location.href = msg.offering_url;
|
||||
} else {
|
||||
console.error("execute-payment failed", resp);
|
||||
}
|
||||
return;
|
||||
}
|
||||
let contract = resp.contract;
|
||||
if (!contract) {
|
||||
throw Error("contract missing");
|
||||
}
|
||||
|
||||
// We have the details for then payment, the merchant page
|
||||
// is responsible to give it to the merchant.
|
||||
sendResponse({
|
||||
H_contract: msg.H_contract,
|
||||
contract: resp.contract,
|
||||
payment: resp.payReq,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -30,7 +30,12 @@ export function prettyAmount(amount: AmountJson) {
|
||||
}
|
||||
|
||||
export function renderContract(contract: Contract): JSX.Element {
|
||||
let merchantName = <strong>{contract.merchant.name}</strong>;
|
||||
let merchantName;
|
||||
if (contract.merchant && contract.merchant.name) {
|
||||
merchantName = <strong>{contract.merchant.name}</strong>;
|
||||
} else {
|
||||
merchantName = <strong>(pub: {contract.merchant_pub})</strong>;
|
||||
}
|
||||
let amount = <strong>{prettyAmount(contract.amount)}</strong>;
|
||||
|
||||
return (
|
||||
|
@ -164,20 +164,10 @@ export interface HistoryRecord {
|
||||
|
||||
|
||||
interface PayReq {
|
||||
amount: AmountJson;
|
||||
coins: CoinPaySig[];
|
||||
H_contract: string;
|
||||
max_fee: AmountJson;
|
||||
merchant_sig: string;
|
||||
merchant_pub: string;
|
||||
order_id: string;
|
||||
exchange: string;
|
||||
refund_deadline: string;
|
||||
timestamp: string;
|
||||
pay_deadline: string;
|
||||
/**
|
||||
* Merchant instance identifier that should receive the
|
||||
* payment, if applicable.
|
||||
*/
|
||||
instance?: string;
|
||||
}
|
||||
|
||||
interface TransactionRecord {
|
||||
@ -352,6 +342,8 @@ export namespace Stores {
|
||||
"contract.merchant_pub",
|
||||
"contract.repurchase_correlation_id"
|
||||
]);
|
||||
fulfillmentUrlIndex = new Index<string,TransactionRecord>(this, "fulfillment_url", "contract.fulfillment_url");
|
||||
orderIdIndex = new Index<string,TransactionRecord>(this, "order_id", "contract.order_id");
|
||||
}
|
||||
|
||||
class DenominationsStore extends Store<DenominationRecord> {
|
||||
@ -552,16 +544,10 @@ export class Wallet {
|
||||
payCoinInfo: PayCoinInfo,
|
||||
chosenExchange: string): Promise<void> {
|
||||
let payReq: PayReq = {
|
||||
amount: offer.contract.amount,
|
||||
coins: payCoinInfo.map((x) => x.sig),
|
||||
H_contract: offer.H_contract,
|
||||
max_fee: offer.contract.max_fee,
|
||||
merchant_sig: offer.merchant_sig,
|
||||
exchange: URI(chosenExchange).href(),
|
||||
refund_deadline: offer.contract.refund_deadline,
|
||||
pay_deadline: offer.contract.pay_deadline,
|
||||
timestamp: offer.contract.timestamp,
|
||||
instance: offer.contract.merchant.instance
|
||||
merchant_pub: offer.contract.merchant_pub,
|
||||
order_id: offer.contract.order_id,
|
||||
exchange: chosenExchange,
|
||||
};
|
||||
let t: TransactionRecord = {
|
||||
contractHash: offer.H_contract,
|
||||
@ -679,18 +665,36 @@ export class Wallet {
|
||||
* Retrieve all necessary information for looking up the contract
|
||||
* with the given hash.
|
||||
*/
|
||||
async executePayment(H_contract: string): Promise<any> {
|
||||
let t = await this.q().get<TransactionRecord>(Stores.transactions,
|
||||
H_contract);
|
||||
async queryPayment(query: any): Promise<any> {
|
||||
let t: TransactionRecord | undefined;
|
||||
|
||||
console.log("query for payment", query);
|
||||
|
||||
switch (query.type) {
|
||||
case "fulfillment_url":
|
||||
t = await this.q().getIndexed(Stores.transactions.fulfillmentUrlIndex, query.value);
|
||||
break;
|
||||
case "order_id":
|
||||
t = await this.q().getIndexed(Stores.transactions.orderIdIndex, query.value);
|
||||
break;
|
||||
case "hash":
|
||||
t = await this.q().get<TransactionRecord>(Stores.transactions, query.value);
|
||||
break;
|
||||
default:
|
||||
throw Error("invalid type");
|
||||
}
|
||||
|
||||
if (!t) {
|
||||
console.log("query for payment failed");
|
||||
return {
|
||||
success: false,
|
||||
contractFound: false,
|
||||
}
|
||||
}
|
||||
console.log("query for payment succeeded:", t);
|
||||
let resp = {
|
||||
success: true,
|
||||
payReq: t.payReq,
|
||||
H_contract: t.contractHash,
|
||||
contract: t.contract,
|
||||
};
|
||||
return resp;
|
||||
|
@ -139,20 +139,20 @@ function makeHandlers(db: IDBDatabase,
|
||||
}
|
||||
return wallet.checkPay(offer);
|
||||
},
|
||||
["execute-payment"]: function (detail: any, sender: MessageSender) {
|
||||
["query-payment"]: function (detail: any, sender: MessageSender) {
|
||||
if (sender.tab && sender.tab.id) {
|
||||
rateLimitCache[sender.tab.id]++;
|
||||
if (rateLimitCache[sender.tab.id] > 10) {
|
||||
console.warn("rate limit for execute payment exceeded");
|
||||
console.warn("rate limit for query-payment exceeded");
|
||||
let msg = {
|
||||
error: "rate limit exceeded for execute-payment",
|
||||
error: "rate limit exceeded for query-payment",
|
||||
rateLimitExceeded: true,
|
||||
hint: "Check for redirect loops",
|
||||
};
|
||||
return Promise.resolve(msg);
|
||||
}
|
||||
}
|
||||
return wallet.executePayment(detail.H_contract);
|
||||
return wallet.queryPayment(detail);
|
||||
},
|
||||
["exchange-info"]: function (detail) {
|
||||
if (!detail.baseUrl) {
|
||||
@ -179,8 +179,10 @@ function makeHandlers(db: IDBDatabase,
|
||||
if (!offer) {
|
||||
return Promise.resolve({ error: "offer missing" });
|
||||
}
|
||||
console.log("handling safe-offer");
|
||||
return wallet.saveOffer(offer);
|
||||
console.log("handling safe-offer", detail);
|
||||
// FIXME: fully migrate to new terminology
|
||||
let checkedOffer = OfferRecord.checked(offer);
|
||||
return wallet.saveOffer(checkedOffer);
|
||||
},
|
||||
["reserve-creation-info"]: function (detail, sender) {
|
||||
if (!detail.baseUrl || typeof detail.baseUrl !== "string") {
|
||||
@ -317,8 +319,7 @@ class ChromeNotifier implements Notifier {
|
||||
*/
|
||||
let paymentRequestCookies: { [n: number]: any } = {};
|
||||
|
||||
function handleHttpPayment(headerList: chrome.webRequest.HttpHeader[],
|
||||
url: string, tabId: number): any {
|
||||
function handleHttpPayment(headerList: chrome.webRequest.HttpHeader[], url: string, tabId: number): any {
|
||||
const headers: { [s: string]: string } = {};
|
||||
for (let kv of headerList) {
|
||||
if (kv.value) {
|
||||
@ -326,35 +327,52 @@ function handleHttpPayment(headerList: chrome.webRequest.HttpHeader[],
|
||||
}
|
||||
}
|
||||
|
||||
const contractUrl = headers["x-taler-contract-url"];
|
||||
if (contractUrl !== undefined) {
|
||||
paymentRequestCookies[tabId] = { type: "fetch", contractUrl };
|
||||
return;
|
||||
let fields = {
|
||||
contract_url: headers["x-taler-contract-url"],
|
||||
contract_query: headers["x-taler-contract-query"],
|
||||
offer_url: headers["x-taler-offer-url"],
|
||||
pay_url: headers["x-taler-pay-url"],
|
||||
}
|
||||
|
||||
const contractHash = headers["x-taler-contract-hash"];
|
||||
let n: number = 0;
|
||||
|
||||
if (contractHash !== undefined) {
|
||||
const payUrl = headers["x-taler-pay-url"];
|
||||
if (payUrl === undefined) {
|
||||
console.log("malformed 402, X-Taler-Pay-Url missing");
|
||||
return;
|
||||
for (let key of Object.keys(fields)) {
|
||||
if ((fields as any)[key]) {
|
||||
n++;
|
||||
}
|
||||
|
||||
// Offer URL is optional
|
||||
const offerUrl = headers["x-taler-offer-url"];
|
||||
paymentRequestCookies[tabId] = {
|
||||
type: "execute",
|
||||
offerUrl,
|
||||
payUrl,
|
||||
contractHash
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
// looks like it's not a taler request, it might be
|
||||
// for a different payment system (or the shop is buggy)
|
||||
console.log("ignoring non-taler 402 response");
|
||||
if (n == 0) {
|
||||
// looks like it's not a taler request, it might be
|
||||
// for a different payment system (or the shop is buggy)
|
||||
console.log("ignoring non-taler 402 response");
|
||||
}
|
||||
|
||||
let contract_query = undefined;
|
||||
// parse " type [ ':' value ] " format
|
||||
if (fields.contract_query) {
|
||||
let res = /[-a-zA-Z0-9_.,]+(:.*)?/.exec(fields.contract_query);
|
||||
if (res) {
|
||||
contract_query = {type: res[0], value: res[1]};
|
||||
if (contract_query.type == "fulfillment_url" && !contract_query.value) {
|
||||
contract_query.value = url;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let payDetail = {
|
||||
contract_query,
|
||||
contract_url: fields.contract_url,
|
||||
offer_url: fields.offer_url,
|
||||
pay_url: fields.pay_url,
|
||||
};
|
||||
|
||||
console.log("got pay detail", payDetail)
|
||||
|
||||
paymentRequestCookies[tabId] = {
|
||||
type: "pay",
|
||||
payDetail,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
@ -1 +1 @@
|
||||
Subproject commit d4de1c912ecaac7991067027b352de61b237c0c9
|
||||
Subproject commit 4831e664d69759da288625911c053d145aa1b68c
|
Loading…
Reference in New Issue
Block a user