implement payback (with rudimentary UI)
This commit is contained in:
parent
bb6d8317a5
commit
4c03a1200e
@ -1,42 +1,46 @@
|
|||||||
import {CryptoApi} from "./cryptoApi";
|
import {CryptoApi} from "./cryptoApi";
|
||||||
import {ReserveRecord, DenominationRecord, denominationRecordFromKeys} from "./types";
|
import {ReserveRecord, DenominationRecord, DenominationStatus} from "./types";
|
||||||
import {test, TestLib} from "talertest";
|
import {test, TestLib} from "talertest";
|
||||||
|
|
||||||
let masterPub1: string = "CQQZ9DY3MZ1ARMN5K1VKDETS04Y2QCKMMCFHZSWJWWVN82BTTH00";
|
let masterPub1: string = "CQQZ9DY3MZ1ARMN5K1VKDETS04Y2QCKMMCFHZSWJWWVN82BTTH00";
|
||||||
|
|
||||||
let denomValid1: DenominationRecord = denominationRecordFromKeys("https://example.com/exchange", {
|
let denomValid1: DenominationRecord = {
|
||||||
"master_sig": "CJFJCQ48Q45PSGJ5KY94N6M2TPARESM2E15BSPBD95YVVPEARAEQ6V6G4Z2XBMS0QM0F3Y9EYVP276FCS90EQ1578ZC8JHFBZ3NGP3G",
|
masterSig: "CJFJCQ48Q45PSGJ5KY94N6M2TPARESM2E15BSPBD95YVVPEARAEQ6V6G4Z2XBMS0QM0F3Y9EYVP276FCS90EQ1578ZC8JHFBZ3NGP3G",
|
||||||
"stamp_start": "/Date(1473148381)/",
|
stampStart: "/Date(1473148381)/",
|
||||||
"stamp_expire_withdraw": "/Date(2482300381)/",
|
stampExpireWithdraw: "/Date(2482300381)/",
|
||||||
"stamp_expire_deposit": "/Date(1851580381)/",
|
stampExpireDeposit: "/Date(1851580381)/",
|
||||||
"denom_pub": "51R7ARKCD5HJTTV5F4G0M818E9SP280A40G2GVH04CR30GHS84R3JHHP6GSM2D9Q6514CGT568R32C9J6CWM4DSH64TM4DSM851K0CA48CVKAC1P6H144C2160T46DHK8CVM4HJ274S38C1M6S338D9N6GWM8DT684T3JCT36S13EC9G88R3EGHQ8S0KJGSQ60SKGD216N33AGJ2651K2E9S60TMCD1N75244HHQ6X33EDJ570R3GGJ2651MACA38D130DA560VK4HHJ68WK2CA26GW3ECSH6D13EC9S88VK2GT66WVK8D9G750K0D9R8RRK4DHQ71332GHK8D23GE26710M2H9K6WVK8HJ38MVKEGA66N23AC9H88VKACT58MV3CCSJ6H1K4DT38GRK0C9M8N33CE1R60V4AHA38H1KECSH6S33JH9N8GRKGH1K68S36GH354520818CMG26C1H60R30C935452081918G2J2G0",
|
denomPub: "51R7ARKCD5HJTTV5F4G0M818E9SP280A40G2GVH04CR30GHS84R3JHHP6GSM2D9Q6514CGT568R32C9J6CWM4DSH64TM4DSM851K0CA48CVKAC1P6H144C2160T46DHK8CVM4HJ274S38C1M6S338D9N6GWM8DT684T3JCT36S13EC9G88R3EGHQ8S0KJGSQ60SKGD216N33AGJ2651K2E9S60TMCD1N75244HHQ6X33EDJ570R3GGJ2651MACA38D130DA560VK4HHJ68WK2CA26GW3ECSH6D13EC9S88VK2GT66WVK8D9G750K0D9R8RRK4DHQ71332GHK8D23GE26710M2H9K6WVK8HJ38MVKEGA66N23AC9H88VKACT58MV3CCSJ6H1K4DT38GRK0C9M8N33CE1R60V4AHA38H1KECSH6S33JH9N8GRKGH1K68S36GH354520818CMG26C1H60R30C935452081918G2J2G0",
|
||||||
"stamp_expire_legal": "/Date(1567756381)/",
|
stampExpireLegal: "/Date(1567756381)/",
|
||||||
"value": {
|
value: {
|
||||||
"currency": "PUDOS",
|
"currency": "PUDOS",
|
||||||
"value": 0,
|
"value": 0,
|
||||||
"fraction": 100000
|
"fraction": 100000
|
||||||
},
|
},
|
||||||
"fee_withdraw": {
|
feeWithdraw: {
|
||||||
"currency": "PUDOS",
|
"currency": "PUDOS",
|
||||||
"value": 0,
|
"value": 0,
|
||||||
"fraction": 10000
|
"fraction": 10000
|
||||||
},
|
},
|
||||||
"fee_deposit": {
|
feeDeposit: {
|
||||||
"currency": "PUDOS",
|
"currency": "PUDOS",
|
||||||
"value": 0,
|
"value": 0,
|
||||||
"fraction": 10000
|
"fraction": 10000
|
||||||
},
|
},
|
||||||
"fee_refresh": {
|
feeRefresh: {
|
||||||
"currency": "PUDOS",
|
"currency": "PUDOS",
|
||||||
"value": 0,
|
"value": 0,
|
||||||
"fraction": 10000
|
"fraction": 10000
|
||||||
},
|
},
|
||||||
"fee_refund": {
|
feeRefund: {
|
||||||
"currency": "PUDOS",
|
"currency": "PUDOS",
|
||||||
"value": 0,
|
"value": 0,
|
||||||
"fraction": 10000
|
"fraction": 10000
|
||||||
}
|
},
|
||||||
});
|
denomPubHash: "dummy",
|
||||||
|
status: DenominationStatus.Unverified,
|
||||||
|
isOffered: true,
|
||||||
|
exchangeBaseUrl: "https://exchange.example.com/",
|
||||||
|
};
|
||||||
|
|
||||||
let denomInvalid1 = JSON.parse(JSON.stringify(denomValid1));
|
let denomInvalid1 = JSON.parse(JSON.stringify(denomValid1));
|
||||||
denomInvalid1.value.value += 1;
|
denomInvalid1.value.value += 1;
|
||||||
@ -55,6 +59,7 @@ test("precoin creation", async (t: TestLib) => {
|
|||||||
let r: ReserveRecord = {
|
let r: ReserveRecord = {
|
||||||
reserve_pub: pub,
|
reserve_pub: pub,
|
||||||
reserve_priv: priv,
|
reserve_priv: priv,
|
||||||
|
hasPayback: false,
|
||||||
exchange_base_url: "https://example.com/exchange",
|
exchange_base_url: "https://example.com/exchange",
|
||||||
created: 0,
|
created: 0,
|
||||||
requested_amount: {currency: "PUDOS", value: 0, fraction: 0},
|
requested_amount: {currency: "PUDOS", value: 0, fraction: 0},
|
||||||
|
@ -22,13 +22,20 @@
|
|||||||
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
PreCoinRecord, CoinRecord, ReserveRecord, AmountJson,
|
PreCoinRecord,
|
||||||
DenominationRecord
|
CoinRecord,
|
||||||
|
ReserveRecord,
|
||||||
|
AmountJson,
|
||||||
|
DenominationRecord,
|
||||||
|
PaybackRequest,
|
||||||
|
RefreshSessionRecord,
|
||||||
|
WireFee,
|
||||||
|
PayCoinInfo,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import {OfferRecord} from "./wallet";
|
import {
|
||||||
import {CoinWithDenom} from "./wallet";
|
OfferRecord,
|
||||||
import {PayCoinInfo} from "./types";
|
CoinWithDenom,
|
||||||
import {RefreshSessionRecord, WireFee} from "./types";
|
} from "./wallet";
|
||||||
|
|
||||||
|
|
||||||
interface WorkerState {
|
interface WorkerState {
|
||||||
@ -230,6 +237,10 @@ export class CryptoApi {
|
|||||||
return this.doRpc<string>("hashString", 1, str);
|
return this.doRpc<string>("hashString", 1, str);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hashDenomPub(denomPub: string): Promise<string> {
|
||||||
|
return this.doRpc<string>("hashDenomPub", 1, denomPub);
|
||||||
|
}
|
||||||
|
|
||||||
isValidDenom(denom: DenominationRecord,
|
isValidDenom(denom: DenominationRecord,
|
||||||
masterPub: string): Promise<boolean> {
|
masterPub: string): Promise<boolean> {
|
||||||
return this.doRpc<boolean>("isValidDenom", 2, denom, masterPub);
|
return this.doRpc<boolean>("isValidDenom", 2, denom, masterPub);
|
||||||
@ -256,6 +267,10 @@ export class CryptoApi {
|
|||||||
return this.doRpc<string>("rsaUnblind", 4, sig, bk, pk);
|
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,
|
createRefreshSession(exchangeBaseUrl: string,
|
||||||
kappa: number,
|
kappa: number,
|
||||||
meltCoin: CoinRecord,
|
meltCoin: CoinRecord,
|
||||||
|
@ -23,8 +23,14 @@
|
|||||||
|
|
||||||
import * as native from "./emscriptif";
|
import * as native from "./emscriptif";
|
||||||
import {
|
import {
|
||||||
PreCoinRecord, PayCoinInfo, AmountJson,
|
PreCoinRecord,
|
||||||
RefreshSessionRecord, RefreshPreCoinRecord, ReserveRecord, CoinStatus,
|
PayCoinInfo,
|
||||||
|
AmountJson,
|
||||||
|
RefreshSessionRecord,
|
||||||
|
RefreshPreCoinRecord,
|
||||||
|
ReserveRecord,
|
||||||
|
CoinStatus,
|
||||||
|
PaybackRequest,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import create = chrome.alarms.create;
|
import create = chrome.alarms.create;
|
||||||
import {OfferRecord} from "./wallet";
|
import {OfferRecord} from "./wallet";
|
||||||
@ -96,8 +102,29 @@ namespace RpcFunctions {
|
|||||||
return preCoin;
|
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({
|
let p = new native.PaymentSignaturePS({
|
||||||
contract_hash: native.HashCode.fromCrock(contractHash),
|
contract_hash: native.HashCode.fromCrock(contractHash),
|
||||||
});
|
});
|
||||||
@ -366,6 +393,10 @@ namespace RpcFunctions {
|
|||||||
const b = native.ByteArray.fromStringWithNull(str);
|
const b = native.ByteArray.fromStringWithNull(str);
|
||||||
return b.hash().toCrock();
|
return b.hash().toCrock();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function hashDenomPub(denomPub: string): string {
|
||||||
|
return native.RsaPublicKey.fromCrock(denomPub).encode().hash().toCrock();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -208,6 +208,7 @@ export enum SignaturePurpose {
|
|||||||
TEST = 4242,
|
TEST = 4242,
|
||||||
MERCHANT_PAYMENT_OK = 1104,
|
MERCHANT_PAYMENT_OK = 1104,
|
||||||
MASTER_WIRE_FEES = 1028,
|
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 {
|
interface RefreshMeltCoinAffirmationPS_Args {
|
||||||
session_hash: HashCode;
|
session_hash: HashCode;
|
||||||
amount_with_fee: AmountNbo;
|
amount_with_fee: AmountNbo;
|
||||||
|
37
src/pages/payback.html
Normal file
37
src/pages/payback.html
Normal file
@ -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>
|
99
src/pages/payback.tsx
Normal file
99
src/pages/payback.tsx
Normal file
@ -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);
|
@ -299,8 +299,12 @@ class WalletBalanceView extends React.Component<any, any> {
|
|||||||
return <span></span>;
|
return <span></span>;
|
||||||
}
|
}
|
||||||
console.log(wallet);
|
console.log(wallet);
|
||||||
|
let paybackAvailable = false;
|
||||||
let listing = Object.keys(wallet).map((key) => {
|
let listing = Object.keys(wallet).map((key) => {
|
||||||
let entry: WalletBalanceEntry = wallet[key];
|
let entry: WalletBalanceEntry = wallet[key];
|
||||||
|
if (entry.paybackAmount.value != 0 || entry.paybackAmount.fraction != 0) {
|
||||||
|
paybackAvailable = true;
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<p>
|
<p>
|
||||||
{bigAmount(entry.available)}
|
{bigAmount(entry.available)}
|
||||||
@ -311,9 +315,12 @@ class WalletBalanceView extends React.Component<any, any> {
|
|||||||
});
|
});
|
||||||
let link = chrome.extension.getURL("/src/pages/auditors.html");
|
let link = chrome.extension.getURL("/src/pages/auditors.html");
|
||||||
let linkElem = <a className="actionLink" href={link} target="_blank">Trusted Auditors and Exchanges</a>;
|
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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{listing.length > 0 ? listing : this.renderEmpty()}
|
{listing.length > 0 ? listing : this.renderEmpty()}
|
||||||
|
{paybackAvailable && paybackLinkElem}
|
||||||
{linkElem}
|
{linkElem}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
90
src/types.ts
90
src/types.ts
@ -73,7 +73,13 @@ export interface ReserveRecord {
|
|||||||
precoin_amount: AmountJson;
|
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 {
|
export interface AuditorRecord {
|
||||||
@ -127,6 +133,9 @@ export class DenominationRecord {
|
|||||||
@Checkable.String
|
@Checkable.String
|
||||||
denomPub: string;
|
denomPub: string;
|
||||||
|
|
||||||
|
@Checkable.String
|
||||||
|
denomPubHash: string;
|
||||||
|
|
||||||
@Checkable.Value(AmountJson)
|
@Checkable.Value(AmountJson)
|
||||||
feeWithdraw: AmountJson;
|
feeWithdraw: AmountJson;
|
||||||
|
|
||||||
@ -276,27 +285,65 @@ export interface RefreshPreCoinRecord {
|
|||||||
publicKey: string;
|
publicKey: string;
|
||||||
privateKey: string;
|
privateKey: string;
|
||||||
coinEv: string;
|
coinEv: string;
|
||||||
blindingKey: string
|
blindingKey: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function denominationRecordFromKeys(exchangeBaseUrl: string, denomIn: Denomination): DenominationRecord {
|
export interface PaybackRequest {
|
||||||
let d: DenominationRecord = {
|
denom_pub: string;
|
||||||
denomPub: denomIn.denom_pub,
|
|
||||||
exchangeBaseUrl: exchangeBaseUrl,
|
/**
|
||||||
feeDeposit: denomIn.fee_deposit,
|
* Signature over the coin public key by the denomination.
|
||||||
masterSig: denomIn.master_sig,
|
*/
|
||||||
feeRefund: denomIn.fee_refund,
|
denom_sig: string;
|
||||||
feeRefresh: denomIn.fee_refresh,
|
|
||||||
feeWithdraw: denomIn.fee_withdraw,
|
coin_pub: string;
|
||||||
stampExpireDeposit: denomIn.stamp_expire_deposit,
|
|
||||||
stampExpireLegal: denomIn.stamp_expire_legal,
|
coin_blind_key_secret: string;
|
||||||
stampExpireWithdraw: denomIn.stamp_expire_withdraw,
|
|
||||||
stampStart: denomIn.stamp_start,
|
coin_sig: string;
|
||||||
status: DenominationStatus.Unverified,
|
}
|
||||||
isOffered: true,
|
|
||||||
value: denomIn.value,
|
@Checkable.Class
|
||||||
};
|
export class PaybackConfirmation {
|
||||||
return d;
|
/**
|
||||||
|
* 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 {
|
export enum CoinStatus {
|
||||||
Fresh, TransactionPending, Dirty, Refreshed,
|
Fresh, TransactionPending, Dirty, Refreshed, PaybackPending, PaybackDone,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -440,6 +487,7 @@ export interface WalletBalanceEntry {
|
|||||||
available: AmountJson;
|
available: AmountJson;
|
||||||
pendingIncoming: AmountJson;
|
pendingIncoming: AmountJson;
|
||||||
pendingPayment: AmountJson;
|
pendingPayment: AmountJson;
|
||||||
|
paybackAmount: AmountJson;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
137
src/wallet.ts
137
src/wallet.ts
@ -44,8 +44,11 @@ import {
|
|||||||
WalletBalanceEntry,
|
WalletBalanceEntry,
|
||||||
WireFee,
|
WireFee,
|
||||||
ExchangeWireFeesRecord,
|
ExchangeWireFeesRecord,
|
||||||
WireInfo, DenominationRecord, DenominationStatus, denominationRecordFromKeys,
|
WireInfo,
|
||||||
|
DenominationRecord,
|
||||||
|
DenominationStatus,
|
||||||
CoinStatus,
|
CoinStatus,
|
||||||
|
PaybackConfirmation,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import {
|
import {
|
||||||
HttpRequestLibrary,
|
HttpRequestLibrary,
|
||||||
@ -410,6 +413,7 @@ export namespace Stores {
|
|||||||
}
|
}
|
||||||
|
|
||||||
exchangeBaseUrlIndex = new Index<string,CoinRecord>(this, "exchangeBaseUrl", "exchangeBaseUrl");
|
exchangeBaseUrlIndex = new Index<string,CoinRecord>(this, "exchangeBaseUrl", "exchangeBaseUrl");
|
||||||
|
denomPubIndex = new Index<string,CoinRecord>(this, "denomPub", "denomPub");
|
||||||
}
|
}
|
||||||
|
|
||||||
class HistoryStore extends Store<HistoryRecord> {
|
class HistoryStore extends Store<HistoryRecord> {
|
||||||
@ -448,6 +452,7 @@ export namespace Stores {
|
|||||||
{keyPath: ["exchangeBaseUrl", "denomPub"] as any as IDBKeyPath});
|
{keyPath: ["exchangeBaseUrl", "denomPub"] as any as IDBKeyPath});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
denomPubHashIndex = new Index<string,DenominationRecord>(this, "denomPubHash", "denomPubHash");
|
||||||
exchangeBaseUrlIndex = new Index<string, DenominationRecord>(this, "exchangeBaseUrl", "exchangeBaseUrl");
|
exchangeBaseUrlIndex = new Index<string, DenominationRecord>(this, "exchangeBaseUrl", "exchangeBaseUrl");
|
||||||
denomPubIndex = new Index<string, DenominationRecord>(this, "denomPub", "denomPub");
|
denomPubIndex = new Index<string, DenominationRecord>(this, "denomPub", "denomPub");
|
||||||
}
|
}
|
||||||
@ -894,9 +899,8 @@ export class Wallet {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
let exchange = await this.updateExchangeFromUrl(reserveRecord.exchange_base_url);
|
let exchange = await this.updateExchangeFromUrl(reserveRecord.exchange_base_url);
|
||||||
let reserve = await this.updateReserve(reserveRecord.reserve_pub,
|
let reserve = await this.updateReserve(reserveRecord.reserve_pub);
|
||||||
exchange);
|
let n = await this.depleteReserve(reserve);
|
||||||
let n = await this.depleteReserve(reserve, exchange);
|
|
||||||
|
|
||||||
if (n != 0) {
|
if (n != 0) {
|
||||||
let depleted: HistoryRecord = {
|
let depleted: HistoryRecord = {
|
||||||
@ -1013,6 +1017,7 @@ export class Wallet {
|
|||||||
const canonExchange = canonicalizeBaseUrl(req.exchange);
|
const canonExchange = canonicalizeBaseUrl(req.exchange);
|
||||||
|
|
||||||
const reserveRecord: ReserveRecord = {
|
const reserveRecord: ReserveRecord = {
|
||||||
|
hasPayback: false,
|
||||||
reserve_pub: keypair.pub,
|
reserve_pub: keypair.pub,
|
||||||
reserve_priv: keypair.priv,
|
reserve_priv: keypair.priv,
|
||||||
exchange_base_url: canonExchange,
|
exchange_base_url: canonExchange,
|
||||||
@ -1148,8 +1153,7 @@ export class Wallet {
|
|||||||
/**
|
/**
|
||||||
* Withdraw coins from a reserve until it is empty.
|
* Withdraw coins from a reserve until it is empty.
|
||||||
*/
|
*/
|
||||||
private async depleteReserve(reserve: ReserveRecord,
|
private async depleteReserve(reserve: ReserveRecord): Promise<number> {
|
||||||
exchange: ExchangeRecord): Promise<number> {
|
|
||||||
console.log("depleting reserve");
|
console.log("depleting reserve");
|
||||||
if (!reserve.current_amount) {
|
if (!reserve.current_amount) {
|
||||||
throw Error("can't withdraw when amount is unknown");
|
throw Error("can't withdraw when amount is unknown");
|
||||||
@ -1158,7 +1162,7 @@ export class Wallet {
|
|||||||
if (!currentAmount) {
|
if (!currentAmount) {
|
||||||
throw Error("can't withdraw when amount is unknown");
|
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);
|
currentAmount);
|
||||||
|
|
||||||
console.log(`withdrawing ${denomsForWithdraw.length} coins`);
|
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
|
* Update the information about a reserve that is stored in the wallet
|
||||||
* by quering the reserve's exchange.
|
* by quering the reserve's exchange.
|
||||||
*/
|
*/
|
||||||
private async updateReserve(reservePub: string,
|
private async updateReserve(reservePub: string): Promise<ReserveRecord> {
|
||||||
exchange: ExchangeRecord): Promise<ReserveRecord> {
|
|
||||||
let reserve = await this.q()
|
let reserve = await this.q()
|
||||||
.get<ReserveRecord>(Stores.reserves, reservePub);
|
.get<ReserveRecord>(Stores.reserves, reservePub);
|
||||||
if (!reserve) {
|
if (!reserve) {
|
||||||
throw Error("reserve not in db");
|
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});
|
reqUrl.query({'reserve_pub': reservePub});
|
||||||
let resp = await this.http.get(reqUrl.href());
|
let resp = await this.http.get(reqUrl.href());
|
||||||
if (resp.status != 200) {
|
if (resp.status != 200) {
|
||||||
@ -1549,6 +1552,20 @@ export class Wallet {
|
|||||||
|
|
||||||
await this.q().put(Stores.exchangeWireFees, oldWireFees);
|
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;
|
return updatedExchangeInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1571,7 +1588,7 @@ export class Wallet {
|
|||||||
const newAndUnseenDenoms: typeof existingDenoms = {};
|
const newAndUnseenDenoms: typeof existingDenoms = {};
|
||||||
|
|
||||||
for (let d of newKeys.denoms) {
|
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)) {
|
if (!(d.denom_pub in existingDenoms)) {
|
||||||
newAndUnseenDenoms[dr.denomPub] = dr;
|
newAndUnseenDenoms[dr.denomPub] = dr;
|
||||||
}
|
}
|
||||||
@ -1608,6 +1625,7 @@ export class Wallet {
|
|||||||
available: z,
|
available: z,
|
||||||
pendingIncoming: z,
|
pendingIncoming: z,
|
||||||
pendingPayment: z,
|
pendingPayment: z,
|
||||||
|
paybackAmount: z,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return entry;
|
return entry;
|
||||||
@ -1643,6 +1661,17 @@ export class Wallet {
|
|||||||
return balance;
|
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,
|
function collectPendingRefresh(r: RefreshSessionRecord,
|
||||||
balance: WalletBalance) {
|
balance: WalletBalance) {
|
||||||
// Don't count finished refreshes, since the refresh already resulted
|
// Don't count finished refreshes, since the refresh already resulted
|
||||||
@ -1699,6 +1728,8 @@ export class Wallet {
|
|||||||
.reduce(collectPendingRefresh, balance);
|
.reduce(collectPendingRefresh, balance);
|
||||||
tx.iter(Stores.reserves)
|
tx.iter(Stores.reserves)
|
||||||
.reduce(collectPendingWithdraw, balance);
|
.reduce(collectPendingWithdraw, balance);
|
||||||
|
tx.iter(Stores.reserves)
|
||||||
|
.reduce(collectPaybacks, balance);
|
||||||
tx.iter(Stores.transactions)
|
tx.iter(Stores.transactions)
|
||||||
.reduce(collectPayments, balance);
|
.reduce(collectPayments, balance);
|
||||||
await tx.finish();
|
await tx.finish();
|
||||||
@ -2085,4 +2116,88 @@ export class Wallet {
|
|||||||
doPaymentSucceeded();
|
doPaymentSucceeded();
|
||||||
return;
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -84,6 +84,14 @@ export async function getReserves(exchangeBaseUrl: string): Promise<ReserveRecor
|
|||||||
return await callBackend("get-reserves", { exchangeBaseUrl });
|
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[]> {
|
export async function getCoins(exchangeBaseUrl: string): Promise<CoinRecord[]> {
|
||||||
return await callBackend("get-coins", { exchangeBaseUrl });
|
return await callBackend("get-coins", { exchangeBaseUrl });
|
||||||
}
|
}
|
||||||
|
@ -36,7 +36,7 @@ import URI = require("urijs");
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
const DB_NAME = "taler";
|
const DB_NAME = "taler";
|
||||||
const DB_VERSION = 16;
|
const DB_VERSION = 17;
|
||||||
|
|
||||||
import {Stores} from "./wallet";
|
import {Stores} from "./wallet";
|
||||||
import {Store, Index} from "./query";
|
import {Store, Index} from "./query";
|
||||||
@ -226,6 +226,15 @@ function makeHandlers(db: IDBDatabase,
|
|||||||
}
|
}
|
||||||
return wallet.getReserves(detail.exchangeBaseUrl);
|
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) {
|
["get-coins"]: function (detail, sender) {
|
||||||
if (typeof detail.exchangeBaseUrl !== "string") {
|
if (typeof detail.exchangeBaseUrl !== "string") {
|
||||||
return Promise.reject(Error("exchangBaseUrl missing"));
|
return Promise.reject(Error("exchangBaseUrl missing"));
|
||||||
|
@ -43,6 +43,7 @@
|
|||||||
"src/pages/confirm-create-reserve.tsx",
|
"src/pages/confirm-create-reserve.tsx",
|
||||||
"src/pages/error.tsx",
|
"src/pages/error.tsx",
|
||||||
"src/pages/logs.tsx",
|
"src/pages/logs.tsx",
|
||||||
|
"src/pages/payback.tsx",
|
||||||
"src/pages/popup.tsx",
|
"src/pages/popup.tsx",
|
||||||
"src/pages/show-db.ts",
|
"src/pages/show-db.ts",
|
||||||
"src/pages/tree.tsx",
|
"src/pages/tree.tsx",
|
||||||
|
@ -62,6 +62,7 @@ module.exports = function (env) {
|
|||||||
"popup": "./src/pages/popup.tsx",
|
"popup": "./src/pages/popup.tsx",
|
||||||
"show-db": "./src/pages/show-db.ts",
|
"show-db": "./src/pages/show-db.ts",
|
||||||
"tree": "./src/pages/tree.tsx",
|
"tree": "./src/pages/tree.tsx",
|
||||||
|
"payback": "./src/pages/payback.tsx",
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
new webpack.optimize.CommonsChunkPlugin({
|
new webpack.optimize.CommonsChunkPlugin({
|
||||||
|
Loading…
Reference in New Issue
Block a user