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( native.EddsaPrivateKey.fromCrock(
meltCoin.coinPriv)).toCrock(); 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 = { let refreshSession: RefreshSession = {
meltCoinPub: meltCoin.coinPub, meltCoinPub: meltCoin.coinPub,
newDenoms: newCoinDenoms.map((d) => d.denom_pub), newDenoms: newCoinDenoms.map((d) => d.denom_pub),
@ -336,6 +341,7 @@ namespace RpcFunctions {
exchangeBaseUrl, exchangeBaseUrl,
transferPrivs, transferPrivs,
finished: false, finished: false,
valueOutput,
}; };
return refreshSession; return refreshSession;

View File

@ -24,12 +24,19 @@
"use strict"; "use strict";
export interface JoinResult<L,R> {
left: L;
right: R;
}
export class Store<T> { export class Store<T> {
name: string; name: string;
validator?: (v: T) => T; validator?: (v: T) => T;
storeParams: IDBObjectStoreParameters; storeParams: IDBObjectStoreParameters;
constructor(name: string, storeParams: IDBObjectStoreParameters, validator?: (v: T) => T) { constructor(name: string, storeParams: IDBObjectStoreParameters,
validator?: (v: T) => T) {
this.name = name; this.name = name;
this.validator = validator; this.validator = validator;
this.storeParams = storeParams; this.storeParams = storeParams;
@ -53,13 +60,16 @@ export class Index<S extends IDBValidKey,T> {
*/ */
export interface QueryStream<T> { export interface QueryStream<T> {
indexJoin<S,I extends IDBValidKey>(index: Index<I,S>, indexJoin<S,I extends IDBValidKey>(index: Index<I,S>,
keyFn: (obj: T) => I): QueryStream<[T, 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>; reduce<S>(f: (v: T, acc: S) => S, start?: S): Promise<S>;
flatMap(f: (x: T) => T[]): QueryStream<T>; flatMap(f: (x: T) => T[]): QueryStream<T>;
toArray(): Promise<T[]>; toArray(): Promise<T[]>;
} }
export let AbortTransaction = Symbol("abort_transaction");
/** /**
* Get an unresolved promise together with its extracted resolve / reject * Get an unresolved promise together with its extracted resolve / reject
@ -96,11 +106,17 @@ abstract class QueryStreamBase<T> implements QueryStream<T> {
} }
indexJoin<S,I extends IDBValidKey>(index: Index<I,S>, indexJoin<S,I extends IDBValidKey>(index: Index<I,S>,
keyFn: (obj: T) => I): QueryStream<[T, S]> { keyFn: (obj: T) => I): QueryStream<[T, S]> {
this.root.addStoreAccess(index.storeName, false); this.root.addStoreAccess(index.storeName, false);
return new QueryStreamIndexJoin(this, index.storeName, index.indexName, keyFn); 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> { filter(f: (x: any) => boolean): QueryStream<T> {
return new QueryStreamFilter(this, f); 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> { class IterQueryStream<T> extends QueryStreamBase<T> {
private storeName: string; private storeName: string;
private options: any; private options: any;
@ -304,7 +356,8 @@ export class QueryRoot {
return new IterQueryStream(this, store.name, {}); 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); this.stores.add(index.storeName);
return new IterQueryStream(this, index.storeName, { return new IterQueryStream(this, index.storeName, {
only, 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. * Add all object from an iterable to the given object store.
* Fails if the object's key is already present * 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. * 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) { if (key === void 0) {
throw Error("key must not be undefined"); throw Error("key must not be undefined");
} }
@ -388,7 +466,9 @@ export class QueryRoot {
const {resolve, promise} = openPromise(); const {resolve, promise} = openPromise();
const doGetIndexed = (tx: IDBTransaction) => { 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 = () => { req.onsuccess = () => {
resolve(req.result); resolve(req.result);
}; };
@ -417,6 +497,9 @@ export class QueryRoot {
tx.oncomplete = () => { tx.oncomplete = () => {
resolve(); resolve();
}; };
tx.onabort = () => {
reject(Error("transaction aborted"));
};
for (let w of this.work) { for (let w of this.work) {
w(tx); w(tx);
} }

View File

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

View File

@ -27,10 +27,11 @@ import {
IExchangeInfo, IExchangeInfo,
Denomination, Denomination,
Notifier, Notifier,
WireInfo, RefreshSession, ReserveRecord, CoinPaySig WireInfo, RefreshSession, ReserveRecord, CoinPaySig, WalletBalance,
WalletBalanceEntry
} from "./types"; } from "./types";
import {HttpResponse, RequestException} from "./http"; import {HttpResponse, RequestException} from "./http";
import {QueryRoot, Store, Index} from "./query"; import {QueryRoot, Store, Index, JoinResult, AbortTransaction} from "./query";
import {Checkable} from "./checkable"; import {Checkable} from "./checkable";
import {canonicalizeBaseUrl} from "./helpers"; import {canonicalizeBaseUrl} from "./helpers";
import {ReserveCreationInfo, Amounts} from "./types"; import {ReserveCreationInfo, Amounts} from "./types";
@ -904,10 +905,31 @@ export class Wallet {
console.log("creating pre coin at", new Date()); console.log("creating pre coin at", new Date());
let preCoin = await this.cryptoApi let preCoin = await this.cryptoApi
.createPreCoin(denom, reserve); .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() await this.q()
.put(Stores.precoins, preCoin) .put(Stores.precoins, preCoin)
.mutate(Stores.reserves, reserve.reserve_pub, mutateReserve)
.finish(); .finish();
await this.processPreCoin(preCoin); if (!aborted) {
await this.processPreCoin(preCoin);
}
} }
@ -1155,26 +1177,99 @@ export class Wallet {
* Retrieve a mapping from currency name to the amount * Retrieve a mapping from currency name to the amount
* that is currenctly available for spending in the wallet. * that is currenctly available for spending in the wallet.
*/ */
async getBalances(): Promise<any> { async getBalances(): Promise<WalletBalance> {
function collectBalances(c: Coin, byCurrency: any) { function ensureEntry(balance: WalletBalance, currency: string) {
if (c.suspended) { let entry: WalletBalanceEntry|undefined = balance[currency];
return byCurrency; let z = Amounts.getZero(currency);
if (!entry) {
balance[currency] = entry = {
available: z,
pendingIncoming: z,
};
} }
let acc: AmountJson = byCurrency[c.currentAmount.currency]; return entry;
if (!acc) {
acc = Amounts.getZero(c.currentAmount.currency);
}
byCurrency[c.currentAmount.currency] = Amounts.add(c.currentAmount,
acc).amount;
return byCurrency;
} }
let byCurrency = await ( function collectBalances(c: Coin, balance: WalletBalance) {
this.q() if (c.suspended) {
.iter(Stores.coins) return balance;
.reduce(collectBalances, {})); }
let currency = c.currentAmount.currency;
let entry = ensureEntry(balance, currency);
entry.available = Amounts.add(entry.available, c.currentAmount).amount;
return balance;
}
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, 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 {substituteFulfillmentUrl} from "../lib/wallet/helpers";
import BrowserClickedEvent = chrome.browserAction.BrowserClickedEvent; import BrowserClickedEvent = chrome.browserAction.BrowserClickedEvent;
import {HistoryRecord, HistoryLevel} from "../lib/wallet/wallet"; 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"; import {abbrev, prettyAmount} from "../lib/wallet/renderHtml";
declare var i18n: any; declare var i18n: any;
@ -104,11 +107,11 @@ export function main() {
<div> <div>
<WalletNavBar /> <WalletNavBar />
<div style="margin:1em"> <div style="margin:1em">
<Router> <Router>
<WalletBalance route="/balance" default/> <WalletBalanceView route="/balance" default/>
<WalletHistory route="/history"/> <WalletHistory route="/history"/>
<WalletDebug route="/debug"/> <WalletDebug route="/debug"/>
</Router> </Router>
</div> </div>
</div> </div>
); );
@ -183,8 +186,8 @@ function ExtensionLink(props: any) {
</a>) </a>)
} }
class WalletBalance extends preact.Component<any, any> { class WalletBalanceView extends preact.Component<any, any> {
myWallet: any; balance: WalletBalance;
gotError = false; gotError = false;
componentWillMount() { componentWillMount() {
@ -203,22 +206,31 @@ class WalletBalance extends preact.Component<any, any> {
} }
this.gotError = false; this.gotError = false;
console.log("got wallet", resp); console.log("got wallet", resp);
this.myWallet = resp.balances; this.balance = resp;
this.setState({}); this.setState({});
}); });
} }
renderEmpty() : JSX.Element { renderEmpty(): JSX.Element {
let helpLink = ( let helpLink = (
<ExtensionLink target="pages/help/empty-wallet.html"> <ExtensionLink target="pages/help/empty-wallet.html">
help help
</ExtensionLink> </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 { render(): JSX.Element {
let wallet = this.myWallet; let wallet = this.balance;
if (this.gotError) { if (this.gotError) {
return i18n`Error: could not retrieve balance information.`; return i18n`Error: could not retrieve balance information.`;
} }
@ -227,7 +239,18 @@ class WalletBalance extends preact.Component<any, any> {
} }
console.log(wallet); console.log(wallet);
let listing = Object.keys(wallet).map((key) => { 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) { if (listing.length > 0) {
return <div>{listing}</div>; return <div>{listing}</div>;