show pending incoming amount

This commit is contained in:
Florian Dold 2016-10-19 18:40:29 +02:00
parent 2780418c3e
commit b0b737f72e
5 changed files with 267 additions and 39 deletions

View File

@ -324,6 +324,11 @@ namespace RpcFunctions {
native.EddsaPrivateKey.fromCrock(
meltCoin.coinPriv)).toCrock();
let valueOutput = Amounts.getZero(newCoinDenoms[0].value.currency);
for (let denom of newCoinDenoms) {
valueOutput = Amounts.add(valueOutput, denom.value).amount;
}
let refreshSession: RefreshSession = {
meltCoinPub: meltCoin.coinPub,
newDenoms: newCoinDenoms.map((d) => d.denom_pub),
@ -336,6 +341,7 @@ namespace RpcFunctions {
exchangeBaseUrl,
transferPrivs,
finished: false,
valueOutput,
};
return refreshSession;

View File

@ -24,12 +24,19 @@
"use strict";
export interface JoinResult<L,R> {
left: L;
right: R;
}
export class Store<T> {
name: string;
validator?: (v: T) => T;
storeParams: IDBObjectStoreParameters;
constructor(name: string, storeParams: IDBObjectStoreParameters, validator?: (v: T) => T) {
constructor(name: string, storeParams: IDBObjectStoreParameters,
validator?: (v: T) => T) {
this.name = name;
this.validator = validator;
this.storeParams = storeParams;
@ -54,12 +61,15 @@ export class Index<S extends IDBValidKey,T> {
export interface QueryStream<T> {
indexJoin<S,I extends IDBValidKey>(index: Index<I,S>,
keyFn: (obj: T) => I): QueryStream<[T, S]>;
filter(f: (x: any) => boolean): QueryStream<T>;
keyJoin<S,I extends IDBValidKey>(store: Store<S>,
keyFn: (obj: T) => I): QueryStream<JoinResult<T,S>>;
filter(f: (T: any) => boolean): QueryStream<T>;
reduce<S>(f: (v: T, acc: S) => S, start?: S): Promise<S>;
flatMap(f: (x: T) => T[]): QueryStream<T>;
toArray(): Promise<T[]>;
}
export let AbortTransaction = Symbol("abort_transaction");
/**
* Get an unresolved promise together with its extracted resolve / reject
@ -101,6 +111,12 @@ abstract class QueryStreamBase<T> implements QueryStream<T> {
return new QueryStreamIndexJoin(this, index.storeName, index.indexName, keyFn);
}
keyJoin<S, I extends IDBValidKey>(store: Store<S>,
keyFn: (obj: T) => I): QueryStream<JoinResult<T, S>> {
this.root.addStoreAccess(store.name, false);
return new QueryStreamKeyJoin(this, store.name, keyFn);
}
filter(f: (x: any) => boolean): QueryStream<T> {
return new QueryStreamFilter(this, f);
}
@ -234,6 +250,42 @@ class QueryStreamIndexJoin<T, S> extends QueryStreamBase<[T, S]> {
}
class QueryStreamKeyJoin<T, S> extends QueryStreamBase<JoinResult<T, S>> {
s: QueryStreamBase<T>;
storeName: string;
key: any;
constructor(s: QueryStreamBase<T>, storeName: string,
key: any) {
super(s.root);
this.s = s;
this.storeName = storeName;
this.key = key;
}
subscribe(f: SubscribeFn) {
this.s.subscribe((isDone, value, tx) => {
if (isDone) {
f(true, undefined, tx);
return;
}
console.log("joining on", this.key(value));
let s = tx.objectStore(this.storeName);
let req = s.openCursor(IDBKeyRange.only(this.key(value)));
req.onsuccess = () => {
let cursor = req.result;
if (cursor) {
f(false, {left:value, right: cursor.value}, tx);
cursor.continue();
} else {
f(true, undefined, tx);
}
}
});
}
}
class IterQueryStream<T> extends QueryStreamBase<T> {
private storeName: string;
private options: any;
@ -304,7 +356,8 @@ export class QueryRoot {
return new IterQueryStream(this, store.name, {});
}
iterIndex<S extends IDBValidKey,T>(index: Index<S,T>, only?: S): QueryStream<T> {
iterIndex<S extends IDBValidKey,T>(index: Index<S,T>,
only?: S): QueryStream<T> {
this.stores.add(index.storeName);
return new IterQueryStream(this, index.storeName, {
only,
@ -326,6 +379,30 @@ export class QueryRoot {
}
mutate<T>(store: Store<T>, key: any, f: (v: T) => T): QueryRoot {
let doPut = (tx: IDBTransaction) => {
let reqGet = tx.objectStore(store.name).get(key);
reqGet.onsuccess = () => {
let r = reqGet.result;
let m: T;
try {
m = f(r);
} catch (e) {
if (e == AbortTransaction) {
tx.abort();
return;
}
throw e;
}
tx.objectStore(store.name).put(m);
}
};
this.addWork(doPut, store.name, true);
return this;
}
/**
* Add all object from an iterable to the given object store.
* Fails if the object's key is already present
@ -380,7 +457,8 @@ export class QueryRoot {
/**
* Get one object from a store by its key.
*/
getIndexed<I extends IDBValidKey,T>(index: Index<I,T>, key: I): Promise<T|undefined> {
getIndexed<I extends IDBValidKey,T>(index: Index<I,T>,
key: I): Promise<T|undefined> {
if (key === void 0) {
throw Error("key must not be undefined");
}
@ -388,7 +466,9 @@ export class QueryRoot {
const {resolve, promise} = openPromise();
const doGetIndexed = (tx: IDBTransaction) => {
const req = tx.objectStore(index.storeName).index(index.indexName).get(key);
const req = tx.objectStore(index.storeName)
.index(index.indexName)
.get(key);
req.onsuccess = () => {
resolve(req.result);
};
@ -417,6 +497,9 @@ export class QueryRoot {
tx.oncomplete = () => {
resolve();
};
tx.onabort = () => {
reject(Error("transaction aborted"));
};
for (let w of this.work) {
w(tx);
}

View File

@ -42,6 +42,12 @@ export class AmountJson {
}
export interface SignedAmountJson {
amount: AmountJson;
isNegative: boolean;
}
export interface ReserveRecord {
reserve_pub: string;
reserve_priv: string,
@ -194,6 +200,12 @@ export interface RefreshSession {
*/
valueWithFee: AmountJson
/**
* Sum of the value of denominations we want
* to withdraw in this session, without fees.
*/
valueOutput: AmountJson;
/**
* Signature to confirm the melting.
*/
@ -308,6 +320,15 @@ export class ExchangeHandle {
static checked: (obj: any) => ExchangeHandle;
}
export interface WalletBalance {
[currency: string]: WalletBalanceEntry;
}
export interface WalletBalanceEntry {
available: AmountJson;
pendingIncoming: AmountJson;
}
@Checkable.Class
export class Contract {

View File

@ -27,10 +27,11 @@ import {
IExchangeInfo,
Denomination,
Notifier,
WireInfo, RefreshSession, ReserveRecord, CoinPaySig
WireInfo, RefreshSession, ReserveRecord, CoinPaySig, WalletBalance,
WalletBalanceEntry
} from "./types";
import {HttpResponse, RequestException} from "./http";
import {QueryRoot, Store, Index} from "./query";
import {QueryRoot, Store, Index, JoinResult, AbortTransaction} from "./query";
import {Checkable} from "./checkable";
import {canonicalizeBaseUrl} from "./helpers";
import {ReserveCreationInfo, Amounts} from "./types";
@ -904,11 +905,32 @@ export class Wallet {
console.log("creating pre coin at", new Date());
let preCoin = await this.cryptoApi
.createPreCoin(denom, reserve);
let aborted = false;
function mutateReserve(r: ReserveRecord) {
let currentAmount = r.current_amount;
if (!currentAmount) {
throw Error("can't withdraw from reserve when current amount is" +
" unknown");
}
let x = Amounts.sub(currentAmount, preCoin.coinValue);
if (x.saturated) {
aborted = true;
throw AbortTransaction;
}
r.current_amount = x.amount;
return r;
}
await this.q()
.put(Stores.precoins, preCoin)
.mutate(Stores.reserves, reserve.reserve_pub, mutateReserve)
.finish();
if (!aborted) {
await this.processPreCoin(preCoin);
}
}
/**
@ -1155,26 +1177,99 @@ export class Wallet {
* Retrieve a mapping from currency name to the amount
* that is currenctly available for spending in the wallet.
*/
async getBalances(): Promise<any> {
function collectBalances(c: Coin, byCurrency: any) {
async getBalances(): Promise<WalletBalance> {
function ensureEntry(balance: WalletBalance, currency: string) {
let entry: WalletBalanceEntry|undefined = balance[currency];
let z = Amounts.getZero(currency);
if (!entry) {
balance[currency] = entry = {
available: z,
pendingIncoming: z,
};
}
return entry;
}
function collectBalances(c: Coin, balance: WalletBalance) {
if (c.suspended) {
return byCurrency;
return balance;
}
let acc: AmountJson = byCurrency[c.currentAmount.currency];
if (!acc) {
acc = Amounts.getZero(c.currentAmount.currency);
}
byCurrency[c.currentAmount.currency] = Amounts.add(c.currentAmount,
acc).amount;
return byCurrency;
let currency = c.currentAmount.currency;
let entry = ensureEntry(balance, currency);
entry.available = Amounts.add(entry.available, c.currentAmount).amount;
return balance;
}
let byCurrency = await (
this.q()
function collectPendingWithdraw(r: ReserveRecord, balance: WalletBalance) {
if (!r.confirmed) {
return balance;
}
let entry = ensureEntry(balance, r.requested_amount.currency);
let amount = r.current_amount;
if (!amount) {
amount = r.requested_amount;
}
if (Amounts.cmp(smallestWithdraw[r.exchange_base_url], amount) < 0) {
entry.pendingIncoming = Amounts.add(entry.pendingIncoming,
amount).amount;
}
return balance;
}
function collectPendingRefresh(r: RefreshSession, balance: WalletBalance) {
if (!r.finished) {
return balance;
}
let entry = ensureEntry(balance, r.valueWithFee.currency);
entry.pendingIncoming = Amounts.add(entry.pendingIncoming,
r.valueOutput).amount;
return balance;
}
function collectSmallestWithdraw(e: IExchangeInfo, sw: any) {
let min: AmountJson|undefined;
for (let d of e.active_denoms) {
let v = Amounts.add(d.value, d.fee_withdraw).amount;
if (!min) {
min = v;
continue;
}
if (Amounts.cmp(v, min) < 0) {
min = v;
}
}
sw[e.baseUrl] = min;
return sw;
}
let balance = {};
// Mapping from exchange pub to smallest
// possible amount we can withdraw
let smallestWithdraw: {[baseUrl: string]: AmountJson} = {};
smallestWithdraw = await (this.q()
.iter(Stores.exchanges)
.reduce(collectSmallestWithdraw, {}));
console.log("smallest withdraw", smallestWithdraw);
await (this.q()
.iter(Stores.coins)
.reduce(collectBalances, {}));
.reduce(collectBalances, balance));
await (this.q()
.iter(Stores.refresh)
.reduce(collectPendingRefresh, balance));
console.log("balances collected");
await (this.q()
.iter(Stores.reserves)
.reduce(collectPendingWithdraw, balance));
console.log("balance", balance);
return balance;
return {balances: byCurrency};
}

View File

@ -28,7 +28,10 @@
import {substituteFulfillmentUrl} from "../lib/wallet/helpers";
import BrowserClickedEvent = chrome.browserAction.BrowserClickedEvent;
import {HistoryRecord, HistoryLevel} from "../lib/wallet/wallet";
import {AmountJson} from "../lib/wallet/types";
import {
AmountJson, WalletBalance, Amounts,
WalletBalanceEntry
} from "../lib/wallet/types";
import {abbrev, prettyAmount} from "../lib/wallet/renderHtml";
declare var i18n: any;
@ -105,7 +108,7 @@ export function main() {
<WalletNavBar />
<div style="margin:1em">
<Router>
<WalletBalance route="/balance" default/>
<WalletBalanceView route="/balance" default/>
<WalletHistory route="/history"/>
<WalletDebug route="/debug"/>
</Router>
@ -183,8 +186,8 @@ function ExtensionLink(props: any) {
</a>)
}
class WalletBalance extends preact.Component<any, any> {
myWallet: any;
class WalletBalanceView extends preact.Component<any, any> {
balance: WalletBalance;
gotError = false;
componentWillMount() {
@ -203,22 +206,31 @@ class WalletBalance extends preact.Component<any, any> {
}
this.gotError = false;
console.log("got wallet", resp);
this.myWallet = resp.balances;
this.balance = resp;
this.setState({});
});
}
renderEmpty() : JSX.Element {
renderEmpty(): JSX.Element {
let helpLink = (
<ExtensionLink target="pages/help/empty-wallet.html">
help
</ExtensionLink>
);
return <div>You have no balance to show. Need some {helpLink} getting started?</div>;
return <div>You have no balance to show. Need some {helpLink}
getting started?</div>;
}
formatPending(amount: AmountJson) {
return (
<span>
(<span style="color: darkgreen">{prettyAmount(amount)}</span> pending)
</span>
);
}
render(): JSX.Element {
let wallet = this.myWallet;
let wallet = this.balance;
if (this.gotError) {
return i18n`Error: could not retrieve balance information.`;
}
@ -227,7 +239,18 @@ class WalletBalance extends preact.Component<any, any> {
}
console.log(wallet);
let listing = Object.keys(wallet).map((key) => {
return <p>{prettyAmount(wallet[key])}</p>
let entry: WalletBalanceEntry = wallet[key];
return (
<p>
{prettyAmount(entry.available)}
{ " "}
{Amounts.isNonZero(entry.pendingIncoming)
? this.formatPending(entry.pendingIncoming)
: []
}
</p>
);
});
if (listing.length > 0) {
return <div>{listing}</div>;