From 4e564dd5c08b938c95f07818bc60957a7df4f5bb Mon Sep 17 00:00:00 2001 From: Cafe137 <77121044+Cafe137@users.noreply.github.com> Date: Wed, 23 Nov 2022 14:20:55 +0100 Subject: [PATCH] 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 --- src/hooks/apiHooks.tsx | 21 ++---------------- src/pages/top-up/Swap.tsx | 46 ++++++++++++++++++++++++++++++++++----- src/utils/SwapError.ts | 21 ++++++++++++++++++ src/utils/desktop.ts | 22 ++++++++++++++++++- src/utils/rpc.ts | 13 ++++++++++- 5 files changed, 97 insertions(+), 26 deletions(-) create mode 100644 src/utils/SwapError.ts diff --git a/src/hooks/apiHooks.tsx b/src/hooks/apiHooks.tsx index 65157b7..b50397e 100644 --- a/src/hooks/apiHooks.tsx +++ b/src/hooks/apiHooks.tsx @@ -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(null) useEffect(() => { - getJson(`${desktopUrl}/config`) + getDesktopConfiguration(desktopUrl) .then(beeConf => { setBeeConfig(beeConf) setError(null) diff --git a/src/pages/top-up/Swap.tsx b/src/pages/top-up/Swap.tsx index 80c0907..fd48634 100644 --- a/src/pages/top-up/Swap.tsx +++ b/src/pages/top-up/Swap.tsx @@ -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) diff --git a/src/utils/SwapError.ts b/src/utils/SwapError.ts new file mode 100644 index 0000000..b360214 --- /dev/null +++ b/src/utils/SwapError.ts @@ -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(promise: Promise, snackbarMessage: string): Promise { + return promise.catch((error: Error) => { + throw new SwapError(snackbarMessage, error) + }) +} diff --git a/src/utils/desktop.ts b/src/utils/desktop.ts index 76102c0..805dbe9 100644 --- a/src/utils/desktop.ts +++ b/src/utils/desktop.ts @@ -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 { 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 { + return getJson(`${desktopUrl}/config`) +} + async function updateDesktopConfiguration(desktopUrl: string, values: Record): Promise { await postJson(`${desktopUrl}/config`, values) } diff --git a/src/utils/rpc.ts b/src/utils/rpc.ts index 9117931..d69e55b 100644 --- a/src/utils/rpc.ts +++ b/src/utils/rpc.ts @@ -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 { + 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 { 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,