aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/cryptoApi-test.ts35
-rw-r--r--src/cryptoApi.ts27
-rw-r--r--src/cryptoWorker.ts37
-rw-r--r--src/emscriptif.ts27
-rw-r--r--src/pages/payback.html37
-rw-r--r--src/pages/payback.tsx99
-rw-r--r--src/pages/popup.tsx7
-rw-r--r--src/types.ts94
-rw-r--r--src/wallet.ts137
-rw-r--r--src/wxApi.ts8
-rw-r--r--src/wxBackend.ts11
-rw-r--r--tsconfig.json1
-rw-r--r--webpack.config.js1
13 files changed, 462 insertions, 59 deletions
diff --git a/src/cryptoApi-test.ts b/src/cryptoApi-test.ts
index dde3ea899..8350defbc 100644
--- a/src/cryptoApi-test.ts
+++ b/src/cryptoApi-test.ts
@@ -1,42 +1,46 @@
import {CryptoApi} from "./cryptoApi";
-import {ReserveRecord, DenominationRecord, denominationRecordFromKeys} from "./types";
+import {ReserveRecord, DenominationRecord, DenominationStatus} from "./types";
import {test, TestLib} from "talertest";
let masterPub1: string = "CQQZ9DY3MZ1ARMN5K1VKDETS04Y2QCKMMCFHZSWJWWVN82BTTH00";
-let denomValid1: DenominationRecord = denominationRecordFromKeys("https://example.com/exchange", {
- "master_sig": "CJFJCQ48Q45PSGJ5KY94N6M2TPARESM2E15BSPBD95YVVPEARAEQ6V6G4Z2XBMS0QM0F3Y9EYVP276FCS90EQ1578ZC8JHFBZ3NGP3G",
- "stamp_start": "/Date(1473148381)/",
- "stamp_expire_withdraw": "/Date(2482300381)/",
- "stamp_expire_deposit": "/Date(1851580381)/",
- "denom_pub": "51R7ARKCD5HJTTV5F4G0M818E9SP280A40G2GVH04CR30GHS84R3JHHP6GSM2D9Q6514CGT568R32C9J6CWM4DSH64TM4DSM851K0CA48CVKAC1P6H144C2160T46DHK8CVM4HJ274S38C1M6S338D9N6GWM8DT684T3JCT36S13EC9G88R3EGHQ8S0KJGSQ60SKGD216N33AGJ2651K2E9S60TMCD1N75244HHQ6X33EDJ570R3GGJ2651MACA38D130DA560VK4HHJ68WK2CA26GW3ECSH6D13EC9S88VK2GT66WVK8D9G750K0D9R8RRK4DHQ71332GHK8D23GE26710M2H9K6WVK8HJ38MVKEGA66N23AC9H88VKACT58MV3CCSJ6H1K4DT38GRK0C9M8N33CE1R60V4AHA38H1KECSH6S33JH9N8GRKGH1K68S36GH354520818CMG26C1H60R30C935452081918G2J2G0",
- "stamp_expire_legal": "/Date(1567756381)/",
- "value": {
+let denomValid1: DenominationRecord = {
+ masterSig: "CJFJCQ48Q45PSGJ5KY94N6M2TPARESM2E15BSPBD95YVVPEARAEQ6V6G4Z2XBMS0QM0F3Y9EYVP276FCS90EQ1578ZC8JHFBZ3NGP3G",
+ stampStart: "/Date(1473148381)/",
+ stampExpireWithdraw: "/Date(2482300381)/",
+ stampExpireDeposit: "/Date(1851580381)/",
+ denomPub: "51R7ARKCD5HJTTV5F4G0M818E9SP280A40G2GVH04CR30GHS84R3JHHP6GSM2D9Q6514CGT568R32C9J6CWM4DSH64TM4DSM851K0CA48CVKAC1P6H144C2160T46DHK8CVM4HJ274S38C1M6S338D9N6GWM8DT684T3JCT36S13EC9G88R3EGHQ8S0KJGSQ60SKGD216N33AGJ2651K2E9S60TMCD1N75244HHQ6X33EDJ570R3GGJ2651MACA38D130DA560VK4HHJ68WK2CA26GW3ECSH6D13EC9S88VK2GT66WVK8D9G750K0D9R8RRK4DHQ71332GHK8D23GE26710M2H9K6WVK8HJ38MVKEGA66N23AC9H88VKACT58MV3CCSJ6H1K4DT38GRK0C9M8N33CE1R60V4AHA38H1KECSH6S33JH9N8GRKGH1K68S36GH354520818CMG26C1H60R30C935452081918G2J2G0",
+ stampExpireLegal: "/Date(1567756381)/",
+ value: {
"currency": "PUDOS",
"value": 0,
"fraction": 100000
},
- "fee_withdraw": {
+ feeWithdraw: {
"currency": "PUDOS",
"value": 0,
"fraction": 10000
},
- "fee_deposit": {
+ feeDeposit: {
"currency": "PUDOS",
"value": 0,
"fraction": 10000
},
- "fee_refresh": {
+ feeRefresh: {
"currency": "PUDOS",
"value": 0,
"fraction": 10000
},
- "fee_refund": {
+ feeRefund: {
"currency": "PUDOS",
"value": 0,
"fraction": 10000
- }
-});
+ },
+ denomPubHash: "dummy",
+ status: DenominationStatus.Unverified,
+ isOffered: true,
+ exchangeBaseUrl: "https://exchange.example.com/",
+};
let denomInvalid1 = JSON.parse(JSON.stringify(denomValid1));
denomInvalid1.value.value += 1;
@@ -55,6 +59,7 @@ test("precoin creation", async (t: TestLib) => {
let r: ReserveRecord = {
reserve_pub: pub,
reserve_priv: priv,
+ hasPayback: false,
exchange_base_url: "https://example.com/exchange",
created: 0,
requested_amount: {currency: "PUDOS", value: 0, fraction: 0},
diff --git a/src/cryptoApi.ts b/src/cryptoApi.ts
index 5657d74d6..8dd14392a 100644
--- a/src/cryptoApi.ts
+++ b/src/cryptoApi.ts
@@ -22,13 +22,20 @@
import {
- PreCoinRecord, CoinRecord, ReserveRecord, AmountJson,
- DenominationRecord
+ PreCoinRecord,
+ CoinRecord,
+ ReserveRecord,
+ AmountJson,
+ DenominationRecord,
+ PaybackRequest,
+ RefreshSessionRecord,
+ WireFee,
+ PayCoinInfo,
} from "./types";
-import {OfferRecord} from "./wallet";
-import {CoinWithDenom} from "./wallet";
-import {PayCoinInfo} from "./types";
-import {RefreshSessionRecord, WireFee} from "./types";
+import {
+ OfferRecord,
+ CoinWithDenom,
+} from "./wallet";
interface WorkerState {
@@ -230,6 +237,10 @@ export class CryptoApi {
return this.doRpc<string>("hashString", 1, str);
}
+ hashDenomPub(denomPub: string): Promise<string> {
+ return this.doRpc<string>("hashDenomPub", 1, denomPub);
+ }
+
isValidDenom(denom: DenominationRecord,
masterPub: string): Promise<boolean> {
return this.doRpc<boolean>("isValidDenom", 2, denom, masterPub);
@@ -256,6 +267,10 @@ export class CryptoApi {
return this.doRpc<string>("rsaUnblind", 4, sig, bk, pk);
}
+ createPaybackRequest(coin: CoinRecord, preCoin: PreCoinRecord): Promise<PaybackRequest> {
+ return this.doRpc<PaybackRequest>("createPaybackRequest", 1, coin, preCoin);
+ }
+
createRefreshSession(exchangeBaseUrl: string,
kappa: number,
meltCoin: CoinRecord,
diff --git a/src/cryptoWorker.ts b/src/cryptoWorker.ts
index 55c08d4b5..a11a0d021 100644
--- a/src/cryptoWorker.ts
+++ b/src/cryptoWorker.ts
@@ -23,8 +23,14 @@
import * as native from "./emscriptif";
import {
- PreCoinRecord, PayCoinInfo, AmountJson,
- RefreshSessionRecord, RefreshPreCoinRecord, ReserveRecord, CoinStatus,
+ PreCoinRecord,
+ PayCoinInfo,
+ AmountJson,
+ RefreshSessionRecord,
+ RefreshPreCoinRecord,
+ ReserveRecord,
+ CoinStatus,
+ PaybackRequest,
} from "./types";
import create = chrome.alarms.create;
import {OfferRecord} from "./wallet";
@@ -96,8 +102,29 @@ namespace RpcFunctions {
return preCoin;
}
+ export function createPaybackRequest(coin: CoinRecord, preCoin: PreCoinRecord): PaybackRequest {
+ if (coin.coinPub != preCoin.coinPub) {
+ throw Error("coin doesn't match precoin");
+ }
+ let p = new native.PaybackRequestPS({
+ coin_pub: native.EddsaPublicKey.fromCrock(coin.coinPub),
+ h_denom_pub: native.RsaPublicKey.fromCrock(coin.denomPub).encode().hash(),
+ coin_blind: native.RsaBlindingKeySecret.fromCrock(preCoin.blindingKey),
+ });
+ let coinPriv = native.EddsaPrivateKey.fromCrock(coin.coinPriv);
+ let coinSig = native.eddsaSign(p.toPurpose(), coinPriv);
+ let paybackRequest: PaybackRequest = {
+ denom_pub: coin.denomPub,
+ denom_sig: coin.denomSig,
+ coin_blind_key_secret: preCoin.blindingKey,
+ coin_pub: coin.coinPub,
+ coin_sig: coinSig.toCrock(),
+ };
+ return paybackRequest;
+ }
- export function isValidPaymentSignature(sig: string, contractHash: string, merchantPub: string) {
+
+ export function isValidPaymentSignature(sig: string, contractHash: string, merchantPub: string): boolean {
let p = new native.PaymentSignaturePS({
contract_hash: native.HashCode.fromCrock(contractHash),
});
@@ -366,6 +393,10 @@ namespace RpcFunctions {
const b = native.ByteArray.fromStringWithNull(str);
return b.hash().toCrock();
}
+
+ export function hashDenomPub(denomPub: string): string {
+ return native.RsaPublicKey.fromCrock(denomPub).encode().hash().toCrock();
+ }
}
diff --git a/src/emscriptif.ts b/src/emscriptif.ts
index 347ee54a0..caa0fb8cc 100644
--- a/src/emscriptif.ts
+++ b/src/emscriptif.ts
@@ -208,6 +208,7 @@ export enum SignaturePurpose {
TEST = 4242,
MERCHANT_PAYMENT_OK = 1104,
MASTER_WIRE_FEES = 1028,
+ WALLET_COIN_PAYBACK = 1203,
}
@@ -966,6 +967,32 @@ export class WithdrawRequestPS extends SignatureStruct {
}
+export interface PaybackRequestPS_args {
+ coin_pub: EddsaPublicKey;
+ h_denom_pub: HashCode;
+ coin_blind: RsaBlindingKeySecret;
+}
+
+
+export class PaybackRequestPS extends SignatureStruct {
+ constructor(w: PaybackRequestPS_args) {
+ super(w);
+ }
+
+ purpose() {
+ return SignaturePurpose.WALLET_COIN_PAYBACK;
+ }
+
+ fieldTypes() {
+ return [
+ ["coin_pub", EddsaPublicKey],
+ ["h_denom_pub", HashCode],
+ ["coin_blind", RsaBlindingKeySecret],
+ ];
+ }
+}
+
+
interface RefreshMeltCoinAffirmationPS_Args {
session_hash: HashCode;
amount_with_fee: AmountNbo;
diff --git a/src/pages/payback.html b/src/pages/payback.html
new file mode 100644
index 000000000..d7b913eec
--- /dev/null
+++ b/src/pages/payback.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+ <meta charset="UTF-8">
+ <title>Taler Wallet: Payback</title>
+
+ <link rel="stylesheet" type="text/css" href="../style/lang.css">
+ <link rel="stylesheet" type="text/css" href="../style/wallet.css">
+
+ <link rel="icon" href="/img/icon.png">
+
+ <script src="/dist/page-common-bundle.js"></script>
+ <script src="/dist/payback-bundle.js"></script>
+
+ <style>
+ body {
+ font-size: 100%;
+ }
+ .tree-item {
+ margin: 2em;
+ border-radius: 5px;
+ border: 1px solid gray;
+ padding: 1em;
+ }
+ .button-linky {
+ background: none;
+ color: black;
+ text-decoration: underline;
+ border: none;
+ }
+ </style>
+
+ <body>
+ <div id="container"></div>
+ </body>
+</html>
diff --git a/src/pages/payback.tsx b/src/pages/payback.tsx
new file mode 100644
index 000000000..9e463d4a0
--- /dev/null
+++ b/src/pages/payback.tsx
@@ -0,0 +1,99 @@
+/*
+ This file is part of TALER
+ (C) 2017 Inria
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * View and edit auditors.
+ *
+ * @author Florian Dold
+ */
+
+
+import {
+ ExchangeRecord,
+ ExchangeForCurrencyRecord,
+ DenominationRecord,
+ AuditorRecord,
+ CurrencyRecord,
+ ReserveRecord,
+ CoinRecord,
+ PreCoinRecord,
+ Denomination,
+ WalletBalance,
+} from "../types";
+import { ImplicitStateComponent, StateHolder } from "../components";
+import {
+ getCurrencies,
+ updateCurrency,
+ getPaybackReserves,
+ withdrawPaybackReserve,
+} from "../wxApi";
+import { prettyAmount } from "../renderHtml";
+import { getTalerStampDate } from "../helpers";
+import * as React from "react";
+import * as ReactDOM from "react-dom";
+
+class Payback extends ImplicitStateComponent<any> {
+ reserves: StateHolder<ReserveRecord[]|null> = this.makeState(null);
+ constructor() {
+ super();
+ let port = chrome.runtime.connect();
+ port.onMessage.addListener((msg: any) => {
+ if (msg.notify) {
+ console.log("got notified");
+ this.update();
+ }
+ });
+ this.update();
+ }
+
+ async update() {
+ let reserves = await getPaybackReserves();
+ this.reserves(reserves);
+ }
+
+ withdrawPayback(pub: string) {
+ withdrawPaybackReserve(pub);
+ }
+
+ render(): JSX.Element {
+ let reserves = this.reserves();
+ if (!reserves) {
+ return <span>loading ...</span>;
+ }
+ if (reserves.length == 0) {
+ return <span>No reserves with payback available.</span>;
+ }
+ return (
+ <div>
+ {reserves.map(r => (
+ <div>
+ <h2>Reserve for ${prettyAmount(r.current_amount!)}</h2>
+ <ul>
+ <li>Exchange: ${r.exchange_base_url}</li>
+ </ul>
+ <button onClick={() => this.withdrawPayback(r.reserve_pub)}>Withdraw again</button>
+ </div>
+ ))}
+ </div>
+ );
+ }
+}
+
+export function main() {
+ ReactDOM.render(<Payback />, document.getElementById("container")!);
+}
+
+document.addEventListener("DOMContentLoaded", main);
diff --git a/src/pages/popup.tsx b/src/pages/popup.tsx
index fc6d39a0a..9b375097f 100644
--- a/src/pages/popup.tsx
+++ b/src/pages/popup.tsx
@@ -299,8 +299,12 @@ class WalletBalanceView extends React.Component<any, any> {
return <span></span>;
}
console.log(wallet);
+ let paybackAvailable = false;
let listing = Object.keys(wallet).map((key) => {
let entry: WalletBalanceEntry = wallet[key];
+ if (entry.paybackAmount.value != 0 || entry.paybackAmount.fraction != 0) {
+ paybackAvailable = true;
+ }
return (
<p>
{bigAmount(entry.available)}
@@ -311,9 +315,12 @@ class WalletBalanceView extends React.Component<any, any> {
});
let link = chrome.extension.getURL("/src/pages/auditors.html");
let linkElem = <a className="actionLink" href={link} target="_blank">Trusted Auditors and Exchanges</a>;
+ let paybackLink = chrome.extension.getURL("/src/pages/payback.html");
+ let paybackLinkElem = <a className="actionLink" href={link} target="_blank">Trusted Auditors and Exchanges</a>;
return (
<div>
{listing.length > 0 ? listing : this.renderEmpty()}
+ {paybackAvailable && paybackLinkElem}
{linkElem}
</div>
);
diff --git a/src/types.ts b/src/types.ts
index e357dfa26..4964d9f45 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -73,7 +73,13 @@ export interface ReserveRecord {
precoin_amount: AmountJson;
- confirmed: boolean,
+ confirmed: boolean;
+
+ /**
+ * We got some payback to this reserve. We'll cease to automatically
+ * withdraw money from it.
+ */
+ hasPayback: boolean;
}
export interface AuditorRecord {
@@ -127,6 +133,9 @@ export class DenominationRecord {
@Checkable.String
denomPub: string;
+ @Checkable.String
+ denomPubHash: string;
+
@Checkable.Value(AmountJson)
feeWithdraw: AmountJson;
@@ -276,27 +285,65 @@ export interface RefreshPreCoinRecord {
publicKey: string;
privateKey: string;
coinEv: string;
- blindingKey: string
-}
-
-export function denominationRecordFromKeys(exchangeBaseUrl: string, denomIn: Denomination): DenominationRecord {
- let d: DenominationRecord = {
- denomPub: denomIn.denom_pub,
- exchangeBaseUrl: exchangeBaseUrl,
- feeDeposit: denomIn.fee_deposit,
- masterSig: denomIn.master_sig,
- feeRefund: denomIn.fee_refund,
- feeRefresh: denomIn.fee_refresh,
- feeWithdraw: denomIn.fee_withdraw,
- stampExpireDeposit: denomIn.stamp_expire_deposit,
- stampExpireLegal: denomIn.stamp_expire_legal,
- stampExpireWithdraw: denomIn.stamp_expire_withdraw,
- stampStart: denomIn.stamp_start,
- status: DenominationStatus.Unverified,
- isOffered: true,
- value: denomIn.value,
- };
- return d;
+ blindingKey: string;
+}
+
+export interface PaybackRequest {
+ denom_pub: string;
+
+ /**
+ * Signature over the coin public key by the denomination.
+ */
+ denom_sig: string;
+
+ coin_pub: string;
+
+ coin_blind_key_secret: string;
+
+ coin_sig: string;
+}
+
+@Checkable.Class
+export class PaybackConfirmation {
+ /**
+ * public key of the reserve that will receive the payback.
+ */
+ @Checkable.String
+ reserve_pub: string;
+
+ /**
+ * How much will the exchange pay back (needed by wallet in
+ * case coin was partially spent and wallet got restored from backup)
+ */
+ @Checkable.Value(AmountJson)
+ amount: AmountJson;
+
+ /**
+ * Time by which the exchange received the /payback request.
+ */
+ @Checkable.String
+ timestamp: string;
+
+ /**
+ * the EdDSA signature of TALER_PaybackConfirmationPS using a current
+ * signing key of the exchange affirming the successful
+ * payback request, and that the exchange promises to transfer the funds
+ * by the date specified (this allows the exchange delaying the transfer
+ * a bit to aggregate additional payback requests into a larger one).
+ */
+ @Checkable.String
+ exchange_sig: string;
+
+ /**
+ * Public EdDSA key of the exchange that was used to generate the signature.
+ * Should match one of the exchange's signing keys from /keys. It is given
+ * explicitly as the client might otherwise be confused by clock skew as to
+ * which signing key was used.
+ */
+ @Checkable.String
+ exchange_pub: string;
+
+ static checked: (obj: any) => PaybackConfirmation;
}
/**
@@ -367,7 +414,7 @@ export interface CoinPaySig {
export enum CoinStatus {
- Fresh, TransactionPending, Dirty, Refreshed,
+ Fresh, TransactionPending, Dirty, Refreshed, PaybackPending, PaybackDone,
}
@@ -440,6 +487,7 @@ export interface WalletBalanceEntry {
available: AmountJson;
pendingIncoming: AmountJson;
pendingPayment: AmountJson;
+ paybackAmount: AmountJson;
}
diff --git a/src/wallet.ts b/src/wallet.ts
index 4c44b5d24..63cd597ea 100644
--- a/src/wallet.ts
+++ b/src/wallet.ts
@@ -44,8 +44,11 @@ import {
WalletBalanceEntry,
WireFee,
ExchangeWireFeesRecord,
- WireInfo, DenominationRecord, DenominationStatus, denominationRecordFromKeys,
+ WireInfo,
+ DenominationRecord,
+ DenominationStatus,
CoinStatus,
+ PaybackConfirmation,
} from "./types";
import {
HttpRequestLibrary,
@@ -410,6 +413,7 @@ export namespace Stores {
}
exchangeBaseUrlIndex = new Index<string,CoinRecord>(this, "exchangeBaseUrl", "exchangeBaseUrl");
+ denomPubIndex = new Index<string,CoinRecord>(this, "denomPub", "denomPub");
}
class HistoryStore extends Store<HistoryRecord> {
@@ -448,6 +452,7 @@ export namespace Stores {
{keyPath: ["exchangeBaseUrl", "denomPub"] as any as IDBKeyPath});
}
+ denomPubHashIndex = new Index<string,DenominationRecord>(this, "denomPubHash", "denomPubHash");
exchangeBaseUrlIndex = new Index<string, DenominationRecord>(this, "exchangeBaseUrl", "exchangeBaseUrl");
denomPubIndex = new Index<string, DenominationRecord>(this, "denomPub", "denomPub");
}
@@ -894,9 +899,8 @@ export class Wallet {
try {
let exchange = await this.updateExchangeFromUrl(reserveRecord.exchange_base_url);
- let reserve = await this.updateReserve(reserveRecord.reserve_pub,
- exchange);
- let n = await this.depleteReserve(reserve, exchange);
+ let reserve = await this.updateReserve(reserveRecord.reserve_pub);
+ let n = await this.depleteReserve(reserve);
if (n != 0) {
let depleted: HistoryRecord = {
@@ -1013,6 +1017,7 @@ export class Wallet {
const canonExchange = canonicalizeBaseUrl(req.exchange);
const reserveRecord: ReserveRecord = {
+ hasPayback: false,
reserve_pub: keypair.pub,
reserve_priv: keypair.priv,
exchange_base_url: canonExchange,
@@ -1148,8 +1153,7 @@ export class Wallet {
/**
* Withdraw coins from a reserve until it is empty.
*/
- private async depleteReserve(reserve: ReserveRecord,
- exchange: ExchangeRecord): Promise<number> {
+ private async depleteReserve(reserve: ReserveRecord): Promise<number> {
console.log("depleting reserve");
if (!reserve.current_amount) {
throw Error("can't withdraw when amount is unknown");
@@ -1158,7 +1162,7 @@ export class Wallet {
if (!currentAmount) {
throw Error("can't withdraw when amount is unknown");
}
- let denomsForWithdraw = await this.getVerifiedWithdrawDenomList(exchange.baseUrl,
+ let denomsForWithdraw = await this.getVerifiedWithdrawDenomList(reserve.exchange_base_url,
currentAmount);
console.log(`withdrawing ${denomsForWithdraw.length} coins`);
@@ -1204,14 +1208,13 @@ export class Wallet {
* Update the information about a reserve that is stored in the wallet
* by quering the reserve's exchange.
*/
- private async updateReserve(reservePub: string,
- exchange: ExchangeRecord): Promise<ReserveRecord> {
+ private async updateReserve(reservePub: string): Promise<ReserveRecord> {
let reserve = await this.q()
.get<ReserveRecord>(Stores.reserves, reservePub);
if (!reserve) {
throw Error("reserve not in db");
}
- let reqUrl = new URI("reserve/status").absoluteTo(exchange.baseUrl);
+ let reqUrl = new URI("reserve/status").absoluteTo(reserve.exchange_base_url);
reqUrl.query({'reserve_pub': reservePub});
let resp = await this.http.get(reqUrl.href());
if (resp.status != 200) {
@@ -1549,6 +1552,20 @@ export class Wallet {
await this.q().put(Stores.exchangeWireFees, oldWireFees);
+ if (exchangeKeysJson.payback) {
+ for (let payback of exchangeKeysJson.payback) {
+ let denom = await this.q().getIndexed(Stores.denominations.denomPubHashIndex, payback.h_denom_pub);
+ if (!denom) {
+ continue;
+ }
+ console.log(`cashing back denom`, denom);
+ let coins = await this.q().iterIndex(Stores.coins.denomPubIndex, denom.denomPub).toArray();
+ for (let coin of coins) {
+ this.payback(coin.coinPub);
+ }
+ }
+ }
+
return updatedExchangeInfo;
}
@@ -1571,7 +1588,7 @@ export class Wallet {
const newAndUnseenDenoms: typeof existingDenoms = {};
for (let d of newKeys.denoms) {
- let dr = denominationRecordFromKeys(exchangeInfo.baseUrl, d);
+ let dr = await this.denominationRecordFromKeys(exchangeInfo.baseUrl, d);
if (!(d.denom_pub in existingDenoms)) {
newAndUnseenDenoms[dr.denomPub] = dr;
}
@@ -1608,6 +1625,7 @@ export class Wallet {
available: z,
pendingIncoming: z,
pendingPayment: z,
+ paybackAmount: z,
};
}
return entry;
@@ -1643,6 +1661,17 @@ export class Wallet {
return balance;
}
+ function collectPaybacks(r: ReserveRecord, balance: WalletBalance) {
+ if (!r.hasPayback) {
+ return balance;
+ }
+ let entry = ensureEntry(balance, r.requested_amount.currency);
+ if (Amounts.cmp(smallestWithdraw[r.exchange_base_url], r.current_amount!) < 0) {
+ entry.paybackAmount = Amounts.add(entry.paybackAmount, r.current_amount!).amount;
+ }
+ return balance;
+ }
+
function collectPendingRefresh(r: RefreshSessionRecord,
balance: WalletBalance) {
// Don't count finished refreshes, since the refresh already resulted
@@ -1699,6 +1728,8 @@ export class Wallet {
.reduce(collectPendingRefresh, balance);
tx.iter(Stores.reserves)
.reduce(collectPendingWithdraw, balance);
+ tx.iter(Stores.reserves)
+ .reduce(collectPaybacks, balance);
tx.iter(Stores.transactions)
.reduce(collectPayments, balance);
await tx.finish();
@@ -2085,4 +2116,88 @@ export class Wallet {
doPaymentSucceeded();
return;
}
+
+ async payback(coinPub: string): Promise<void> {
+ let coin = await this.q().get(Stores.coins, coinPub);
+ if (!coin) {
+ throw Error(`Coin ${coinPub} not found, can't request payback`);
+ }
+ let preCoin = await this.q().get(Stores.precoins, coin.coinPub);
+ if (!preCoin) {
+ throw Error(`Precoin of coin ${coinPub} not found`);
+ }
+ let reserve = await this.q().get(Stores.reserves, preCoin.reservePub);
+ if (!reserve) {
+ throw Error(`Reserve of coin ${coinPub} not found`);
+ }
+ switch (coin.status) {
+ case CoinStatus.Refreshed:
+ throw Error(`Can't do payback for coin ${coinPub} since it's refreshed`);
+ case CoinStatus.PaybackDone:
+ console.log(`Coin ${coinPub} already payed back`);
+ return;
+ }
+ coin.status = CoinStatus.PaybackPending;
+ // Even if we didn't get the payback yet, we suspend withdrawal, since
+ // technically we might update reserve status before we get the response
+ // from the reserve for the payback request.
+ reserve.hasPayback = true;
+ await this.q().put(Stores.coins, coin).put(Stores.reserves, reserve);
+
+ let paybackRequest = await this.cryptoApi.createPaybackRequest(coin, preCoin);
+ let reqUrl = new URI("payback").absoluteTo(preCoin.exchangeBaseUrl);
+ let resp = await this.http.get(reqUrl.href());
+ if (resp.status != 200) {
+ throw Error();
+ }
+ let paybackConfirmation = PaybackConfirmation.checked(JSON.parse(resp.responseText));
+ if (paybackConfirmation.reserve_pub != preCoin.reservePub) {
+ throw Error(`Coin's reserve doesn't match reserve on payback`);
+ }
+ coin = await this.q().get(Stores.coins, coinPub);
+ if (!coin) {
+ throw Error(`Coin ${coinPub} not found, can't confirm payback`);
+ }
+ coin.status = CoinStatus.PaybackDone;
+ await this.q().put(Stores.coins, coin);
+ await this.updateReserve(preCoin.reservePub);
+ }
+
+
+ async denominationRecordFromKeys(exchangeBaseUrl: string, denomIn: Denomination): Promise<DenominationRecord> {
+ let denomPubHash = await this.cryptoApi.hashDenomPub(denomIn.denom_pub);
+ let d: DenominationRecord = {
+ denomPubHash,
+ denomPub: denomIn.denom_pub,
+ exchangeBaseUrl: exchangeBaseUrl,
+ feeDeposit: denomIn.fee_deposit,
+ masterSig: denomIn.master_sig,
+ feeRefund: denomIn.fee_refund,
+ feeRefresh: denomIn.fee_refresh,
+ feeWithdraw: denomIn.fee_withdraw,
+ stampExpireDeposit: denomIn.stamp_expire_deposit,
+ stampExpireLegal: denomIn.stamp_expire_legal,
+ stampExpireWithdraw: denomIn.stamp_expire_withdraw,
+ stampStart: denomIn.stamp_start,
+ status: DenominationStatus.Unverified,
+ isOffered: true,
+ value: denomIn.value,
+ };
+ return d;
+ }
+
+ async withdrawPaybackReserve(reservePub: string): Promise<void> {
+ let reserve = await this.q().get(Stores.reserves, reservePub);
+ if (!reserve) {
+ throw Error(`Reserve ${reservePub} does not exist`);
+ }
+ reserve.hasPayback = false;
+ await this.q().put(Stores.reserves, reserve);
+ this.depleteReserve(reserve);
+ }
+
+ async getPaybackReserves(): Promise<ReserveRecord[]> {
+ return await this.q().iter(Stores.reserves).filter(r => r.hasPayback).toArray()
+ }
+
}
diff --git a/src/wxApi.ts b/src/wxApi.ts
index bdc02af1b..0f460085e 100644
--- a/src/wxApi.ts
+++ b/src/wxApi.ts
@@ -84,6 +84,14 @@ export async function getReserves(exchangeBaseUrl: string): Promise<ReserveRecor
return await callBackend("get-reserves", { exchangeBaseUrl });
}
+export async function getPaybackReserves(): Promise<ReserveRecord[]> {
+ return await callBackend("get-payback-reserves");
+}
+
+export async function withdrawPaybackReserve(reservePub: string): Promise<ReserveRecord[]> {
+ return await callBackend("withdraw-payback-reserve", { reservePub });
+}
+
export async function getCoins(exchangeBaseUrl: string): Promise<CoinRecord[]> {
return await callBackend("get-coins", { exchangeBaseUrl });
}
diff --git a/src/wxBackend.ts b/src/wxBackend.ts
index 716dc66be..1588ec857 100644
--- a/src/wxBackend.ts
+++ b/src/wxBackend.ts
@@ -36,7 +36,7 @@ import URI = require("urijs");
"use strict";
const DB_NAME = "taler";
-const DB_VERSION = 16;
+const DB_VERSION = 17;
import {Stores} from "./wallet";
import {Store, Index} from "./query";
@@ -226,6 +226,15 @@ function makeHandlers(db: IDBDatabase,
}
return wallet.getReserves(detail.exchangeBaseUrl);
},
+ ["get-payback-reserves"]: function (detail, sender) {
+ return wallet.getPaybackReserves();
+ },
+ ["withdraw-payback-reserve"]: function (detail, sender) {
+ if (typeof detail.reservePub !== "string") {
+ return Promise.reject(Error("reservePub missing"));
+ }
+ return wallet.withdrawPaybackReserve(detail.reservePub);
+ },
["get-coins"]: function (detail, sender) {
if (typeof detail.exchangeBaseUrl !== "string") {
return Promise.reject(Error("exchangBaseUrl missing"));
diff --git a/tsconfig.json b/tsconfig.json
index 8dc8cb721..67bb4f847 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -43,6 +43,7 @@
"src/pages/confirm-create-reserve.tsx",
"src/pages/error.tsx",
"src/pages/logs.tsx",
+ "src/pages/payback.tsx",
"src/pages/popup.tsx",
"src/pages/show-db.ts",
"src/pages/tree.tsx",
diff --git a/webpack.config.js b/webpack.config.js
index 2a1f13dce..429591220 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -62,6 +62,7 @@ module.exports = function (env) {
"popup": "./src/pages/popup.tsx",
"show-db": "./src/pages/show-db.ts",
"tree": "./src/pages/tree.tsx",
+ "payback": "./src/pages/payback.tsx",
},
plugins: [
new webpack.optimize.CommonsChunkPlugin({