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:
+2
-19
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user