new balances API, remove defunct 'return funds to own account' implementation

This commit is contained in:
Florian Dold 2020-07-28 23:17:12 +05:30
parent 43655adff0
commit 732e764b37
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
9 changed files with 88 additions and 440 deletions

View File

@ -197,14 +197,7 @@ walletCli
.action(async (args) => {
await withWallet(args, async (wallet) => {
const balance = await wallet.getBalances();
if (args.balance.json) {
console.log(JSON.stringify(balance, undefined, 2));
} else {
const currencies = Object.keys(balance.byCurrency).sort();
for (const c of currencies) {
console.log(Amounts.stringify(balance.byCurrency[c].available));
}
}
console.log(JSON.stringify(balance, undefined, 2));
});
});

View File

@ -17,7 +17,7 @@
/**
* Imports.
*/
import { WalletBalance, WalletBalanceEntry } from "../types/walletTypes";
import { BalancesResponse } from "../types/walletTypes";
import { TransactionHandle } from "../util/query";
import { InternalWalletState } from "./state";
import { Stores, CoinStatus } from "../types/dbTypes";
@ -27,63 +27,49 @@ import { Logger } from "../util/logging";
const logger = new Logger("withdraw.ts");
interface WalletBalance {
available: AmountJson;
pendingIncoming: AmountJson;
pendingOutgoing: AmountJson;
}
/**
* Get balance information.
*/
export async function getBalancesInsideTransaction(
ws: InternalWalletState,
tx: TransactionHandle,
): Promise<WalletBalance> {
): Promise<BalancesResponse> {
const balanceStore: Record<string, WalletBalance> = {};
/**
* Add amount to a balance field, both for
* the slicing by exchange and currency.
*/
function addTo(
balance: WalletBalance,
field: keyof WalletBalanceEntry,
amount: AmountJson,
exchange: string,
): void {
const z = Amounts.getZero(amount.currency);
const balanceIdentity = {
available: z,
paybackAmount: z,
pendingIncoming: z,
pendingPayment: z,
pendingIncomingDirty: z,
pendingIncomingRefresh: z,
pendingIncomingWithdraw: z,
};
let entryCurr = balance.byCurrency[amount.currency];
if (!entryCurr) {
balance.byCurrency[amount.currency] = entryCurr = {
...balanceIdentity,
const initBalance = (currency: string): WalletBalance => {
const b = balanceStore[currency];
if (!b) {
balanceStore[currency] = {
available: Amounts.getZero(currency),
pendingIncoming: Amounts.getZero(currency),
pendingOutgoing: Amounts.getZero(currency),
};
}
let entryEx = balance.byExchange[exchange];
if (!entryEx) {
balance.byExchange[exchange] = entryEx = { ...balanceIdentity };
}
entryCurr[field] = Amounts.add(entryCurr[field], amount).amount;
entryEx[field] = Amounts.add(entryEx[field], amount).amount;
return balanceStore[currency];
}
const balanceStore = {
byCurrency: {},
byExchange: {},
};
// Initialize balance to zero, even if we didn't start withdrawing yet.
await tx.iter(Stores.reserves).forEach((r) => {
const z = Amounts.getZero(r.currency);
addTo(balanceStore, "available", z, r.exchangeBaseUrl);
initBalance(r.currency);
});
await tx.iter(Stores.coins).forEach((c) => {
if (c.suspended) {
return;
}
// Only count fresh coins, as dormant coins will
// already be in a refresh session.
if (c.status === CoinStatus.Fresh) {
addTo(balanceStore, "available", c.currentAmount, c.exchangeBaseUrl);
const b = initBalance(c.currentAmount.currency);
b.available = Amounts.add(b.available, c.currentAmount).amount;
}
});
@ -96,51 +82,38 @@ export async function getBalancesInsideTransaction(
for (let i = 0; i < r.oldCoinPubs.length; i++) {
const session = r.refreshSessionPerCoin[i];
if (session) {
addTo(
balanceStore,
"pendingIncoming",
session.amountRefreshOutput,
session.exchangeBaseUrl,
);
addTo(
balanceStore,
"pendingIncomingRefresh",
session.amountRefreshOutput,
session.exchangeBaseUrl,
);
const b = initBalance(session.amountRefreshOutput.currency);
// We are always assuming the refresh will succeed, thus we
// report the output as available balance.
b.available = Amounts.add(session.amountRefreshOutput).amount;
}
}
});
// FIXME: re-implement
// await tx.iter(Stores.withdrawalGroups).forEach((wds) => {
// let w = wds.totalCoinValue;
// for (let i = 0; i < wds.planchets.length; i++) {
// if (wds.withdrawn[i]) {
// const p = wds.planchets[i];
// if (p) {
// w = Amounts.sub(w, p.coinValue).amount;
// }
// }
// }
// addTo(balanceStore, "pendingIncoming", w, wds.exchangeBaseUrl);
// });
await tx.iter(Stores.purchases).forEach((t) => {
if (t.timestampFirstSuccessfulPay) {
await tx.iter(Stores.withdrawalGroups).forEach((wds) => {
if (wds.timestampFinish) {
return;
}
for (const c of t.coinDepositPermissions) {
addTo(
balanceStore,
"pendingPayment",
Amounts.parseOrThrow(c.contribution),
c.exchange_url,
);
}
const b = initBalance(wds.denomsSel.totalWithdrawCost.currency);
b.pendingIncoming = Amounts.add(b.pendingIncoming, wds.denomsSel.totalCoinValue).amount;
});
return balanceStore;
const balancesResponse: BalancesResponse = {
balances: [],
};
Object.keys(balanceStore).sort().forEach((c) => {
const v = balanceStore[c];
balancesResponse.balances.push({
available: Amounts.stringify(v.available),
pendingIncoming: Amounts.stringify(v.pendingIncoming),
pendingOutgoing: Amounts.stringify(v.pendingOutgoing),
hasPendingTransactions: false,
requiresUserInput: false,
});
})
return balancesResponse;
}
/**
@ -148,7 +121,7 @@ export async function getBalancesInsideTransaction(
*/
export async function getBalances(
ws: InternalWalletState,
): Promise<WalletBalance> {
): Promise<BalancesResponse> {
logger.trace("starting to compute balance");
const wbal = await ws.db.runWithReadTransaction(

View File

@ -15,7 +15,7 @@
*/
import { HttpRequestLibrary } from "../util/http";
import { NextUrlResult, WalletBalance } from "../types/walletTypes";
import { NextUrlResult, BalancesResponse } from "../types/walletTypes";
import { CryptoApi, CryptoWorkerFactory } from "../crypto/workers/cryptoApi";
import { AsyncOpMemoMap, AsyncOpMemoSingle } from "../util/asyncMemo";
import { Logger } from "../util/logging";
@ -34,7 +34,7 @@ export class InternalWalletState {
memoGetPending: AsyncOpMemoSingle<
PendingOperationsResponse
> = new AsyncOpMemoSingle();
memoGetBalance: AsyncOpMemoSingle<WalletBalance> = new AsyncOpMemoSingle();
memoGetBalance: AsyncOpMemoSingle<BalancesResponse> = new AsyncOpMemoSingle();
memoProcessRefresh: AsyncOpMemoMap<void> = new AsyncOpMemoMap();
memoProcessRecoup: AsyncOpMemoMap<void> = new AsyncOpMemoMap();
cryptoApi: CryptoApi;

View File

@ -21,7 +21,7 @@
/**
* Imports.
*/
import { OperationErrorDetails, WalletBalance } from "./walletTypes";
import { OperationErrorDetails, BalancesResponse } from "./walletTypes";
import { WithdrawalSource, RetryInfo, ReserveRecordStatus } from "./dbTypes";
import { Timestamp, Duration } from "../util/time";
@ -243,7 +243,7 @@ export interface PendingOperationsResponse {
/**
* Current wallet balance, including pending balances.
*/
walletBalance: WalletBalance;
walletBalance: BalancesResponse;
/**
* When is the next pending operation due to be re-tried?

View File

@ -146,48 +146,26 @@ export interface ExchangeWithdrawDetails {
walletVersion: string;
}
/**
* Mapping from currency/exchange to detailed balance
* information.
*/
export interface WalletBalance {
/**
* Mapping from currency name to detailed balance info.
*/
byExchange: { [exchangeBaseUrl: string]: WalletBalanceEntry };
/**
* Mapping from currency name to detailed balance info.
*/
byCurrency: { [currency: string]: WalletBalanceEntry };
export interface Balance {
available: AmountString;
pendingIncoming: AmountString;
pendingOutgoing: AmountString;
// Does the balance for this currency have a pending
// transaction?
hasPendingTransactions: boolean;
// Is there a pending transaction that would affect the balance
// and requires user input?
requiresUserInput: boolean;
}
/**
* Detailed wallet balance for a particular currency.
*/
export interface WalletBalanceEntry {
/**
* Directly available amount.
*/
available: AmountJson;
/**
* Amount that we're waiting for (refresh, withdrawal).
*/
pendingIncoming: AmountJson;
/**
* Amount that's marked for a pending payment.
*/
pendingPayment: AmountJson;
/**
* Amount that was paid back and we could withdraw again.
*/
paybackAmount: AmountJson;
pendingIncomingWithdraw: AmountJson;
pendingIncomingRefresh: AmountJson;
pendingIncomingDirty: AmountJson;
export interface BalancesResponse {
balances: Balance[];
}
/**
* For terseness.
*/

View File

@ -60,7 +60,6 @@ import {
ReturnCoinsRequest,
SenderWireInfos,
TipStatus,
WalletBalance,
PreparePayResult,
AcceptWithdrawalResponse,
PurchaseDetails,
@ -70,6 +69,7 @@ import {
ManualWithdrawalDetails,
GetExchangeTosResult,
AcceptManualWithdrawalResult,
BalancesResponse,
} from "./types/walletTypes";
import { Logger } from "./util/logging";
@ -515,7 +515,7 @@ export class Wallet {
/**
* Get detailed balance information, sliced by exchange and by currency.
*/
async getBalances(): Promise<WalletBalance> {
async getBalances(): Promise<BalancesResponse> {
return this.ws.memoGetBalance.memo(() => getBalances(this.ws));
}

View File

@ -29,8 +29,6 @@ import * as i18n from "../i18n";
import { AmountJson } from "../../util/amounts";
import * as Amounts from "../../util/amounts";
import { WalletBalance, WalletBalanceEntry } from "../../types/walletTypes";
import { abbrev, renderAmount, PageLink } from "../renderHtml";
import * as wxApi from "../wxApi";
@ -40,6 +38,7 @@ import moment from "moment";
import { Timestamp } from "../../util/time";
import { classifyTalerUri, TalerUriType } from "../../util/taleruri";
import { PermissionsCheckbox } from "./welcome";
import { BalancesResponse, Balance } from "../../types/walletTypes";
// FIXME: move to newer react functions
/* eslint-disable react/no-deprecated */
@ -172,7 +171,7 @@ function EmptyBalanceView(): JSX.Element {
}
class WalletBalanceView extends React.Component<any, any> {
private balance: WalletBalance;
private balance: BalancesResponse;
private gotError = false;
private canceler: (() => void) | undefined = undefined;
private unmount = false;
@ -196,7 +195,7 @@ class WalletBalanceView extends React.Component<any, any> {
return;
}
this.updateBalanceRunning = true;
let balance: WalletBalance;
let balance: BalancesResponse;
try {
balance = await wxApi.getBalance();
} catch (e) {
@ -219,10 +218,14 @@ class WalletBalanceView extends React.Component<any, any> {
this.setState({});
}
formatPending(entry: WalletBalanceEntry): JSX.Element {
formatPending(entry: Balance): JSX.Element {
let incoming: JSX.Element | undefined;
let payment: JSX.Element | undefined;
const available = Amounts.parseOrThrow(entry.available);
const pendingIncoming = Amounts.parseOrThrow(entry.pendingIncoming);
const pendingOutgoing = Amounts.parseOrThrow(entry.pendingOutgoing);
console.log(
"available: ",
entry.pendingIncoming ? renderAmount(entry.available) : null,
@ -232,7 +235,7 @@ class WalletBalanceView extends React.Component<any, any> {
entry.pendingIncoming ? renderAmount(entry.pendingIncoming) : null,
);
if (Amounts.isNonZero(entry.pendingIncoming)) {
if (Amounts.isNonZero(pendingIncoming)) {
incoming = (
<i18n.Translate wrap="span">
<span style={{ color: "darkgreen" }}>
@ -244,18 +247,6 @@ class WalletBalanceView extends React.Component<any, any> {
);
}
if (Amounts.isNonZero(entry.pendingPayment)) {
payment = (
<i18n.Translate wrap="span">
<span style={{ color: "red" }}>
{"-"}
{renderAmount(entry.pendingPayment)}
</span>{" "}
being spent
</i18n.Translate>
);
}
const l = [incoming, payment].filter((x) => x !== undefined);
if (l.length === 0) {
return <span />;
@ -288,11 +279,11 @@ class WalletBalanceView extends React.Component<any, any> {
return <span></span>;
}
console.log(wallet);
const listing = Object.keys(wallet.byCurrency).map((key) => {
const entry: WalletBalanceEntry = wallet.byCurrency[key];
const listing = wallet.balances.map((entry) => {
const av = Amounts.parseOrThrow(entry.available);
return (
<p key={key}>
{bigAmount(entry.available)} {this.formatPending(entry)}
<p key={av.currency}>
{bigAmount(av)} {this.formatPending(entry)}
</p>
);
});
@ -314,7 +305,6 @@ function formatAndCapitalize(text: string): string {
return text;
}
const HistoryComponent = (props: any): JSX.Element => {
return <span>TBD</span>;
};
@ -330,7 +320,6 @@ class WalletSettings extends React.Component<any, any> {
}
}
function reload(): void {
try {
chrome.runtime.reload();

View File

@ -23,293 +23,8 @@
/**
* Imports.
*/
import { AmountJson } from "../../util/amounts";
import { Amounts } from "../../util/amounts";
import { SenderWireInfos, WalletBalance } from "../../types/walletTypes";
import * as i18n from "../i18n";
import * as wire from "../../util/wire";
import { getBalance, getSenderWireInfos, returnCoins } from "../wxApi";
import { renderAmount } from "../renderHtml";
import * as React from "react";
interface ReturnSelectionItemProps extends ReturnSelectionListProps {
exchangeUrl: string;
senderWireInfos: SenderWireInfos;
}
interface ReturnSelectionItemState {
selectedValue: string;
supportedWires: string[];
selectedWire: string;
currency: string;
}
class ReturnSelectionItem extends React.Component<
ReturnSelectionItemProps,
ReturnSelectionItemState
> {
constructor(props: ReturnSelectionItemProps) {
super(props);
const exchange = this.props.exchangeUrl;
const wireTypes = this.props.senderWireInfos.exchangeWireTypes;
const supportedWires = this.props.senderWireInfos.senderWires.filter(
(x) => {
return (
wireTypes[exchange] &&
wireTypes[exchange].indexOf((x as any).type) >= 0
);
},
);
this.state = {
currency: props.balance.byExchange[props.exchangeUrl].available.currency,
selectedValue: Amounts.stringify(
props.balance.byExchange[props.exchangeUrl].available,
),
selectedWire: "",
supportedWires,
};
}
render(): JSX.Element {
const exchange = this.props.exchangeUrl;
const byExchange = this.props.balance.byExchange;
const wireTypes = this.props.senderWireInfos.exchangeWireTypes;
return (
<div key={exchange}>
<h2>Exchange {exchange}</h2>
<p>Available amount: {renderAmount(byExchange[exchange].available)}</p>
<p>
Supported wire methods:{" "}
{wireTypes[exchange].length ? wireTypes[exchange].join(", ") : "none"}
</p>
<p>
Wire {""}
<input
type="text"
size={this.state.selectedValue.length || 1}
value={this.state.selectedValue}
onChange={(evt) =>
this.setState({ selectedValue: evt.target.value })
}
style={{ textAlign: "center" }}
/>{" "}
{this.props.balance.byExchange[exchange].available.currency} {""}
to account {""}
<select
value={this.state.selectedWire}
onChange={(evt) =>
this.setState({ selectedWire: evt.target.value })
}
>
<option style={{ display: "none" }}>Select account</option>
{this.state.supportedWires.map((w, n) => (
<option value={n.toString()} key={JSON.stringify(w)}>
{n + 1}: {wire.summarizeWire(w)}
</option>
))}
</select>
.
</p>
{this.state.selectedWire ? (
<button
className="pure-button button-success"
onClick={() => this.select()}
>
{i18n.str`Wire to bank account`}
</button>
) : null}
</div>
);
}
select(): void {
let val: number;
let selectedWire: number;
try {
val = Number.parseFloat(this.state.selectedValue);
selectedWire = Number.parseInt(this.state.selectedWire);
} catch (e) {
console.error(e);
return;
}
this.props.selectDetail({
amount: Amounts.fromFloat(val, this.state.currency),
exchange: this.props.exchangeUrl,
senderWire: this.state.supportedWires[selectedWire],
});
}
}
interface ReturnSelectionListProps {
balance: WalletBalance;
senderWireInfos: SenderWireInfos;
selectDetail(d: SelectedDetail): void;
}
class ReturnSelectionList extends React.Component<
ReturnSelectionListProps,
{}
> {
render(): JSX.Element {
const byExchange = this.props.balance.byExchange;
const exchanges = Object.keys(byExchange);
if (!exchanges.length) {
return (
<p className="errorbox">Currently no funds available to transfer.</p>
);
}
return (
<div>
{exchanges.map((e) => (
<ReturnSelectionItem key={e} exchangeUrl={e} {...this.props} />
))}
</div>
);
}
}
interface SelectedDetail {
amount: AmountJson;
senderWire: any;
exchange: string;
}
interface ReturnConfirmationProps {
detail: SelectedDetail;
cancel(): void;
confirm(): void;
}
class ReturnConfirmation extends React.Component<ReturnConfirmationProps, {}> {
render(): JSX.Element {
return (
<div>
<p>
Please confirm if you want to transmit{" "}
<strong>{renderAmount(this.props.detail.amount)}</strong> at {""}
{this.props.detail.exchange} to account {""}
<strong style={{ whiteSpace: "nowrap" }}>
{wire.summarizeWire(this.props.detail.senderWire)}
</strong>
.
</p>
<button
className="pure-button button-success"
onClick={() => this.props.confirm()}
>
{i18n.str`Confirm`}
</button>
<button className="pure-button" onClick={() => this.props.cancel()}>
{i18n.str`Cancel`}
</button>
</div>
);
}
}
interface ReturnCoinsState {
balance: WalletBalance | undefined;
senderWireInfos: SenderWireInfos | undefined;
selectedReturn: SelectedDetail | undefined;
/**
* Last confirmed detail, so we can show a nice box.
*/
lastConfirmedDetail: SelectedDetail | undefined;
}
class ReturnCoins extends React.Component<{}, ReturnCoinsState> {
constructor(props: {}) {
super(props);
const port = chrome.runtime.connect();
port.onMessage.addListener((msg: any) => {
if (msg.notify) {
console.log("got notified");
this.update();
}
});
this.update();
this.state = {} as any;
}
async update(): Promise<void> {
const balance = await getBalance();
const senderWireInfos = await getSenderWireInfos();
console.log("got swi", senderWireInfos);
console.log("got bal", balance);
this.setState({ balance, senderWireInfos });
}
selectDetail(d: SelectedDetail): void {
this.setState({ selectedReturn: d });
}
async confirm(): Promise<void> {
const selectedReturn = this.state.selectedReturn;
if (!selectedReturn) {
return;
}
await returnCoins(selectedReturn);
await this.update();
this.setState({
selectedReturn: undefined,
lastConfirmedDetail: selectedReturn,
});
}
async cancel(): Promise<void> {
this.setState({
selectedReturn: undefined,
lastConfirmedDetail: undefined,
});
}
render(): JSX.Element {
const balance = this.state.balance;
const senderWireInfos = this.state.senderWireInfos;
if (!balance || !senderWireInfos) {
return <span>...</span>;
}
if (this.state.selectedReturn) {
return (
<div id="main">
<ReturnConfirmation
detail={this.state.selectedReturn}
cancel={() => this.cancel()}
confirm={() => this.confirm()}
/>
</div>
);
}
return (
<div id="main">
<h1>Wire electronic cash back to own bank account</h1>
<p>
You can send coins back into your own bank account. Note that
you&apos;re acting as a merchant when doing this, and thus the same
fees apply.
</p>
{this.state.lastConfirmedDetail ? (
<p className="okaybox">
Transfer of {renderAmount(this.state.lastConfirmedDetail.amount)}{" "}
successfully initiated.
</p>
) : null}
<ReturnSelectionList
selectDetail={(d) => this.selectDetail(d)}
balance={balance}
senderWireInfos={senderWireInfos}
/>
</div>
);
}
}
export function createReturnCoinsPage(): JSX.Element {
return <ReturnCoins />;
return <span>Not implemented yet.</span>;
}

View File

@ -34,12 +34,12 @@ import {
ConfirmPayResult,
SenderWireInfos,
TipStatus,
WalletBalance,
PurchaseDetails,
WalletDiagnostics,
PreparePayResult,
AcceptWithdrawalResponse,
ExtendedPermissionsResponse,
BalancesResponse,
} from "../types/walletTypes";
/**
@ -185,7 +185,7 @@ export function resetDb(): Promise<void> {
/**
* Get balances for all currencies/exchanges.
*/
export function getBalance(): Promise<WalletBalance> {
export function getBalance(): Promise<BalancesResponse> {
return callBackend("balances", {});
}