Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: initial staking upd #542

Merged
merged 6 commits into from
Apr 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
# 0.4.1

- Feat: Refresh vault data on every block
- Feat: Add veYFI quick deposit

# 0.4.0

- Fix: Add a missing context in the \_app file (settings). (13/01/2024)
Expand Down
92 changes: 92 additions & 0 deletions apps/common/components/Counter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import {useLayoutEffect, useRef} from 'react';
import {animate} from 'framer-motion';
import {formatAmount, parseAmount} from '@builtbymom/web3/utils';

import type {ReactElement} from 'react';

export function Counter({
value,
decimals = 18,
idealDecimals,
decimalsToDisplay
}: {
value: number; // Value to animate
decimals: number; // Number of decimals of that token
idealDecimals?: number; // Ideal decimals to display
decimalsToDisplay?: number[]; // Decimals to display
}): ReactElement {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const nodeRef = useRef<any>();
const valueRef = useRef(value || 0);

useLayoutEffect((): (() => void) => {
const node = nodeRef.current;
if (node) {
const controls = animate(Number(valueRef.current || 0), value, {
duration: 1,
onUpdate(value) {
let hasBeenSet = false;
valueRef.current = value;
if (Number.isNaN(value) || value === 0) {
const formatedValue = formatAmount(0, idealDecimals, idealDecimals);
node.textContent = formatedValue;
} else if (decimalsToDisplay && decimalsToDisplay.length > 0) {
const allDecimalsToTests = [...decimalsToDisplay, decimals];
if (idealDecimals) {
allDecimalsToTests.unshift(idealDecimals);
}
for (const decimalToDisplay of allDecimalsToTests) {
if (decimalToDisplay > decimals) {
const formatedValue = formatAmount(value.toFixed(decimals), decimals, decimals);
node.textContent = formatedValue;
hasBeenSet = true;
break;
}
const formatedValue = formatAmount(
value.toFixed(decimals),
decimalToDisplay,
decimalToDisplay
);
if (
Number.isNaN(parseAmount(formatedValue)) ||
formatedValue === 'NaN' ||
parseAmount(formatedValue) === 0
) {
continue;
}
node.textContent = formatedValue;
hasBeenSet = true;
break;
}
if (!hasBeenSet) {
if (Number.isNaN(value) || value === 0) {
const formatedValue = formatAmount(0, idealDecimals, idealDecimals);
node.textContent = formatedValue;
} else {
const formatedValue = formatAmount(value.toFixed(decimals), decimals, decimals);
node.textContent = formatedValue;
}
}
} else {
const formatedValue = formatAmount(
value.toFixed(decimals),
decimals || idealDecimals,
decimals || idealDecimals
);
node.textContent = formatedValue;
}
}
});
return () => controls.stop();
}
return () => undefined;
}, [value, decimals, decimalsToDisplay, idealDecimals]);

return (
<span
className={'font-number'}
suppressHydrationWarning
ref={nodeRef}
/>
);
}
36 changes: 33 additions & 3 deletions apps/common/components/Input.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import type {ReactElement} from 'react';
import {cl} from '@builtbymom/web3/utils';

import type {ReactElement, ReactNode} from 'react';

type TAmountInputProps = {
value: string;
onChange?: (value: string) => void;
label?: string;
placeholder?: string;
legend?: string;
legend?: string | ReactNode;
error?: string;
isDisabled?: boolean;
isLoading?: boolean;
Expand Down Expand Up @@ -33,11 +35,39 @@ export function Input(props: TAmountInputProps): ReactElement {
</div>
{(error ?? legend) && (
<legend
className={`mt-1 pl-2 text-xs md:mr-0 ${error ? 'text-[#EA5204]' : 'text-neutral-600'}`}
className={`mt-1 pl-0.5 text-xs opacity-70 md:mr-0 ${error ? 'text-[#EA5204]' : 'text-neutral-600'}`}
suppressHydrationWarning>
{error ?? legend}
</legend>
)}
</div>
);
}

export function FakeInput(
props: Omit<TAmountInputProps, 'value' | 'placeholder' | 'onChange' | 'isDisabled' | 'error'> & {value: ReactNode}
): ReactElement {
const {value, label, legend, className = ''} = props;
return (
<div className={`w-full ${className}`}>
{label && <p className={'mb-1 w-full truncate text-base text-neutral-600'}>{label}</p>}
<div className={'relative flex w-full items-center justify-center'}>
<div
className={cl(
`h-10 w-full border-0 border-none rounded-lg bg-neutral-300 p-2 font-mono text-base font-normal outline-none`,
value === undefined ? 'text-neutral-600/60' : ''
)}
aria-label={label}>
{value || '0.00'}
</div>
</div>
{legend && (
<legend
className={`mt-1 pl-0.5 text-xs text-neutral-600 opacity-70 md:mr-0`}
suppressHydrationWarning>
{legend}
</legend>
)}
</div>
);
}
54 changes: 54 additions & 0 deletions apps/common/components/RenderCounterAmount.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import {type ReactElement, useMemo} from 'react';
import {cl, formatTAmount, isZero} from '@builtbymom/web3/utils';

import type {TAmount} from '@builtbymom/web3/utils';

export function RenderAmount(props: TAmount & {shouldHideTooltip?: boolean}): ReactElement {
const normalizedRawValue = useMemo((): string => {
return formatTAmount({
...props,
options: {
...props.options,
minimumFractionDigits: 2,
maximumFractionDigits: props?.options?.maximumFractionDigits || Math.max(2, Number(props.decimals)),
shouldDisplaySymbol: true,
shouldCompactValue: props.options?.shouldCompactValue || false
}
});
}, [props]);

if (props.shouldHideTooltip) {
return <span className={'font-number'}>{formatTAmount(props)}</span>;
}

const shouldShowTooltip =
props.value &&
!isZero(props.value) &&
((props.value < 0.001 && props.symbol !== 'percent') || (props.value < 0.0001 && props.symbol === 'percent'));

return (
<span
suppressHydrationWarning
className={cl(
shouldShowTooltip
? 'tooltip underline decoration-neutral-600/30 decoration-dotted underline-offset-4 transition-opacity hover:decoration-neutral-600 font-number'
: 'font-number'
)}>
{shouldShowTooltip ? (
<span
suppressHydrationWarning
className={'tooltipLight bottom-full mb-1'}>
<div
className={
'font-number w-fit border border-neutral-300 bg-neutral-100 p-1 px-2 text-center text-xxs text-neutral-900'
}>
{normalizedRawValue}
</div>
</span>
) : (
<span />
)}
{formatTAmount(props)}
</span>
);
}
20 changes: 20 additions & 0 deletions apps/common/contexts/useYearn.helper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,26 @@ export function useYearnTokens({
return tokens;
}, [isReady, availableTokens, migratableTokens, retiredTokens, availableTokenListTokens]);

/**************************************************************************************************
** The following function can be used to clone the tokens list for the forknet. This is useful
** for debuging purpose and should not be used in production.
*************************************************************************************************/
// eslint-disable-next-line @typescript-eslint/no-unused-vars, unused-imports/no-unused-vars
function cloneForForknet(tokens: TUseBalancesTokens[]): TUseBalancesTokens[] {
const clonedTokens: TUseBalancesTokens[] = [];
tokens.forEach((token): void => {
clonedTokens.push({...token});
if (token.chainID === 1) {
clonedTokens.push({...token, chainID: 1337});
}
});
return clonedTokens;
}
const shouldEnableForknet = false;
if (shouldEnableForknet) {
return cloneForForknet(allTokens);
}

return allTokens;
}

Expand Down
28 changes: 18 additions & 10 deletions apps/common/contexts/useYearn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import {deserialize, serialize} from 'wagmi';
import {useWeb3} from '@builtbymom/web3/contexts/useWeb3';
import {isZeroAddress, toAddress, toNormalizedBN, zeroNormalizedBN} from '@builtbymom/web3/utils';
import {useLocalStorageValue} from '@react-hookz/web';
import {Solver, type TSolver} from '@vaults/types/solvers';
import {useFetchYearnEarnedForUser} from '@yearn-finance/web-lib/hooks/useFetchYearnEarnedForUser';
import {useFetchYearnPrices} from '@yearn-finance/web-lib/hooks/useFetchYearnPrices';
import {useFetchYearnVaults} from '@yearn-finance/web-lib/hooks/useFetchYearnVaults';
import {Solver} from '@yearn-finance/web-lib/utils/schemas/yDaemonTokenListBalances';

import {useYearnBalances} from './useYearn.helper';

Expand All @@ -15,7 +15,6 @@ import type {KeyedMutator} from 'swr';
import type {TYChainTokens, TYToken} from '@yearn-finance/web-lib/types';
import type {TYDaemonEarned} from '@yearn-finance/web-lib/utils/schemas/yDaemonEarnedSchema';
import type {TYDaemonPricesChain} from '@yearn-finance/web-lib/utils/schemas/yDaemonPricesSchema';
import type {TSolver} from '@yearn-finance/web-lib/utils/schemas/yDaemonTokenListBalances';
import type {TYDaemonVault, TYDaemonVaults} from '@yearn-finance/web-lib/utils/schemas/yDaemonVaultsSchemas';
import type {TUseBalancesTokens} from '@builtbymom/web3/hooks/useBalances.multichains';
import type {TAddress, TDict, TNormalizedBN} from '@builtbymom/web3/types';
Expand All @@ -35,12 +34,12 @@ export type TYearnContext = {
zapSlippage: number;
maxLoss: bigint;
zapProvider: TSolver;
isStakingOpBoostedVaults: boolean;
isAutoStakingEnabled: boolean;
mutateVaultList: KeyedMutator<TYDaemonVaults>;
set_maxLoss: (value: bigint) => void;
set_zapSlippage: (value: number) => void;
set_zapProvider: (value: TSolver) => void;
set_isStakingOpBoostedVaults: (value: boolean) => void;
set_isAutoStakingEnabled: (value: boolean) => void;
//
//Yearn wallet context
getToken: ({address, chainID}: TTokenAndChain) => TYToken;
Expand Down Expand Up @@ -80,13 +79,13 @@ const YearnContext = createContext<TYearnContext>({
maxLoss: DEFAULT_MAX_LOSS,
zapSlippage: 0.1,
zapProvider: Solver.enum.Cowswap,
isStakingOpBoostedVaults: true,
isAutoStakingEnabled: true,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
mutateVaultList: (): any => undefined,
set_maxLoss: (): void => undefined,
set_zapSlippage: (): void => undefined,
set_zapProvider: (): void => undefined,
set_isStakingOpBoostedVaults: (): void => undefined,
set_isAutoStakingEnabled: (): void => undefined,
//
//Yearn wallet context
getToken: (): TYToken => defaultToken,
Expand All @@ -112,7 +111,7 @@ export const YearnContextApp = memo(function YearnContextApp({children}: {childr
const {value: zapProvider, set: set_zapProvider} = useLocalStorageValue<TSolver>('yearn.fi/zap-provider', {
defaultValue: Solver.enum.Cowswap
});
const {value: isStakingOpBoostedVaults, set: set_isStakingOpBoostedVaults} = useLocalStorageValue<boolean>(
const {value: isAutoStakingEnabled, set: set_isAutoStakingEnabled} = useLocalStorageValue<boolean>(
'yearn.fi/staking-op-boosted-vaults',
{
defaultValue: true
Expand All @@ -121,7 +120,16 @@ export const YearnContextApp = memo(function YearnContextApp({children}: {childr

const prices = useFetchYearnPrices();
const earned = useFetchYearnEarnedForUser();
const {vaults, vaultsMigrations, vaultsRetired, isLoading, mutate} = useFetchYearnVaults();
const {vaults: rawVaults, vaultsMigrations, vaultsRetired, isLoading, mutate} = useFetchYearnVaults();

const vaults = useMemo(() => {
const vaults: TDict<TYDaemonVault> = {};
for (const vault of Object.values(rawVaults)) {
vaults[toAddress(vault.address)] = {...vault};
}
return vaults;
}, [rawVaults]);

const {balances, isLoadingBalances, onRefresh} = useYearnBalances({
vaults,
vaultsMigrations,
Expand Down Expand Up @@ -200,11 +208,11 @@ export const YearnContextApp = memo(function YearnContextApp({children}: {childr
zapSlippage: zapSlippage ?? DEFAULT_SLIPPAGE,
maxLoss: maxLoss ?? DEFAULT_MAX_LOSS,
zapProvider: zapProvider ?? Solver.enum.Cowswap,
isStakingOpBoostedVaults: isStakingOpBoostedVaults ?? true,
isAutoStakingEnabled: isAutoStakingEnabled ?? true,
set_zapSlippage,
set_maxLoss,
set_zapProvider,
set_isStakingOpBoostedVaults,
set_isAutoStakingEnabled,
vaults,
vaultsMigrations,
vaultsRetired,
Expand Down
3 changes: 3 additions & 0 deletions apps/common/utils/constants.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
import {toAddress} from '@builtbymom/web3/utils';

export const DEFAULT_SLIPPAGE = 0.5;
export const DEFAULT_MAX_LOSS = 1n;
export const YGAUGES_ZAP_ADDRESS = toAddress('0x1104215963474A0FA0Ac09f4E212EF7282F2A0bC'); //Address of the zap to deposit & stake in the veYFI gauge
17 changes: 11 additions & 6 deletions apps/vaults-v3/components/SettingsPopover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@ import {Fragment, useMemo} from 'react';
import {cl} from '@builtbymom/web3/utils';
import {Popover, Transition} from '@headlessui/react';
import {isSolverDisabled} from '@vaults/contexts/useSolver';
import {Solver} from '@vaults/types/solvers';
import {Renderable} from '@yearn-finance/web-lib/components/Renderable';
import {IconSettings} from '@yearn-finance/web-lib/icons/IconSettings';
import {Solver} from '@yearn-finance/web-lib/utils/schemas/yDaemonTokenListBalances';
import {Switch} from '@common/components/Switch';
import {useYearn} from '@common/contexts/useYearn';

import type {ReactElement} from 'react';
import type {TSolver} from '@yearn-finance/web-lib/utils/schemas/yDaemonTokenListBalances';
import type {TYDaemonVault} from '@yearn-finance/web-lib/utils/schemas/yDaemonVaultsSchemas';
import type {TSolver} from '@vaults/types/solvers';

type TSettingPopover = {
vault: TYDaemonVault;
Expand Down Expand Up @@ -197,7 +197,7 @@ function ZapSection({chainID}: {chainID: number}): ReactElement {
}

function StakingSection({currentVault}: {currentVault: TYDaemonVault}): ReactElement | null {
const {isStakingOpBoostedVaults, set_isStakingOpBoostedVaults} = useYearn();
const {isAutoStakingEnabled, set_isAutoStakingEnabled} = useYearn();

if (!currentVault.staking.available) {
return null;
Expand All @@ -207,13 +207,18 @@ function StakingSection({currentVault}: {currentVault: TYDaemonVault}): ReactEle
<>
<div className={'my-6 h-px w-full bg-neutral-900/20'} />
<div className={'mt-6'}>
<Label>{'OP Boosted Vaults'}</Label>
<Label>{'Staking Vaults'}</Label>
<legend className={'pb-2 text-xs text-neutral-500'}>
{
'Some Vaults offer boosted yields or token rewards via staking. Enable automatic staking to (you guessed it) automatically stake for these boosts.'
}
</legend>
<div className={'mt-1 flex flex-row space-x-2'}>
<div className={'flex grow items-center justify-between'}>
<p className={'mr-2 text-sm'}>{'Stake automatically'}</p>
<Switch
isEnabled={isStakingOpBoostedVaults}
onSwitch={(): void => set_isStakingOpBoostedVaults(!isStakingOpBoostedVaults)}
isEnabled={isAutoStakingEnabled}
onSwitch={(): void => set_isAutoStakingEnabled(!isAutoStakingEnabled)}
/>
</div>
</div>
Expand Down
Loading
Loading