feat: add bee desktop toolkit (#311)
* feat: add light node upgrade * refactor: improve upgrade page * feat: pretty print xdai and add xbzz faucets * feat: display xBZZ balance (#312) * refactor: change rpc provider * fix: remove version alert * fix: load really xBZZ balance instead of xDAI (#314) * feat: add bee desktop api key support * chore: remove dead code * chore: revert useless change * refactor: extract desktop utils module (#339) * refactor: extract desktop utils module * fix: add 0x prefix if it missing from address * refactor: extract BalanceProvider * fix: remove double finally * fix: remove token fallbacks * fix: reuse address and handle balance errors * chore: disable eslint for any * refactor: remove upgrade page * refactor: cleanup, debounce and axios * refactor: change fetch to axios * chore: remove dead code * chore: revert import ordering * refactor: use axios instead of fetch * refactor: use token instead of string Co-authored-by: Cafe137 <aron@aronsoos.com> Co-authored-by: Vojtech Simetka <vojtech@simetka.cz>
This commit is contained in:
Generated
+1173
File diff suppressed because it is too large
Load Diff
@@ -35,6 +35,7 @@
|
|||||||
"axios": "0.24.0",
|
"axios": "0.24.0",
|
||||||
"bignumber.js": "9.0.1",
|
"bignumber.js": "9.0.1",
|
||||||
"ethereumjs-wallet": "^1.0.2",
|
"ethereumjs-wallet": "^1.0.2",
|
||||||
|
"ethers": "^5.6.1",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"formik": "2.2.9",
|
"formik": "2.2.9",
|
||||||
"formik-material-ui": "3.0.1",
|
"formik-material-ui": "3.0.1",
|
||||||
|
|||||||
+34
-1
@@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
BeeModes,
|
||||||
ChainState,
|
ChainState,
|
||||||
ChequebookAddressResponse,
|
ChequebookAddressResponse,
|
||||||
Health,
|
Health,
|
||||||
@@ -7,7 +8,6 @@ import {
|
|||||||
NodeInfo,
|
NodeInfo,
|
||||||
Peer,
|
Peer,
|
||||||
Topology,
|
Topology,
|
||||||
BeeModes,
|
|
||||||
} from '@ethersphere/bee-js'
|
} from '@ethersphere/bee-js'
|
||||||
import { createContext, ReactChild, ReactElement, useContext, useEffect, useState } from 'react'
|
import { createContext, ReactChild, ReactElement, useContext, useEffect, useState } from 'react'
|
||||||
import semver from 'semver'
|
import semver from 'semver'
|
||||||
@@ -15,8 +15,14 @@ import { engines } from '../../package.json'
|
|||||||
import { useLatestBeeRelease } from '../hooks/apiHooks'
|
import { useLatestBeeRelease } from '../hooks/apiHooks'
|
||||||
import { Token } from '../models/Token'
|
import { Token } from '../models/Token'
|
||||||
import type { Balance, ChequebookBalance, Settlements } from '../types'
|
import type { Balance, ChequebookBalance, Settlements } from '../types'
|
||||||
|
import { Rpc } from '../utils/rpc'
|
||||||
import { Context as SettingsContext } from './Settings'
|
import { Context as SettingsContext } from './Settings'
|
||||||
|
|
||||||
|
interface RpcBalance {
|
||||||
|
bzz: Token
|
||||||
|
xdai: Token
|
||||||
|
}
|
||||||
|
|
||||||
export enum CheckState {
|
export enum CheckState {
|
||||||
OK = 'OK',
|
OK = 'OK',
|
||||||
WARNING = 'Warning',
|
WARNING = 'Warning',
|
||||||
@@ -40,6 +46,7 @@ interface Status {
|
|||||||
|
|
||||||
interface ContextInterface {
|
interface ContextInterface {
|
||||||
status: Status
|
status: Status
|
||||||
|
balance: RpcBalance
|
||||||
latestPublishedVersion?: string
|
latestPublishedVersion?: string
|
||||||
latestUserVersion?: string
|
latestUserVersion?: string
|
||||||
latestUserVersionExact?: string
|
latestUserVersionExact?: string
|
||||||
@@ -77,6 +84,10 @@ const initialValues: ContextInterface = {
|
|||||||
topology: { isEnabled: false, checkState: CheckState.ERROR },
|
topology: { isEnabled: false, checkState: CheckState.ERROR },
|
||||||
chequebook: { isEnabled: false, checkState: CheckState.ERROR },
|
chequebook: { isEnabled: false, checkState: CheckState.ERROR },
|
||||||
},
|
},
|
||||||
|
balance: {
|
||||||
|
bzz: new Token('0', 16),
|
||||||
|
xdai: new Token('0', 18),
|
||||||
|
},
|
||||||
latestPublishedVersion: undefined,
|
latestPublishedVersion: undefined,
|
||||||
latestUserVersion: undefined,
|
latestUserVersion: undefined,
|
||||||
latestUserVersionExact: undefined,
|
latestUserVersionExact: undefined,
|
||||||
@@ -193,6 +204,8 @@ export function Provider({ children }: Props): ReactElement {
|
|||||||
const [peerCheques, setPeerCheques] = useState<LastChequesResponse | null>(null)
|
const [peerCheques, setPeerCheques] = useState<LastChequesResponse | null>(null)
|
||||||
const [settlements, setSettlements] = useState<Settlements | null>(null)
|
const [settlements, setSettlements] = useState<Settlements | null>(null)
|
||||||
const [chainState, setChainState] = useState<ChainState | null>(null)
|
const [chainState, setChainState] = useState<ChainState | null>(null)
|
||||||
|
const [bzz, setBzz] = useState<Token>(initialValues.balance.bzz)
|
||||||
|
const [xdai, setXdai] = useState<Token>(initialValues.balance.xdai)
|
||||||
|
|
||||||
const { latestBeeRelease } = useLatestBeeRelease()
|
const { latestBeeRelease } = useLatestBeeRelease()
|
||||||
|
|
||||||
@@ -232,6 +245,22 @@ export function Provider({ children }: Props): ReactElement {
|
|||||||
refresh()
|
refresh()
|
||||||
}, [beeDebugApi]) // eslint-disable-line react-hooks/exhaustive-deps
|
}, [beeDebugApi]) // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (nodeAddresses?.ethereum) {
|
||||||
|
// debounced calls
|
||||||
|
const xdai = Rpc.eth_getBalance(nodeAddresses.ethereum)
|
||||||
|
const bzz = Rpc.eth_getBalanceERC20(nodeAddresses.ethereum)
|
||||||
|
|
||||||
|
if (xdai?.then) {
|
||||||
|
xdai.then(balance => setXdai(new Token(balance, 18)))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bzz?.then) {
|
||||||
|
bzz.then(balance => setBzz(new Token(balance, 16)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [nodeAddresses])
|
||||||
|
|
||||||
const refresh = async () => {
|
const refresh = async () => {
|
||||||
// Don't want to refresh when already refreshing
|
// Don't want to refresh when already refreshing
|
||||||
if (isRefreshing) return
|
if (isRefreshing) return
|
||||||
@@ -388,6 +417,10 @@ export function Provider({ children }: Props): ReactElement {
|
|||||||
chequebookBalance,
|
chequebookBalance,
|
||||||
error,
|
error,
|
||||||
),
|
),
|
||||||
|
balance: {
|
||||||
|
xdai,
|
||||||
|
bzz,
|
||||||
|
},
|
||||||
latestUserVersion,
|
latestUserVersion,
|
||||||
latestUserVersionExact,
|
latestUserVersionExact,
|
||||||
latestPublishedVersion,
|
latestPublishedVersion,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ interface ContextInterface {
|
|||||||
setApiUrl: (url: string) => void
|
setApiUrl: (url: string) => void
|
||||||
setDebugApiUrl: (url: string) => void
|
setDebugApiUrl: (url: string) => void
|
||||||
lockedApiSettings: boolean
|
lockedApiSettings: boolean
|
||||||
|
desktopApiKey: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialValues: ContextInterface = {
|
const initialValues: ContextInterface = {
|
||||||
@@ -20,6 +21,7 @@ const initialValues: ContextInterface = {
|
|||||||
setApiUrl: () => {}, // eslint-disable-line
|
setApiUrl: () => {}, // eslint-disable-line
|
||||||
setDebugApiUrl: () => {}, // eslint-disable-line
|
setDebugApiUrl: () => {}, // eslint-disable-line
|
||||||
lockedApiSettings: false,
|
lockedApiSettings: false,
|
||||||
|
desktopApiKey: '',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Context = createContext<ContextInterface>(initialValues)
|
export const Context = createContext<ContextInterface>(initialValues)
|
||||||
@@ -43,10 +45,22 @@ export function Provider({
|
|||||||
const [beeApi, setBeeApi] = useState<Bee | null>(null)
|
const [beeApi, setBeeApi] = useState<Bee | null>(null)
|
||||||
const [beeDebugApi, setBeeDebugApi] = useState<BeeDebug | null>(null)
|
const [beeDebugApi, setBeeDebugApi] = useState<BeeDebug | null>(null)
|
||||||
const [lockedApiSettings] = useState<boolean>(Boolean(extLockedApiSettings))
|
const [lockedApiSettings] = useState<boolean>(Boolean(extLockedApiSettings))
|
||||||
|
const [desktopApiKey, setDesktopApiKey] = useState<string>(initialValues.desktopApiKey)
|
||||||
|
|
||||||
const url = beeApiUrl || apiUrl
|
const url = beeApiUrl || apiUrl
|
||||||
const debugUrl = beeDebugApiUrl || apiDebugUrl
|
const debugUrl = beeDebugApiUrl || apiDebugUrl
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const urlSearchParams = new URLSearchParams(window.location.search)
|
||||||
|
const newApiKey = urlSearchParams.get('v')
|
||||||
|
|
||||||
|
if (newApiKey) {
|
||||||
|
localStorage.setItem('apiKey', newApiKey)
|
||||||
|
window.location.search = ''
|
||||||
|
setDesktopApiKey(newApiKey)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
try {
|
try {
|
||||||
setBeeApi(new Bee(url))
|
setBeeApi(new Bee(url))
|
||||||
@@ -75,6 +89,7 @@ export function Provider({
|
|||||||
setApiUrl,
|
setApiUrl,
|
||||||
setDebugApiUrl,
|
setDebugApiUrl,
|
||||||
lockedApiSettings,
|
lockedApiSettings,
|
||||||
|
desktopApiKey,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
export async function requestBzz(address: string): Promise<void> {
|
||||||
|
await axios.post(`https://xbzz-faucet.apyos.dev/xbzz/${address}`)
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
import { getJson, postJson } from './net'
|
||||||
|
|
||||||
|
interface DesktopStatus {
|
||||||
|
status: 0 | 1 | 2
|
||||||
|
address: string | null
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
config: Record<string, any>
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getDesktopStatus(): Promise<DesktopStatus> {
|
||||||
|
const response = await getJson(`http://${getDesktopHost()}/status`)
|
||||||
|
|
||||||
|
return response as DesktopStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getGasFromFaucet(address: string): Promise<void> {
|
||||||
|
await axios.post(`http://getxdai.co/${address}/0.1`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function upgradeToLightNode(rpcProvider: string): Promise<void> {
|
||||||
|
await updateDesktopConfiguration({
|
||||||
|
'chain-enable': true,
|
||||||
|
'swap-enable': true,
|
||||||
|
'swap-endpoint': rpcProvider,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateDesktopConfiguration(values: Record<string, unknown>): Promise<void> {
|
||||||
|
await postJson(`http://${getDesktopHost()}/config`, values)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function restartBeeNode(): Promise<void> {
|
||||||
|
await postJson(`http://${getDesktopHost()}/restart`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDesktopHost(): string {
|
||||||
|
return window.location.host
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
export function getJson(url: string): Promise<Record<string, any>> {
|
||||||
|
return sendRequest(url, 'GET')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function postJson(url: string, data?: Record<string, any>): Promise<Record<string, unknown>> {
|
||||||
|
return sendRequest(url, 'POST', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendRequest(
|
||||||
|
url: string,
|
||||||
|
method: 'GET' | 'POST',
|
||||||
|
data?: Record<string, unknown>,
|
||||||
|
): Promise<Record<string, any>> {
|
||||||
|
const authorization = localStorage.getItem('apiKey')
|
||||||
|
|
||||||
|
if (!authorization) {
|
||||||
|
throw Error('API key not found in local storage')
|
||||||
|
}
|
||||||
|
const headers = {
|
||||||
|
authorization,
|
||||||
|
}
|
||||||
|
const response = await axios(url, {
|
||||||
|
method,
|
||||||
|
headers,
|
||||||
|
data,
|
||||||
|
})
|
||||||
|
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import { debounce } from '@material-ui/core'
|
||||||
|
import axios from 'axios'
|
||||||
|
import { Contract, providers } from 'ethers'
|
||||||
|
|
||||||
|
const PROVIDER = 'https://gno.getblock.io/mainnet/?api_key=d7b92d96-9784-49a8-a800-b3edd1647fc7'
|
||||||
|
|
||||||
|
async function eth_getBalance(address: string): Promise<string> {
|
||||||
|
if (!address.startsWith('0x')) {
|
||||||
|
address = `0x${address}`
|
||||||
|
}
|
||||||
|
const response = await axios(PROVIDER, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'content-type': 'application/json',
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
method: 'eth_getBalance',
|
||||||
|
params: [address, 'latest'],
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return response.data.result
|
||||||
|
}
|
||||||
|
|
||||||
|
const partialERC20tokenABI = [
|
||||||
|
{
|
||||||
|
constant: true,
|
||||||
|
inputs: [
|
||||||
|
{
|
||||||
|
name: '_owner',
|
||||||
|
type: 'address',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
name: 'balanceOf',
|
||||||
|
outputs: [
|
||||||
|
{
|
||||||
|
name: 'balance',
|
||||||
|
type: 'uint256',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
payable: false,
|
||||||
|
type: 'function',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const provider = new providers.JsonRpcProvider(PROVIDER)
|
||||||
|
|
||||||
|
async function eth_getBalanceERC20(
|
||||||
|
address: string,
|
||||||
|
tokenAddress = '0xdbf3ea6f5bee45c02255b2c26a16f300502f68da',
|
||||||
|
): Promise<string> {
|
||||||
|
if (!address.startsWith('0x')) {
|
||||||
|
address = `0x${address}`
|
||||||
|
}
|
||||||
|
const contract = new Contract(tokenAddress, partialERC20tokenABI, provider)
|
||||||
|
const balance = await contract.balanceOf(address)
|
||||||
|
|
||||||
|
return balance.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Rpc = {
|
||||||
|
eth_getBalance: debounce(eth_getBalance, 1_000),
|
||||||
|
eth_getBalanceERC20: debounce(eth_getBalanceERC20, 1_000),
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user