feat: add prerequisite checks before swap (#588)

* feat: add prerequisite checks before swap

* fix: add missing authentication on desktop config call

* refactor(wip): introduce swap error

* refactor: use wrapWithSwapError

* fix: log originalError instead of error

* fix: show snackbar when error is unexpected
This commit is contained in:
Cafe137
2022-11-23 14:20:55 +01:00
committed by GitHub
parent 1c53364fcd
commit 4e564dd5c0
5 changed files with 97 additions and 26 deletions
+2 -19
View File
@@ -1,8 +1,7 @@
import axios from 'axios'
import { useEffect, useState } from 'react'
import { getLatestBeeDesktopVersion } from '../utils/desktop'
import { getJson } from '../utils/net'
import { GITHUB_REPO_URL } from '../constants'
import { BeeConfig, getDesktopConfiguration, getLatestBeeDesktopVersion } from '../utils/desktop'
export interface LatestBeeReleaseHook {
latestBeeRelease: LatestBeeRelease | null
@@ -109,22 +108,6 @@ export function useNewBeeDesktopVersion(
return { newBeeDesktopVersion }
}
export interface BeeConfig {
'api-addr': string
'debug-api-addr': string
'debug-api-enable': boolean
password: string
'swap-enable': boolean
'swap-initial-deposit': bigint
mainnet: boolean
'full-node': boolean
'cors-allowed-origins': string
'resolver-options': string
'use-postage-snapshot': boolean
'data-dir': string
'swap-endpoint'?: string
}
export interface GetBeeConfig {
config: BeeConfig | null
isLoading: boolean
@@ -137,7 +120,7 @@ export const useGetBeeConfig = (desktopUrl: string): GetBeeConfig => {
const [error, setError] = useState<Error | null>(null)
useEffect(() => {
getJson<BeeConfig>(`${desktopUrl}/config`)
getDesktopConfiguration(desktopUrl)
.then(beeConf => {
setBeeConfig(beeConf)
setError(null)
+41 -5
View File
@@ -20,12 +20,22 @@ import { Context as SettingsContext } from '../../providers/Settings'
import { Context as BalanceProvider } from '../../providers/WalletBalance'
import { ROUTES } from '../../routes'
import { sleepMs } from '../../utils'
import { getBzzPriceAsDai, performSwap, restartBeeNode, upgradeToLightNode } from '../../utils/desktop'
import {
getBzzPriceAsDai,
getDesktopConfiguration,
performSwap,
restartBeeNode,
upgradeToLightNode,
} from '../../utils/desktop'
import { Rpc } from '../../utils/rpc'
import { isSwapError, SwapError, wrapWithSwapError } from '../../utils/SwapError'
import { TopUpProgressIndicator } from './TopUpProgressIndicator'
const MINIMUM_XDAI = '0.1'
const MINIMUM_XBZZ = '0.1'
const GENERIC_SWAP_FAILED_ERROR_MESSAGE = 'Failed to swap. The full error is printed to the console.'
interface Props {
header: string
}
@@ -127,6 +137,22 @@ export function Swap({ header }: Props): ReactElement {
}
}
async function performSwapWithChecks(daiToSwap: DaiToken) {
const desktopConfiguration = await wrapWithSwapError(
getDesktopConfiguration(desktopUrl),
'Unable to reach Desktop API. Is Swarm Desktop running?',
)
if (!desktopConfiguration['swap-endpoint']) {
throw new SwapError('Swap endpoint is not configured in Swarm Desktop')
}
await wrapWithSwapError(
Rpc.getNetworkChainId(desktopConfiguration['swap-endpoint']),
`Swap endpoint not reachable at ${desktopConfiguration['swap-endpoint']}`,
)
await wrapWithSwapError(performSwap(desktopUrl, daiToSwap.toString), GENERIC_SWAP_FAILED_ERROR_MESSAGE)
}
async function onSwap() {
if (hasSwapped || !daiToSwap) {
return
@@ -135,16 +161,26 @@ export function Swap({ header }: Props): ReactElement {
setSwapped(true)
try {
await performSwap(desktopUrl, daiToSwap.toString)
await performSwapWithChecks(daiToSwap)
const message = canUpgradeToLightNode
? 'Successfully swapped. Beginning light node upgrade...'
: 'Successfully swapped. Balances will refresh soon. You may now leave the page.'
: 'Successfully swapped. Balances will refresh soon. You may now navigate away.'
enqueueSnackbar(message, { variant: 'success' })
if (canUpgradeToLightNode) await restart()
} catch (error) {
console.error(error) // eslint-disable-line
enqueueSnackbar(`Failed to swap: ${error}`, { variant: 'error' })
if (isSwapError(error)) {
// we have a custom and user friendly error message
enqueueSnackbar(error.snackbarMessage, { variant: 'error' })
if (error.originalError) {
console.error(error.originalError) // eslint-disable-line
}
} else {
// we have an unexpected error
enqueueSnackbar(`${GENERIC_SWAP_FAILED_ERROR_MESSAGE} ${error}`, { variant: 'error' })
console.error(error) // eslint-disable-line
}
} finally {
balance?.refresh()
setLoading(false)
+21
View File
@@ -0,0 +1,21 @@
export class SwapError extends Error {
snackbarMessage: string
originalError?: Error
constructor(snackbarMessage: string, error?: Error) {
super(error?.message || snackbarMessage)
this.name = 'SwapError'
this.originalError = error
this.snackbarMessage = snackbarMessage
}
}
export function isSwapError(error: unknown): error is SwapError {
return error instanceof Error && error.name === 'SwapError'
}
export function wrapWithSwapError<T>(promise: Promise<T>, snackbarMessage: string): Promise<T> {
return promise.catch((error: Error) => {
throw new SwapError(snackbarMessage, error)
})
}
+21 -1
View File
@@ -2,7 +2,23 @@ import axios from 'axios'
import { BEE_DESKTOP_LATEST_RELEASE_PAGE_API } from '../constants'
import { DaiToken } from '../models/DaiToken'
import { Token } from '../models/Token'
import { postJson } from './net'
import { getJson, postJson } from './net'
export interface BeeConfig {
'api-addr': string
'debug-api-addr': string
'debug-api-enable': boolean
password: string
'swap-enable': boolean
'swap-initial-deposit': bigint
mainnet: boolean
'full-node': boolean
'cors-allowed-origins': string
'resolver-options': string
'use-postage-snapshot': boolean
'data-dir': string
'swap-endpoint'?: string
}
export async function getBzzPriceAsDai(desktopUrl: string): Promise<Token> {
const response = await axios.get(`${desktopUrl}/price`)
@@ -23,6 +39,10 @@ export async function setJsonRpcInDesktop(desktopUrl: string, value: string): Pr
})
}
export function getDesktopConfiguration(desktopUrl: string): Promise<BeeConfig> {
return getJson(`${desktopUrl}/config`)
}
async function updateDesktopConfiguration(desktopUrl: string, values: Record<string, unknown>): Promise<void> {
await postJson(`${desktopUrl}/config`, values)
}
+12 -1
View File
@@ -2,6 +2,16 @@ import { debounce } from '@material-ui/core'
import { Contract, providers, Wallet, BigNumber as BN } from 'ethers'
import { bzzABI, BZZ_TOKEN_ADDRESS } from './bzz-abi'
const NETWORK_ID = 100
async function getNetworkChainId(url: string): Promise<number> {
const provider = new providers.JsonRpcProvider(url, NETWORK_ID)
await provider.ready
const network = await provider.getNetwork()
return network.chainId
}
async function eth_getBalance(address: string, provider: providers.JsonRpcProvider): Promise<string> {
if (!address.startsWith('0x')) {
address = `0x${address}`
@@ -78,7 +88,7 @@ export async function sendBzzTransaction(
}
async function makeReadySigner(privateKey: string, jsonRpcProvider: string) {
const provider = new providers.JsonRpcProvider(jsonRpcProvider, 100)
const provider = new providers.JsonRpcProvider(jsonRpcProvider, NETWORK_ID)
await provider.ready
const signer = new Wallet(privateKey, provider)
@@ -86,6 +96,7 @@ async function makeReadySigner(privateKey: string, jsonRpcProvider: string) {
}
export const Rpc = {
getNetworkChainId,
sendNativeTransaction,
sendBzzTransaction,
_eth_getBalance: eth_getBalance,