Skip to content

Commit

Permalink
feat: implement basic Staking/unstaking (#611)
Browse files Browse the repository at this point in the history
  • Loading branch information
rosepuppy authored May 31, 2024
1 parent f634018 commit 548bb2a
Show file tree
Hide file tree
Showing 23 changed files with 1,351 additions and 7 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
"@cosmjs/stargate": "^0.32.1",
"@cosmjs/tendermint-rpc": "^0.32.1",
"@dydxprotocol/v4-abacus": "1.7.43",
"@dydxprotocol/v4-client-js": "^1.1.10",
"@dydxprotocol/v4-client-js": "^1.1.15",
"@dydxprotocol/v4-localization": "^1.1.100",
"@ethersproject/providers": "^5.7.2",
"@js-joda/core": "^5.5.3",
Expand Down
8 changes: 4 additions & 4 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 19 additions & 0 deletions public/configs/v1/env.json
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,7 @@
"geo": "https://api.dydx.exchange/v4/geo",
"faucet": "https://faucet.v4dev.dydx.exchange"
},
"stakingValidators": [],
"featureFlags": {
"reduceOnlySupported": true,
"usePessimisticCollateralCheck": false,
Expand Down Expand Up @@ -317,6 +318,7 @@
"nobleValidator": "https://noble-testnet-rpc.polkachu.com/",
"geo": "https://api.dydx.exchange/v4/geo"
},
"stakingValidators": [],
"featureFlags": {
"reduceOnlySupported": true,
"usePessimisticCollateralCheck": false,
Expand Down Expand Up @@ -353,6 +355,7 @@
"geo": "https://api.dydx.exchange/v4/geo",
"faucet": "http://dev3-faucet-lb-public-1644791410.us-east-2.elb.amazonaws.com"
},
"stakingValidators": [],
"featureFlags": {
"reduceOnlySupported": true,
"usePessimisticCollateralCheck": false,
Expand Down Expand Up @@ -389,6 +392,7 @@
"geo": "https://api.dydx.exchange/v4/geo",
"faucet": "https://faucet.v4dev4.dydx.exchange"
},
"stakingValidators": [],
"featureFlags": {
"reduceOnlySupported": true,
"usePessimisticCollateralCheck": false,
Expand Down Expand Up @@ -424,6 +428,7 @@
"nobleValidator": "https://noble-testnet-rpc.polkachu.com/",
"geo": "https://api.dydx.exchange/v4/geo"
},
"stakingValidators": [],
"featureFlags": {
"reduceOnlySupported": true,
"usePessimisticCollateralCheck": false,
Expand Down Expand Up @@ -460,6 +465,7 @@
"nobleValidator": "https://noble-testnet-rpc.polkachu.com/",
"geo": "https://api.dydx.exchange/v4/geo"
},
"stakingValidators": [],
"featureFlags": {
"reduceOnlySupported": true,
"usePessimisticCollateralCheck": false,
Expand Down Expand Up @@ -508,6 +514,7 @@
"url": "https://play.google.com/store/apps/details?id=trade.opsdao.dydxchain&pli=1"
}
},
"stakingValidators": [],
"featureFlags": {
"reduceOnlySupported": true,
"usePessimisticCollateralCheck": false,
Expand Down Expand Up @@ -544,6 +551,7 @@
"nobleValidator": "https://noble-testnet-rpc.polkachu.com/",
"geo": "https://api.dydx.exchange/v4/geo"
},
"stakingValidators": [],
"featureFlags": {
"reduceOnlySupported": true,
"usePessimisticCollateralCheck": false,
Expand Down Expand Up @@ -584,6 +592,10 @@
"geo": "https://api.dydx.exchange/v4/geo",
"faucet": "https://faucet.v4testnet.dydx.exchange"
},
"stakingValidators": [
"dydxvaloper1vvc9vl6z9pu0vt2y79d0ln8zp6qmpmrhxx99h4",
"dydxvaloper10lzv79d96l7jh07z76ry6cnn6ftnnl8fgketzu"
],
"featureFlags": {
"reduceOnlySupported": true,
"usePessimisticCollateralCheck": false,
Expand Down Expand Up @@ -620,6 +632,7 @@
"geo": "https://api.dydx.exchange/v4/geo",
"faucet": "https://faucet.v4testnet.dydx.exchange"
},
"stakingValidators": [],
"featureFlags": {
"reduceOnlySupported": true,
"usePessimisticCollateralCheck": true,
Expand Down Expand Up @@ -655,6 +668,7 @@
"geo": "https://api.dydx.exchange/v4/geo",
"faucet": "https://faucet.v4testnet.dydx.exchange"
},
"stakingValidators": [],
"featureFlags": {
"reduceOnlySupported": true,
"usePessimisticCollateralCheck": false,
Expand Down Expand Up @@ -691,6 +705,7 @@
"geo": "https://api.dydx.exchange/v4/geo",
"faucet": "https://faucet.v4testnet.dydx.exchange"
},
"stakingValidators": [],
"featureFlags": {
"reduceOnlySupported": true,
"usePessimisticCollateralCheck": true,
Expand Down Expand Up @@ -726,6 +741,7 @@
"geo": "https://api.dydx.exchange/v4/geo",
"faucet": "https://faucet.v4testnet.dydx.exchange"
},
"stakingValidators": [],
"featureFlags": {
"reduceOnlySupported": true,
"usePessimisticCollateralCheck": false,
Expand Down Expand Up @@ -762,6 +778,7 @@
"geo": "https://api.dydx.exchange/v4/geo",
"faucet": "https://faucet.v4testnet.dydx.exchange"
},
"stakingValidators": [],
"featureFlags": {
"reduceOnlySupported": true,
"usePessimisticCollateralCheck": false,
Expand Down Expand Up @@ -798,6 +815,7 @@
"geo": "https://api.dydx.exchange/v4/geo",
"faucet": "https://faucet.v4testnet.dydx.exchange"
},
"stakingValidators": [],
"featureFlags": {
"reduceOnlySupported": true,
"usePessimisticCollateralCheck": false,
Expand Down Expand Up @@ -834,6 +852,7 @@
"nobleValidator": "[noble validator endpoint for mainnet]",
"geo": "[geo endpoint for mainnet]"
},
"stakingValidators": [],
"featureFlags": {
"reduceOnlySupported": true,
"usePessimisticCollateralCheck": false,
Expand Down
84 changes: 84 additions & 0 deletions src/components/ValidatorName.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { useState } from 'react';

import { Validator } from '@dydxprotocol/v4-client-js/build/node_modules/@dydxprotocol/v4-proto/src/codegen/cosmos/staking/v1beta1/staking';
import styled from 'styled-components';

import { Link } from './Link';
import { Output, OutputType } from './Output';

export type ValidatorNameProps = {
validator?: Validator;
};

const FaviconIcon = ({ url, fallbackText }: { url?: string; fallbackText?: string }) => {
const [iconFail, setIconFail] = useState<boolean>(false);

if (url && !iconFail) {
const parsedUrl = new URL(url);
const baseUrl = `${parsedUrl.protocol}//${parsedUrl.hostname}`;
return (
<$Img
src={`${baseUrl}/favicon.ico`}
alt="validator favicon"
onError={() => setIconFail(true)}
/>
);
}
if (fallbackText) {
return <$IconContainer>{fallbackText.charAt(0)}</$IconContainer>;
}

return null;
};

export const ValidatorName = ({ validator }: ValidatorNameProps) => {
if (!validator) {
return null;
}
const output = (
<$Output
type={OutputType.Text}
value={validator?.description?.moniker}
slotLeft={
<FaviconIcon
url={validator?.description?.website}
fallbackText={validator?.description?.moniker}
/>
}
/>
);

if (validator?.description?.website) {
return (
<Link href={validator?.description?.website} withIcon>
{output}
</Link>
);
}
return output;
};

const $IconContainer = styled.div`
display: flex;
align-items: center;
justify-content: center;
width: 1.5em;
height: 1.5em;
background-color: var(--color-layer-6);
border-radius: 50%;
font-weight: bold;
color: var(--color-text-1);
margin-right: 0.25em;
`;

const $Img = styled.img`
width: 1.5em;
height: 1.5em;
border-radius: 50%;
object-fit: cover;
margin-right: 0.25em;
`;

const $Output = styled(Output)`
color: var(--color-text-1);
`;
1 change: 1 addition & 0 deletions src/constants/abacus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ export const ErrorType = Abacus.exchange.dydx.abacus.output.input.ErrorType;
// ------ Wallet ------ //
export type Wallet = Abacus.exchange.dydx.abacus.output.Wallet;
export type AccountBalance = Abacus.exchange.dydx.abacus.output.AccountBalance;
export type StakingDelegation = Abacus.exchange.dydx.abacus.output.StakingDelegation;
export type TradingRewards = Abacus.exchange.dydx.abacus.output.TradingRewards;
export type HistoricalTradingReward = Abacus.exchange.dydx.abacus.output.HistoricalTradingReward;
export const HistoricalTradingRewardsPeriod =
Expand Down
2 changes: 2 additions & 0 deletions src/constants/dialogs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,11 @@ export enum DialogTypes {
RestrictedGeo = 'RestrictedGeo',
RestrictedWallet = 'RestrictedWallet',
SelectMarginMode = 'SelectMarginMode',
Stake = 'Stake',
Trade = 'Trade',
Triggers = 'Triggers',
Transfer = 'Transfer',
Unstake = 'Unstake',
Withdraw = 'Withdraw',
WithdrawalGated = 'WithdrawalGated',
}
Expand Down
5 changes: 5 additions & 0 deletions src/hooks/useDydxClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,10 @@ const useDydxClientContext = () => {
[compositeClient]
);

const getValidators = useCallback(async () => {
return compositeClient?.validatorClient.get.getAllValidators();
}, [compositeClient]);

return {
// Client initialization
connect: setNetworkConfig,
Expand All @@ -328,5 +332,6 @@ const useDydxClientContext = () => {
screenAddresses,
getWithdrawalAndTransferGatingStatus,
getWithdrawalCapacityByDenom,
getValidators,
};
};
28 changes: 28 additions & 0 deletions src/hooks/useStakingAPY.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { useCallback } from 'react';

import { useQuery } from '@tanstack/react-query';

// TODO: This api doesn't work due to cors, need to contact protocolstaking.info
export const useStakingAPY = () => {
const queryFn = useCallback(async () => {
const response = await fetch('https://api.protocolstaking.info/v0/protocols/dydx', {
headers: {
accept: 'application/json',
'x-access-key': import.meta.env.VITE_PROTOCOL_STAKING_API_KEY,
},
});

const data = await response.json();
return data;
}, []);

const { data } = useQuery({
queryKey: ['stakingAPY'],
queryFn,
enabled: true,
refetchOnWindowFocus: false,
refetchOnReconnect: false,
});

return data;
};
79 changes: 79 additions & 0 deletions src/hooks/useStakingValidator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { useCallback } from 'react';

import { useQuery } from '@tanstack/react-query';
import { groupBy } from 'lodash';
import { shallowEqual, useSelector } from 'react-redux';

import { ENVIRONMENT_CONFIG_MAP } from '@/constants/networks';

import { getStakingDelegations } from '@/state/accountSelectors';
import { getSelectedNetwork } from '@/state/appSelectors';

import { useDydxClient } from './useDydxClient';

export const useStakingValidator = () => {
const { getValidators, isCompositeClientConnected } = useDydxClient();
const selectedNetwork = useSelector(getSelectedNetwork);
const currentDelegations = useSelector(getStakingDelegations, shallowEqual)?.map((delegation) => {
return {
validator: delegation.validator.toLowerCase(),
amount: delegation.amount,
};
});
const validatorWhitelist = ENVIRONMENT_CONFIG_MAP[selectedNetwork].stakingValidators?.map(
(delegation) => {
return delegation.toLowerCase();
}
);

const queryFn = useCallback(async () => {
const validatorOptions: string[] = [];
const intersection = validatorWhitelist.filter((delegation) =>
currentDelegations?.map((d) => d.validator).includes(delegation)
);

if (intersection.length > 0) {
validatorOptions.push(...intersection);
} else {
validatorOptions.push(...validatorWhitelist);
}

const response = await getValidators();

const filteredValidators = response?.validators.filter((validator) =>
validatorOptions.includes(validator.operatorAddress.toLowerCase())
);

const stakingValidators =
response?.validators.filter((validator) =>
currentDelegations
?.map((d) => d.validator)
.includes(validator.operatorAddress.toLowerCase())
) ?? [];

if (!filteredValidators || filteredValidators.length === 0) {
return undefined;
}

// Find the validator with the fewest tokens
const validatorWithFewestTokens = filteredValidators.reduce((prev, curr) => {
return BigInt(curr.tokens) < BigInt(prev.tokens) ? curr : prev;
});

return {
selectedValidator: validatorWithFewestTokens,
stakingValidators: groupBy(stakingValidators, ({ operatorAddress }) => operatorAddress),
currentDelegations,
};
}, [validatorWhitelist, getValidators, currentDelegations]);

const { data } = useQuery({
queryKey: ['stakingValidator', selectedNetwork],
queryFn,
enabled: Boolean(isCompositeClientConnected && validatorWhitelist?.length > 0),
refetchOnWindowFocus: false,
refetchOnReconnect: false,
});

return data;
};
Loading

0 comments on commit 548bb2a

Please sign in to comment.