create delegations context and page
This commit is contained in:
@@ -1,15 +1,15 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { Box, SxProps } from '@mui/material';
|
||||
import { IdentityKeyFormField } from '@nymproject/react/mixnodes/IdentityKeyFormField';
|
||||
import { CurrencyFormField } from '@nymproject/react/currency/CurrencyFormField';
|
||||
import { CurrencyDenom, DecCoin } from '@nymproject/types';
|
||||
import { useChain } from '@cosmos-kit/react';
|
||||
import { useWalletContext } from '@src/context/wallet';
|
||||
import { useDelegationsContext } from '@src/context/delegations';
|
||||
import { SimpleModal } from './SimpleModal';
|
||||
import { ModalListItem } from './ModalListItem';
|
||||
import { DelegationModalProps } from './DelegationModal';
|
||||
import { unymToNym, validateAmount } from '../../utils/currency';
|
||||
import { validateAmount } from '../../utils/currency';
|
||||
import { urls } from '../../utils';
|
||||
import { NYM_MIXNET_CONTRACT } from '../../api/constants';
|
||||
|
||||
const MIN_AMOUNT_TO_DELEGATE = 10;
|
||||
|
||||
@@ -31,25 +31,9 @@ export const DelegateModal: FCWithChildren<{
|
||||
const [amount, setAmount] = useState<DecCoin | undefined>({ amount: '10', denom: 'nym' });
|
||||
const [isValidated, setValidated] = useState<boolean>(false);
|
||||
const [errorAmount, setErrorAmount] = useState<string | undefined>();
|
||||
const [balance, setBalance] = useState<{
|
||||
status: 'loading' | 'success';
|
||||
data?: string;
|
||||
}>({ status: 'loading', data: undefined });
|
||||
|
||||
const { address, getCosmWasmClient, getSigningCosmWasmClient } = useChain('nyx');
|
||||
|
||||
const getBalance = async (walletAddress: string) => {
|
||||
const account = await getCosmWasmClient();
|
||||
const uNYMBalance = await account.getBalance(walletAddress, 'unym');
|
||||
const NYMBalance = unymToNym(uNYMBalance.amount);
|
||||
|
||||
setBalance({ status: 'success', data: NYMBalance });
|
||||
};
|
||||
useEffect(() => {
|
||||
if (address) {
|
||||
getBalance(address);
|
||||
}
|
||||
}, [address]);
|
||||
const { address, balance } = useWalletContext();
|
||||
const { handleDelegate } = useDelegationsContext();
|
||||
|
||||
const validate = async () => {
|
||||
let newValidatedValue = true;
|
||||
@@ -59,7 +43,6 @@ export const DelegateModal: FCWithChildren<{
|
||||
newValidatedValue = false;
|
||||
errorAmountMessage = 'Please enter a valid amount';
|
||||
}
|
||||
console.log(amount, MIN_AMOUNT_TO_DELEGATE);
|
||||
|
||||
if (amount && +amount.amount < MIN_AMOUNT_TO_DELEGATE) {
|
||||
errorAmountMessage = `Min. delegation amount: ${MIN_AMOUNT_TO_DELEGATE} ${denom.toUpperCase()}`;
|
||||
@@ -81,32 +64,13 @@ export const DelegateModal: FCWithChildren<{
|
||||
|
||||
const delegateToMixnode = async ({
|
||||
delegationMixId,
|
||||
delgationAddress,
|
||||
delegationAmount,
|
||||
}: {
|
||||
delegationMixId: number;
|
||||
delgationAddress: string;
|
||||
delegationAmount: string;
|
||||
}) => {
|
||||
const amountToDelegate = (Number(delegationAmount) * 1000000).toString();
|
||||
const uNymFunds = [{ amount: amountToDelegate, denom: 'unym' }];
|
||||
const memo: string = 'test delegation';
|
||||
const fee = { gas: '1000000', amount: [{ amount: '1000000', denom: 'unym' }] };
|
||||
|
||||
try {
|
||||
const signerClient = await getSigningCosmWasmClient();
|
||||
const tx = await signerClient.execute(
|
||||
delgationAddress,
|
||||
NYM_MIXNET_CONTRACT,
|
||||
{
|
||||
delegate_to_mixnode: {
|
||||
mix_id: delegationMixId,
|
||||
},
|
||||
},
|
||||
fee,
|
||||
memo,
|
||||
uNymFunds,
|
||||
);
|
||||
const tx = await handleDelegate(delegationMixId, delegationAmount);
|
||||
return tx;
|
||||
} catch (e) {
|
||||
console.error('Failed to delegateToMixnode', e);
|
||||
@@ -126,10 +90,13 @@ export const DelegateModal: FCWithChildren<{
|
||||
|
||||
const tx = await delegateToMixnode({
|
||||
delegationMixId: mixId,
|
||||
delgationAddress: address,
|
||||
delegationAmount: amount.amount,
|
||||
});
|
||||
|
||||
if (!tx) {
|
||||
throw new Error('Failed to delegate');
|
||||
}
|
||||
|
||||
onOk({
|
||||
status: 'success',
|
||||
message: 'This operation can take up to one hour to process',
|
||||
@@ -138,7 +105,7 @@ export const DelegateModal: FCWithChildren<{
|
||||
],
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Failed to addDelegation', e);
|
||||
console.error('Failed to delegate', e);
|
||||
onOk({
|
||||
status: 'error',
|
||||
message: (e as Error).message,
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
import React, { createContext, useCallback, useContext, useMemo, useState } from 'react';
|
||||
import { ExecuteResult } from '@cosmjs/cosmwasm-stargate';
|
||||
import { Delegation, PendingEpochEventKind } from '@nymproject/contract-clients/Mixnet.types';
|
||||
import { useWalletContext } from './wallet';
|
||||
import { useMainContext } from './main';
|
||||
|
||||
const fee = { gas: '1000000', amount: [{ amount: '1000000', denom: 'unym' }] };
|
||||
|
||||
export type PendingEvent = ReturnType<typeof getEventsByAddress>;
|
||||
|
||||
export type DelegationWithRewards = Delegation & {
|
||||
rewards: string;
|
||||
identityKey: string;
|
||||
pending: PendingEvent;
|
||||
};
|
||||
|
||||
const getEventsByAddress = (kind: PendingEpochEventKind, address: String) => {
|
||||
if ('delegate' in kind && kind.delegate.owner === address) {
|
||||
return {
|
||||
kind: 'delegate' as const,
|
||||
mixId: kind.delegate.mix_id,
|
||||
amount: kind.delegate.amount,
|
||||
};
|
||||
}
|
||||
|
||||
if ('undelegate' in kind && kind.undelegate.owner === address) {
|
||||
return {
|
||||
kind: 'undelegate' as const,
|
||||
mixId: kind.undelegate.mix_id,
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
interface DelegationsState {
|
||||
delegations?: DelegationWithRewards[];
|
||||
handleGetDelegations: () => Promise<void>;
|
||||
handleDelegate: (mixId: number, amount: string) => Promise<ExecuteResult | undefined>;
|
||||
handleUndelegate: (mixId: number) => Promise<ExecuteResult | undefined>;
|
||||
}
|
||||
|
||||
export const DelegationsContext = createContext<DelegationsState>({
|
||||
delegations: undefined,
|
||||
handleGetDelegations: async () => {
|
||||
throw new Error('Please connect your wallet');
|
||||
},
|
||||
handleDelegate: async () => {
|
||||
throw new Error('Please connect your wallet');
|
||||
},
|
||||
handleUndelegate: async () => {
|
||||
throw new Error('Please connect your wallet');
|
||||
},
|
||||
});
|
||||
|
||||
export const DelegationsProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const [delegations, setDelegations] = useState<DelegationWithRewards[]>();
|
||||
const { address, nymQueryClient, nymClient } = useWalletContext();
|
||||
const { fetchMixnodes } = useMainContext();
|
||||
|
||||
const handleGetPendingEvents = async () => {
|
||||
if (!nymQueryClient) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!address) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const response = await nymQueryClient.getPendingEpochEvents({});
|
||||
const pendingEvents: PendingEvent[] = [];
|
||||
|
||||
response.events.forEach((e) => {
|
||||
const event = getEventsByAddress(e.event.kind, address);
|
||||
if (event) {
|
||||
pendingEvents.push(event);
|
||||
}
|
||||
});
|
||||
|
||||
return pendingEvents;
|
||||
};
|
||||
|
||||
const handleGetDelegationRewards = async (mixId: number) => {
|
||||
if (!nymQueryClient) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!address) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const response = await nymQueryClient.getPendingDelegatorReward({ address, mixId });
|
||||
|
||||
return response;
|
||||
};
|
||||
|
||||
const handleGetDelegations = useCallback(async () => {
|
||||
if (!nymQueryClient) {
|
||||
setDelegations(undefined);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!address) {
|
||||
setDelegations(undefined);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Get all mixnodes - Required to get the identity key for each delegation
|
||||
const mixnodes = await fetchMixnodes();
|
||||
|
||||
// Get delegations
|
||||
const delegationsResponse = await nymQueryClient.getDelegatorDelegations({ delegator: address });
|
||||
|
||||
// Get rewards for each delegation
|
||||
const rewardsResponse = await Promise.all(
|
||||
delegationsResponse.delegations.map((d) => handleGetDelegationRewards(d.mix_id)),
|
||||
);
|
||||
|
||||
// Get all pending events
|
||||
const pendingEvents = await handleGetPendingEvents();
|
||||
|
||||
const delegationsWithRewards: DelegationWithRewards[] = [];
|
||||
|
||||
// Merge delegations with rewards and pending events
|
||||
delegationsResponse.delegations.forEach((d, index) => {
|
||||
delegationsWithRewards.push({
|
||||
...d,
|
||||
pending: pendingEvents?.find((e) => (e?.mixId === d.mix_id ? e.kind : undefined)),
|
||||
identityKey: mixnodes?.find((m) => m.mix_id === d.mix_id)?.mix_node.identity_key || '',
|
||||
rewards: rewardsResponse[index]?.amount_earned_detailed || '0',
|
||||
});
|
||||
});
|
||||
|
||||
// Add pending events that are not in the delegations list
|
||||
pendingEvents?.forEach((e) => {
|
||||
if (e && !delegationsWithRewards.find((d) => d.mix_id === e.mixId)) {
|
||||
delegationsWithRewards.push({
|
||||
mix_id: e.mixId,
|
||||
height: 0,
|
||||
cumulative_reward_ratio: '0',
|
||||
owner: address,
|
||||
amount: {
|
||||
amount: '0',
|
||||
denom: 'unym',
|
||||
},
|
||||
rewards: '0',
|
||||
identityKey: mixnodes?.find((m) => m.mix_id === e.mixId)?.mix_node.identity_key || '',
|
||||
pending: e,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
setDelegations(delegationsWithRewards);
|
||||
|
||||
return undefined;
|
||||
}, [address, nymQueryClient]);
|
||||
|
||||
const handleDelegate = async (mixId: number, amount: string) => {
|
||||
if (!address) {
|
||||
throw new Error('Please connect your wallet');
|
||||
}
|
||||
|
||||
const amountToDelegate = (Number(amount) * 1000000).toString();
|
||||
const uNymFunds = [{ amount: amountToDelegate, denom: 'unym' }];
|
||||
try {
|
||||
const tx = await nymClient?.delegateToMixnode({ mixId }, fee, 'Delegation from Nym Explorer', uNymFunds);
|
||||
|
||||
return tx;
|
||||
} catch (e) {
|
||||
console.error('Failed to delegate to mixnode', e);
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
const handleUndelegate = async (mixId: number) => {
|
||||
const tx = await nymClient?.undelegateFromMixnode({ mixId }, fee);
|
||||
|
||||
return tx;
|
||||
};
|
||||
|
||||
const contextValue: DelegationsState = useMemo(
|
||||
() => ({
|
||||
delegations,
|
||||
handleGetDelegations,
|
||||
handleDelegate,
|
||||
handleUndelegate,
|
||||
}),
|
||||
[delegations, handleGetDelegations],
|
||||
);
|
||||
|
||||
return <DelegationsContext.Provider value={contextValue}>{children}</DelegationsContext.Provider>;
|
||||
};
|
||||
|
||||
export const useDelegationsContext = () => {
|
||||
const context = useContext(DelegationsContext);
|
||||
if (!context) {
|
||||
throw new Error('useDelegationsContext must be used within a DelegationsProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './useIsMobile';
|
||||
export * from './useIsMounted';
|
||||
export * from './useGetMixnodeStatusColor';
|
||||
export * from './useNymClient';
|
||||
|
||||
@@ -2,7 +2,7 @@ import * as React from 'react';
|
||||
|
||||
export const ElipsSVG: FCWithChildren = () => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="25" viewBox="0 0 24 25" fill="none">
|
||||
<circle cx="12" cy="12.5" r="12" fill="url(#paint0_angular_2549_7570)" />
|
||||
<circle cx="12" cy="12.5" r="10" fill="url(#paint0_angular_2549_7570)" />
|
||||
<defs>
|
||||
<radialGradient
|
||||
id="paint0_angular_2549_7570"
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { Box, Button, Card, Chip, Tooltip, Typography } from '@mui/material';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { DelegationModal, DelegationModalProps, Title, UniversalDataGrid } from '@src/components';
|
||||
import { useWalletContext } from '@src/context/wallet';
|
||||
import { ConnectKeplrWallet } from '@src/components/Wallet/ConnectKeplrWallet';
|
||||
import { GridColDef } from '@mui/x-data-grid';
|
||||
import { unymToNym } from '@src/utils/currency';
|
||||
import {
|
||||
DelegationWithRewards,
|
||||
DelegationsProvider,
|
||||
PendingEvent,
|
||||
useDelegationsContext,
|
||||
} from '@src/context/delegations';
|
||||
|
||||
const mapToDelegationsRow = (delegation: DelegationWithRewards, index: number) => ({
|
||||
identity: delegation.identityKey,
|
||||
mix_id: delegation.mix_id,
|
||||
amount: `${unymToNym(delegation.amount.amount)} NYM`,
|
||||
rewards: `${unymToNym(delegation.rewards)} NYM`,
|
||||
id: index,
|
||||
pending: delegation.pending,
|
||||
});
|
||||
|
||||
const DelegationsPage = () => {
|
||||
const [confirmationModalProps, setConfirmationModalProps] = React.useState<DelegationModalProps | undefined>();
|
||||
const [isLoading, setIsLoading] = React.useState(false);
|
||||
|
||||
const { isWalletConnected } = useWalletContext();
|
||||
const { handleGetDelegations, handleUndelegate, delegations } = useDelegationsContext();
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
let timeoutId: NodeJS.Timeout;
|
||||
|
||||
const fetchDelegations = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await handleGetDelegations();
|
||||
} catch (error) {
|
||||
setConfirmationModalProps({
|
||||
status: 'error',
|
||||
message: "Couldn't fetch delegations. Please try again later.",
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
|
||||
timeoutId = setTimeout(() => {
|
||||
fetchDelegations();
|
||||
}, 60_000);
|
||||
}
|
||||
};
|
||||
|
||||
fetchDelegations();
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeoutId);
|
||||
};
|
||||
}, [handleGetDelegations]);
|
||||
|
||||
const getTooltipTitle = (pending: PendingEvent) => {
|
||||
if (pending?.kind === 'undelegate') {
|
||||
return 'You have an undelegation pending';
|
||||
}
|
||||
|
||||
if (pending?.kind === 'delegate') {
|
||||
return `You have a delegation pending worth ${unymToNym(pending.amount.amount)} NYM`;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const onUndelegate = async (mixId: number) => {
|
||||
setConfirmationModalProps({ status: 'loading' });
|
||||
|
||||
try {
|
||||
const tx = await handleUndelegate(mixId);
|
||||
|
||||
if (tx) {
|
||||
setConfirmationModalProps({
|
||||
status: 'success',
|
||||
message: 'Undelegation successful',
|
||||
transactions: [{ url: `${tx.transactionHash}`, hash: tx.transactionHash }],
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
setConfirmationModalProps({ status: 'error', message: error.message });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const columns: GridColDef[] = [
|
||||
{
|
||||
field: 'identity',
|
||||
headerName: 'Identity Key',
|
||||
width: 400,
|
||||
disableColumnMenu: true,
|
||||
disableReorder: true,
|
||||
sortable: false,
|
||||
headerAlign: 'left',
|
||||
},
|
||||
{
|
||||
field: 'mix_id',
|
||||
headerName: 'Mix ID',
|
||||
width: 150,
|
||||
disableColumnMenu: true,
|
||||
disableReorder: true,
|
||||
sortable: false,
|
||||
headerAlign: 'left',
|
||||
},
|
||||
{
|
||||
field: 'amount',
|
||||
headerName: 'Amount',
|
||||
width: 150,
|
||||
disableColumnMenu: true,
|
||||
disableReorder: true,
|
||||
sortable: false,
|
||||
headerAlign: 'left',
|
||||
},
|
||||
{
|
||||
field: 'rewards',
|
||||
headerName: 'Rewards',
|
||||
width: 150,
|
||||
disableColumnMenu: true,
|
||||
disableReorder: true,
|
||||
sortable: false,
|
||||
headerAlign: 'left',
|
||||
},
|
||||
{
|
||||
field: 'undelegate',
|
||||
headerName: '',
|
||||
minWidth: 150,
|
||||
flex: 1,
|
||||
disableColumnMenu: true,
|
||||
disableReorder: true,
|
||||
sortable: false,
|
||||
headerAlign: 'right',
|
||||
renderCell: (params) => {
|
||||
const { pending } = params.row;
|
||||
|
||||
return (
|
||||
<Box sx={{ width: '100%', display: 'flex', justifyContent: 'end' }}>
|
||||
{pending ? (
|
||||
<Tooltip
|
||||
placement="left"
|
||||
title={getTooltipTitle(pending as PendingEvent)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
PopperProps={{}}
|
||||
>
|
||||
<Chip size="small" label="Pending events" />
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onUndelegate(params.row.mix_id);
|
||||
}}
|
||||
>
|
||||
Undelegate
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const handleRowClick = (params: any) => {
|
||||
navigate(`/network-components/mixnode/${params.row.mix_id}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
{confirmationModalProps && (
|
||||
<DelegationModal
|
||||
{...confirmationModalProps}
|
||||
open={Boolean(confirmationModalProps)}
|
||||
onClose={async () => {
|
||||
await handleGetDelegations();
|
||||
setConfirmationModalProps(undefined);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center">
|
||||
<Title text="Your Delegations" />
|
||||
<Button variant="contained" color="primary" component={Link} to="/network-components/mixnodes">
|
||||
Delegate
|
||||
</Button>
|
||||
</Box>
|
||||
{!isWalletConnected ? (
|
||||
<Box>
|
||||
<Typography mb={2} variant="h6">
|
||||
Connect your wallet to view your delegations.
|
||||
</Typography>
|
||||
<ConnectKeplrWallet />
|
||||
</Box>
|
||||
) : null}
|
||||
<Card
|
||||
sx={{
|
||||
mt: 2,
|
||||
padding: 2,
|
||||
height: '100%',
|
||||
}}
|
||||
>
|
||||
<UniversalDataGrid
|
||||
onRowClick={handleRowClick}
|
||||
rows={delegations?.map(mapToDelegationsRow) || []}
|
||||
columns={columns}
|
||||
loading={isLoading}
|
||||
/>
|
||||
</Card>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export const Delegations = () => (
|
||||
<DelegationsProvider>
|
||||
<DelegationsPage />
|
||||
</DelegationsProvider>
|
||||
);
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import { Routes as ReactRouterRoutes, Route } from 'react-router-dom';
|
||||
import { Delegations } from '@src/pages/Delegations';
|
||||
import { PageOverview } from '../pages/Overview';
|
||||
import { PageMixnodesMap } from '../pages/MixnodesMap';
|
||||
import { Page404 } from '../pages/404';
|
||||
@@ -10,6 +11,7 @@ export const Routes: FCWithChildren = () => (
|
||||
<Route path="/" element={<PageOverview />} />
|
||||
<Route path="/network-components/*" element={<NetworkComponentsRoutes />} />
|
||||
<Route path="/nodemap" element={<PageMixnodesMap />} />
|
||||
<Route path="/delegations" element={<Delegations />} />
|
||||
<Route path="*" element={<Page404 />} />
|
||||
</ReactRouterRoutes>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user