Fix/rest delegations (#2392)

* chore: create use delegations hook

* chore: add polling to use delegations

* chore: remove polling logic from hook

* chore: remove delegations from staking form

* chore: fix more delegations queries

* chore: rename file

* fix: spelling issues
This commit is contained in:
Dexter Edwards
2022-12-13 15:37:11 +00:00
committed by GitHub
parent 718c93f486
commit d4231ea531
10 changed files with 249 additions and 318 deletions
+1 -1
View File
@@ -4,7 +4,7 @@ NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/testnet-network.json
NX_VEGA_URL=https://api.n07.testnet.vega.xyz/graphql
NX_ETHEREUM_PROVIDER_URL=https://ropsten.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
NX_ETHERSCAN_URL=https://ropsten.etherscan.io
NX_VEGA_REST=https://api.n07.testnet.vega.xyz
NX_VEGA_REST=https://api.vega.xyz
NX_FAIRGROUND=false
NX_VEGA_NETWORKS='{"DEVNET":"https://dev.token.vega.xyz","STAGNET3":"https://stagnet3.token.vega.xyz","TESTNET":"https://token.fairground.wtf","MAINNET":"https://token.vega.xyz"}'
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
@@ -17,35 +17,6 @@ export interface Delegations_epoch {
id: string;
}
export interface Delegations_party_delegations_node {
__typename: "Node";
/**
* The node url eg n01.vega.xyz
*/
id: string;
name: string;
}
export interface Delegations_party_delegations {
__typename: "Delegation";
/**
* The amount field formatted by the client
*/
amountFormatted: string;
/**
* Amount delegated
*/
amount: string;
/**
* URL of node you are delegating to
*/
node: Delegations_party_delegations_node;
/**
* Epoch of delegation
*/
epoch: number;
}
export interface Delegations_party_stake {
__typename: "PartyStake";
/**
@@ -118,7 +89,6 @@ export interface Delegations_party {
* Party identifier
*/
id: string;
delegations: Delegations_party_delegations[] | null;
/**
* The staking information for this Party
*/
+125 -177
View File
@@ -1,24 +1,22 @@
import { gql, useApolloClient } from '@apollo/client';
import * as Sentry from '@sentry/react';
import { gql, useQuery } from '@apollo/client';
import keyBy from 'lodash/keyBy';
import uniq from 'lodash/uniq';
import React from 'react';
import { useTranslation } from 'react-i18next';
import noIcon from '../../images/token-no-icon.png';
import vegaBlack from '../../images/vega_black.png';
import { BigNumber } from '../../lib/bignumber';
import { addDecimal } from '../../lib/decimals';
import type { WalletCardAssetProps } from '../wallet-card';
import type {
Delegations,
Delegations_party_delegations,
DelegationsVariables,
} from './__generated__/Delegations';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { useContracts } from '../../contexts/contracts/contracts-context';
import { isAssetTypeERC20 } from '@vegaprotocol/react-helpers';
import { isAssetTypeERC20, toBigNum } from '@vegaprotocol/react-helpers';
import { AccountType } from '@vegaprotocol/types';
import { usePartyDelegations } from './use-party-delegations';
import { useAppState } from '../../contexts/app-state/app-state-context';
const DELEGATIONS_QUERY = gql`
query Delegations($partyId: ID!) {
@@ -27,15 +25,6 @@ const DELEGATIONS_QUERY = gql`
}
party(id: $partyId) {
id
delegations {
amountFormatted @client
amount
node {
id
name
}
epoch
}
stake {
currentStakeAvailable
currentStakeAvailableFormatted @client
@@ -61,173 +50,132 @@ const DELEGATIONS_QUERY = gql`
`;
export const usePollForDelegations = () => {
const {
appState: { decimals },
} = useAppState();
const { token: vegaToken } = useContracts();
const { t } = useTranslation();
const { keypair } = useVegaWallet();
const client = useApolloClient();
const [delegations, setDelegations] = React.useState<
Delegations_party_delegations[]
>([]);
const [delegatedNodes, setDelegatedNodes] = React.useState<
const delegations = usePartyDelegations(keypair?.pub);
const { data } = useQuery<Delegations, DelegationsVariables>(
DELEGATIONS_QUERY,
{
nodeId: string;
name: string;
hasStakePending: boolean;
currentEpochStake?: BigNumber;
nextEpochStake?: BigNumber;
}[]
>([]);
const [accounts, setAccounts] = React.useState<WalletCardAssetProps[]>([]);
const [currentStakeAvailable, setCurrentStakeAvailable] =
React.useState<BigNumber>(new BigNumber(0));
React.useEffect(() => {
// eslint-disable-next-line
let interval: any;
let mounted = true;
if (keypair?.pub) {
// start polling for delegation
interval = setInterval(() => {
client
.query<Delegations, DelegationsVariables>({
query: DELEGATIONS_QUERY,
variables: { partyId: keypair.pub },
fetchPolicy: 'network-only',
})
.then((res) => {
if (!mounted) return;
const filter =
res.data.party?.delegations?.filter((d) => {
return d.epoch.toString() === res.data.epoch.id;
}) || [];
const sortedDelegations = [...filter].sort((a, b) => {
return new BigNumber(b.amountFormatted)
.minus(a.amountFormatted)
.toNumber();
});
setDelegations(sortedDelegations);
setCurrentStakeAvailable(
new BigNumber(
res.data.party?.stake.currentStakeAvailableFormatted || 0
)
);
const accounts = res.data.party?.accounts || [];
setAccounts(
accounts
.filter((a) => a.type === AccountType.General)
.map((a) => {
const isVega =
isAssetTypeERC20(a.asset) &&
a.asset.source.contractAddress === vegaToken.address;
return {
isVega,
name: a.asset.name,
subheading: isVega ? t('collateral') : a.asset.symbol,
symbol: a.asset.symbol,
decimals: a.asset.decimals,
balance: new BigNumber(
addDecimal(new BigNumber(a.balance), a.asset.decimals)
),
image: isVega ? vegaBlack : noIcon,
border: isVega,
address: isAssetTypeERC20(a.asset)
? a.asset.source.contractAddress
: undefined,
};
})
.sort((a, b) => {
// Put VEGA at the top of the list
if (a.isVega) {
return -1;
}
if (b.isVega) {
return 1;
}
// Secondary sort by name
if (a.name < b.name) {
return -1;
}
if (a.name > b.name) {
return 1;
}
return 0;
})
);
const delegatedNextEpoch = keyBy(
res.data.party?.delegations?.filter((d) => {
return d.epoch === Number(res.data.epoch.id) + 1;
}) || [],
'node.id'
);
const delegatedThisEpoch = keyBy(
res.data.party?.delegations?.filter((d) => {
return d.epoch === Number(res.data.epoch.id);
}) || [],
'node.id'
);
const nodesDelegated = uniq([
...Object.keys(delegatedNextEpoch),
...Object.keys(delegatedThisEpoch),
]);
const delegatedAmounts = nodesDelegated
.map((d) => ({
nodeId: d,
name:
delegatedThisEpoch[d]?.node?.name ||
delegatedNextEpoch[d]?.node?.name,
hasStakePending: !!(
(delegatedThisEpoch[d]?.amountFormatted ||
delegatedNextEpoch[d]?.amountFormatted) &&
delegatedThisEpoch[d]?.amountFormatted !==
delegatedNextEpoch[d]?.amountFormatted &&
delegatedNextEpoch[d] !== undefined
),
currentEpochStake:
delegatedThisEpoch[d] &&
new BigNumber(delegatedThisEpoch[d].amountFormatted),
nextEpochStake:
delegatedNextEpoch[d] &&
new BigNumber(delegatedNextEpoch[d].amountFormatted),
}))
.sort((a, b) => {
if (
new BigNumber(a.currentEpochStake || 0).isLessThan(
b.currentEpochStake || 0
)
)
return 1;
if (
new BigNumber(a.currentEpochStake || 0).isGreaterThan(
b.currentEpochStake || 0
)
)
return -1;
if ((!a.name && b.name) || a.name < b.name) return 1;
if ((!b.name && a.name) || a.name > b.name) return -1;
if (a.nodeId > b.nodeId) return 1;
if (a.nodeId < b.nodeId) return -1;
return 0;
});
setDelegatedNodes(delegatedAmounts);
})
.catch((err: Error) => {
Sentry.captureException(err);
// If query fails stop interval. Its almost certain that the query
// will just continue to fail
clearInterval(interval);
});
}, 100000);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion, @typescript-eslint/no-non-null-asserted-optional-chain
variables: { partyId: keypair?.pub! },
skip: !keypair?.pub,
}
);
return () => {
clearInterval(interval);
mounted = false;
};
}, [client, keypair?.pub, t, vegaToken.address]);
const filter =
delegations?.filter((d) => {
return d.epochSeq === data?.epoch.id;
}) || [];
const sortedDelegations = [...filter].sort((a, b) => {
return new BigNumber(b.amount).minus(a.amount).toNumber();
});
return { delegations, currentStakeAvailable, delegatedNodes, accounts };
const partyAccounts = data?.party?.accounts || [];
const accounts = partyAccounts
.filter((a) => a.type === AccountType.General)
.map((a) => {
const isVega =
isAssetTypeERC20(a.asset) &&
a.asset.source.contractAddress === vegaToken.address;
return {
isVega,
name: a.asset.name,
subheading: isVega ? t('collateral') : a.asset.symbol,
symbol: a.asset.symbol,
decimals: a.asset.decimals,
balance: new BigNumber(
addDecimal(new BigNumber(a.balance), a.asset.decimals)
),
image: isVega ? vegaBlack : noIcon,
border: isVega,
address: isAssetTypeERC20(a.asset)
? a.asset.source.contractAddress
: undefined,
};
})
.sort((a, b) => {
// Put VEGA at the top of the list
if (a.isVega) {
return -1;
}
if (b.isVega) {
return 1;
}
// Secondary sort by name
if (a.name < b.name) {
return -1;
}
if (a.name > b.name) {
return 1;
}
return 0;
});
const delegatedNextEpoch = keyBy(
delegations?.filter((d) => {
return Number(d.epochSeq) === Number(data?.epoch.id) + 1;
}) || [],
'nodeId'
);
const delegatedThisEpoch = keyBy(
delegations?.filter((d) => {
return d.epochSeq === data?.epoch.id;
}) || [],
'nodeId'
);
const nodesDelegated = uniq([
...Object.keys(delegatedNextEpoch),
...Object.keys(delegatedThisEpoch),
]);
const delegatedAmounts = nodesDelegated
.map((d) => ({
nodeId: d,
name:
delegatedThisEpoch[d]?.node?.name || delegatedNextEpoch[d]?.node?.name,
hasStakePending: !!(
(delegatedThisEpoch[d]?.amount || delegatedNextEpoch[d]?.amount) &&
delegatedThisEpoch[d]?.amount !== delegatedNextEpoch[d]?.amount &&
delegatedNextEpoch[d] !== undefined
),
currentEpochStake:
delegatedThisEpoch[d] &&
toBigNum(delegatedThisEpoch[d].amount, decimals),
nextEpochStake:
delegatedNextEpoch[d] &&
toBigNum(delegatedNextEpoch[d].amount, decimals),
}))
.sort((a, b) => {
if (
new BigNumber(a.currentEpochStake || 0).isLessThan(
b.currentEpochStake || 0
)
)
return 1;
if (
new BigNumber(a.currentEpochStake || 0).isGreaterThan(
b.currentEpochStake || 0
)
)
return -1;
if ((!a.name && b.name) || a.name || '' < (b.name || '')) return 1;
if ((!b.name && a.name) || a.name || '' > (b.name || '')) return -1;
if (a.nodeId > b.nodeId) return 1;
if (a.nodeId < b.nodeId) return -1;
return 0;
});
return {
delegations: sortedDelegations,
currentStakeAvailable: new BigNumber(
data?.party?.stake.currentStakeAvailableFormatted || 0
),
delegatedNodes: delegatedAmounts,
accounts,
};
};
@@ -0,0 +1,77 @@
import { useFetch } from '@vegaprotocol/react-helpers';
import { useEffect, useMemo, useState } from 'react';
export interface Delegation {
party: string;
nodeId: string;
amount: string;
epochSeq: string;
}
export interface RankingScore {
stakeScore: string;
performanceScore: string;
previousStatus: string;
status: string;
votingPower: number;
rankingScore: string;
}
export interface Node {
id: string;
pubKey: string;
tmPubKey: string;
ethereumAdddress: string;
infoUrl: string;
location: string;
stakedByOperator: string;
stakedByDelegates: string;
stakedTotal: string;
maxIntendedStake: string;
pendingStake: string;
epochData?: any;
status: string;
delegations: Delegation[];
rewardScore?: any;
rankingScore: RankingScore;
name: string;
avatarUrl: string;
}
export interface NodesQuery {
nodes: Node[];
}
export interface DelegationsQuery {
delegations: Delegation[];
}
export interface DelegationsNode extends Delegation {
node: Node | undefined;
}
export const usePartyDelegations = (partyId: string | undefined) => {
const delegationsUrl = `${process.env['NX_VEGA_REST']}delegations?party=${partyId}`;
const { state: delegationsData, refetch: refetchDelegations } =
useFetch<DelegationsQuery>(delegationsUrl);
const { state: nodesData } = useFetch<NodesQuery>(
`${process.env['NX_VEGA_REST']}nodes`
);
useEffect(() => {
const interval = setInterval(() => refetchDelegations(), 10000);
return () => {
if (interval) {
clearInterval(interval);
}
};
}, [refetchDelegations]);
const delegations = useMemo<DelegationsNode[] | undefined>(() => {
if (!delegationsData.data) return [];
return delegationsData.data?.delegations.map((d) => ({
...d,
node: nodesData.data?.nodes.find(({ id }) => d.nodeId === id),
}));
}, [delegationsData.data, nodesData.data?.nodes]);
return delegations;
};
@@ -25,6 +25,7 @@ import { usePollForDelegations } from './hooks';
import type { VegaKeyExtended } from '@vegaprotocol/wallet';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { Button, ButtonLink } from '@vegaprotocol/ui-toolkit';
import { toBigNum } from '@vegaprotocol/react-helpers';
export const VegaWallet = () => {
const { t } = useTranslation();
@@ -122,12 +123,13 @@ const VegaWalletConnected = ({ vegaKeys }: VegaWalletConnectedProps) => {
usePollForDelegations();
const unstaked = React.useMemo(() => {
const totalDelegated = delegations.reduce<BigNumber>(
(acc, cur) => acc.plus(cur.amountFormatted),
new BigNumber(0)
);
const totalDelegated =
delegations?.reduce<BigNumber>(
(acc, cur) => acc.plus(toBigNum(cur.amount, decimals)),
new BigNumber(0)
) || new BigNumber(0);
return BigNumber.max(currentStakeAvailable.minus(totalDelegated), 0);
}, [currentStakeAvailable, delegations]);
}, [currentStakeAvailable, decimals, delegations]);
const footer = (
<div className="flex justify-end">
-29
View File
@@ -21,34 +21,6 @@ export interface Staking_party_stake {
currentStakeAvailableFormatted: string;
}
export interface Staking_party_delegations_node {
__typename: "Node";
/**
* The node url eg n01.vega.xyz
*/
id: string;
}
export interface Staking_party_delegations {
__typename: "Delegation";
/**
* Amount delegated
*/
amount: string;
/**
* The amount field formatted by the client
*/
amountFormatted: string;
/**
* Epoch of delegation
*/
epoch: number;
/**
* URL of node you are delegating to
*/
node: Staking_party_delegations_node;
}
export interface Staking_party {
__typename: "Party";
/**
@@ -59,7 +31,6 @@ export interface Staking_party {
* The staking information for this Party
*/
stake: Staking_party_stake;
delegations: Staking_party_delegations[] | null;
}
export interface Staking_epoch_timestamps {
+13 -49
View File
@@ -31,25 +31,7 @@ import type {
} from '@vegaprotocol/wallet';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { useNetworkParam } from '@vegaprotocol/react-helpers';
export const PARTY_DELEGATIONS_QUERY = gql`
query PartyDelegations($partyId: ID!) {
party(id: $partyId) {
id
delegations {
amount
amountFormatted @client
node {
id
}
epoch
}
}
epoch {
id
}
}
`;
import { usePartyDelegations } from '../../components/vega-wallet/use-party-delegations';
enum FormState {
Default,
@@ -75,6 +57,7 @@ interface StakingFormProps {
nodeName: string;
availableStakeToAdd: BigNumber;
availableStakeToRemove: BigNumber;
currentEpoch: string | undefined;
}
export const StakingForm = ({
@@ -83,6 +66,7 @@ export const StakingForm = ({
nodeName,
availableStakeToAdd,
availableStakeToRemove,
currentEpoch,
}: StakingFormProps) => {
const params = useSearchParams();
const navigate = useNavigate();
@@ -99,6 +83,7 @@ export const StakingForm = ({
RemoveType.EndOfEpoch
);
// Clear the amount when the staking method changes
const delegations = usePartyDelegations(pubkey);
React.useEffect(() => {
setAmount('');
}, [action, setAmount]);
@@ -159,39 +144,18 @@ export const StakingForm = ({
}
React.useEffect(() => {
// eslint-disable-next-line
let interval: any;
if (formState === FormState.Pending) {
// start polling for delegation
interval = setInterval(() => {
client
.query<PartyDelegations, PartyDelegationsVariables>({
query: PARTY_DELEGATIONS_QUERY,
variables: { partyId: pubkey },
fetchPolicy: 'network-only',
})
.then((res) => {
const delegation = res.data.party?.delegations?.find((d) => {
return (
d.node.id === nodeId &&
d.epoch === Number(res.data.epoch.id) + 1
);
});
const delegation = delegations?.find((d) => {
return (
d.nodeId === nodeId && Number(d.epochSeq) === Number(currentEpoch) + 1
);
});
if (delegation) {
setFormState(FormState.Success);
clearInterval(interval);
}
})
.catch((err) => {
Sentry.captureException(err);
});
}, 1000);
if (delegation) {
setFormState(FormState.Success);
}
}
return () => clearInterval(interval);
}, [formState, client, pubkey, nodeId]);
}, [formState, client, pubkey, nodeId, delegations, currentEpoch]);
if (formState === FormState.Failure) {
return <StakeFailure nodeName={nodeName} />;
+23 -16
View File
@@ -12,6 +12,9 @@ import { StakingNodesContainer } from './staking-nodes-container';
import { StakingWalletsContainer } from './staking-wallets-container';
import { ValidatorTable } from './validator-table';
import { YourStake } from './your-stake';
import { usePartyDelegations } from '../../components/vega-wallet/use-party-delegations';
import { useAppState } from '../../contexts/app-state/app-state-context';
import { toBigNum } from '@vegaprotocol/react-helpers';
export const StakingNodeContainer = () => {
return (
@@ -36,6 +39,9 @@ interface StakingNodeProps {
export const StakingNode = ({ vegaKey, data }: StakingNodeProps) => {
const { node } = useParams<{ node: string }>();
const {
appState: { decimals },
} = useAppState();
const { t } = useTranslation();
const nodeInfo = React.useMemo(() => {
@@ -45,36 +51,36 @@ export const StakingNode = ({ vegaKey, data }: StakingNodeProps) => {
const currentEpoch = React.useMemo(() => {
return data?.epoch.id;
}, [data?.epoch.id]);
const partyDelegations = usePartyDelegations(vegaKey.pub);
const stakeThisEpoch = React.useMemo(() => {
const delegations = data?.party?.delegations || [];
const delegations = partyDelegations || [];
const amountsThisEpoch = delegations
.filter((d) => d.node.id === node)
.filter((d) => d.epoch === Number(currentEpoch))
.map((d) => new BigNumber(d.amountFormatted));
.filter((d) => d.nodeId === node)
.filter((d) => d.epochSeq === currentEpoch)
.map((d) => toBigNum(d.amount, decimals));
return BigNumber.sum.apply(null, [new BigNumber(0), ...amountsThisEpoch]);
}, [data?.party?.delegations, node, currentEpoch]);
}, [partyDelegations, node, currentEpoch, decimals]);
const stakeNextEpoch = React.useMemo(() => {
const delegations = data?.party?.delegations || [];
const delegations = partyDelegations || [];
const amountsNextEpoch = delegations
.filter((d) => d.node.id === node)
.filter((d) => d.epoch === Number(currentEpoch) + 1)
.map((d) => new BigNumber(d.amountFormatted));
.filter((d) => d.nodeId === node)
.filter((d) => Number(d.epochSeq) === Number(currentEpoch) + 1)
.map((d) => toBigNum(d.amount, decimals));
if (!amountsNextEpoch.length) {
return stakeThisEpoch;
}
return BigNumber.sum.apply(null, [new BigNumber(0), ...amountsNextEpoch]);
}, [currentEpoch, data?.party?.delegations, node, stakeThisEpoch]);
}, [currentEpoch, decimals, node, partyDelegations, stakeThisEpoch]);
const currentDelegationAmount = React.useMemo(() => {
if (!data?.party?.delegations) return new BigNumber(0);
const amounts = data.party.delegations
.filter((d) => d.epoch === Number(currentEpoch) + 1)
.map((d) => new BigNumber(d.amountFormatted));
if (!partyDelegations?.length) return new BigNumber(0);
const amounts = partyDelegations
.filter((d) => Number(d.epochSeq) === Number(currentEpoch) + 1)
.map((d) => toBigNum(d.amount, decimals));
return BigNumber.sum.apply(null, [new BigNumber(0), ...amounts]);
}, [currentEpoch, data?.party?.delegations]);
}, [currentEpoch, decimals, partyDelegations]);
const unstaked = React.useMemo(() => {
const value = new BigNumber(
@@ -125,6 +131,7 @@ export const StakingNode = ({ vegaKey, data }: StakingNodeProps) => {
</section>
<section>
<StakingForm
currentEpoch={data?.epoch.id}
pubkey={vegaKey.pub}
nodeId={nodeInfo.id}
nodeName={nodeInfo.name}
@@ -15,14 +15,6 @@ export const STAKING_QUERY = gql`
currentStakeAvailable
currentStakeAvailableFormatted @client
}
delegations {
amount
amountFormatted @client
epoch
node {
id
}
}
}
epoch {
id
+3 -3
View File
@@ -42,11 +42,11 @@ export const useFetch = <T>(
const fetchReducer = (state: State<T>, action: Action<T>): State<T> => {
switch (action.type) {
case ActionType.LOADING:
return { ...initialState, loading: true };
return { ...state, loading: true };
case ActionType.FETCHED:
return { ...initialState, data: action.payload, loading: false };
return { ...state, data: action.payload, loading: false };
case ActionType.ERROR:
return { ...initialState, error: action.error, loading: false };
return { ...state, error: action.error, loading: false };
}
};