Merge pull request #783 from nymtech/tauri-wallet-frontend

tauri wallet front-end
This commit is contained in:
Fouad
2021-09-23 20:59:27 +01:00
committed by GitHub
21 changed files with 447 additions and 176 deletions
+1
View File
@@ -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.
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg?style=for-the-badge)](https://opensource.org/licenses/Apache-2.0)
[![Build Status](https://img.shields.io/github/workflow/status/nymtech/nym/Continuous%20integration/develop?style=for-the-badge&logo=github-actions)](https://github.com/nymtech/nym/actions?query=branch%3Adevelop)
+19
View File
@@ -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 -1
View File
@@ -1,7 +1,7 @@
<!DOCTYPE html>
<html>
<head>
<title>Tauri Web Wallet</title>
<title>Nym Wallet</title>
</head>
<body>
<div id="root"></div>
+76 -44
View File
@@ -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 -1
View File
@@ -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 }
}
+3 -3
View File
@@ -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)
+67 -1
View File
@@ -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 })
+49 -28
View File
@@ -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"
+44 -17
View File
@@ -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 -3
View File
@@ -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>
</>
+34 -25
View File
@@ -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>
)
}
+57 -2
View File
@@ -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))
+8 -2
View File
@@ -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>
}
/>
)}
</>
+2 -2
View File
@@ -6,8 +6,8 @@ export enum EnumNodeType {
}
export type TNodeOwnership = {
ownsMixnode: boolean
ownsGateway: boolean
hasOwnership: boolean
nodeType?: EnumNodeType
}
export type TClientDetails = {
+5 -2
View File
@@ -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'
+7 -1
View File
@@ -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)
}