Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Show total balance in account selector #1468

Merged
merged 5 commits into from
May 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
25 changes: 19 additions & 6 deletions playwright/tests/syncTabs.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
} from '../utils/test-inputs'
import { addPersistedStorage, clearPersistedStorage } from '../utils/storage'
import { fillPrivateKeyWithoutPassword, fillPrivateKeyAndPassword } from '../utils/fillPrivateKey'
import type { AccountsRow } from '../../src/vendors/oasisscan/index'

test.beforeEach(async ({ context, page }) => {
await warnSlowApi(context)
Expand Down Expand Up @@ -256,17 +257,29 @@ test.describe('syncTabs', () => {
await expect(tab2.getByText('Loading account')).toBeVisible()
await expect(tab2.getByText('Loading account')).toBeHidden()

// Delay getBalance so addWallet is called after wallet is locked.
let grpcBalance: Route
await page.route('**/oasis-core.Staking/Account', route => (grpcBalance = route))
// Delay getAccountBalanceWithFallback so addWallet is called after wallet is locked.
let apiBalance: Route
await context.route('**/chain/account/info/*', route => (apiBalance = route))

await page.getByPlaceholder('Enter your private key here').fill(privateKey2)
await page.keyboard.press('Enter')
await tab2.getByRole('button', { name: /Lock profile/ }).click()
await grpcBalance!.fulfill({
contentType: 'application/grpc-web-text+proto',
body: 'AAAAAAGggAAAAB5ncnBjLXN0YXR1czowDQpncnBjLW1lc3NhZ2U6DQo=',
await apiBalance!.fulfill({
body: JSON.stringify({
code: 0,
data: {
rank: 0,
address: '',
available: '0',
escrow: '0',
debonding: '0',
total: '0',
nonce: 1,
allowances: [],
} satisfies AccountsRow,
}),
})
await page.waitForTimeout(100)

// TODO: https://github.com/oasisprotocol/oasis-wallet-web/pull/975#discussion_r1019567305
// await expect(page.getByTestId('fatalerror-stacktrace')).toBeHidden()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const renderComponent = (store: any) =>
<ThemeProvider>
<Account
address="oasis1qq3xrq0urs8qcffhvmhfhz4p0mu7ewc8rscnlwxe"
balance={1000n.toString()}
balance={{ available: '200', debonding: '0', delegations: '800', total: '1000' }}
type={WalletType.Mnemonic}
onClick={() => {}}
isActive={false}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,7 @@ describe('<AccountSelector />', () => {
wallets: {
oasis1qq3xrq0urs8qcffhvmhfhz4p0mu7ewc8rscnlwxe: {
address: 'oasis1qq3xrq0urs8qcffhvmhfhz4p0mu7ewc8rscnlwxe',
balance: {
available: 100n.toString(),
validator: { escrow: 5000n.toString(), escrow_debonding: 300n.toString() },
},
balance: { available: '100', debonding: '0', delegations: '0', total: '100' },
publicKey: '00',
type: WalletType.Ledger,
},
Expand Down
8 changes: 4 additions & 4 deletions src/app/components/Toolbar/Features/AccountSelector/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,15 @@ import { ResponsiveContext } from 'grommet/es6/contexts/ResponsiveContext'
import React, { memo, useContext } from 'react'
import { useTranslation } from 'react-i18next'
import { useDispatch, useSelector } from 'react-redux'
import { StringifiedBigInt } from 'types/StringifiedBigInt'
import { BalanceDetails } from '../../../../state/account/types'

interface Props {
closeHandler: () => any
}

interface AccountProps {
address: string
balance?: StringifiedBigInt
balance: BalanceDetails | undefined
type: WalletType
onClick: (address: string) => void
path?: number[]
Expand Down Expand Up @@ -91,7 +91,7 @@ export const Account = memo((props: AccountProps) => {
{walletTypes[props.type]} {props.pathDisplay && <Text size="small">({props.pathDisplay})</Text>}
</Box>
<Box height={'24px'}>
{props.balance ? <AmountFormatter amount={props.balance} /> : <Spinner />}
{props.balance ? <AmountFormatter amount={props.balance.total} /> : <Spinner />}
</Box>
</Box>
</Box>
Expand All @@ -115,7 +115,7 @@ export const AccountSelector = memo((props: Props) => {
<Account
key={wallet.address}
address={wallet.address}
balance={wallet.balance?.available} // TODO: get total balance
balance={wallet.balance}
type={wallet.type}
onClick={switchAccount}
isActive={wallet.address === activeAddress}
Expand Down
36 changes: 36 additions & 0 deletions src/app/lib/getAccountBalanceWithFallback.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { addressToPublicKey, parseRpcBalance } from 'app/lib/helpers'
import { call } from 'typed-redux-saga'
import { getExplorerAPIs, getOasisNic } from '../state/network/saga'
import { Account } from '../state/account/types'

function* getBalanceGRPC(address: string) {
const nic = yield* call(getOasisNic)
const publicKey = yield* call(addressToPublicKey, address)
const account = yield* call([nic, nic.stakingAccount], { owner: publicKey, height: 0 })
const grpcBalance = parseRpcBalance(account)
return {
address,
available: grpcBalance.available,
delegations: null,
debonding: null,
total: null,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@csillag: As far as I can tell, currently when the API call fails, and we fall back to the gRPC, this is all done silently. So just by looking at the UI, there is no way to tell whether we are seeing the real total balance, or just the portion returned by the gRPC fallback.

This fallback returns null total, rather than available + 0 + 0 so the displayed balances shouldn't be misleading. But I noticed we don't differentiate loading and null. It just keeps loading
image

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed
image

}
}

export function* getAccountBalanceWithFallback(address: string) {
const { getAccount } = yield* call(getExplorerAPIs)

try {
const account: Account = yield* call(getAccount, address)
return account
} catch (apiError: any) {
console.error('get account failed, continuing to RPC fallback.', apiError)
try {
const account: Account = yield* call(getBalanceGRPC, address)
return account
} catch (rpcError) {
console.error('get account with RPC failed, continuing without updated account.', rpcError)
throw apiError
}
}
}
3 changes: 1 addition & 2 deletions src/app/lib/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { bech32 } from 'bech32'
import { quantity, staking, types } from '@oasisprotocol/client'
import { WalletBalance } from 'app/state/wallet/types'
import { decode as base64decode } from 'base64-arraybuffer'
import BigNumber from 'bignumber.js'
import { StringifiedBigInt } from 'types/StringifiedBigInt'
Expand Down Expand Up @@ -94,7 +93,7 @@ export function formatWeiAsWrose(
return getRoseString(roseBN, minimumFractionDigits, maximumFractionDigits)
}

export function parseRpcBalance(account: types.StakingAccount): WalletBalance {
export function parseRpcBalance(account: types.StakingAccount) {
const zero = stringBigint2uint('0')

return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ describe('<ImportAccountsSelectionModal />', () => {
importAccountsActions.accountsListed([
{
address: 'oasis1qzyqaxestzlum26e2vdgvkerm6d9qgdp7gh2pxqe',
balance: { available: '0', validator: { escrow: '0', escrow_debonding: '0' } },
balance: { available: '0', debonding: '0', delegations: '0', total: '0' },
path: [44, 474, 0],
pathDisplay: `m/44'/474'/0'`,
publicKey: '00',
Expand All @@ -77,7 +77,7 @@ describe('<ImportAccountsSelectionModal />', () => {
importAccountsActions.accountsListed([
{
address: 'oasis1qzyqaxestzlum26e2vdgvkerm6d9qgdp7gh2pxqe',
balance: { available: '0', validator: { escrow: '0', escrow_debonding: '0' } },
balance: { available: '0', debonding: '0', delegations: '0', total: '0' },
path: [44, 474, 0],
pathDisplay: `m/44'/474'/0'`,
publicKey: '00',
Expand All @@ -86,7 +86,7 @@ describe('<ImportAccountsSelectionModal />', () => {
},
{
address: 'oasis1qqv25adrld8jjquzxzg769689lgf9jxvwgjs8tha',
balance: { available: '0', validator: { escrow: '0', escrow_debonding: '0' } },
balance: { available: '0', debonding: '0', delegations: '0', total: '0' },
path: [44, 474, 1],
pathDisplay: `m/44'/474'/1'`,
publicKey: '00',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ function ImportAccountsSelector({ accounts }: ImportAccountsSelectorSelectorProp
{accounts.map(a => (
<Account
address={a.address}
balance={a.balance?.available} // TODO: get total balance
balance={a.balance}
type={a.type}
onClick={toggleAccount}
isActive={a.selected}
Expand Down
47 changes: 13 additions & 34 deletions src/app/state/account/saga.ts
Original file line number Diff line number Diff line change
@@ -1,63 +1,42 @@
import { PayloadAction } from '@reduxjs/toolkit'
import { addressToPublicKey, parseRpcBalance } from 'app/lib/helpers'
import { all, call, delay, fork, join, put, select, take, takeLatest } from 'typed-redux-saga'
import { WalletError, WalletErrors } from 'types/errors'

import { accountActions } from '.'
import { getExplorerAPIs, getOasisNic } from '../network/saga'
import { getExplorerAPIs } from '../network/saga'
import { takeLatestCancelable } from '../takeLatestCancelable'
import { stakingActions } from '../staking'
import { fetchAccount as stakingFetchAccount } from '../staking/saga'
import { transactionActions } from '../transaction'
import { selectAddress } from '../wallet/selectors'
import { selectAccountAddress, selectAccountAvailableBalance } from './selectors'
import { getAccountBalanceWithFallback } from '../../lib/getAccountBalanceWithFallback'

const ACCOUNT_REFETCHING_INTERVAL = 30 * 1000
const TRANSACTIONS_LIMIT = 20

function* getBalanceGRPC(address: string) {
const nic = yield* call(getOasisNic)
const publicKey = yield* call(addressToPublicKey, address)
const account = yield* call([nic, nic.stakingAccount], { owner: publicKey, height: 0 })
const balance = parseRpcBalance(account)
return {
address,
available: balance.available,
delegations: null,
debonding: null,
total: null,
}
}

export function* fetchAccount(action: PayloadAction<string>) {
const address = action.payload

yield* put(accountActions.setLoading(true))
const { getAccount, getTransactionsList } = yield* call(getExplorerAPIs)
const { getTransactionsList } = yield* call(getExplorerAPIs)

yield* all([
join(
yield* fork(function* () {
try {
const account = yield* call(getAccount, address)
const account = yield* call(getAccountBalanceWithFallback, address)
yield* put(accountActions.accountLoaded(account))
} catch (apiError: any) {
console.error('get account failed, continuing to RPC fallback.', apiError)
try {
const account = yield* call(getBalanceGRPC, address)
yield* put(accountActions.accountLoaded(account))
} catch (rpcError) {
console.error('get account with RPC failed, continuing without updated account.', rpcError)
if (apiError instanceof WalletError) {
yield* put(accountActions.accountError({ code: apiError.type, message: apiError.message }))
} else {
yield* put(
accountActions.accountError({
code: WalletErrors.UnknownError,
message: apiError.message,
}),
)
}
if (apiError instanceof WalletError) {
yield* put(accountActions.accountError({ code: apiError.type, message: apiError.message }))
} else {
yield* put(
accountActions.accountError({
code: WalletErrors.UnknownError,
message: apiError.message,
}),
)
}
}
}),
Expand Down
3 changes: 3 additions & 0 deletions src/app/state/account/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@ import { StringifiedBigInt } from 'types/StringifiedBigInt'

export interface BalanceDetails {
available: StringifiedBigInt | null
/** This is delayed in getAccount by 20 seconds on oasisscan and 5 seconds on oasismonitor. */
debonding: StringifiedBigInt | null
/** This is delayed in getAccount by 20 seconds on oasisscan and 5 seconds on oasismonitor. */
delegations: StringifiedBigInt | null
/** This is delayed in getAccount by 20 seconds on oasisscan and 5 seconds on oasismonitor. */
total: StringifiedBigInt | null
}

Expand Down
6 changes: 3 additions & 3 deletions src/app/state/importaccounts/saga.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ import { importAccountsActions } from '.'
import { accountsPerPage, importAccountsSaga, numberOfAccountPages, sign } from './saga'
import * as matchers from 'redux-saga-test-plan/matchers'
import { Ledger, LedgerSigner } from 'app/lib/ledger'
import { getBalance } from '../wallet/saga'
import { addressToPublicKey, publicKeyToAddress } from 'app/lib/helpers'
import { ImportAccountsListAccount, ImportAccountsStep } from './types'
import { WalletErrors } from 'types/errors'
import { OasisTransaction } from 'app/lib/transaction'
import { WalletType } from 'app/state/wallet/types'
import delayP from '@redux-saga/delay-p'
import { getAccountBalanceWithFallback } from '../../lib/getAccountBalanceWithFallback'

describe('importAccounts Sagas', () => {
describe('enumerateAccountsFromLedger', () => {
Expand All @@ -29,7 +29,7 @@ describe('importAccounts Sagas', () => {
[matchers.call.fn(TransportWebUSB.create), { close: () => {} }],
[matchers.call.fn(Ledger.getOasisApp), undefined],
[matchers.call.fn(Ledger.deriveAccountUsingOasisApp), validAccount],
[matchers.call.fn(getBalance), {}],
[matchers.call.fn(getAccountBalanceWithFallback), {}],
])
.dispatch(importAccountsActions.enumerateAccountsFromLedger())
.put.actionType(importAccountsActions.accountGenerated.type)
Expand Down Expand Up @@ -123,7 +123,7 @@ describe('importAccounts Sagas', () => {
},
],
[matchers.call.fn(publicKeyToAddress), mockAddress],
[matchers.call.fn(getBalance), {}],
[matchers.call.fn(getAccountBalanceWithFallback), {}],
[matchers.call.fn(delayP), null], // https://github.com/jfairbank/redux-saga-test-plan/issues/257
])
.dispatch(importAccountsActions.enumerateAccountsFromMnemonic('mnemonic'))
Expand Down
6 changes: 3 additions & 3 deletions src/app/state/importaccounts/saga.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import { PayloadAction } from '@reduxjs/toolkit'
import TransportWebUSB from '@ledgerhq/hw-transport-webusb'
import * as oasis from '@oasisprotocol/client'
import { hex2uint, publicKeyToAddress, uint2hex } from 'app/lib/helpers'
import { publicKeyToAddress, uint2hex } from 'app/lib/helpers'
import { Ledger, LedgerSigner } from 'app/lib/ledger'
import { OasisTransaction } from 'app/lib/transaction'
import { all, call, delay, fork, put, select, takeEvery } from 'typed-redux-saga'
import { ErrorPayload, WalletError, WalletErrors } from 'types/errors'
import { WalletType } from 'app/state/wallet/types'
import { importAccountsActions } from '.'
import { selectChainContext } from '../network/selectors'
import { getBalance } from '../wallet/saga'
import { ImportAccountsListAccount, ImportAccountsStep } from './types'
import type Transport from '@ledgerhq/hw-transport'
import {
Expand All @@ -19,6 +18,7 @@ import {
selectImportAccountsFullList,
selectImportAccountsPageNumber,
} from './selectors'
import { getAccountBalanceWithFallback } from '../../lib/getAccountBalanceWithFallback'

function* setStep(step: ImportAccountsStep) {
yield* put(importAccountsActions.setStep(step))
Expand Down Expand Up @@ -103,7 +103,7 @@ function* fetchBalanceForAccount(account: ImportAccountsListAccount) {
if (currentStep === ImportAccountsStep.Idle) {
yield* setStep(ImportAccountsStep.LoadingBalances)
}
const balance = yield* call(getBalance, hex2uint(account.publicKey))
const balance = yield* call(getAccountBalanceWithFallback, account.address)
yield* put(
importAccountsActions.updateAccountBalance({
address: account.address,
Expand Down
5 changes: 3 additions & 2 deletions src/app/state/importaccounts/types.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { ErrorPayload } from 'types/errors'
import { WalletBalance, WalletType } from '../wallet/types'
import { WalletType } from '../wallet/types'
import { BalanceDetails } from '../account/types'

/* --- STATE --- */
export interface ImportAccountsListAccount {
address: string
balance?: WalletBalance
balance?: BalanceDetails
path: number[]
pathDisplay: string
privateKey?: string
Expand Down
4 changes: 3 additions & 1 deletion src/app/state/transaction/saga.ts
Original file line number Diff line number Diff line change
Expand Up @@ -285,8 +285,10 @@ function assertValidAddress(address: string) {

function* assertSufficientBalance(amount: bigint) {
const wallet = yield* select(selectActiveWallet)
// If balance is missing, allow this to pass. It's just more likely that transaction will fail after submitting.
if (wallet?.balance.available == null) return

const balance = BigInt(wallet!.balance.available)
const balance = BigInt(wallet.balance.available)
if (amount > balance) {
throw new WalletError(WalletErrors.InsufficientBalance, 'Insufficient balance')
}
Expand Down