fix(wallet-bonding): bonding context mock

This commit is contained in:
pierre
2022-07-11 22:02:32 +02:00
committed by fmtabbara
parent abf9ccb823
commit 5fcce2de48
20 changed files with 521 additions and 407 deletions
+199 -39
View File
@@ -1,4 +1,4 @@
import { MajorCurrencyAmount, MixnodeStatus, TransactionExecuteResult } from '@nymproject/types';
import { FeeDetails, MajorCurrencyAmount, MixnodeStatus, TransactionExecuteResult } from '@nymproject/types';
import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
import type { Network } from 'src/types';
import { TBondGatewayArgs, TBondMixNodeArgs } from 'src/types';
@@ -7,9 +7,28 @@ import {
bondMixNode as bondMixNodeRequest,
claimOperatorRewards,
compoundOperatorRewards,
simulateBondGateway,
simulateBondMixnode,
simulateUnbondGateway,
simulateUnbondMixnode,
simulateVestingBondGateway,
simulateVestingBondMixnode,
simulateVestingUnbondGateway,
simulateVestingUnbondMixnode,
simulateUpdateMixnode,
simulateVestingUpdateMixnode,
unbondGateway as unbondGatewayRequest,
unbondMixNode as unbondMixNodeRequest,
unbondMixNode as unbondMixnodeRequest,
vestingBondGateway,
vestingBondMixNode,
vestingUnbondGateway,
vestingUnbondMixnode,
updateMixnode as updateMixnodeRequest,
vestingUpdateMixnode as updateMixnodeVestingRequest,
} from '../requests';
import { useGetFee } from '../hooks/useGetFee';
import { useCheckOwnership } from '../hooks/useCheckOwnership';
import { AppContext } from './main';
const bounded: BondedMixnode = {
bond: { denom: 'NYM', amount: '1234' },
@@ -53,22 +72,51 @@ export interface BondedGateway {
location?: string; // TODO not yet available, only available in Network Explorer API
}
export type TokenPool = 'locked' | 'balance';
export type FeeOperation =
| 'bondMixnode'
| 'bondMixnodeWithVesting'
| 'bondGateway'
| 'bondGatewayWithVesting'
| 'unbondMixnode'
| 'unbondGateway'
| 'updateMixnode'
| 'bondMore'
| 'compoundRewards'
| 'redeemRewards';
export type TBondingContext = {
isLoading: boolean;
loading: boolean;
error?: string;
bondedMixnode?: BondedMixnode | null;
bondedGateway?: BondedGateway | null;
refresh: () => Promise<void>;
bondMixnode: (data: TBondMixNodeArgs) => Promise<TransactionExecuteResult>;
bondGateway: (data: TBondGatewayArgs) => Promise<TransactionExecuteResult>;
unbondMixnode: () => Promise<TransactionExecuteResult>;
unbondGateway: () => Promise<TransactionExecuteResult>;
redeemRewards: () => Promise<TransactionExecuteResult[]>;
compoundRewards: () => Promise<TransactionExecuteResult[]>;
bondMixnode: (
data: Omit<TBondMixNodeArgs, 'fee'>,
tokenPool: TokenPool,
) => Promise<TransactionExecuteResult | undefined>;
bondGateway: (
data: Omit<TBondGatewayArgs, 'fee'>,
tokenPool: TokenPool,
) => Promise<TransactionExecuteResult | undefined>;
bondMore: (signature: string, additionalBond: MajorCurrencyAmount) => Promise<TransactionExecuteResult | undefined>;
unbondMixnode: () => Promise<TransactionExecuteResult | undefined>;
unbondGateway: () => Promise<TransactionExecuteResult | undefined>;
redeemRewards: () => Promise<TransactionExecuteResult[] | undefined>;
compoundRewards: () => Promise<TransactionExecuteResult[] | undefined>;
updateMixnode: (pm: number) => Promise<TransactionExecuteResult | undefined>;
fee?: FeeDetails;
getFee: <T>(feeOperation: FeeOperation, args: T) => Promise<FeeDetails | undefined>;
feeDetails?: FeeDetails;
feeLoading: boolean;
feeError?: string;
resetFeeState: () => void;
};
export const BondingContext = createContext<TBondingContext>({
isLoading: true,
loading: true,
feeLoading: false,
refresh: async () => undefined,
bondMixnode: async () => {
throw new Error('Not implemented');
@@ -88,6 +136,16 @@ export const BondingContext = createContext<TBondingContext>({
compoundRewards: async () => {
throw new Error('Not implemented');
},
getFee(): Promise<FeeDetails> {
throw new Error('Not implemented');
},
resetFeeState(): void {},
updateMixnode: async () => {
throw new Error('Not implemented');
},
bondMore(): Promise<TransactionExecuteResult | undefined> {
throw new Error('Not implemented');
},
});
export const BondingContextProvider = ({
@@ -97,13 +155,24 @@ export const BondingContextProvider = ({
network?: Network;
children?: React.ReactNode;
}): JSX.Element => {
const [isLoading, setIsLoading] = useState(true);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string>();
const [bondedMixnode, setBondedMixnode] = useState<BondedMixnode | null>(null);
const [bondedGateway, setBondedGateway] = useState<BondedGateway | null>(null);
const { fee, resetFeeState, feeError, isFeeLoading } = useGetFee();
const { ownership, checkOwnership } = useCheckOwnership();
const { userBalance } = useContext(AppContext);
const isVesting = Boolean(ownership.vestingPledge);
useEffect(() => {
if (feeError) {
setError(`An error occurred while retrieving fee: ${feeError}`);
}
}, [feeError]);
const resetState = () => {
setIsLoading(true);
setLoading(true);
setError(undefined);
setBondedGateway(null);
setBondedMixnode(null);
@@ -122,7 +191,7 @@ export const BondingContextProvider = ({
if (bounded && !('stake' in bounded)) {
setBondedGateway(bounded);
}
setIsLoading(false);
setLoading(false);
}, [network]);
useEffect(() => {
@@ -130,75 +199,159 @@ export const BondingContextProvider = ({
refresh();
}, [network]);
const bondMixnode = async (data: TBondMixNodeArgs) => {
// TODO some logic
let tx;
const bondMixnode = async (data: Omit<TBondMixNodeArgs, 'fee'>, tokenPool: TokenPool) => {
let tx: TransactionExecuteResult | undefined;
const payload = {
...data,
fee: fee?.fee,
};
setLoading(true);
try {
tx = await bondMixNodeRequest(data);
if (tokenPool === 'balance') {
tx = await bondMixNodeRequest(payload);
await userBalance.fetchBalance();
}
if (tokenPool === 'locked') {
tx = await vestingBondMixNode(payload);
await userBalance.fetchTokenAllocation();
}
return tx;
} catch (e: any) {
throw new Error(e);
setError(`an error occurred: ${e}`);
} finally {
setLoading(false);
}
return tx;
return undefined;
};
const bondGateway = async (data: TBondGatewayArgs) => {
// TODO some logic
let tx;
const bondGateway = async (data: Omit<TBondGatewayArgs, 'fee'>, tokenPool: TokenPool) => {
let tx: TransactionExecuteResult | undefined;
const payload = {
...data,
fee: fee?.fee,
};
setLoading(true);
try {
tx = await bondGatewayRequest(data);
if (tokenPool === 'balance') {
tx = await bondGatewayRequest(payload);
await userBalance.fetchBalance();
}
if (tokenPool === 'locked') {
tx = await vestingBondGateway(payload);
await userBalance.fetchTokenAllocation();
}
return tx;
} catch (e: any) {
throw new Error(e);
setError(`an error occurred: ${e}`);
} finally {
setLoading(false);
}
return tx;
return undefined;
};
const unbondMixnode = async () => {
// TODO some logic
let tx;
setLoading(true);
try {
tx = await unbondMixNodeRequest();
if (isVesting) tx = await vestingUnbondMixnode(fee?.fee);
if (!isVesting) tx = await unbondMixnodeRequest(fee?.fee);
} catch (e: any) {
throw new Error(e);
setError(`an error occurred: ${e}`);
} finally {
await checkOwnership();
setLoading(false);
}
return tx;
};
const unbondGateway = async () => {
// TODO some logic
let tx;
setLoading(true);
try {
tx = await unbondGatewayRequest();
if (isVesting) tx = await vestingUnbondGateway(fee?.fee);
if (!isVesting) tx = await unbondGatewayRequest(fee?.fee);
} catch (e: any) {
throw new Error(e);
setError(`an error occurred: ${e}`);
} finally {
await checkOwnership();
setLoading(false);
}
return tx;
};
const updateMixnode = async (pm: number) => {
let tx;
setLoading(true);
try {
if (isVesting) tx = await updateMixnodeRequest(pm);
if (!isVesting) tx = await updateMixnodeVestingRequest(pm);
} catch (e: any) {
setError(`an error occurred: ${e}`);
} finally {
setLoading(false);
}
return tx;
};
const redeemRewards = async () => {
// TODO some logic
let tx;
setLoading(true);
try {
tx = await claimOperatorRewards();
tx = await claimOperatorRewards(); // TODO use fee, need a new request
} catch (e: any) {
throw new Error(e);
setError(`an error occurred: ${e}`);
} finally {
setLoading(false);
}
return tx;
};
const compoundRewards = async () => {
// TODO some logic
let tx;
setLoading(true);
try {
tx = await compoundOperatorRewards();
tx = await compoundOperatorRewards(); // TODO use fee, need a new request
} catch (e: any) {
throw new Error(e);
setError(`an error occurred: ${e}`);
} finally {
setLoading(false);
}
return tx;
};
const bondMore = async (_signature: string, _additionalBond: MajorCurrencyAmount) =>
// TODO to implement
undefined;
const feeOps = useMemo(
() => ({
bondMixnode: simulateBondMixnode,
bondMixnodeWithVesting: simulateVestingBondMixnode,
bondGateway: simulateBondGateway,
bondGatewayWithVesting: simulateVestingBondGateway,
unbondMixnode: isVesting ? simulateVestingUnbondMixnode : simulateUnbondMixnode,
unbondGateway: isVesting ? simulateVestingUnbondGateway : simulateUnbondGateway,
updateMixnode: isVesting ? simulateVestingUpdateMixnode : simulateUpdateMixnode,
bondMore: () => undefined as unknown as Promise<FeeDetails>, // TODO fee request to implement
compoundRewards: () => undefined as unknown as Promise<FeeDetails>, // TODO fee request to implement
redeemRewards: () => undefined as unknown as Promise<FeeDetails>, // TODO fee request to implement
}),
[isVesting],
);
const getFee = async (feeOperation: FeeOperation, args: any) => {
let details;
try {
details = feeOps[feeOperation](args);
} catch (e: any) {
setError(`An error occurred while retrieving fee: ${e}`);
}
return details;
};
const memoizedValue = useMemo(
() => ({
isLoading,
loading,
error,
bondMixnode,
bondedMixnode,
@@ -206,11 +359,18 @@ export const BondingContextProvider = ({
bondGateway,
unbondMixnode,
unbondGateway,
updateMixnode,
refresh,
redeemRewards,
compoundRewards,
feeLoading: isFeeLoading,
feeError,
getFee,
fee,
resetFeeState,
bondMore,
}),
[isLoading, error, bondedMixnode, bondedGateway],
[loading, error, bondedMixnode, bondedGateway, isFeeLoading, feeError, fee, resetFeeState, isVesting],
);
return <BondingContext.Provider value={memoizedValue}>{children}</BondingContext.Provider>;
+60 -9
View File
@@ -1,7 +1,7 @@
import { TransactionExecuteResult } from '@nymproject/types';
import { FeeDetails, MajorCurrencyAmount, TransactionExecuteResult } from '@nymproject/types';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import type { Network } from 'src/types';
import { BondedGateway, BondedMixnode, BondingContext } from '../bonding';
import { BondedGateway, BondedMixnode, BondingContext, FeeOperation } from '../bonding';
import { mockSleep } from './utils';
const SLEEP_MS = 1000;
@@ -37,6 +37,11 @@ const TxResultMock: TransactionExecuteResult = {
fee: { amount: '1', denom: 'NYM' },
};
const feeMock: FeeDetails = {
amount: { denom: 'NYM', amount: '1' },
fee: { Auto: 1 },
};
export const MockBondingContextProvider = ({
network,
children,
@@ -44,7 +49,9 @@ export const MockBondingContextProvider = ({
network?: Network;
children?: React.ReactNode;
}): JSX.Element => {
const [isLoading, setIsLoading] = useState(true);
const [loading, setLoading] = useState(true);
const [feeLoading, setFeeLoading] = useState(false);
const [fee, setFee] = useState<FeeDetails | undefined>();
const [error, setError] = useState<string>();
const [bondedData, setBondedData] = useState<BondedMixnode | BondedGateway | null>(null);
const [bondedMixnode, setBondedMixnode] = useState<BondedMixnode | null>(null);
@@ -54,7 +61,7 @@ export const MockBondingContextProvider = ({
const triggerStateUpdate = () => setTrigger(new Date());
const resetState = () => {
setIsLoading(true);
setLoading(true);
setError(undefined);
setBondedGateway(null);
setBondedMixnode(null);
@@ -74,7 +81,7 @@ export const MockBondingContextProvider = ({
if (bounded && !('stake' in bounded)) {
setBondedGateway(bounded);
}
setIsLoading(false);
setLoading(false);
}, [network]);
useEffect(() => {
@@ -83,44 +90,82 @@ export const MockBondingContextProvider = ({
}, [network, bondedData]);
const bondMixnode = async (): Promise<TransactionExecuteResult> => {
setLoading(true);
await mockSleep(SLEEP_MS);
setBondedData(bondedMixnodeMock);
setLoading(false);
return TxResultMock;
};
const bondGateway = async (): Promise<TransactionExecuteResult> => {
setLoading(true);
await mockSleep(SLEEP_MS);
setBondedData(bondedGatewayMock);
setLoading(false);
return TxResultMock;
};
const unbondMixnode = async (): Promise<TransactionExecuteResult> => {
setLoading(true);
await mockSleep(SLEEP_MS);
setBondedData(null);
setLoading(false);
return TxResultMock;
};
const unbondGateway = async (): Promise<TransactionExecuteResult> => {
setLoading(true);
await mockSleep(SLEEP_MS);
setBondedData(null);
setLoading(false);
return TxResultMock;
};
const redeemRewards = async (): Promise<TransactionExecuteResult[]> => {
const redeemRewards = async (): Promise<TransactionExecuteResult[] | undefined> => {
setLoading(true);
await mockSleep(SLEEP_MS);
triggerStateUpdate();
setLoading(false);
return [TxResultMock];
};
const compoundRewards = async (): Promise<TransactionExecuteResult[]> => {
const compoundRewards = async (): Promise<TransactionExecuteResult[] | undefined> => {
setLoading(true);
await mockSleep(SLEEP_MS);
triggerStateUpdate();
setLoading(false);
return [TxResultMock];
};
const updateMixnode = async (): Promise<TransactionExecuteResult> => {
setLoading(true);
await mockSleep(SLEEP_MS);
triggerStateUpdate();
setLoading(false);
return TxResultMock;
};
const bondMore = async (_signature: string, _additionalBond: MajorCurrencyAmount) => {
setLoading(true);
await mockSleep(SLEEP_MS);
triggerStateUpdate();
setLoading(false);
return TxResultMock;
};
const getFee = async (_feeOperation: FeeOperation, _args: any) => {
setFeeLoading(true);
await mockSleep(SLEEP_MS);
setFeeLoading(false);
setFee(feeMock);
return feeMock;
};
const resetFeeState = () => {};
const memoizedValue = useMemo(
() => ({
isLoading,
loading,
error,
bondMixnode,
bondGateway,
@@ -129,8 +174,14 @@ export const MockBondingContextProvider = ({
refresh,
redeemRewards,
compoundRewards,
fee,
feeLoading,
getFee,
resetFeeState,
updateMixnode,
bondMore,
}),
[isLoading, error, bondedMixnode, bondedGateway, trigger],
[loading, error, bondedMixnode, bondedGateway, trigger, fee],
);
return <BondingContext.Provider value={memoizedValue}>{children}</BondingContext.Provider>;
@@ -17,15 +17,8 @@ import {
NodeData,
} from '../types';
import AmountModal from './AmountModal';
import { AppContext, urls } from '../../../context';
import { AppContext, urls, useBondingContext } from '../../../context';
import SummaryModal from './SummaryModal';
import {
bondGateway as bondGatewayRequest,
bondMixNode as bondMixNodeRequest,
vestingBondGateway,
vestingBondMixNode,
} from '../../../requests';
import { useGetFee } from '../../../hooks/useGetFee';
const initialState: BondState = {
showModal: false,
@@ -71,15 +64,20 @@ const BondingCard = () => {
const [state, dispatch] = useReducer(reducer, initialState);
const { formStep, showModal } = state;
const { userBalance, clientDetails, network } = useContext(AppContext);
const { fee } = useGetFee();
const { clientDetails, network } = useContext(AppContext);
const { error, bondMixnode: bondMixnodeRequest, bondGateway: bondGatewayRequest } = useBondingContext();
useEffect(() => {
dispatch({ type: 'reset' });
}, [clientDetails]);
useEffect(() => {
if (error) {
dispatch({ type: 'set_error', payload: error });
}
}, [error]);
const bondMixnode = async () => {
let tx: TransactionExecuteResult | undefined;
const { signature, identityKey, sphinxKey, host, version, mixPort, verlocPort, httpApiPort } =
state.nodeData as NodeData<MixnodeData>;
const { profitMargin, amount, tokenPool } = state.amountData as MixnodeAmount;
@@ -96,30 +94,20 @@ const BondingCard = () => {
http_api_port: httpApiPort,
},
pledge: amount,
fee: fee?.fee,
};
if (tokenPool !== 'locked' && tokenPool !== 'balance') {
throw new Error(`token pool [${tokenPool}] not supported`);
}
try {
if (tokenPool === 'balance') {
tx = await bondMixNodeRequest(payload);
await userBalance.fetchBalance();
}
if (tokenPool === 'locked') {
tx = await vestingBondMixNode(payload);
await userBalance.fetchTokenAllocation();
}
const tx = await bondMixnodeRequest(payload, tokenPool);
if (tx) {
dispatch({ type: 'set_bond_status', payload: 'success' });
return tx;
} catch (e: any) {
dispatch({ type: 'set_error', payload: e });
} else {
dispatch({ type: 'set_bond_status', payload: 'error' });
}
return undefined;
return tx;
};
const bondGateway = async () => {
let tx: TransactionExecuteResult | undefined;
const { signature, identityKey, sphinxKey, host, version, location, mixPort, clientsPort } =
state.nodeData as NodeData<GatewayData>;
const { amount, tokenPool } = state.amountData as GatewayAmount;
@@ -135,21 +123,15 @@ const BondingCard = () => {
clients_port: clientsPort,
},
pledge: amount,
fee: fee?.fee,
};
try {
if (tokenPool === 'balance') {
tx = await bondGatewayRequest(payload);
await userBalance.fetchBalance();
}
if (tokenPool === 'locked') {
tx = await vestingBondGateway(payload);
await userBalance.fetchTokenAllocation();
}
if (tokenPool !== 'locked' && tokenPool !== 'balance') {
throw new Error(`token pool [${tokenPool}] not supported`);
}
const tx = await bondGatewayRequest(payload, tokenPool);
if (tx) {
dispatch({ type: 'set_bond_status', payload: 'success' });
return tx;
} catch (e: any) {
dispatch({ type: 'set_error', payload: e });
} else {
dispatch({ type: 'set_bond_status', payload: 'error' });
}
return tx;
};
@@ -1,14 +1,8 @@
import React, { useEffect } from 'react';
import { Divider, Stack, Typography } from '@mui/material';
import {
simulateBondGateway,
simulateBondMixnode,
simulateVestingBondGateway,
simulateVestingBondMixnode,
} from '../../../requests';
import { GatewayAmount, GatewayData, MixnodeAmount, MixnodeData, NodeData } from '../types';
import { useGetFee } from '../../../hooks/useGetFee';
import { SimpleModal } from '../../../components/Modals/SimpleModal';
import { useBondingContext } from '../../../context';
export interface Props {
open: boolean;
@@ -21,7 +15,7 @@ export interface Props {
}
const SummaryModal = ({ open, onClose, onSubmit, node, amount, onCancel, onError }: Props) => {
const { fee, getFee, resetFeeState, feeError, isFeeLoading } = useGetFee();
const { fee, getFee, resetFeeState, feeError, feeLoading } = useBondingContext();
useEffect(() => {
if (feeError) onError(feeError);
@@ -31,7 +25,7 @@ const SummaryModal = ({ open, onClose, onSubmit, node, amount, onCancel, onError
const { signature, host, version, mixPort, identityKey, sphinxKey } = node;
try {
if (node.nodeType === 'mixnode') {
await getFee(amount.tokenPool === 'locked' ? simulateVestingBondMixnode : simulateBondMixnode, {
await getFee(amount.tokenPool === 'locked' ? 'bondMixnodeWithVesting' : 'bondMixnode', {
ownerSignature: signature,
mixnode: {
identity_key: identityKey,
@@ -46,7 +40,7 @@ const SummaryModal = ({ open, onClose, onSubmit, node, amount, onCancel, onError
pledge: amount.amount,
});
} else {
await getFee(amount.tokenPool === 'locked' ? simulateVestingBondGateway : simulateBondGateway, {
await getFee(amount.tokenPool === 'locked' ? 'bondGatewayWithVesting' : 'bondGateway', {
ownerSignature: signature,
gateway: {
identity_key: identityKey,
@@ -98,7 +92,7 @@ const SummaryModal = ({ open, onClose, onSubmit, node, amount, onCancel, onError
<Divider sx={{ my: 1 }} />
<Stack direction="row" justifyContent="space-between">
<Typography>Fee for this operation</Typography>
{isFeeLoading ? (
{feeLoading ? (
<Typography>loading</Typography>
) : (
<Typography>{fee ? `${fee.amount?.amount} ${fee.amount?.denom}` : ''}</Typography>
@@ -4,7 +4,7 @@ import EditIcon from '@mui/icons-material/Edit';
import { BondedGateway } from '../../../context';
import { NodeTable, BondedNodeCard, Cell, Header, NodeMenu } from '../components';
import { GatewayFlow } from './types';
import Unbond from './unbond';
import Unbond from '../unbond';
const headers: Header[] = [
{
@@ -1,133 +0,0 @@
import * as React from 'react';
import { useContext, useEffect, useState } from 'react';
import { MajorCurrencyAmount } from '@nymproject/types';
import { Link } from '@nymproject/react/link/Link';
import { Typography } from '@mui/material';
import { ErrorOutline } from '@mui/icons-material';
import { AppContext, BondedGateway, BondedMixnode, urls } from '../../../../context';
import SummaryModal from './SummaryModal';
import { ConfirmationModal } from '../../../../components';
import {
simulateUnbondGateway,
simulateVestingUnbondGateway,
unbondGateway,
vestingUnbondGateway,
} from '../../../../requests';
import { useCheckOwnership } from '../../../../hooks/useCheckOwnership';
import { useGetFee } from '../../../../hooks/useGetFee';
import { LoadingModal } from '../../../../components/Modals/LoadingModal';
interface Props {
node: BondedGateway;
show: boolean;
onClose: () => void;
}
type UnbondStatus = 'success' | 'error';
const Unbond = ({ node, show, onClose }: Props) => {
const [isLoading, setIsLoading] = useState(false);
const [step, setStep] = useState<1 | 2>(1);
const [txHash, setTxHash] = useState<string>();
const [status, setStatus] = useState<UnbondStatus>();
const [error, setError] = useState<string>();
const { fee, getFee, resetFeeState, isFeeLoading } = useGetFee();
const { network } = useContext(AppContext);
const { checkOwnership, ownership } = useCheckOwnership();
const isVesting = Boolean(ownership.vestingPledge);
const unbond = async () => {
let tx;
setIsLoading(true);
try {
if (isVesting) tx = await vestingUnbondGateway(fee?.fee);
if (!isVesting) tx = await unbondGateway(fee?.fee);
setTxHash(tx?.transaction_hash);
setStatus('success');
} catch (err: any) {
setStatus('error');
setError(err as string);
} finally {
await checkOwnership();
setIsLoading(false);
}
};
const fetchFee = async () => {
try {
if (isVesting) await getFee(simulateVestingUnbondGateway, {});
if (!isVesting) await getFee(simulateUnbondGateway, {});
} catch (e: any) {
setStatus('error');
setError(e as string);
}
};
useEffect(() => {
fetchFee();
}, [node, isVesting]);
const submit = async () => {
if (status === 'error') {
// Fetch fee failed
return;
}
unbond();
resetFeeState();
setStep(2);
};
const reset = () => {
setStep(1);
onClose();
};
if (isFeeLoading || isLoading) return <LoadingModal />;
return (
<>
<SummaryModal
open={show && step === 1}
onClose={reset}
onConfirm={submit}
onCancel={reset}
bond={node.bond}
rewards={(node as BondedMixnode).operatorRewards}
fee={fee?.amount as MajorCurrencyAmount}
/>
{status === 'success' && (
<ConfirmationModal
open={show && step === 2}
onClose={reset}
onConfirm={reset}
title="Unbonding succesfull"
confirmButton="Done"
maxWidth="xs"
>
<Typography sx={{ mb: 2 }}>This operation can take up to one hour to process</Typography>
<Link href={`${urls(network).blockExplorer}/transaction/${txHash}`} noIcon>
View on blockchain
</Link>
</ConfirmationModal>
)}
{status === 'error' && (
<ConfirmationModal
open={show}
onClose={reset}
onConfirm={reset}
title="Unbonding failed"
confirmButton="Done"
maxWidth="xs"
>
<Typography variant="caption">Error: {error}</Typography>
<ErrorOutline color="error" />
</ConfirmationModal>
)}
</>
);
};
export default Unbond;
@@ -9,7 +9,7 @@ import NodeSettings from './node-settings';
import BondMore from './bond-more';
import { MixnodeFlow } from './types';
import RedeemRewards from './redeem';
import Unbond from './unbond';
import Unbond from '../unbond';
import CompoundRewards from './compound';
import { Bond as BondIcon, Unbond as UnbondIcon } from '../../../svg-icons';
@@ -1,9 +1,10 @@
import * as React from 'react';
import { useContext, useEffect, useState } from 'react';
import { useContext, useState } from 'react';
import { MajorCurrencyAmount, TransactionExecuteResult } from '@nymproject/types';
import { Link } from '@nymproject/react/link/Link';
import { Typography } from '@mui/material';
import { AppContext, BondedMixnode, urls } from '../../../../context';
import { ErrorOutline } from '@mui/icons-material';
import { AppContext, BondedMixnode, urls, useBondingContext } from '../../../../context';
import SummaryModal from './SummaryModal';
import { ConfirmationModal } from '../../../../components';
import BondModal from './BondModal';
@@ -17,20 +18,18 @@ interface Props {
const BondMore = ({ mixnode, show, onClose }: Props) => {
const [addBond, setAddBond] = useState<MajorCurrencyAmount>({ amount: '0', denom: 'NYM' });
const [signature, setSignature] = useState<string>();
const [fee, setFee] = useState<MajorCurrencyAmount>({ amount: '0', denom: 'NYM' });
const [step, setStep] = useState<1 | 2 | 3>(1);
const [tx, setTx] = useState<TransactionExecuteResult>();
const { network } = useContext(AppContext);
useEffect(() => {
setFee({ amount: '42', denom: 'NYM' }); // TODO fetch real fee amount
}, [addBond]);
const { bondMore, error } = useBondingContext();
const submit = async () => {
// TODO send request to update bond
setStep(3); // on success
// setTx(requestResult)
const txResult = await bondMore(signature as string, addBond);
if (txResult) {
setStep(3);
}
setTx(txResult);
};
const reset = () => {
@@ -59,10 +58,9 @@ const BondMore = ({ mixnode, show, onClose }: Props) => {
onCancel={() => setStep(1)}
currentBond={mixnode.bond}
addBond={addBond}
fee={fee as MajorCurrencyAmount}
/>
<ConfirmationModal
open={show && step === 3}
open={show && step === 3 && !error}
onClose={reset}
onConfirm={reset}
title="Bonding successful"
@@ -74,6 +72,19 @@ const BondMore = ({ mixnode, show, onClose }: Props) => {
View on blockchain
</Link>
</ConfirmationModal>
{error && (
<ConfirmationModal
open={show}
onClose={reset}
onConfirm={reset}
title="Operation failed"
confirmButton="Done"
maxWidth="xs"
>
<Typography variant="caption">Error: {error}</Typography>
<ErrorOutline color="error" />
</ConfirmationModal>
)}
</>
);
};
@@ -1,7 +1,11 @@
import * as React from 'react';
import { Divider, Stack, Typography } from '@mui/material';
import { MajorCurrencyAmount } from '@nymproject/types';
import { useEffect } from 'react';
import { ErrorOutline } from '@mui/icons-material';
import { SimpleModal } from '../../../../components/Modals/SimpleModal';
import { useBondingContext } from '../../../../context';
import { ConfirmationModal } from '../../../../components';
export interface Props {
open: boolean;
@@ -10,33 +14,60 @@ export interface Props {
onCancel: () => void;
currentBond: MajorCurrencyAmount;
addBond: MajorCurrencyAmount;
fee: MajorCurrencyAmount;
}
const SummaryModal = ({ open, onClose, onConfirm, onCancel, currentBond, addBond, fee }: Props) => (
<SimpleModal
open={open}
onClose={onClose}
onOk={onConfirm}
onBack={onCancel}
header="Bond mor details"
okLabel="Confirm"
>
<Stack direction="row" justifyContent="space-between">
<Typography fontWeight={400}>Current bond</Typography>
<Typography fontWeight={400}>{`${currentBond.amount} ${currentBond.denom}`}</Typography>
</Stack>
<Divider sx={{ my: 1 }} />
<Stack direction="row" justifyContent="space-between">
<Typography fontWeight={400}>Additional bond</Typography>
<Typography fontWeight={400}>{`${addBond.amount} ${addBond.denom}`}</Typography>
</Stack>
<Divider sx={{ my: 1 }} />
<Stack direction="row" justifyContent="space-between">
<Typography fontWeight={400}>Fee for this operation</Typography>
<Typography fontWeight={400}>{`${fee.amount} ${fee.denom}`}</Typography>
</Stack>
</SimpleModal>
);
const SummaryModal = ({ open, onClose, onConfirm, onCancel, currentBond, addBond }: Props) => {
const { getFee, fee, error } = useBondingContext();
const fetchFee = async () => {
await getFee('bondMore', {});
};
useEffect(() => {
fetchFee();
}, []);
if (error) {
return (
<ConfirmationModal
open={open}
onClose={onClose}
onConfirm={onClose}
title="Operation failed"
confirmButton="Done"
maxWidth="xs"
>
<Typography variant="caption">Error: {error}</Typography>
<ErrorOutline color="error" />
</ConfirmationModal>
);
}
return (
<SimpleModal
open={open}
onClose={onClose}
onOk={onConfirm}
onBack={onCancel}
header="Bond mor details"
okLabel="Confirm"
>
<Stack direction="row" justifyContent="space-between">
<Typography fontWeight={400}>Current bond</Typography>
<Typography fontWeight={400}>{`${currentBond.amount} ${currentBond.denom}`}</Typography>
</Stack>
<Divider sx={{ my: 1 }} />
<Stack direction="row" justifyContent="space-between">
<Typography fontWeight={400}>Additional bond</Typography>
<Typography fontWeight={400}>{`${addBond.amount} ${addBond.denom}`}</Typography>
</Stack>
<Divider sx={{ my: 1 }} />
<Stack direction="row" justifyContent="space-between">
<Typography fontWeight={400}>Fee for this operation</Typography>
<Typography fontWeight={400}>{fee ? `${fee.amount?.amount} ${fee.amount?.denom}` : ''}</Typography>
</Stack>
</SimpleModal>
);
};
export default SummaryModal;
@@ -1,9 +1,10 @@
import * as React from 'react';
import { useContext, useEffect, useState } from 'react';
import { MajorCurrencyAmount, TransactionExecuteResult } from '@nymproject/types';
import { TransactionExecuteResult } from '@nymproject/types';
import { Link } from '@nymproject/react/link/Link';
import { Typography } from '@mui/material';
import { AppContext, BondedMixnode, urls } from '../../../../context';
import { ErrorOutline } from '@mui/icons-material';
import { AppContext, BondedMixnode, urls, useBondingContext } from '../../../../context';
import SummaryModal from './SummaryModal';
import { ConfirmationModal } from '../../../../components';
@@ -14,20 +15,26 @@ interface Props {
}
const CompoundRewards = ({ mixnode, show, onClose }: Props) => {
const [fee, setFee] = useState<MajorCurrencyAmount>({ amount: '0', denom: 'NYM' });
const [step, setStep] = useState<1 | 2>(1);
const [tx, setTx] = useState<TransactionExecuteResult>();
const { network } = useContext(AppContext);
const { compoundRewards, error, fee, getFee } = useBondingContext();
const fetchFee = async () => {
await getFee('compoundRewards', {});
};
useEffect(() => {
setFee({ amount: '42', denom: 'NYM' }); // TODO fetch real fee amount
fetchFee();
}, []);
const submit = async () => {
// TODO send request to compound rewards
setStep(2); // on success
// setTx(requestResult)
const txResult = await compoundRewards();
if (txResult) {
setStep(2);
}
setTx(txResult?.[0]);
};
const reset = () => {
@@ -43,7 +50,7 @@ const CompoundRewards = ({ mixnode, show, onClose }: Props) => {
onConfirm={submit}
onCancel={reset}
rewards={mixnode.operatorRewards}
fee={fee as MajorCurrencyAmount}
fee={fee?.amount}
/>
<ConfirmationModal
open={show && step === 2}
@@ -58,6 +65,19 @@ const CompoundRewards = ({ mixnode, show, onClose }: Props) => {
View on blockchain
</Link>
</ConfirmationModal>
{error && (
<ConfirmationModal
open={show}
onClose={reset}
onConfirm={reset}
title="Operation failed"
confirmButton="Done"
maxWidth="xs"
>
<Typography variant="caption">Error: {error}</Typography>
<ErrorOutline color="error" />
</ConfirmationModal>
)}
</>
);
};
@@ -9,7 +9,7 @@ export interface Props {
onConfirm: () => Promise<void>;
onCancel: () => void;
rewards: MajorCurrencyAmount;
fee: MajorCurrencyAmount;
fee?: MajorCurrencyAmount | null;
}
const SummaryModal = ({ open, onClose, onConfirm, onCancel, rewards, fee }: Props) => (
@@ -29,7 +29,7 @@ const SummaryModal = ({ open, onClose, onConfirm, onCancel, rewards, fee }: Prop
<Divider sx={{ my: 1 }} />
<Stack direction="row" justifyContent="space-between">
<Typography fontWeight={400}>Fee for this operation</Typography>
<Typography fontWeight={400}>{`${fee.amount} ${fee.denom}`}</Typography>
<Typography fontWeight={400}>{fee ? `${fee?.amount} ${fee?.denom}` : ''}</Typography>
</Stack>
<Divider sx={{ my: 1 }} />
<Typography fontWeight={400}>Rewards will be added to your bonding pool</Typography>
@@ -3,8 +3,9 @@ import { useContext, useEffect, useState } from 'react';
import { MajorCurrencyAmount, TransactionExecuteResult } from '@nymproject/types';
import { Link } from '@nymproject/react/link/Link';
import { Typography } from '@mui/material';
import { ErrorOutline } from '@mui/icons-material';
import ProfitMarginModal from './ProfitMarginModal';
import { AppContext, BondedMixnode, urls } from '../../../../context';
import { AppContext, BondedMixnode, urls, useBondingContext } from '../../../../context';
import SummaryModal from './SummaryModal';
import { ConfirmationModal } from '../../../../components';
@@ -18,21 +19,36 @@ interface Props {
const MOCK_ESTIMATED_OP_REWARD: MajorCurrencyAmount = { amount: '42', denom: 'NYM' };
const NodeSettings = ({ mixnode, show, onClose }: Props) => {
const [status, setStatus] = useState<'success' | 'error'>();
const [profitMargin, setProfitMargin] = useState<number>();
const [fee, setFee] = useState<MajorCurrencyAmount>({ amount: '0', denom: 'NYM' });
const [step, setStep] = useState<1 | 2 | 3>(1);
const [step, setStep] = useState<1 | 2>(1);
const [tx, setTx] = useState<TransactionExecuteResult>();
const { network } = useContext(AppContext);
const { updateMixnode, error, fee, getFee } = useBondingContext();
useEffect(() => {
setFee({ amount: '42', denom: 'NYM' }); // TODO fetch real fee amount
}, [profitMargin]);
if (error) {
setStatus('error');
}
}, [error]);
const fetchFee = async () => {
await getFee('updateMixnode', {});
};
useEffect(() => {
fetchFee();
}, []);
const submit = async () => {
// TODO send request to update profit margin
setStep(3); // on success
// setTx(requestResult)
const txResult = await updateMixnode(profitMargin as number);
if (txResult) {
setStatus('success');
} else {
setStatus('error');
}
setTx(txResult);
};
const reset = () => {
@@ -60,21 +76,36 @@ const NodeSettings = ({ mixnode, show, onClose }: Props) => {
onCancel={() => setStep(1)}
currentPm={mixnode.profitMargin}
newPm={profitMargin as number}
fee={fee as MajorCurrencyAmount}
fee={fee?.amount}
/>
<ConfirmationModal
open={show && step === 3}
onClose={reset}
onConfirm={reset}
title="Operation successful"
confirmButton="Done"
maxWidth="xs"
>
<Typography sx={{ mb: 2 }}>This operation can take up to one hour to process</Typography>
<Link href={`${urls(network).blockExplorer}/transaction/${tx?.transaction_hash}`} noIcon>
View on blockchain
</Link>
</ConfirmationModal>
{status === 'success' && (
<ConfirmationModal
open={show}
onClose={reset}
onConfirm={reset}
title="Operation successful"
confirmButton="Done"
maxWidth="xs"
>
<Typography sx={{ mb: 2 }}>This operation can take up to one hour to process</Typography>
<Link href={`${urls(network).blockExplorer}/transaction/${tx?.transaction_hash}`} noIcon>
View on blockchain
</Link>
</ConfirmationModal>
)}
{status === 'error' && (
<ConfirmationModal
open={show}
onClose={reset}
onConfirm={reset}
title="Operation failed"
confirmButton="Done"
maxWidth="xs"
>
<Typography variant="caption">Error: {error}</Typography>
<ErrorOutline color="error" />
</ConfirmationModal>
)}
</>
);
};
@@ -10,7 +10,7 @@ export interface Props {
onCancel: () => void;
currentPm: number;
newPm: number;
fee: MajorCurrencyAmount;
fee?: MajorCurrencyAmount | null;
}
const SummaryModal = ({ open, onClose, onConfirm, onCancel, currentPm, newPm, fee }: Props) => (
@@ -35,7 +35,7 @@ const SummaryModal = ({ open, onClose, onConfirm, onCancel, currentPm, newPm, fe
<Divider sx={{ my: 1 }} />
<Stack direction="row" justifyContent="space-between">
<Typography fontWeight={400}>Fee for this operation</Typography>
<Typography fontWeight={400}>{`${fee.amount} ${fee.denom}`}</Typography>
<Typography fontWeight={400}>{fee ? `${fee?.amount} ${fee?.denom}` : ''}</Typography>
</Stack>
</SimpleModal>
);
@@ -1,9 +1,10 @@
import * as React from 'react';
import { useContext, useEffect, useState } from 'react';
import { MajorCurrencyAmount, TransactionExecuteResult } from '@nymproject/types';
import { TransactionExecuteResult } from '@nymproject/types';
import { Link } from '@nymproject/react/link/Link';
import { Typography } from '@mui/material';
import { AppContext, BondedMixnode, urls } from '../../../../context';
import { ErrorOutline } from '@mui/icons-material';
import { AppContext, BondedMixnode, urls, useBondingContext } from '../../../../context';
import SummaryModal from './SummaryModal';
import { ConfirmationModal } from '../../../../components';
@@ -14,20 +15,26 @@ interface Props {
}
const RedeemRewards = ({ mixnode, show, onClose }: Props) => {
const [fee, setFee] = useState<MajorCurrencyAmount>({ amount: '0', denom: 'NYM' });
const [step, setStep] = useState<1 | 2>(1);
const [tx, setTx] = useState<TransactionExecuteResult>();
const { network } = useContext(AppContext);
const { redeemRewards, error, fee, getFee } = useBondingContext();
const fetchFee = async () => {
await getFee('redeemRewards', {});
};
useEffect(() => {
setFee({ amount: '42', denom: 'NYM' }); // TODO fetch real fee amount
fetchFee();
}, []);
const submit = async () => {
// TODO send request to redeem rewards
setStep(2); // on success
// setTx(requestResult)
const txResult = await redeemRewards();
if (txResult) {
setStep(2);
}
setTx(txResult?.[0]);
};
const reset = () => {
@@ -43,7 +50,7 @@ const RedeemRewards = ({ mixnode, show, onClose }: Props) => {
onConfirm={submit}
onCancel={reset}
rewards={mixnode.operatorRewards}
fee={fee as MajorCurrencyAmount}
fee={fee?.amount}
/>
<ConfirmationModal
open={show && step === 2}
@@ -58,6 +65,19 @@ const RedeemRewards = ({ mixnode, show, onClose }: Props) => {
View on blockchain
</Link>
</ConfirmationModal>
{error && (
<ConfirmationModal
open={show}
onClose={reset}
onConfirm={reset}
title="Operation failed"
confirmButton="Done"
maxWidth="xs"
>
<Typography variant="caption">Error: {error}</Typography>
<ErrorOutline color="error" />
</ConfirmationModal>
)}
</>
);
};
@@ -9,7 +9,7 @@ export interface Props {
onConfirm: () => Promise<void>;
onCancel: () => void;
rewards: MajorCurrencyAmount;
fee: MajorCurrencyAmount;
fee?: MajorCurrencyAmount | null;
}
const SummaryModal = ({ open, onClose, onConfirm, onCancel, rewards, fee }: Props) => (
@@ -29,7 +29,7 @@ const SummaryModal = ({ open, onClose, onConfirm, onCancel, rewards, fee }: Prop
<Divider sx={{ my: 1 }} />
<Stack direction="row" justifyContent="space-between">
<Typography fontWeight={400}>Fee for this operation</Typography>
<Typography fontWeight={400}>{`${fee.amount} ${fee.denom}`}</Typography>
<Typography fontWeight={400}>{fee ? `${fee.amount} ${fee.denom}` : ''}</Typography>
</Stack>
<Divider sx={{ my: 1 }} />
<Typography fontWeight={400}>Rewards will be transferred to the account you are logged in with</Typography>
@@ -1,49 +0,0 @@
import * as React from 'react';
import { Divider, Stack, Typography } from '@mui/material';
import { MajorCurrencyAmount } from '@nymproject/types';
import { SimpleModal } from '../../../../components/Modals/SimpleModal';
export interface Props {
open: boolean;
onClose: () => void;
onConfirm: () => Promise<void>;
onCancel: () => void;
bond: MajorCurrencyAmount;
rewards?: MajorCurrencyAmount;
fee: MajorCurrencyAmount;
}
const SummaryModal = ({ open, onClose, onConfirm, onCancel, bond, rewards, fee }: Props) => (
<SimpleModal
open={open}
onClose={onClose}
onOk={onConfirm}
onBack={onCancel}
header="Unbond"
subHeader="Unbond and remove your node from the mixnet"
okLabel="Unbond"
>
<Stack direction="row" justifyContent="space-between">
<Typography fontWeight={400}>Amount to unbond</Typography>
<Typography fontWeight={400}>{`${bond.amount} ${bond.denom}`}</Typography>
</Stack>
<Divider sx={{ my: 1 }} />
{rewards?.amount && (
<>
<Stack direction="row" justifyContent="space-between">
<Typography fontWeight={400}>Operator rewards</Typography>
<Typography fontWeight={400}>{`${rewards.amount} ${rewards.denom}`}</Typography>
</Stack>
<Divider sx={{ my: 1 }} />
</>
)}
<Stack direction="row" justifyContent="space-between">
<Typography fontWeight={400}>Fee for this operation</Typography>
<Typography fontWeight={400}>{`${fee.amount} ${fee.denom}`}</Typography>
</Stack>
<Divider sx={{ my: 1 }} />
<Typography fontWeight={400}>Tokens will be transferred to account you are logged in with now</Typography>
</SimpleModal>
);
export default SummaryModal;
@@ -1,3 +0,0 @@
import Unbond from './Unbond';
export default Unbond;
@@ -1,7 +1,7 @@
import * as React from 'react';
import { Divider, Stack, Typography } from '@mui/material';
import { MajorCurrencyAmount } from '@nymproject/types';
import { SimpleModal } from '../../../../components/Modals/SimpleModal';
import { SimpleModal } from '../../../components/Modals/SimpleModal';
export interface Props {
open: boolean;
@@ -4,21 +4,14 @@ import { MajorCurrencyAmount } from '@nymproject/types';
import { Link } from '@nymproject/react/link/Link';
import { Typography } from '@mui/material';
import { ErrorOutline } from '@mui/icons-material';
import { AppContext, BondedMixnode, urls } from '../../../../context';
import { AppContext, BondedGateway, BondedMixnode, urls, useBondingContext } from '../../../context';
import SummaryModal from './SummaryModal';
import { ConfirmationModal } from '../../../../components';
import {
simulateUnbondMixnode,
simulateVestingUnbondMixnode,
unbondMixNode,
vestingUnbondMixnode,
} from '../../../../requests';
import { useCheckOwnership } from '../../../../hooks/useCheckOwnership';
import { useGetFee } from '../../../../hooks/useGetFee';
import { LoadingModal } from '../../../../components/Modals/LoadingModal';
import { ConfirmationModal } from '../../../components';
import { LoadingModal } from '../../../components/Modals/LoadingModal';
import { NodeType } from '../types';
interface Props {
node: BondedMixnode;
node: BondedMixnode | BondedGateway;
show: boolean;
onClose: () => void;
}
@@ -26,49 +19,55 @@ interface Props {
type UnbondStatus = 'success' | 'error';
const Unbond = ({ node, show, onClose }: Props) => {
const [isLoading, setIsLoading] = useState(false);
const [step, setStep] = useState<1 | 2>(1);
const [txHash, setTxHash] = useState<string>();
const [status, setStatus] = useState<UnbondStatus>();
const [error, setError] = useState<string>();
const { fee, getFee, resetFeeState, isFeeLoading } = useGetFee();
const [nodeType, setNodeType] = useState<NodeType>('mixnode');
const { network } = useContext(AppContext);
const { checkOwnership, ownership } = useCheckOwnership();
const { fee, getFee, resetFeeState, feeLoading, feeError, loading, unbondMixnode, unbondGateway, error } =
useBondingContext();
const isVesting = Boolean(ownership.vestingPledge);
useEffect(() => {
if (error || feeError) {
setStatus('error');
}
}, [error, feeError]);
useEffect(() => {
if ('profitMarfin' in node) {
setNodeType('mixnode');
} else {
setNodeType('gateway');
}
}, [node]);
const unbond = async () => {
let tx;
setIsLoading(true);
try {
if (isVesting) tx = await vestingUnbondMixnode(fee?.fee);
if (!isVesting) tx = await unbondMixNode(fee?.fee);
setTxHash(tx?.transaction_hash);
setStatus('success');
} catch (err: any) {
setStatus('error');
setError(err as string);
} finally {
await checkOwnership();
setIsLoading(false);
if (nodeType === 'mixnode') {
tx = await unbondMixnode();
} else {
tx = await unbondGateway();
}
if (!tx) {
setStatus('error');
}
setStatus('success');
setTxHash(tx?.transaction_hash);
return tx;
};
const fetchFee = async () => {
try {
if (isVesting) await getFee(simulateVestingUnbondMixnode, {});
if (!isVesting) await getFee(simulateUnbondMixnode, {});
} catch (e: any) {
setStatus('error');
setError(e as string);
if (nodeType === 'mixnode') {
await getFee('unbondMixnode', {});
} else {
await getFee('unbondGateway', {});
}
};
useEffect(() => {
fetchFee();
}, [node, isVesting]);
}, [node]);
const submit = async () => {
if (status === 'error') {
@@ -85,7 +84,7 @@ const Unbond = ({ node, show, onClose }: Props) => {
onClose();
};
if (isFeeLoading || isLoading) return <LoadingModal />;
if (feeLoading || loading) return <LoadingModal />;
return (
<>
@@ -95,7 +94,7 @@ const Unbond = ({ node, show, onClose }: Props) => {
onConfirm={submit}
onCancel={reset}
bond={node.bond}
rewards={(node as BondedMixnode).operatorRewards}
rewards={nodeType === 'mixnode' ? (node as BondedMixnode).operatorRewards : undefined}
fee={fee?.amount as MajorCurrencyAmount}
/>
{status === 'success' && (