Skip to content

Commit

Permalink
Merge pull request #1468 from oasisprotocol/lw/balance
Browse files Browse the repository at this point in the history
Show total balance in account selector
  • Loading branch information
lukaw3d committed May 25, 2023
2 parents 9bf9c78 + 4cb69d9 commit e9bd265
Show file tree
Hide file tree
Showing 23 changed files with 141 additions and 128 deletions.
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,
}
}

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

0 comments on commit e9bd265

Please sign in to comment.