respond to failed payments
This commit is contained in:
parent
ce7f7b8321
commit
27a42f9257
@ -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
|
||||
|
||||
addHandler("taler-execute-contract", function(e: CustomEvent) {
|
||||
addHandler("taler-execute-contract", (e: CustomEvent) => {
|
||||
console.log("got taler-execute-contract in content page");
|
||||
const msg = {
|
||||
type: "execute-payment",
|
||||
@ -195,6 +208,9 @@ namespace TalerNotify {
|
||||
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", {
|
||||
detail: {
|
||||
H_contract: e.detail.H_contract,
|
||||
|
@ -25,7 +25,7 @@
|
||||
*/
|
||||
|
||||
const DB_NAME = "taler";
|
||||
const DB_VERSION = 5;
|
||||
const DB_VERSION = 6;
|
||||
|
||||
/**
|
||||
* Return a promise that resolves
|
||||
@ -41,7 +41,7 @@ export function openTalerDb(): Promise<IDBDatabase> {
|
||||
resolve(req.result);
|
||||
};
|
||||
req.onupgradeneeded = (e) => {
|
||||
let db = req.result;
|
||||
const db = req.result;
|
||||
console.log("DB: upgrade needed: oldVersion = " + e.oldVersion);
|
||||
switch (e.oldVersion) {
|
||||
case 0: // DB does not exist yet
|
||||
@ -49,7 +49,6 @@ export function openTalerDb(): Promise<IDBDatabase> {
|
||||
{keyPath: "baseUrl"});
|
||||
exchanges.createIndex("pubKey", "masterPublicKey");
|
||||
db.createObjectStore("reserves", {keyPath: "reserve_pub"});
|
||||
db.createObjectStore("denoms", {keyPath: "denomPub"});
|
||||
const coins = db.createObjectStore("coins", {keyPath: "coinPub"});
|
||||
coins.createIndex("exchangeBaseUrl", "exchangeBaseUrl");
|
||||
const transactions = db.createObjectStore("transactions",
|
||||
|
@ -93,9 +93,6 @@ export class Denomination {
|
||||
@Checkable.String
|
||||
master_sig: string;
|
||||
|
||||
@Checkable.Optional(Checkable.String)
|
||||
pub_hash: string;
|
||||
|
||||
static checked: (obj: any) => Denomination;
|
||||
}
|
||||
|
||||
@ -103,7 +100,23 @@ export class Denomination {
|
||||
export interface IExchangeInfo {
|
||||
baseUrl: 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 {
|
||||
@ -151,13 +164,48 @@ export interface CoinPaySig {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Coin as stored in the "coins" data store
|
||||
* of the wallet database.
|
||||
*/
|
||||
export interface Coin {
|
||||
/**
|
||||
* Public key of the coin.
|
||||
*/
|
||||
coinPub: string;
|
||||
|
||||
/**
|
||||
* Private key to authorize operations on the coin.
|
||||
*/
|
||||
coinPriv: string;
|
||||
|
||||
/**
|
||||
* Key used by the exchange used to sign the coin.
|
||||
*/
|
||||
denomPub: string;
|
||||
|
||||
/**
|
||||
* Unblinded signature by the exchange.
|
||||
*/
|
||||
denomSig: string;
|
||||
|
||||
/**
|
||||
* Amount that's left on the coin.
|
||||
*/
|
||||
currentAmount: AmountJson;
|
||||
|
||||
/**
|
||||
* Base URL that identifies the exchange from which we got the
|
||||
* coin.
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
|
@ -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
|
||||
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]*)\)\/?/);
|
||||
if (!m) {
|
||||
return null;
|
||||
@ -306,6 +221,16 @@ function copy(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)
|
||||
@ -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
|
||||
* by looking at the database.
|
||||
@ -421,12 +359,20 @@ export class Wallet {
|
||||
let exchange: IExchangeInfo = mc[0];
|
||||
console.log("got coin for exchange", url);
|
||||
let coin: Coin = mc[1];
|
||||
if (coin.suspended) {
|
||||
console.log("skipping suspended coin",
|
||||
coin.denomPub,
|
||||
"from exchange",
|
||||
exchange.baseUrl);
|
||||
return;
|
||||
}
|
||||
let cd = {
|
||||
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) {
|
||||
throw Error("denom not found (database inconsistent)");
|
||||
console.warn("denom not found (database inconsistent)");
|
||||
return;
|
||||
}
|
||||
if (cd.denom.value.currency !== paymentAmount.currency) {
|
||||
console.warn("same pubkey for different currencies");
|
||||
@ -588,7 +534,9 @@ export class Wallet {
|
||||
let exchangeUrl = Object.keys(mcs)[0];
|
||||
|
||||
return this.cryptoApi.signDeposit(offer, mcs[exchangeUrl])
|
||||
.then((ds) => this.recordConfirmPay(offer, ds, exchangeUrl))
|
||||
.then((ds) => this.recordConfirmPay(offer,
|
||||
ds,
|
||||
exchangeUrl))
|
||||
.then(() => ({}));
|
||||
});
|
||||
});
|
||||
@ -856,8 +804,8 @@ export class Wallet {
|
||||
/**
|
||||
* Withdraw coins from a reserve until it is empty.
|
||||
*/
|
||||
private depleteReserve(reserve, exchange: ExchangeInfo): Promise<void> {
|
||||
let denomsAvailable: Denomination[] = copy(exchange.denoms);
|
||||
private depleteReserve(reserve, exchange: IExchangeInfo): Promise<void> {
|
||||
let denomsAvailable: Denomination[] = copy(exchange.active_denoms);
|
||||
let denomsForWithdraw = getWithdrawDenomList(reserve.current_amount,
|
||||
denomsAvailable);
|
||||
|
||||
@ -877,7 +825,7 @@ export class Wallet {
|
||||
* by quering the reserve's exchange.
|
||||
*/
|
||||
private updateReserve(reservePub: string,
|
||||
exchange: ExchangeInfo): Promise<Reserve> {
|
||||
exchange: IExchangeInfo): Promise<Reserve> {
|
||||
return Query(this.db)
|
||||
.get("reserves", reservePub)
|
||||
.then((reserve) => {
|
||||
@ -935,7 +883,8 @@ export class Wallet {
|
||||
amount: AmountJson): Promise<ReserveCreationInfo> {
|
||||
let p = this.updateExchangeFromUrl(baseUrl);
|
||||
return p.then((exchangeInfo: IExchangeInfo) => {
|
||||
let selectedDenoms = getWithdrawDenomList(amount, exchangeInfo.denoms);
|
||||
let selectedDenoms = getWithdrawDenomList(amount,
|
||||
exchangeInfo.active_denoms);
|
||||
let acc = Amounts.getZero(amount.currency);
|
||||
for (let d of selectedDenoms) {
|
||||
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
|
||||
* exchange entry in then DB.
|
||||
*/
|
||||
updateExchangeFromUrl(baseUrl): Promise<ExchangeInfo> {
|
||||
updateExchangeFromUrl(baseUrl): Promise<IExchangeInfo> {
|
||||
baseUrl = canonicalizeBaseUrl(baseUrl);
|
||||
let reqUrl = URI("keys").absoluteTo(baseUrl);
|
||||
return this.http.get(reqUrl).then((resp) => {
|
||||
@ -972,38 +921,138 @@ export class Wallet {
|
||||
throw Error("/keys request failed");
|
||||
}
|
||||
let exchangeKeysJson = KeysJson.checked(JSON.parse(resp.responseText));
|
||||
|
||||
return Query(this.db).get("exchanges", baseUrl).then((r) => {
|
||||
let exchangeInfo;
|
||||
console.dir(r);
|
||||
|
||||
if (!r) {
|
||||
exchangeInfo = ExchangeInfo.fresh(baseUrl);
|
||||
console.log("making fresh exchange");
|
||||
} else {
|
||||
exchangeInfo = new ExchangeInfo(r);
|
||||
console.log("using old exchange");
|
||||
}
|
||||
|
||||
return exchangeInfo.mergeKeys(exchangeKeysJson, this.cryptoApi)
|
||||
.then(() => {
|
||||
return Query(this.db)
|
||||
.put("exchanges", exchangeInfo)
|
||||
.finish()
|
||||
.then(() => exchangeInfo);
|
||||
});
|
||||
|
||||
});
|
||||
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) => {
|
||||
let exchangeInfo: IExchangeInfo;
|
||||
console.dir(r);
|
||||
|
||||
if (!r) {
|
||||
exchangeInfo = {
|
||||
baseUrl,
|
||||
all_denoms: [],
|
||||
active_denoms: [],
|
||||
last_update_time: updateTimeSec,
|
||||
masterPublicKey: exchangeKeysJson.master_public_key,
|
||||
};
|
||||
console.log("making fresh exchange");
|
||||
} else {
|
||||
if (updateTimeSec < r.last_update_time) {
|
||||
console.log("outdated /keys, not updating")
|
||||
return Promise.resolve(r);
|
||||
}
|
||||
exchangeInfo = r;
|
||||
console.log("updating old exchange");
|
||||
}
|
||||
|
||||
return this.updateExchangeInfo(exchangeInfo, exchangeKeysJson)
|
||||
.then((updatedExchangeInfo: IExchangeInfo) => {
|
||||
let q1 = Query(this.db)
|
||||
.put("exchanges", updatedExchangeInfo)
|
||||
.finish()
|
||||
.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);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieve a mapping from currency name to the amount
|
||||
* that is currenctly available for spending in the wallet.
|
||||
*/
|
||||
getBalances(): Promise<any> {
|
||||
function collectBalances(c: Coin, byCurrency) {
|
||||
if (c.suspended) {
|
||||
return byCurrency;
|
||||
}
|
||||
let acc: AmountJson = byCurrency[c.currentAmount.currency];
|
||||
if (!acc) {
|
||||
acc = Amounts.getZero(c.currentAmount.currency);
|
||||
|
@ -141,6 +141,13 @@ function makeHandlers(db: IDBDatabase,
|
||||
// TODO: limit history length
|
||||
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() {
|
||||
this.setColor("#00F");
|
||||
this.setText("...");
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user