Merge pull request #783 from nymtech/tauri-wallet-frontend
tauri wallet front-end
This commit is contained in:
@@ -15,6 +15,7 @@ The platform is composed of multiple Rust crates. Top-level executable binary cr
|
||||
* nym-gateway - acts sort of like a mailbox for mixnet messages, removing the need for directly delivery to potentially offline or firewalled devices.
|
||||
* nym-network-monitor - sends packets through the full system to check that they are working as expected, and stores node uptime histories as the basis of a rewards system ("mixmining" or "proof-of-mixing").
|
||||
* nym-explorer - a (projected) block explorer and (existing) mixnet viewer.
|
||||
* nym-wallet (currently in development)- a desktop wallet implemented using the [Tauri](https://tauri.studio/en/docs/about/intro) framework.
|
||||
|
||||
[](https://opensource.org/licenses/Apache-2.0)
|
||||
[](https://github.com/nymtech/nym/actions?query=branch%3Adevelop)
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
<!--
|
||||
Copyright 2020 - Nym Technologies SA <contact@nymtech.net>
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
|
||||
# Nym Tauri Wallet
|
||||
|
||||
A Rust and Tauri desktop wallet implementation.
|
||||
|
||||
## Installation prerequisites
|
||||
|
||||
* `Yarn`
|
||||
* `NodeJS >= v16.8.0`
|
||||
* `Rust & cargo >= v1.51`
|
||||
|
||||
## Installation & usage
|
||||
|
||||
* `yarn install`
|
||||
* `yarn dev`
|
||||
@@ -1,7 +1,7 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Tauri Web Wallet</title>
|
||||
<title>Nym Wallet</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import React, { useContext } from 'react'
|
||||
import React, { useContext, useEffect, useState } from 'react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import {
|
||||
Backdrop,
|
||||
Box,
|
||||
Button,
|
||||
CircularProgress,
|
||||
FormControl,
|
||||
@@ -14,20 +15,45 @@ import {
|
||||
import { useTheme } from '@material-ui/styles'
|
||||
import { ClientContext } from '../context/main'
|
||||
import { NymCard } from '.'
|
||||
import { getContractParams, setContractParams } from '../requests'
|
||||
import { TauriStateParams } from '../types'
|
||||
|
||||
export const Admin: React.FC = () => {
|
||||
const { showAdmin, handleShowAdmin } = useContext(ClientContext)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [params, setParams] = useState<TauriStateParams>()
|
||||
|
||||
const onCancel = () => {
|
||||
setParams(undefined)
|
||||
setIsLoading(false)
|
||||
handleShowAdmin()
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const requestContractParams = async () => {
|
||||
if (showAdmin) {
|
||||
setIsLoading(true)
|
||||
const params = await getContractParams()
|
||||
setParams(params)
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
requestContractParams()
|
||||
}, [showAdmin])
|
||||
|
||||
return (
|
||||
<Backdrop open={showAdmin} style={{ zIndex: 2, overflow: 'auto' }}>
|
||||
<Slide in={showAdmin}>
|
||||
<Paper style={{ margin: 'auto' }}>
|
||||
<NymCard title="Admin" subheader="Contract administration" noPadding>
|
||||
<AdminForm onCancel={onCancel} />
|
||||
{isLoading && (
|
||||
<Box style={{ display: 'flex', justifyContent: 'center' }}>
|
||||
<CircularProgress size={48} />
|
||||
</Box>
|
||||
)}
|
||||
{!isLoading && params && (
|
||||
<AdminForm onCancel={onCancel} params={params} />
|
||||
)}
|
||||
</NymCard>
|
||||
</Paper>
|
||||
</Slide>
|
||||
@@ -35,14 +61,18 @@ export const Admin: React.FC = () => {
|
||||
)
|
||||
}
|
||||
|
||||
const AdminForm: React.FC<{ onCancel: () => void }> = ({ onCancel }) => {
|
||||
const AdminForm: React.FC<{
|
||||
params: TauriStateParams
|
||||
onCancel: () => void
|
||||
}> = ({ params, onCancel }) => {
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors, isSubmitting },
|
||||
} = useForm()
|
||||
} = useForm({ defaultValues: { ...params } })
|
||||
|
||||
const onSubmit = (data: any) => {
|
||||
const onSubmit = async (data: TauriStateParams) => {
|
||||
await setContractParams(data)
|
||||
console.log(data)
|
||||
onCancel()
|
||||
}
|
||||
@@ -51,110 +81,112 @@ const AdminForm: React.FC<{ onCancel: () => void }> = ({ onCancel }) => {
|
||||
|
||||
return (
|
||||
<FormControl fullWidth>
|
||||
<div style={{ padding: theme.spacing(3, 5), maxWidth: 700 }}>
|
||||
<div
|
||||
style={{ padding: theme.spacing(3, 5), maxWidth: 700, minWidth: 400 }}
|
||||
>
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12}>
|
||||
<TextField
|
||||
{...register('minimumMixnodeBond')}
|
||||
{...register('minimum_mixnode_bond')}
|
||||
required
|
||||
variant="outlined"
|
||||
id="minimumMixnodeBond"
|
||||
name="minimumMixnodeBond"
|
||||
id="minimum_mixnode_bond"
|
||||
name="minimum_mixnode_bond"
|
||||
label="Minumum mixnode bond"
|
||||
fullWidth
|
||||
error={!!errors.minimumMixnodeBond}
|
||||
helperText={errors?.minimumMixnodeBond?.message}
|
||||
error={!!errors.minimum_mixnode_bond}
|
||||
helperText={errors?.minimum_mixnode_bond?.message}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<TextField
|
||||
{...register('minimumGatewayBond')}
|
||||
{...register('minimum_gateway_bond')}
|
||||
required
|
||||
variant="outlined"
|
||||
id="minimumGatewayBond"
|
||||
name="minimumGatewayBond"
|
||||
id="minimum_gateway_bond"
|
||||
name="minimum_gateway_bond"
|
||||
label="Minumum gateway bond"
|
||||
fullWidth
|
||||
error={!!errors.minimumGatewayBond}
|
||||
helperText={errors?.minimumGatewayBond?.message}
|
||||
error={!!errors.minimum_gateway_bond}
|
||||
helperText={errors?.minimum_gateway_bond?.message}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<TextField
|
||||
{...register('mixnodeBondRewardRate')}
|
||||
{...register('mixnode_bond_reward_rate')}
|
||||
required
|
||||
variant="outlined"
|
||||
id="mixnodeBondRewardRate"
|
||||
name="mixnodeBondRewardRate"
|
||||
id="mixnode_bond_reward_rate"
|
||||
name="mixnode_bond_reward_rate"
|
||||
label="Mixnode bond reward rate"
|
||||
fullWidth
|
||||
error={!!errors.mixnodeBondRewardRate}
|
||||
helperText={errors?.mixnodeBondRewardRate?.message}
|
||||
error={!!errors.mixnode_bond_reward_rate}
|
||||
helperText={errors?.mixnode_bond_reward_rate?.message}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<TextField
|
||||
{...register('gatewayBondRewardRate')}
|
||||
{...register('gateway_bond_reward_rate')}
|
||||
required
|
||||
variant="outlined"
|
||||
id="gatewayBondRewardRate"
|
||||
name="gatewayBondRewardRate"
|
||||
id="gateway_bond_reward_rate"
|
||||
name="gateway_bond_reward_rate"
|
||||
label="Gateway bond reward rate"
|
||||
fullWidth
|
||||
error={!!errors.gatewayBondRewardRate}
|
||||
helperText={errors?.gatewayBondRewardRate?.message}
|
||||
error={!!errors.gateway_bond_reward_rate}
|
||||
helperText={errors?.gateway_bond_reward_rate?.message}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<TextField
|
||||
{...register('mixnodeDelegationRewardRate')}
|
||||
{...register('mixnode_delegation_reward_rate')}
|
||||
required
|
||||
variant="outlined"
|
||||
id="mixnodeDelegationRewardRate"
|
||||
name="mixnodeDelegationRewardRate"
|
||||
id="mixnode_delegation_reward_rate"
|
||||
name="mixnode_delegation_reward_rate"
|
||||
label="Mixnode Delegation Reward Rate"
|
||||
fullWidth
|
||||
error={!!errors.mixnodeDelegationRewardRate}
|
||||
helperText={errors?.mixnodeDelegationRewardRate?.message}
|
||||
error={!!errors.mixnode_delegation_reward_rate}
|
||||
helperText={errors?.mixnode_delegation_reward_rate?.message}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<TextField
|
||||
{...register('gatewayDelegationRewardRate')}
|
||||
{...register('gateway_delegation_reward_rate')}
|
||||
required
|
||||
variant="outlined"
|
||||
id="gatewayDelegationRewardRate"
|
||||
name="gatewayDelegationRewardRate"
|
||||
id="gateway_delegation_reward_rate"
|
||||
name="gateway_delegation_reward_rate"
|
||||
label="Gateway Delegation Reward Rate"
|
||||
fullWidth
|
||||
error={!!errors.gatewayDelegationRewardRate}
|
||||
helperText={errors?.gatewayDelegationRewardRate?.message}
|
||||
error={!!errors.gateway_delegation_reward_rate}
|
||||
helperText={errors?.gateway_delegation_reward_rate?.message}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<TextField
|
||||
{...register('epochLength')}
|
||||
{...register('epoch_length')}
|
||||
required
|
||||
variant="outlined"
|
||||
id="epochLength"
|
||||
name="epochLength"
|
||||
label="Epoch length (hours)"
|
||||
fullWidth
|
||||
error={!!errors.epochLength}
|
||||
helperText={errors?.epochLength?.message}
|
||||
error={!!errors.epoch_length}
|
||||
helperText={errors?.epoch_length?.message}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<TextField
|
||||
{...register('mixNodeActiveSetSize')}
|
||||
{...register('mixnode_active_set_size', { valueAsNumber: true })}
|
||||
required
|
||||
variant="outlined"
|
||||
id="mixNodeActiveSetSize"
|
||||
name="mixNodeActiveSetSize"
|
||||
id="mixnode_active_set_size"
|
||||
name="mixnode_active_set_size"
|
||||
label="Mixnode Active Set Size "
|
||||
fullWidth
|
||||
error={!!errors.epochLength}
|
||||
helperText={errors?.epochLength?.message}
|
||||
error={!!errors.mixnode_active_set_size}
|
||||
helperText={errors?.mixnode_active_set_size?.message}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
@@ -9,9 +9,11 @@ import React from 'react'
|
||||
import { EnumNodeType } from '../types/global'
|
||||
|
||||
export const NodeTypeSelector = ({
|
||||
disabled,
|
||||
nodeType,
|
||||
setNodeType,
|
||||
}: {
|
||||
disabled: boolean
|
||||
nodeType: EnumNodeType
|
||||
setNodeType: (nodeType: EnumNodeType) => void
|
||||
}) => {
|
||||
@@ -32,11 +34,13 @@ export const NodeTypeSelector = ({
|
||||
value={EnumNodeType.mixnode}
|
||||
control={<Radio />}
|
||||
label="Mixnode"
|
||||
disabled={disabled}
|
||||
/>
|
||||
<FormControlLabel
|
||||
value={EnumNodeType.gateway}
|
||||
control={<Radio />}
|
||||
label="Gateway"
|
||||
disabled={disabled}
|
||||
/>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { createContext, useEffect, useState } from 'react'
|
||||
import { useHistory } from 'react-router-dom'
|
||||
import { Coin, TClientDetails, TSignInWithMnemonic } from '../types'
|
||||
import { TClientDetails, TSignInWithMnemonic } from '../types'
|
||||
import { TUseGetBalance, useGetBalance } from '../hooks/useGetBalance'
|
||||
|
||||
export const ADMIN_ADDRESS = 'punk1h3w4nj7kny5dfyjw2le4vm74z03v9vd4dstpu0'
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { checkGatewayOwnership, checkMixnodeOwnership } from '../requests'
|
||||
import { EnumNodeType, TNodeOwnership } from '../types'
|
||||
|
||||
export const useCheckOwnership = () => {
|
||||
const [ownership, setOwnership] = useState<TNodeOwnership>({
|
||||
hasOwnership: false,
|
||||
nodeType: undefined,
|
||||
})
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [error, setError] = useState<string>()
|
||||
|
||||
const checkOwnership = async () => {
|
||||
const status = {} as TNodeOwnership
|
||||
|
||||
setIsLoading(true)
|
||||
|
||||
try {
|
||||
const ownsMixnode = await checkMixnodeOwnership()
|
||||
const ownsGateway = await checkGatewayOwnership()
|
||||
|
||||
if (ownsMixnode) {
|
||||
status.hasOwnership = true
|
||||
status.nodeType = EnumNodeType.mixnode
|
||||
}
|
||||
|
||||
if (ownsGateway) {
|
||||
status.hasOwnership = true
|
||||
status.nodeType = EnumNodeType.gateway
|
||||
}
|
||||
|
||||
setOwnership(status)
|
||||
} catch (e) {
|
||||
setError(e as string)
|
||||
}
|
||||
}
|
||||
|
||||
return { isLoading, error, ownership, checkOwnership }
|
||||
}
|
||||
@@ -11,17 +11,17 @@ export type TUseGetBalance = {
|
||||
|
||||
export const useGetBalance = (): TUseGetBalance => {
|
||||
const [balance, setBalance] = useState<Balance>()
|
||||
const [error, setEror] = useState<string>()
|
||||
const [error, setError] = useState<string>()
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
const fetchBalance = () => {
|
||||
setIsLoading(true)
|
||||
setEror(undefined)
|
||||
setError(undefined)
|
||||
invoke('get_balance')
|
||||
.then((balance) => {
|
||||
setBalance(balance as Balance)
|
||||
})
|
||||
.catch((e) => setEror(e))
|
||||
.catch(setError)
|
||||
setTimeout(() => {
|
||||
setIsLoading(false)
|
||||
}, 1000)
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
import { invoke } from '@tauri-apps/api'
|
||||
import { Coin, Operation, TCreateAccount, TSignInWithMnemonic } from '../types'
|
||||
import {
|
||||
Balance,
|
||||
Coin,
|
||||
DelegationResult,
|
||||
EnumNodeType,
|
||||
Gateway,
|
||||
MixNode,
|
||||
Operation,
|
||||
TauriStateParams,
|
||||
TauriTxResult,
|
||||
TCreateAccount,
|
||||
TSignInWithMnemonic,
|
||||
} from '../types'
|
||||
|
||||
export const createAccount = async (): Promise<TCreateAccount> =>
|
||||
await invoke('create_new_account')
|
||||
@@ -17,3 +29,57 @@ export const majorToMinor = async (amount: string): Promise<Coin> =>
|
||||
|
||||
export const getGasFee = async (operation: Operation): Promise<Coin> =>
|
||||
await invoke('get_fee', { operation })
|
||||
|
||||
export const delegate = async ({
|
||||
type,
|
||||
identity,
|
||||
amount,
|
||||
}: {
|
||||
type: EnumNodeType
|
||||
identity: string
|
||||
amount: Coin
|
||||
}): Promise<DelegationResult> =>
|
||||
await invoke(`delegate_to_${type}`, { identity, amount })
|
||||
|
||||
export const undelegate = async ({
|
||||
type,
|
||||
identity,
|
||||
}: {
|
||||
type: EnumNodeType
|
||||
identity: string
|
||||
}): Promise<DelegationResult> =>
|
||||
await invoke(`undelegate_from_${type}`, { identity })
|
||||
|
||||
export const send = async (args: {
|
||||
amount: Coin
|
||||
address: string
|
||||
memo: string
|
||||
}): Promise<TauriTxResult> => await invoke('send', args)
|
||||
export const checkMixnodeOwnership = async (): Promise<boolean> =>
|
||||
await invoke('owns_mixnode')
|
||||
|
||||
export const checkGatewayOwnership = async (): Promise<boolean> =>
|
||||
await invoke('owns_gateway')
|
||||
|
||||
export const bond = async ({
|
||||
type,
|
||||
data,
|
||||
amount,
|
||||
}: {
|
||||
type: EnumNodeType
|
||||
data: MixNode | Gateway
|
||||
amount: Coin
|
||||
}): Promise<any> => await invoke(`bond_${type}`, { [type]: data, bond: amount })
|
||||
|
||||
export const unbond = async (type: EnumNodeType) =>
|
||||
await invoke(`unbond_${type}`)
|
||||
|
||||
export const getBalance = async (): Promise<Balance> =>
|
||||
await invoke('get_balance')
|
||||
|
||||
export const getContractParams = async (): Promise<TauriStateParams> =>
|
||||
await invoke('get_state_params')
|
||||
|
||||
export const setContractParams = async (
|
||||
params: TauriStateParams
|
||||
): Promise<TauriStateParams> => await invoke('update_state_params', { params })
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react'
|
||||
import React, { useContext } from 'react'
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
@@ -11,14 +11,16 @@ import {
|
||||
Theme,
|
||||
} from '@material-ui/core'
|
||||
import { useTheme } from '@material-ui/styles'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { Alert } from '@material-ui/lab'
|
||||
import { yupResolver } from '@hookform/resolvers/yup'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { EnumNodeType } from '../../types/global'
|
||||
import { NodeTypeSelector } from '../../components/NodeTypeSelector'
|
||||
import { bond, majorToMinor } from '../../requests'
|
||||
import { validationSchema } from './validationSchema'
|
||||
import { Coin, Gateway, MixNode } from '../../types'
|
||||
import { invoke } from '@tauri-apps/api'
|
||||
import { Alert } from '@material-ui/lab'
|
||||
import { ClientContext } from '../../context/main'
|
||||
import { checkHasEnoughFunds } from '../../utils'
|
||||
|
||||
type TBondFormFields = {
|
||||
withAdvancedOptions: boolean
|
||||
@@ -57,29 +59,27 @@ const formatData = (data: TBondFormFields) => {
|
||||
host: data.host,
|
||||
version: data.version,
|
||||
mix_port: data.mixPort,
|
||||
amount: data.amount,
|
||||
nodeType: data.nodeType,
|
||||
}
|
||||
|
||||
if (data.nodeType === EnumNodeType.mixnode) {
|
||||
payload.verloc_port = data.verlocPort
|
||||
payload.http_api_port = data.httpApiPort
|
||||
return payload as MixNode & { amount: number; nodeType: EnumNodeType }
|
||||
}
|
||||
|
||||
if (data.nodeType == EnumNodeType.gateway) {
|
||||
return payload as MixNode
|
||||
} else {
|
||||
payload.clients_port = data.clientsPort
|
||||
payload.location = data.location
|
||||
return payload as Gateway & { amount: number; nodeType: EnumNodeType }
|
||||
return payload as Gateway
|
||||
}
|
||||
}
|
||||
|
||||
export const BondForm = ({
|
||||
disabled,
|
||||
fees,
|
||||
onError,
|
||||
onSuccess,
|
||||
}: {
|
||||
fees: { [key in EnumNodeType]: Coin }
|
||||
disabled: boolean
|
||||
fees?: { [key in EnumNodeType]: Coin }
|
||||
onError: (message?: string) => void
|
||||
onSuccess: (message?: string) => void
|
||||
}) => {
|
||||
@@ -87,6 +87,7 @@ export const BondForm = ({
|
||||
register,
|
||||
handleSubmit,
|
||||
setValue,
|
||||
setError,
|
||||
watch,
|
||||
formState: { errors, isSubmitting },
|
||||
} = useForm<TBondFormFields>({
|
||||
@@ -94,6 +95,8 @@ export const BondForm = ({
|
||||
defaultValues,
|
||||
})
|
||||
|
||||
const { getBalance } = useContext(ClientContext)
|
||||
|
||||
const watchNodeType = watch('nodeType', defaultValues.nodeType)
|
||||
const watchAdvancedOptions = watch(
|
||||
'withAdvancedOptions',
|
||||
@@ -101,13 +104,18 @@ export const BondForm = ({
|
||||
)
|
||||
|
||||
const onSubmit = async (data: TBondFormFields) => {
|
||||
const hasEnoughFunds = await checkHasEnoughFunds(data.amount)
|
||||
if (!hasEnoughFunds) {
|
||||
return setError('amount', { message: 'Not enough funds in wallet' })
|
||||
}
|
||||
|
||||
const formattedData = formatData(data)
|
||||
await invoke(`bond_${data.nodeType}`, {
|
||||
[data.nodeType]: formattedData,
|
||||
bond: { amount: formattedData?.amount, denom: 'punk' },
|
||||
})
|
||||
.then((res: any) => {
|
||||
onSuccess(res)
|
||||
const amount = await majorToMinor(data.amount)
|
||||
|
||||
await bond({ type: data.nodeType, data: formattedData, amount })
|
||||
.then(() => {
|
||||
getBalance.fetchBalance()
|
||||
onSuccess(`Successfully bonded to ${data.identityKey}`)
|
||||
})
|
||||
.catch((e) => {
|
||||
onError(e)
|
||||
@@ -129,17 +137,20 @@ export const BondForm = ({
|
||||
if (nodeType === EnumNodeType.mixnode)
|
||||
setValue('location', undefined)
|
||||
}}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Alert severity="info">
|
||||
{`A fee of ${
|
||||
watchNodeType === EnumNodeType.mixnode
|
||||
? fees.mixnode.amount
|
||||
: fees.gateway.amount
|
||||
} PUNK will apply to this transaction`}
|
||||
</Alert>
|
||||
</Grid>
|
||||
{fees && (
|
||||
<Grid item>
|
||||
<Alert severity="info">
|
||||
{`A fee of ${
|
||||
watchNodeType === EnumNodeType.mixnode
|
||||
? fees.mixnode.amount
|
||||
: fees.gateway.amount
|
||||
} PUNK will apply to this transaction`}
|
||||
</Alert>
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<TextField
|
||||
@@ -152,6 +163,7 @@ export const BondForm = ({
|
||||
fullWidth
|
||||
error={!!errors.identityKey}
|
||||
helperText={errors.identityKey?.message}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
@@ -165,6 +177,7 @@ export const BondForm = ({
|
||||
error={!!errors.sphinxKey}
|
||||
helperText={errors.sphinxKey?.message}
|
||||
fullWidth
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={9}>
|
||||
@@ -183,6 +196,7 @@ export const BondForm = ({
|
||||
<InputAdornment position="end">punks</InputAdornment>
|
||||
),
|
||||
}}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
@@ -197,6 +211,7 @@ export const BondForm = ({
|
||||
fullWidth
|
||||
error={!!errors.host}
|
||||
helperText={errors.host?.message}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
@@ -213,6 +228,7 @@ export const BondForm = ({
|
||||
fullWidth
|
||||
error={!!errors.location}
|
||||
helperText={errors.location?.message}
|
||||
disabled={disabled}
|
||||
/>
|
||||
)}
|
||||
</Grid>
|
||||
@@ -228,6 +244,7 @@ export const BondForm = ({
|
||||
fullWidth
|
||||
error={!!errors.version}
|
||||
helperText={errors.version?.message}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
@@ -275,6 +292,7 @@ export const BondForm = ({
|
||||
helperText={
|
||||
errors.mixPort?.message && 'A valid port value is required'
|
||||
}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Grid>
|
||||
{watchNodeType === EnumNodeType.mixnode ? (
|
||||
@@ -292,6 +310,7 @@ export const BondForm = ({
|
||||
errors.verlocPort?.message &&
|
||||
'A valid port value is required'
|
||||
}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
@@ -308,6 +327,7 @@ export const BondForm = ({
|
||||
errors.httpApiPort?.message &&
|
||||
'A valid port value is required'
|
||||
}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Grid>
|
||||
</>
|
||||
@@ -325,6 +345,7 @@ export const BondForm = ({
|
||||
errors.clientsPort?.message &&
|
||||
'A valid port value is required'
|
||||
}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
@@ -343,7 +364,7 @@ export const BondForm = ({
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
disabled={isSubmitting}
|
||||
disabled={isSubmitting || disabled}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
type="submit"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import React, { useContext, useEffect, useState } from 'react'
|
||||
import { Box, Button, CircularProgress, Theme } from '@material-ui/core'
|
||||
import { Alert } from '@material-ui/lab'
|
||||
import { useTheme } from '@material-ui/styles'
|
||||
@@ -9,34 +9,59 @@ import {
|
||||
RequestStatus,
|
||||
} from '../../components/RequestStatus'
|
||||
import { Layout } from '../../layouts'
|
||||
import { getGasFee } from '../../requests'
|
||||
import { getGasFee, unbond } from '../../requests'
|
||||
import { TFee } from '../../types'
|
||||
import { useCheckOwnership } from '../../hooks/useCheckOwnership'
|
||||
import { ClientContext } from '../../context/main'
|
||||
|
||||
export const Bond = () => {
|
||||
const [status, setStatus] = useState(EnumRequestStatus.loading)
|
||||
const [status, setStatus] = useState(EnumRequestStatus.initial)
|
||||
const [message, setMessage] = useState<string>()
|
||||
const [fees, setFees] = useState<TFee>()
|
||||
|
||||
const { checkOwnership, ownership } = useCheckOwnership()
|
||||
const { getBalance } = useContext(ClientContext)
|
||||
|
||||
const theme: Theme = useTheme()
|
||||
|
||||
useEffect(() => {
|
||||
const getFees = async () => {
|
||||
const mixnode = await getGasFee('BondMixnode')
|
||||
const gateway = await getGasFee('BondGateway')
|
||||
setFees({
|
||||
mixnode: mixnode,
|
||||
gateway: gateway,
|
||||
})
|
||||
setStatus(EnumRequestStatus.initial)
|
||||
if (status === EnumRequestStatus.initial) {
|
||||
const initialiseForm = async () => {
|
||||
await checkOwnership()
|
||||
setFees({
|
||||
mixnode: await getGasFee('BondMixnode'),
|
||||
gateway: await getGasFee('BondGateway'),
|
||||
})
|
||||
setStatus(EnumRequestStatus.initial)
|
||||
}
|
||||
initialiseForm()
|
||||
}
|
||||
}, [status])
|
||||
|
||||
getFees()
|
||||
}, [])
|
||||
|
||||
console.log(fees, status, message)
|
||||
return (
|
||||
<Layout>
|
||||
<NymCard title="Bond" subheader="Bond a node or gateway" noPadding>
|
||||
{ownership?.hasOwnership && (
|
||||
<Alert
|
||||
severity="warning"
|
||||
action={
|
||||
<Button
|
||||
disabled={status === EnumRequestStatus.loading}
|
||||
onClick={async () => {
|
||||
setStatus(EnumRequestStatus.loading)
|
||||
await unbond(ownership.nodeType!)
|
||||
getBalance.fetchBalance()
|
||||
setStatus(EnumRequestStatus.initial)
|
||||
}}
|
||||
>
|
||||
Unbond
|
||||
</Button>
|
||||
}
|
||||
style={{ margin: theme.spacing(2) }}
|
||||
>
|
||||
{`Looks like you already have a ${ownership.nodeType} bonded.`}
|
||||
</Alert>
|
||||
)}
|
||||
{status === EnumRequestStatus.loading && (
|
||||
<Box
|
||||
style={{
|
||||
@@ -50,7 +75,7 @@ export const Bond = () => {
|
||||
)}
|
||||
{status === EnumRequestStatus.initial && (
|
||||
<BondForm
|
||||
fees={fees!}
|
||||
fees={!ownership.hasOwnership ? fees : undefined}
|
||||
onError={(e?: string) => {
|
||||
setMessage(e)
|
||||
setStatus(EnumRequestStatus.error)
|
||||
@@ -59,6 +84,7 @@ export const Bond = () => {
|
||||
setMessage(message)
|
||||
setStatus(EnumRequestStatus.success)
|
||||
}}
|
||||
disabled={ownership?.hasOwnership}
|
||||
/>
|
||||
)}
|
||||
{(status === EnumRequestStatus.error ||
|
||||
@@ -88,9 +114,10 @@ export const Bond = () => {
|
||||
<Button
|
||||
onClick={() => {
|
||||
setStatus(EnumRequestStatus.initial)
|
||||
checkOwnership()
|
||||
}}
|
||||
>
|
||||
Resend?
|
||||
Again?
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -14,10 +14,10 @@ import { NodeTypeSelector } from '../../components/NodeTypeSelector'
|
||||
import { EnumNodeType, TFee } from '../../types'
|
||||
import { yupResolver } from '@hookform/resolvers/yup'
|
||||
import { validationSchema } from './validationSchema'
|
||||
import { invoke } from '@tauri-apps/api'
|
||||
import { Alert } from '@material-ui/lab'
|
||||
import { ClientContext } from '../../context/main'
|
||||
import { majorToMinor } from '../../requests'
|
||||
import { delegate, majorToMinor } from '../../requests'
|
||||
import { checkHasEnoughFunds } from '../../utils'
|
||||
|
||||
type TDelegateForm = {
|
||||
nodeType: EnumNodeType
|
||||
@@ -46,6 +46,7 @@ export const DelegateForm = ({
|
||||
setValue,
|
||||
watch,
|
||||
handleSubmit,
|
||||
setError,
|
||||
formState: { errors, isSubmitting },
|
||||
} = useForm<TDelegateForm>({
|
||||
defaultValues,
|
||||
@@ -57,15 +58,24 @@ export const DelegateForm = ({
|
||||
const { getBalance } = useContext(ClientContext)
|
||||
|
||||
const onSubmit = async (data: TDelegateForm) => {
|
||||
const hasEnoughFunds = await checkHasEnoughFunds(data.amount)
|
||||
if (!hasEnoughFunds) {
|
||||
return setError('amount', {
|
||||
message: 'Not enough funds in wallet',
|
||||
})
|
||||
}
|
||||
|
||||
const amount = await majorToMinor(data.amount)
|
||||
|
||||
await invoke(`delegate_to_${data.nodeType}`, {
|
||||
await delegate({
|
||||
type: data.nodeType,
|
||||
identity: data.identity,
|
||||
amount,
|
||||
})
|
||||
.then((res: any) => {
|
||||
console.log(res)
|
||||
onSuccess(res)
|
||||
.then((res) => {
|
||||
onSuccess(
|
||||
`Successfully delegated ${data.amount} punk to ${res.source_address}`
|
||||
)
|
||||
getBalance.fetchBalance()
|
||||
})
|
||||
.catch((e) => {
|
||||
@@ -83,6 +93,7 @@ export const DelegateForm = ({
|
||||
<NodeTypeSelector
|
||||
nodeType={watchNodeType}
|
||||
setNodeType={(nodeType) => setValue('nodeType', nodeType)}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
EnumRequestStatus,
|
||||
RequestStatus,
|
||||
} from '../../components/RequestStatus'
|
||||
import { Alert } from '@material-ui/lab'
|
||||
import { Alert, AlertTitle } from '@material-ui/lab'
|
||||
import { TFee } from '../../types'
|
||||
import { getGasFee } from '../../requests'
|
||||
|
||||
@@ -76,7 +76,12 @@ export const Delegate = () => {
|
||||
An error occurred with the request: {message}
|
||||
</Alert>
|
||||
}
|
||||
Success={<Alert severity="success">{message}</Alert>}
|
||||
Success={
|
||||
<Alert severity="success">
|
||||
<AlertTitle>Delegation complete</AlertTitle>
|
||||
{message}
|
||||
</Alert>
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
@@ -93,7 +98,7 @@ export const Delegate = () => {
|
||||
setStatus(EnumRequestStatus.initial)
|
||||
}}
|
||||
>
|
||||
Resend?
|
||||
Finish
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -3,14 +3,14 @@ import { useForm, FormProvider } from 'react-hook-form'
|
||||
import { yupResolver } from '@hookform/resolvers/yup'
|
||||
import { Button, Step, StepLabel, Stepper, Theme } from '@material-ui/core'
|
||||
import { useTheme } from '@material-ui/styles'
|
||||
import { invoke } from '@tauri-apps/api'
|
||||
import { SendForm } from './SendForm'
|
||||
import { SendReview } from './SendReview'
|
||||
import { SendConfirmation } from './SendConfirmation'
|
||||
import { ClientContext } from '../../context/main'
|
||||
import { validationSchema } from './validationSchema'
|
||||
import { TauriTxResult } from '../../types/rust/tauritxresult'
|
||||
import { majorToMinor } from '../../requests'
|
||||
import { TauriTxResult } from '../../types'
|
||||
import { majorToMinor, send } from '../../requests'
|
||||
import { checkHasEnoughFunds } from '../../utils'
|
||||
|
||||
const defaultValues = {
|
||||
amount: '',
|
||||
@@ -59,31 +59,40 @@ export const SendWizard = () => {
|
||||
}
|
||||
|
||||
const handleSend = async () => {
|
||||
setIsLoading(true)
|
||||
setActiveStep((s) => s + 1)
|
||||
const formState = methods.getValues()
|
||||
const amount = await majorToMinor(formState.amount)
|
||||
|
||||
invoke('send', {
|
||||
amount,
|
||||
address: formState.to,
|
||||
memo: formState.memo,
|
||||
})
|
||||
.then((res: any) => {
|
||||
const { details } = res as TauriTxResult
|
||||
setActiveStep((s) => s + 1)
|
||||
setConfirmedData({
|
||||
...details,
|
||||
amount: { denom: 'punk', amount: formState.amount },
|
||||
const hasEnoughFunds = await checkHasEnoughFunds(formState.amount)
|
||||
if (!hasEnoughFunds) {
|
||||
methods.setError('amount', {
|
||||
message: 'Not enough funds in wallet',
|
||||
})
|
||||
return handlePreviousStep()
|
||||
} else {
|
||||
setIsLoading(true)
|
||||
setActiveStep((s) => s + 1)
|
||||
const amount = await majorToMinor(formState.amount)
|
||||
|
||||
send({
|
||||
amount,
|
||||
address: formState.to,
|
||||
memo: formState.memo,
|
||||
})
|
||||
.then((res: any) => {
|
||||
const { details } = res as TauriTxResult
|
||||
setActiveStep((s) => s + 1)
|
||||
setConfirmedData({
|
||||
...details,
|
||||
amount: { denom: 'Major', amount: formState.amount },
|
||||
})
|
||||
setIsLoading(false)
|
||||
getBalance.fetchBalance()
|
||||
})
|
||||
setIsLoading(false)
|
||||
getBalance.fetchBalance()
|
||||
})
|
||||
.catch((e) => {
|
||||
setRequestError(e)
|
||||
setIsLoading(false)
|
||||
console.log(e)
|
||||
})
|
||||
.catch((e) => {
|
||||
setRequestError(e)
|
||||
setIsLoading(false)
|
||||
console.log(e)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import React, { useState } from 'react'
|
||||
import { Alert } from '@material-ui/lab'
|
||||
import { Button, Theme } from '@material-ui/core'
|
||||
import { useTheme } from '@material-ui/styles'
|
||||
|
||||
export const UnbondForm = () => {
|
||||
const theme: Theme = useTheme()
|
||||
return (
|
||||
<div>
|
||||
<Alert severity="info" style={{ margin: theme.spacing(3) }}>
|
||||
You don't currently have a bonded node
|
||||
</Alert>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-end',
|
||||
borderTop: `1px solid ${theme.palette.grey[200]}`,
|
||||
background: theme.palette.grey[100],
|
||||
padding: theme.spacing(2),
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
type="submit"
|
||||
disableElevation
|
||||
>
|
||||
Unbond
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,13 +1,68 @@
|
||||
import React from 'react'
|
||||
import React, { useContext, useEffect, useState } from 'react'
|
||||
import { NymCard } from '../../components'
|
||||
import { UnbondForm } from './UnbondForm'
|
||||
import { Layout } from '../../layouts'
|
||||
import { useCheckOwnership } from '../../hooks/useCheckOwnership'
|
||||
import { Alert } from '@material-ui/lab'
|
||||
import { Box, Button, CircularProgress, Theme } from '@material-ui/core'
|
||||
import { ClientContext } from '../../context/main'
|
||||
import { unbond } from '../../requests'
|
||||
import { useTheme } from '@material-ui/styles'
|
||||
|
||||
export const Unbond = () => {
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const { checkOwnership, ownership } = useCheckOwnership()
|
||||
const { getBalance } = useContext(ClientContext)
|
||||
|
||||
const theme: Theme = useTheme()
|
||||
|
||||
useEffect(() => {
|
||||
const initialiseForm = async () => {
|
||||
await checkOwnership()
|
||||
}
|
||||
initialiseForm()
|
||||
}, [ownership.hasOwnership, checkOwnership])
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<NymCard title="Unbond" subheader="Unbond a mixnode or gateway" noPadding>
|
||||
<UnbondForm />
|
||||
{ownership?.hasOwnership && (
|
||||
<Alert
|
||||
severity="warning"
|
||||
action={
|
||||
<Button
|
||||
disabled={isLoading}
|
||||
onClick={async () => {
|
||||
setIsLoading(true)
|
||||
await unbond(ownership.nodeType!)
|
||||
getBalance.fetchBalance()
|
||||
setIsLoading(false)
|
||||
}}
|
||||
>
|
||||
Unbond
|
||||
</Button>
|
||||
}
|
||||
style={{ margin: theme.spacing(2) }}
|
||||
>
|
||||
{`Looks like you already have a ${ownership.nodeType} bonded.`}
|
||||
</Alert>
|
||||
)}
|
||||
{!ownership.hasOwnership && (
|
||||
<Alert severity="info" style={{ margin: theme.spacing(3) }}>
|
||||
You don't currently have a bonded node
|
||||
</Alert>
|
||||
)}
|
||||
{isLoading && (
|
||||
<Box
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
padding: theme.spacing(3),
|
||||
}}
|
||||
>
|
||||
<CircularProgress size={48} />
|
||||
</Box>
|
||||
)}
|
||||
</NymCard>
|
||||
</Layout>
|
||||
)
|
||||
|
||||
@@ -10,12 +10,12 @@ import {
|
||||
} from '@material-ui/core'
|
||||
import { Alert } from '@material-ui/lab'
|
||||
import { useTheme } from '@material-ui/styles'
|
||||
import { invoke } from '@tauri-apps/api'
|
||||
import { yupResolver } from '@hookform/resolvers/yup'
|
||||
import { validationSchema } from './validationSchema'
|
||||
import { NodeTypeSelector } from '../../components/NodeTypeSelector'
|
||||
import { EnumNodeType, TFee } from '../../types'
|
||||
import { ClientContext } from '../../context/main'
|
||||
import { minorToMajor, undelegate } from '../../requests'
|
||||
|
||||
type TFormData = {
|
||||
nodeType: EnumNodeType
|
||||
@@ -50,11 +50,12 @@ export const UndelegateForm = ({
|
||||
const { getBalance } = useContext(ClientContext)
|
||||
|
||||
const onSubmit = async (data: TFormData) => {
|
||||
await invoke(`undelegate_from_${data.nodeType}`, {
|
||||
await undelegate({
|
||||
type: data.nodeType,
|
||||
identity: data.identity,
|
||||
})
|
||||
.then((res: any) => {
|
||||
onSuccess(res)
|
||||
.then(async (res) => {
|
||||
onSuccess(`Successfully undelegated from ${res.source_address}`)
|
||||
getBalance.fetchBalance()
|
||||
})
|
||||
.catch((e) => onError(e))
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { Alert } from '@material-ui/lab'
|
||||
import { Alert, AlertTitle } from '@material-ui/lab'
|
||||
import { useTheme } from '@material-ui/styles'
|
||||
import { NymCard } from '../../components'
|
||||
import { UndelegateForm } from './UndelegateForm'
|
||||
@@ -76,7 +76,13 @@ export const Undelegate = () => {
|
||||
An error occurred with the request: {message}
|
||||
</Alert>
|
||||
}
|
||||
Success={<Alert severity="success">{message}</Alert>}
|
||||
Success={
|
||||
<Alert severity="success">
|
||||
{' '}
|
||||
<AlertTitle>Undelegation complete</AlertTitle>
|
||||
{message}
|
||||
</Alert>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -6,8 +6,8 @@ export enum EnumNodeType {
|
||||
}
|
||||
|
||||
export type TNodeOwnership = {
|
||||
ownsMixnode: boolean
|
||||
ownsGateway: boolean
|
||||
hasOwnership: boolean
|
||||
nodeType?: EnumNodeType
|
||||
}
|
||||
|
||||
export type TClientDetails = {
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
export * from './account'
|
||||
export * from './balance'
|
||||
export * from './coin'
|
||||
export * from './mixnode'
|
||||
export * from './delegationresult'
|
||||
export * from './denom'
|
||||
export * from './gateway'
|
||||
export * from './mixnode'
|
||||
export * from './operation'
|
||||
export * from './stateparams'
|
||||
export * from './tauritxresult'
|
||||
export * from './transactiondetails'
|
||||
export * from './operation'
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { invoke } from '@tauri-apps/api'
|
||||
import bs58 from 'bs58'
|
||||
import { minor, valid } from 'semver'
|
||||
import { getBalance, majorToMinor } from '../requests'
|
||||
import { Coin } from '../types'
|
||||
|
||||
export const validateKey = (key: string): boolean => {
|
||||
@@ -75,7 +76,6 @@ export const validateVersion = (version: string): boolean => {
|
||||
const validVersion = valid(version)
|
||||
return validVersion !== null && minorVersion >= 11
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -90,3 +90,9 @@ export const validateRawPort = (rawPort: number): boolean =>
|
||||
|
||||
export const truncate = (text: string, trim: number) =>
|
||||
text.substring(0, trim) + '...'
|
||||
|
||||
export const checkHasEnoughFunds = async (allocationValue: string) => {
|
||||
const minorValue = await majorToMinor(allocationValue)
|
||||
const walletValue = await getBalance()
|
||||
return !(+walletValue.coin.amount - +minorValue.amount < 0)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user