From 732e764b376dff6b0262b869b29dbdee0c455a0e Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Tue, 28 Jul 2020 23:17:12 +0530 Subject: new balances API, remove defunct 'return funds to own account' implementation --- src/headless/taler-wallet-cli.ts | 9 +- src/operations/balance.ts | 127 +++++++---------- src/operations/state.ts | 4 +- src/types/pending.ts | 4 +- src/types/walletTypes.ts | 52 ++----- src/wallet.ts | 4 +- src/webex/pages/popup.tsx | 37 ++--- src/webex/pages/return-coins.tsx | 287 +-------------------------------------- src/webex/wxApi.ts | 4 +- 9 files changed, 88 insertions(+), 440 deletions(-) diff --git a/src/headless/taler-wallet-cli.ts b/src/headless/taler-wallet-cli.ts index ca0f0f5d1..ed6146a6b 100644 --- a/src/headless/taler-wallet-cli.ts +++ b/src/headless/taler-wallet-cli.ts @@ -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)); }); }); diff --git a/src/operations/balance.ts b/src/operations/balance.ts index 81b4da6e0..b503b7546 100644 --- a/src/operations/balance.ts +++ b/src/operations/balance.ts @@ -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 { +): Promise { + + const balanceStore: Record = {}; + /** * 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 { +): Promise { logger.trace("starting to compute balance"); const wbal = await ws.db.runWithReadTransaction( diff --git a/src/operations/state.ts b/src/operations/state.ts index 97d591244..cfec85d0f 100644 --- a/src/operations/state.ts +++ b/src/operations/state.ts @@ -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 = new AsyncOpMemoSingle(); + memoGetBalance: AsyncOpMemoSingle = new AsyncOpMemoSingle(); memoProcessRefresh: AsyncOpMemoMap = new AsyncOpMemoMap(); memoProcessRecoup: AsyncOpMemoMap = new AsyncOpMemoMap(); cryptoApi: CryptoApi; diff --git a/src/types/pending.ts b/src/types/pending.ts index e6c879992..85f7585c5 100644 --- a/src/types/pending.ts +++ b/src/types/pending.ts @@ -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? diff --git a/src/types/walletTypes.ts b/src/types/walletTypes.ts index d68f41564..f37815333 100644 --- a/src/types/walletTypes.ts +++ b/src/types/walletTypes.ts @@ -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; -/** - * 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; + // Does the balance for this currency have a pending + // transaction? + hasPendingTransactions: boolean; - pendingIncomingWithdraw: AmountJson; - pendingIncomingRefresh: AmountJson; - pendingIncomingDirty: AmountJson; + // Is there a pending transaction that would affect the balance + // and requires user input? + requiresUserInput: boolean; } +export interface BalancesResponse { + balances: Balance[]; +} + + /** * For terseness. */ diff --git a/src/wallet.ts b/src/wallet.ts index 2abd6c0c8..e8ce97190 100644 --- a/src/wallet.ts +++ b/src/wallet.ts @@ -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 { + async getBalances(): Promise { return this.ws.memoGetBalance.memo(() => getBalances(this.ws)); } diff --git a/src/webex/pages/popup.tsx b/src/webex/pages/popup.tsx index e4eea4d9e..8a99a6d90 100644 --- a/src/webex/pages/popup.tsx +++ b/src/webex/pages/popup.tsx @@ -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 { - 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 { 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 { 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 { entry.pendingIncoming ? renderAmount(entry.pendingIncoming) : null, ); - if (Amounts.isNonZero(entry.pendingIncoming)) { + if (Amounts.isNonZero(pendingIncoming)) { incoming = ( @@ -244,18 +247,6 @@ class WalletBalanceView extends React.Component { ); } - if (Amounts.isNonZero(entry.pendingPayment)) { - payment = ( - - - {"-"} - {renderAmount(entry.pendingPayment)} - {" "} - being spent - - ); - } - const l = [incoming, payment].filter((x) => x !== undefined); if (l.length === 0) { return ; @@ -288,11 +279,11 @@ class WalletBalanceView extends React.Component { return ; } 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 ( -

- {bigAmount(entry.available)} {this.formatPending(entry)} +

+ {bigAmount(av)} {this.formatPending(entry)}

); }); @@ -314,7 +305,6 @@ function formatAndCapitalize(text: string): string { return text; } - const HistoryComponent = (props: any): JSX.Element => { return TBD; }; @@ -330,7 +320,6 @@ class WalletSettings extends React.Component { } } - function reload(): void { try { chrome.runtime.reload(); diff --git a/src/webex/pages/return-coins.tsx b/src/webex/pages/return-coins.tsx index ccdb6db53..e8cf8c9dd 100644 --- a/src/webex/pages/return-coins.tsx +++ b/src/webex/pages/return-coins.tsx @@ -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 ( -
-

Exchange {exchange}

-

Available amount: {renderAmount(byExchange[exchange].available)}

-

- Supported wire methods:{" "} - {wireTypes[exchange].length ? wireTypes[exchange].join(", ") : "none"} -

-

- Wire {""} - - this.setState({ selectedValue: evt.target.value }) - } - style={{ textAlign: "center" }} - />{" "} - {this.props.balance.byExchange[exchange].available.currency} {""} - to account {""} - - . -

- {this.state.selectedWire ? ( - - ) : null} -
- ); - } - - 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 ( -

Currently no funds available to transfer.

- ); - } - return ( -
- {exchanges.map((e) => ( - - ))} -
- ); - } -} - -interface SelectedDetail { - amount: AmountJson; - senderWire: any; - exchange: string; -} - -interface ReturnConfirmationProps { - detail: SelectedDetail; - cancel(): void; - confirm(): void; -} - -class ReturnConfirmation extends React.Component { - render(): JSX.Element { - return ( -
-

- Please confirm if you want to transmit{" "} - {renderAmount(this.props.detail.amount)} at {""} - {this.props.detail.exchange} to account {""} - - {wire.summarizeWire(this.props.detail.senderWire)} - - . -

- - -
- ); - } -} - -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 { - 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 { - const selectedReturn = this.state.selectedReturn; - if (!selectedReturn) { - return; - } - await returnCoins(selectedReturn); - await this.update(); - this.setState({ - selectedReturn: undefined, - lastConfirmedDetail: selectedReturn, - }); - } - - async cancel(): Promise { - this.setState({ - selectedReturn: undefined, - lastConfirmedDetail: undefined, - }); - } - - render(): JSX.Element { - const balance = this.state.balance; - const senderWireInfos = this.state.senderWireInfos; - if (!balance || !senderWireInfos) { - return ...; - } - if (this.state.selectedReturn) { - return ( -
- this.cancel()} - confirm={() => this.confirm()} - /> -
- ); - } - return ( -
-

Wire electronic cash back to own bank account

-

- You can send coins back into your own bank account. Note that - you're acting as a merchant when doing this, and thus the same - fees apply. -

- {this.state.lastConfirmedDetail ? ( -

- Transfer of {renderAmount(this.state.lastConfirmedDetail.amount)}{" "} - successfully initiated. -

- ) : null} - this.selectDetail(d)} - balance={balance} - senderWireInfos={senderWireInfos} - /> -
- ); - } -} - export function createReturnCoinsPage(): JSX.Element { - return ; + return Not implemented yet.; } diff --git a/src/webex/wxApi.ts b/src/webex/wxApi.ts index 99a581007..4e11463d6 100644 --- a/src/webex/wxApi.ts +++ b/src/webex/wxApi.ts @@ -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 { /** * Get balances for all currencies/exchanges. */ -export function getBalance(): Promise { +export function getBalance(): Promise { return callBackend("balances", {}); } -- cgit v1.2.3