diff --git a/content_scripts/notify.ts b/content_scripts/notify.ts index 7f0673748..959c0e557 100644 --- a/content_scripts/notify.ts +++ b/content_scripts/notify.ts @@ -282,7 +282,9 @@ namespace TalerNotify { addHandler("taler-payment-failed", (msg: any, sendResponse: any) => { const walletMsg = { type: "payment-failed", - detail: {}, + detail: { + contractHash: msg.H_contract + }, }; chrome.runtime.sendMessage(walletMsg, (resp) => { sendResponse(); @@ -290,8 +292,20 @@ namespace TalerNotify { }); addHandler("taler-payment-succeeded", (msg: any, sendResponse: any) => { + if (!msg.H_contract) { + console.error("H_contract missing in taler-payment-succeeded"); + return; + } console.log("got taler-payment-succeeded"); - sendResponse(); + const walletMsg = { + type: "payment-succeeded", + detail: { + contractHash: msg.H_contract, + }, + }; + chrome.runtime.sendMessage(walletMsg, (resp) => { + sendResponse(); + }) }); addHandler("taler-get-payment", (msg: any, sendResponse: any) => { diff --git a/gulpfile.js b/gulpfile.js index 70d8b2254..14287bb71 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -58,6 +58,9 @@ const paths = { "pages/*.{ts,tsx}", "!**/*.d.ts", ], + decl: [ + "lib/refs.d.ts", + ], dev: [ "test/tests/*.{ts,tsx}", ], @@ -73,10 +76,10 @@ const paths = { "lib/module-trampoline.js", "popup/**/*.{html,css}", "pages/**/*.{html,css}", - "lib/**/*.d.ts", "background/*.html", ], extra: [ + "lib/**/*.d.ts", "AUTHORS", "README", "COPYING", @@ -220,7 +223,12 @@ gulp.task("compile-prod", ["clean"], function () { tsArgs.outDir = "."; // We don't want source maps for production tsArgs.sourceMap = undefined; - return gulp.src(paths.ts.release, {base: "."}) + let opts = {base: "."}; + const files = concatStreams( + gulp.src(paths.ts.release, opts), + gulp.src(paths.ts.decl, opts)); + + return files .pipe(ts(tsArgs)) .pipe(gulp.dest("build/ext/")); }); @@ -274,6 +282,7 @@ gulp.task("srcdist", [], function () { // We can't just concat patterns due to exclude patterns const files = concatStreams( gulp.src(paths.ts.release, opts), + gulp.src(paths.ts.decl, opts), gulp.src(paths.ts.dev, opts), gulp.src(paths.dist, opts), gulp.src(paths.extra, opts)); @@ -346,9 +355,13 @@ function tsconfig(confBase) { // Generate the tsconfig file // that should be used during development. gulp.task("tsconfig", function() { - return gulp.src(Array.prototype.concat(paths.ts.release, paths.ts.dev), {base: "."}) - .pipe(tsconfig(tsBaseArgs)) - .pipe(gulp.dest(".")); + let opts = {base: "."}; + const files = concatStreams( + gulp.src(paths.ts.release, opts), + gulp.src(paths.ts.dev, opts), + gulp.src(paths.ts.decl, opts)); + return files.pipe(tsconfig(tsBaseArgs)) + .pipe(gulp.dest(".")); }); diff --git a/lib/refs.ts b/lib/refs.ts deleted file mode 100644 index a9c2c5eb8..000000000 --- a/lib/refs.ts +++ /dev/null @@ -1,6 +0,0 @@ - -// Help the TypeScript compiler find declarations. - -/// -/// -/// diff --git a/lib/wallet/cryptoLib.ts b/lib/wallet/cryptoLib.ts index 3b9d6d228..2782327ac 100644 --- a/lib/wallet/cryptoLib.ts +++ b/lib/wallet/cryptoLib.ts @@ -203,6 +203,8 @@ namespace RpcFunctions { let newAmount = new native.Amount(cd.coin.currentAmount); newAmount.sub(coinSpend); cd.coin.currentAmount = newAmount.toJson(); + cd.coin.dirty = true; + cd.coin.transactionPending = true; let d = new native.DepositRequestPS({ h_contract: native.HashCode.fromCrock(offer.H_contract), @@ -338,4 +340,8 @@ namespace RpcFunctions { return refreshSession; } + export function hashString(str: string): string { + const b = native.ByteArray.fromStringWithNull(str); + return b.hash().toCrock(); + } } \ No newline at end of file diff --git a/lib/wallet/db.ts b/lib/wallet/db.ts index 55e943393..9133330a2 100644 --- a/lib/wallet/db.ts +++ b/lib/wallet/db.ts @@ -25,7 +25,7 @@ */ const DB_NAME = "taler"; -const DB_VERSION = 8; +const DB_VERSION = 10; /** * Return a promise that resolves @@ -59,14 +59,15 @@ export function openTalerDb(): Promise { "contract.repurchase_correlation_id" ]); - db.createObjectStore("precoins", - {keyPath: "coinPub", autoIncrement: true}); + db.createObjectStore("precoins", {keyPath: "coinPub"}); const history = db.createObjectStore("history", { keyPath: "id", autoIncrement: true }); history.createIndex("timestamp", "timestamp"); + db.createObjectStore("refresh", + {keyPath: "meltCoinPub"}); break; default: if (e.oldVersion != DB_VERSION) { diff --git a/lib/wallet/types.ts b/lib/wallet/types.ts index f1b1eedce..9d634618a 100644 --- a/lib/wallet/types.ts +++ b/lib/wallet/types.ts @@ -279,6 +279,19 @@ export interface Coin { * to fix it. */ suspended?: boolean; + + /** + * Was the coin revealed in a transaction? + */ + dirty: boolean; + + /** + * Is the coin currently involved in a transaction? + * + * This delays refreshing until the transaction is finished or + * aborted. + */ + transactionPending: boolean; } diff --git a/lib/wallet/wallet.ts b/lib/wallet/wallet.ts index 43f4227dd..2c5210607 100644 --- a/lib/wallet/wallet.ts +++ b/lib/wallet/wallet.ts @@ -27,7 +27,7 @@ import { IExchangeInfo, Denomination, Notifier, - WireInfo, RefreshSession, ReserveRecord + WireInfo, RefreshSession, ReserveRecord, CoinPaySig } from "./types"; import {HttpResponse, RequestException} from "./http"; import {QueryRoot} from "./query"; @@ -135,11 +135,22 @@ interface ExchangeCoins { [exchangeUrl: string]: CoinWithDenom[]; } +interface PayReq { + amount: AmountJson; + coins: CoinPaySig[]; + H_contract: string; + max_fee: AmountJson; + merchant_sig: string; + exchange: string; + refund_deadline: string; + timestamp: string; + transaction_id: number; +} interface Transaction { contractHash: string; contract: Contract; - payReq: any; + payReq: PayReq; merchantSig: string; } @@ -492,16 +503,17 @@ export class Wallet { private async recordConfirmPay(offer: Offer, payCoinInfo: PayCoinInfo, chosenExchange: string): Promise { - let payReq: any = {}; - payReq["amount"] = offer.contract.amount; - payReq["coins"] = payCoinInfo.map((x) => x.sig); - payReq["H_contract"] = offer.H_contract; - payReq["max_fee"] = offer.contract.max_fee; - payReq["merchant_sig"] = offer.merchant_sig; - payReq["exchange"] = URI(chosenExchange).href(); - payReq["refund_deadline"] = offer.contract.refund_deadline; - payReq["timestamp"] = offer.contract.timestamp; - payReq["transaction_id"] = offer.contract.transaction_id; + 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, + timestamp: offer.contract.timestamp, + transaction_id: offer.contract.transaction_id, + }; let t: Transaction = { contractHash: offer.H_contract, contract: offer.contract, @@ -792,6 +804,8 @@ export class Wallet { denomSig: denomSig, currentAmount: pc.coinValue, exchangeBaseUrl: pc.exchangeBaseUrl, + dirty: false, + transactionPending: false, }; return coin; } @@ -1127,18 +1141,53 @@ export class Wallet { let newCoinDenoms = getWithdrawDenomList(availableAmount, availableDenoms); - newCoinDenoms = [newCoinDenoms[0]]; console.log("refreshing into", newCoinDenoms); + if (newCoinDenoms.length == 0) { + console.log("not refreshing, value too small"); + return; + } + let refreshSession: RefreshSession = await ( this.cryptoApi.createRefreshSession(exchange.baseUrl, - 3, - coin, - newCoinDenoms, - oldDenom.fee_refresh)); + 3, + coin, + newCoinDenoms, + oldDenom.fee_refresh)); - let reqUrl = URI("refresh/melt").absoluteTo(exchange!.baseUrl); + coin.currentAmount = Amounts.sub(coin.currentAmount, + refreshSession.valueWithFee).amount; + + // FIXME: we should check whether the amount still matches! + await this.q() + .put("refresh", refreshSession) + .put("coins", coin) + .finish(); + + await this.refreshMelt(refreshSession); + + let r = await this.q().get("refresh", oldCoinPub); + if (!r) { + throw Error("refresh session does not exist anymore"); + } + await this.refreshReveal(r); + } + + + async refreshMelt(refreshSession: RefreshSession): Promise { + if (refreshSession.norevealIndex != undefined) { + console.error("won't melt again"); + return; + } + + let coin = await this.q().get("coins", refreshSession.meltCoinPub); + if (!coin) { + console.error("can't melt coin, it does not exist"); + return; + } + + let reqUrl = URI("refresh/melt").absoluteTo(refreshSession.exchangeBaseUrl); let meltCoin = { coin_pub: coin.coinPub, denom_pub: coin.denomPub, @@ -1148,7 +1197,7 @@ export class Wallet { }; let coinEvs = refreshSession.preCoinsForGammas.map((x) => x.map((y) => y.coinEv)); let req = { - "new_denoms": newCoinDenoms.map((d) => d.denom_pub), + "new_denoms": refreshSession.newDenoms, "melt_coin": meltCoin, "transfer_pubs": refreshSession.transferPubs, "coin_evs": coinEvs, @@ -1178,11 +1227,10 @@ export class Wallet { refreshSession.norevealIndex = norevealIndex; - this.refreshReveal(refreshSession); - - // FIXME: implement rest + await this.q().put("refresh", refreshSession).finish(); } + async refreshReveal(refreshSession: RefreshSession): Promise { let norevealIndex = refreshSession.norevealIndex; if (norevealIndex == undefined) { @@ -1196,12 +1244,54 @@ export class Wallet { "transfer_privs": privs, }; - let reqUrl = URI("refresh/reveal").absoluteTo(refreshSession.exchangeBaseUrl); + let reqUrl = URI("refresh/reveal") + .absoluteTo(refreshSession.exchangeBaseUrl); console.log("reveal request:", req); let resp = await this.http.postJson(reqUrl, req); console.log("session:", refreshSession); console.log("reveal response:", resp); + + if (resp.status != 200) { + console.log("error: /refresh/reveal returned status " + resp.status); + return; + } + + let respJson = JSON.parse(resp.responseText); + + if (!respJson.ev_sigs || !Array.isArray(respJson.ev_sigs)) { + console.log("/refresh/reveal did not contain ev_sigs"); + } + + let exchange = await this.q().get("exchanges", refreshSession.exchangeBaseUrl); + if (!exchange) { + console.error(`exchange ${refreshSession.exchangeBaseUrl} not found`); + return; + } + + for (let i = 0; i < respJson.ev_sigs.length; i++) { + let denom = exchange.all_denoms.find((d) => d.denom_pub == refreshSession.newDenoms[i]); + if (!denom) { + console.error("denom not found"); + continue; + } + let pc = refreshSession.preCoinsForGammas[refreshSession.norevealIndex!][i]; + let denomSig = await this.cryptoApi.rsaUnblind(respJson.ev_sigs[i].ev_sig, + pc.blindingKey, + denom.denom_pub); + let coin: Coin = { + coinPub: pc.publicKey, + coinPriv: pc.privateKey, + denomPub: denom.denom_pub, + denomSig: denomSig, + currentAmount: denom.value, + exchangeBaseUrl: refreshSession.exchangeBaseUrl, + dirty: false, + transactionPending: false, + }; + + await this.q().put("coins", coin).finish(); + } } @@ -1283,4 +1373,29 @@ export class Wallet { return {isRepurchase: false}; } } + + + async paymentSucceeded(contractHash: string): Promise { + const doPaymentSucceeded = async () => { + let t = await this.q().get("transactions", contractHash); + if (!t) { + console.error("contract not found"); + return; + } + for (let pc of t.payReq.coins) { + let c = await this.q().get("coins", pc.coin_pub); + if (!c) { + console.error("coin not found"); + return; + } + c.transactionPending = false; + await this.q().put("coins", c).finish(); + } + for (let c of t.payReq.coins) { + this.refresh(c.coin_pub); + } + }; + doPaymentSucceeded(); + return; + } } diff --git a/lib/wallet/wxMessaging.ts b/lib/wallet/wxMessaging.ts index 07f5cc1d8..1c3876772 100644 --- a/lib/wallet/wxMessaging.ts +++ b/lib/wallet/wxMessaging.ts @@ -218,6 +218,13 @@ function makeHandlers(db: IDBDatabase, wallet.updateExchanges(); return Promise.resolve(); }, + ["payment-succeeded"]: function (detail, sender) { + let contractHash = detail.contractHash; + if (!contractHash) { + return Promise.reject(Error("contractHash missing")); + } + return wallet.paymentSucceeded(contractHash); + }, }; } diff --git a/tsconfig.json b/tsconfig.json index fa6cde6d3..e9efcb50c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,8 +13,10 @@ "noImplicitAny": true }, "files": [ + "lib/components.ts", + "test/tests/taler.ts", + "lib/refs.d.ts", "lib/i18n.ts", - "lib/refs.ts", "lib/shopApi.ts", "lib/taler-wallet-lib.ts", "lib/wallet/checkable.ts", @@ -38,7 +40,6 @@ "pages/show-db.ts", "pages/confirm-contract.tsx", "pages/confirm-create-reserve.tsx", - "pages/tree.tsx", - "test/tests/taler.ts" + "pages/tree.tsx" ] } \ No newline at end of file diff --git a/web-common b/web-common index e9eb34dfb..02271055c 160000 --- a/web-common +++ b/web-common @@ -1 +1 @@ -Subproject commit e9eb34dfb56c8e5a18fa7d555aa651a4532e8f3c +Subproject commit 02271055cbe9e62cffb3713d109a77015df625d1