respond to failed payments

This commit is contained in:
Florian Dold 2016-05-24 17:30:27 +02:00
parent ce7f7b8321
commit 27a42f9257
5 changed files with 244 additions and 124 deletions

View File

@ -165,9 +165,22 @@ namespace TalerNotify {
}); });
}); });
addHandler("taler-payment-failed", (e: CustomEvent) => {
const msg = {
type: "payment-failed",
detail: {},
};
chrome.runtime.sendMessage(msg, (resp) => {
let evt = new CustomEvent("taler-payment-failed-ok", {
detail: {}
});
document.dispatchEvent(evt);
});
});
// Should be: taler-request-payment, taler-result-payment // Should be: taler-request-payment, taler-result-payment
addHandler("taler-execute-contract", function(e: CustomEvent) { addHandler("taler-execute-contract", (e: CustomEvent) => {
console.log("got taler-execute-contract in content page"); console.log("got taler-execute-contract in content page");
const msg = { const msg = {
type: "execute-payment", type: "execute-payment",
@ -195,6 +208,9 @@ namespace TalerNotify {
throw Error("contract missing"); throw Error("contract missing");
} }
// We have the details for then payment, the merchant page
// is responsible to give it to the merchant.
let evt = new CustomEvent("taler-notify-payment", { let evt = new CustomEvent("taler-notify-payment", {
detail: { detail: {
H_contract: e.detail.H_contract, H_contract: e.detail.H_contract,

View File

@ -25,7 +25,7 @@
*/ */
const DB_NAME = "taler"; const DB_NAME = "taler";
const DB_VERSION = 5; const DB_VERSION = 6;
/** /**
* Return a promise that resolves * Return a promise that resolves
@ -41,7 +41,7 @@ export function openTalerDb(): Promise<IDBDatabase> {
resolve(req.result); resolve(req.result);
}; };
req.onupgradeneeded = (e) => { req.onupgradeneeded = (e) => {
let db = req.result; const db = req.result;
console.log("DB: upgrade needed: oldVersion = " + e.oldVersion); console.log("DB: upgrade needed: oldVersion = " + e.oldVersion);
switch (e.oldVersion) { switch (e.oldVersion) {
case 0: // DB does not exist yet case 0: // DB does not exist yet
@ -49,7 +49,6 @@ export function openTalerDb(): Promise<IDBDatabase> {
{keyPath: "baseUrl"}); {keyPath: "baseUrl"});
exchanges.createIndex("pubKey", "masterPublicKey"); exchanges.createIndex("pubKey", "masterPublicKey");
db.createObjectStore("reserves", {keyPath: "reserve_pub"}); db.createObjectStore("reserves", {keyPath: "reserve_pub"});
db.createObjectStore("denoms", {keyPath: "denomPub"});
const coins = db.createObjectStore("coins", {keyPath: "coinPub"}); const coins = db.createObjectStore("coins", {keyPath: "coinPub"});
coins.createIndex("exchangeBaseUrl", "exchangeBaseUrl"); coins.createIndex("exchangeBaseUrl", "exchangeBaseUrl");
const transactions = db.createObjectStore("transactions", const transactions = db.createObjectStore("transactions",

View File

@ -93,9 +93,6 @@ export class Denomination {
@Checkable.String @Checkable.String
master_sig: string; master_sig: string;
@Checkable.Optional(Checkable.String)
pub_hash: string;
static checked: (obj: any) => Denomination; static checked: (obj: any) => Denomination;
} }
@ -103,7 +100,23 @@ export class Denomination {
export interface IExchangeInfo { export interface IExchangeInfo {
baseUrl: string; baseUrl: string;
masterPublicKey: string; masterPublicKey: string;
denoms: Denomination[];
/**
* All denominations we ever received from the exchange.
* Expired denominations may be garbage collected.
*/
all_denoms: Denomination[];
/**
* Denominations we received with the last update.
* Subset of "denoms".
*/
active_denoms: Denomination[];
/**
* Timestamp for last update.
*/
last_update_time: number;
} }
export interface WireInfo { export interface WireInfo {
@ -151,13 +164,48 @@ export interface CoinPaySig {
} }
/**
* Coin as stored in the "coins" data store
* of the wallet database.
*/
export interface Coin { export interface Coin {
/**
* Public key of the coin.
*/
coinPub: string; coinPub: string;
/**
* Private key to authorize operations on the coin.
*/
coinPriv: string; coinPriv: string;
/**
* Key used by the exchange used to sign the coin.
*/
denomPub: string; denomPub: string;
/**
* Unblinded signature by the exchange.
*/
denomSig: string; denomSig: string;
/**
* Amount that's left on the coin.
*/
currentAmount: AmountJson; currentAmount: AmountJson;
/**
* Base URL that identifies the exchange from which we got the
* coin.
*/
exchangeBaseUrl: string; exchangeBaseUrl: string;
/**
* We have withdrawn the coin, but it's not accepted by the exchange anymore.
* We have to tell an auditor and wait for compensation or for the exchange
* to fix it.
*/
suspended?: boolean;
} }

View File

@ -79,91 +79,6 @@ export class KeysJson {
} }
class ExchangeInfo implements IExchangeInfo {
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): ExchangeInfo {
return new ExchangeInfo({baseUrl});
}
/**
* Merge new key information into the exchange info.
* If the new key information is invalid (missing fields,
* invalid signatures), an exception is thrown, but the
* exchange info is updated with the new information up until
* the first error.
*/
mergeKeys(newKeys: KeysJson, cryptoApi: CryptoApi): Promise<void> {
if (!this.masterPublicKey) {
this.masterPublicKey = newKeys.master_public_key;
}
if (this.masterPublicKey != newKeys.master_public_key) {
throw Error("public keys do not match");
}
let ps = newKeys.denoms.map((newDenom) => {
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 (!deepEquals(a, b)) {
console.log("old/new:");
console.dir(a);
console.dir(b);
throw Error("denomination modified");
}
found = true;
break;
}
}
if (found) {
return Promise.resolve();
}
return cryptoApi
.isValidDenom(newDenom, this.masterPublicKey)
.then((valid) => {
if (!valid) {
console.error("invalid denomination",
newDenom,
"with key",
this.masterPublicKey);
throw Error("signature on denomination invalid");
}
return cryptoApi.hashRsaPub(newDenom.denom_pub);
})
.then((h) => {
this.denoms.push(Object.assign({}, newDenom, {pub_hash: h}));
});
});
return Promise.all(ps).then(() => void 0);
}
}
@Checkable.Class @Checkable.Class
export class CreateReserveRequest { export class CreateReserveRequest {
/** /**
@ -264,7 +179,7 @@ function flatMap<T, U>(xs: T[], f: (x: T) => U[]): U[] {
} }
function getTalerStampSec(stamp: string) { function getTalerStampSec(stamp: string): number {
const m = stamp.match(/\/?Date\(([0-9]*)\)\/?/); const m = stamp.match(/\/?Date\(([0-9]*)\)\/?/);
if (!m) { if (!m) {
return null; return null;
@ -306,6 +221,16 @@ function copy(o) {
return JSON.parse(JSON.stringify(o)); return JSON.parse(JSON.stringify(o));
} }
/**
* Result of updating exisiting information
* about an exchange with a new '/keys' response.
*/
interface KeyUpdateInfo {
updatedExchangeInfo: IExchangeInfo;
addedDenominations: Denomination[];
removedDenominations: Denomination[];
}
/** /**
* Get a list of denominations (with repetitions possible) * Get a list of denominations (with repetitions possible)
@ -381,6 +306,19 @@ export class Wallet {
} }
} }
updateExchanges(): void {
console.log("updating exchanges");
Query(this.db)
.iter("exchanges")
.reduce((exchange: IExchangeInfo) => {
this.updateExchangeFromUrl(exchange.baseUrl)
.catch((e) => {
console.error("updating exchange failed", e);
});
});
}
/** /**
* Resume various pending operations that are pending * Resume various pending operations that are pending
* by looking at the database. * by looking at the database.
@ -421,12 +359,20 @@ export class Wallet {
let exchange: IExchangeInfo = mc[0]; let exchange: IExchangeInfo = mc[0];
console.log("got coin for exchange", url); console.log("got coin for exchange", url);
let coin: Coin = mc[1]; let coin: Coin = mc[1];
if (coin.suspended) {
console.log("skipping suspended coin",
coin.denomPub,
"from exchange",
exchange.baseUrl);
return;
}
let cd = { let cd = {
coin: coin, coin: coin,
denom: exchange.denoms.find((e) => e.denom_pub === coin.denomPub) denom: exchange.active_denoms.find((e) => e.denom_pub === coin.denomPub)
}; };
if (!cd.denom) { if (!cd.denom) {
throw Error("denom not found (database inconsistent)"); console.warn("denom not found (database inconsistent)");
return;
} }
if (cd.denom.value.currency !== paymentAmount.currency) { if (cd.denom.value.currency !== paymentAmount.currency) {
console.warn("same pubkey for different currencies"); console.warn("same pubkey for different currencies");
@ -588,7 +534,9 @@ export class Wallet {
let exchangeUrl = Object.keys(mcs)[0]; let exchangeUrl = Object.keys(mcs)[0];
return this.cryptoApi.signDeposit(offer, mcs[exchangeUrl]) return this.cryptoApi.signDeposit(offer, mcs[exchangeUrl])
.then((ds) => this.recordConfirmPay(offer, ds, exchangeUrl)) .then((ds) => this.recordConfirmPay(offer,
ds,
exchangeUrl))
.then(() => ({})); .then(() => ({}));
}); });
}); });
@ -856,8 +804,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, exchange: ExchangeInfo): Promise<void> { private depleteReserve(reserve, exchange: IExchangeInfo): Promise<void> {
let denomsAvailable: Denomination[] = copy(exchange.denoms); let denomsAvailable: Denomination[] = copy(exchange.active_denoms);
let denomsForWithdraw = getWithdrawDenomList(reserve.current_amount, let denomsForWithdraw = getWithdrawDenomList(reserve.current_amount,
denomsAvailable); denomsAvailable);
@ -877,7 +825,7 @@ export class Wallet {
* by quering the reserve's exchange. * by quering the reserve's exchange.
*/ */
private updateReserve(reservePub: string, private updateReserve(reservePub: string,
exchange: ExchangeInfo): Promise<Reserve> { exchange: IExchangeInfo): Promise<Reserve> {
return Query(this.db) return Query(this.db)
.get("reserves", reservePub) .get("reserves", reservePub)
.then((reserve) => { .then((reserve) => {
@ -935,7 +883,8 @@ export class Wallet {
amount: AmountJson): Promise<ReserveCreationInfo> { amount: AmountJson): Promise<ReserveCreationInfo> {
let p = this.updateExchangeFromUrl(baseUrl); let p = this.updateExchangeFromUrl(baseUrl);
return p.then((exchangeInfo: IExchangeInfo) => { return p.then((exchangeInfo: IExchangeInfo) => {
let selectedDenoms = getWithdrawDenomList(amount, exchangeInfo.denoms); let selectedDenoms = getWithdrawDenomList(amount,
exchangeInfo.active_denoms);
let acc = Amounts.getZero(amount.currency); let acc = Amounts.getZero(amount.currency);
for (let d of selectedDenoms) { for (let d of selectedDenoms) {
acc = Amounts.add(acc, d.fee_withdraw).amount; acc = Amounts.add(acc, d.fee_withdraw).amount;
@ -964,7 +913,7 @@ export class Wallet {
* Optionally link the reserve entry to the new or existing * Optionally link the reserve entry to the new or existing
* exchange entry in then DB. * exchange entry in then DB.
*/ */
updateExchangeFromUrl(baseUrl): Promise<ExchangeInfo> { updateExchangeFromUrl(baseUrl): Promise<IExchangeInfo> {
baseUrl = canonicalizeBaseUrl(baseUrl); 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) => {
@ -972,29 +921,126 @@ export class Wallet {
throw Error("/keys request failed"); throw Error("/keys request failed");
} }
let exchangeKeysJson = KeysJson.checked(JSON.parse(resp.responseText)); let exchangeKeysJson = KeysJson.checked(JSON.parse(resp.responseText));
return this.updateExchangeFromJson(baseUrl, exchangeKeysJson);
});
}
private updateExchangeFromJson(baseUrl: string,
exchangeKeysJson: KeysJson): Promise<IExchangeInfo> {
let updateTimeSec = getTalerStampSec(exchangeKeysJson.list_issue_date);
if (!updateTimeSec) {
throw Error("invalid update time");
}
return Query(this.db).get("exchanges", baseUrl).then((r) => { return Query(this.db).get("exchanges", baseUrl).then((r) => {
let exchangeInfo; let exchangeInfo: IExchangeInfo;
console.dir(r); console.dir(r);
if (!r) { if (!r) {
exchangeInfo = ExchangeInfo.fresh(baseUrl); exchangeInfo = {
baseUrl,
all_denoms: [],
active_denoms: [],
last_update_time: updateTimeSec,
masterPublicKey: exchangeKeysJson.master_public_key,
};
console.log("making fresh exchange"); console.log("making fresh exchange");
} else { } else {
exchangeInfo = new ExchangeInfo(r); if (updateTimeSec < r.last_update_time) {
console.log("using old exchange"); console.log("outdated /keys, not updating")
return Promise.resolve(r);
}
exchangeInfo = r;
console.log("updating old exchange");
} }
return exchangeInfo.mergeKeys(exchangeKeysJson, this.cryptoApi) return this.updateExchangeInfo(exchangeInfo, exchangeKeysJson)
.then(() => { .then((updatedExchangeInfo: IExchangeInfo) => {
return Query(this.db) let q1 = Query(this.db)
.put("exchanges", exchangeInfo) .put("exchanges", updatedExchangeInfo)
.finish() .finish()
.then(() => exchangeInfo); .then(() => updatedExchangeInfo);
let q2 = Query(this.db)
.iter("coins",
{indexName: "exchangeBaseUrl", only: baseUrl})
.reduce((coin: Coin, suspendedCoins: Coin[]) => {
if (!updatedExchangeInfo.active_denoms.find((c) => c.denom_pub == coin.denomPub)) {
return [].concat(suspendedCoins, [coin]);
}
return [].concat(suspendedCoins);
}, [])
.then((suspendedCoins: Coin[]) => {
let q = Query(this.db);
suspendedCoins.map((c) => {
console.log("suspending coin", c);
c.suspended = true;
q.put("coins", c);
});
return q.finish();
});
return Promise.all([q1, q2]).then(() => updatedExchangeInfo);
});
});
}
private updateExchangeInfo(exchangeInfo: IExchangeInfo,
newKeys: KeysJson): Promise<IExchangeInfo> {
if (exchangeInfo.masterPublicKey != newKeys.master_public_key) {
throw Error("public keys do not match");
}
exchangeInfo.active_denoms = [];
let ps = newKeys.denoms.map((newDenom) => {
// did we find the new denom in the list of all (old) denoms?
let found = false;
for (let oldDenom of exchangeInfo.all_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 (!deepEquals(a, b)) {
console.error("denomination parameters were modified, old/new:");
console.dir(a);
console.dir(b);
// FIXME: report to auditors
}
found = true;
break;
}
}
if (found) {
exchangeInfo.active_denoms.push(newDenom);
// No need to check signatures
return Promise.resolve();
}
return this.cryptoApi
.isValidDenom(newDenom, exchangeInfo.masterPublicKey)
.then((valid) => {
if (!valid) {
console.error("invalid denomination",
newDenom,
"with key",
exchangeInfo.masterPublicKey);
// FIXME: report to auditors
}
return this.cryptoApi.hashRsaPub(newDenom.denom_pub);
})
.then((h) => {
exchangeInfo.active_denoms.push(newDenom);
exchangeInfo.all_denoms.push(newDenom);
});
}); });
}); return Promise.all(ps).then(() => exchangeInfo);
});
} }
@ -1004,6 +1050,9 @@ export class Wallet {
*/ */
getBalances(): Promise<any> { getBalances(): Promise<any> {
function collectBalances(c: Coin, byCurrency) { function collectBalances(c: Coin, byCurrency) {
if (c.suspended) {
return byCurrency;
}
let acc: AmountJson = byCurrency[c.currentAmount.currency]; let acc: AmountJson = byCurrency[c.currentAmount.currency];
if (!acc) { if (!acc) {
acc = Amounts.getZero(c.currentAmount.currency); acc = Amounts.getZero(c.currentAmount.currency);

View File

@ -141,6 +141,13 @@ function makeHandlers(db: IDBDatabase,
// TODO: limit history length // TODO: limit history length
return wallet.getHistory(); return wallet.getHistory();
}, },
["payment-failed"]: function(detail, sender) {
// 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.
wallet.updateExchanges();
return Promise.resolve();
},
}; };
} }
@ -155,6 +162,7 @@ class ChromeBadge implements Badge {
} }
startBusy() { startBusy() {
this.setColor("#00F");
this.setText("..."); this.setText("...");
} }