diff --git a/CHANGELOG.md b/CHANGELOG.md index 5590c4413..ee90437e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +# 0.1.25 (28/08/2023) +- Gauge voting for veYFI under the `gauge-voting` feature flag + # 0.1.24 (24/08/2023) - Add `useFeatureFlag` hook to handle feature flags diff --git a/apps/common/components/Dropdown.tsx b/apps/common/components/Dropdown.tsx new file mode 100644 index 000000000..56074c384 --- /dev/null +++ b/apps/common/components/Dropdown.tsx @@ -0,0 +1,207 @@ +import React, {Fragment, useState} from 'react'; +import {Combobox, Transition} from '@headlessui/react'; +import {useThrottledState} from '@react-hookz/web'; +import {cl} from '@yearn-finance/web-lib/utils/cl'; +import performBatchedUpdates from '@yearn-finance/web-lib/utils/performBatchedUpdates'; +import IconChevron from '@common/icons/IconChevron'; + +import {ImageWithFallback} from './ImageWithFallback'; + +import type {ReactElement} from 'react'; + +const DropdownOption = (option: TDropdownOption): ReactElement => { + const {label, description, icon} = option; + return ( + + {({active}): ReactElement => ( +
+ {icon && ( +
+ +
+ )} +
+

+ {label} +

+ {description && ( +

+ {description} +

+ )} +
+
+ )} +
+ ); +}; + +const DropdownEmpty = ({isSearching}: {isSearching: boolean}): ReactElement => { + if(isSearching) { + return ( +
+
+

{'Nothing found.'}

+
+
+ ); + } + + return ( +
+
+ +
+
+ ); +}; + +export type TDropdownOption = { + id: string; + label: string; + description?: string; + icon?: string; +} + +export type TDropdownProps = { + selected?: TDropdownOption; + options: TDropdownOption[]; + onChange?: (selected: TDropdownOption) => void; + label?: string; + legend?: string; + isDisabled?: boolean; + className?: string; +} + +const Dropdown = ({selected, options, onChange, label, legend, isDisabled, className}: TDropdownProps): ReactElement => { + const [isOpen, set_isOpen] = useThrottledState(false, 400); + const [search, set_search] = useState(''); + + const isSearching = search !== ''; + const filteredOptions = isSearching + ? options.filter(({label}): boolean => label.toLowerCase().includes(search.toLowerCase())) + : options; + + return ( +
+
+ {label && } +
+ {isOpen ? ( +
{ + e.stopPropagation(); + e.preventDefault(); + set_isOpen(false); + }} /> + ) : null} + { + performBatchedUpdates((): void => { + if(onChange) { + onChange(option); + } + set_isOpen(false); + }); + }} + disabled={isDisabled} + > + <> + set_isOpen((state: boolean): boolean => !state)} + className={cl( + 'flex h-10 w-full items-center justify-between p-2 text-base md:px-3', + isDisabled ? 'bg-neutral-300 text-neutral-600' : 'bg-neutral-0 text-neutral-900' + )}> +
+ {selected?.icon && ( +
+ +
+ )} +

+ option?.label ?? '-' } + spellCheck={false} + onChange={(event): void => { + performBatchedUpdates((): void => { + set_isOpen(true); + set_search(event.target.value); + }); + }} + /> +

+
+
+ +
+
+ { + performBatchedUpdates((): void => { + set_isOpen(false); + set_search(''); + }); + }}> + + {filteredOptions.length === 0 ? ( + + ) : ( + filteredOptions.map(({id, label, description, icon}): ReactElement => ( + + )) + )} + + + +
+
+ {legend && ( +

+ {legend} +

+ )} +
+
+ ); +}; + +export {Dropdown}; diff --git a/apps/common/components/Pagination.tsx b/apps/common/components/Pagination.tsx new file mode 100644 index 000000000..164d68d02 --- /dev/null +++ b/apps/common/components/Pagination.tsx @@ -0,0 +1,63 @@ +import ReactPaginate from 'react-paginate'; +import IconPaginationArrow from '@common/icons/IconPaginationArrow'; + +import type {ReactElement} from 'react'; + +type TProps = { + range: [from: number, to: number]; + pageCount: number; + numberOfItems: number; + onPageChange: (selectedItem: { + selected: number; + }) => void; +} + +export function Pagination(props: TProps): ReactElement { + const {range: [from, to], pageCount, numberOfItems, onPageChange} = props; + + return ( + <> +
+ + {'Previous'} + + + {'Next'} + +
+ + + ); +} diff --git a/apps/common/components/Table.tsx b/apps/common/components/Table.tsx new file mode 100644 index 000000000..2fdad9d3b --- /dev/null +++ b/apps/common/components/Table.tsx @@ -0,0 +1,162 @@ +/* eslint-disable tailwindcss/classnames-order */ +import {useCallback, useMemo, useState} from 'react'; +import {sort} from '@veYFI/utils'; +import {cl} from '@yearn-finance/web-lib/utils/cl'; +import {isZero} from '@yearn-finance/web-lib/utils/isZero'; +import {Pagination} from '@common/components/Pagination'; +import {usePagination} from '@common/hooks/usePagination'; +import IconChevronPlain from '@common/icons/IconChevronPlain'; + +import type {ReactElement} from 'react'; + +type TSortOrder = 'asc' | 'desc'; + +type TState = { + sortedBy: Extract | undefined, + order: TSortOrder +} + +const switchOrder = (order: TSortOrder): TSortOrder => order === 'desc' ? 'asc' : 'desc'; + +type TMetadata = { + key: Extract; + label: string; + className?: string; + sortable?: boolean; + fullWidth?: boolean; + columnSpan?: number; + format?: (item: T) => string | number; + transform?: (item: T) => ReactElement; +} + +type TTableProps = { + metadata: TMetadata[]; + data: T[]; + columns?: number; + initialSortBy?: Extract; + onRowClick?: (item: T) => void; + itemsPerPage?: number; +} + +function Table({metadata, data, columns, initialSortBy, onRowClick, itemsPerPage}: TTableProps): ReactElement { + const [{sortedBy, order}, set_state] = useState>({sortedBy: initialSortBy, order: 'desc'}); + + const sortedData = useMemo((): T[] => { + return sortedBy && order ? sort(data, sortedBy, order) : data; + }, [data, order, sortedBy]); + + const {currentItems, paginationProps} = usePagination({data: sortedData, itemsPerPage: itemsPerPage || sortedData.length}); + + const handleSort = useCallback((key: Extract): void => { + const willChangeSortKey = sortedBy !== key; + const newOrder = switchOrder(willChangeSortKey ? 'asc' : order); + set_state({sortedBy: newOrder ? key : undefined, order: newOrder}); + }, [order, sortedBy]); + + const gridColsVariants = { + 1: 'md:grid-cols-1', + 2: 'md:grid-cols-2', + 3: 'md:grid-cols-3', + 4: 'md:grid-cols-4', + 5: 'md:grid-cols-5', + 6: 'md:grid-cols-6', + 7: 'md:grid-cols-7', + 8: 'md:grid-cols-8', + 9: 'md:grid-cols-9', + 10: 'md:grid-cols-10', + 11: 'md:grid-cols-11', + 12: 'md:grid-cols-12' + }; + + const numberOfColumns = Math.min(columns ?? (metadata.length), 12) as keyof typeof gridColsVariants; + + const colSpanVariants = { + 1: 'md:col-span-1', + 2: 'md:col-span-2', + 3: 'md:col-span-3', + 4: 'md:col-span-4', + 5: 'md:col-span-5', + 6: 'md:col-span-6', + 7: 'md:col-span-7', + 8: 'md:col-span-8', + 9: 'md:col-span-9', + 10: 'md:col-span-10', + 11: 'md:col-span-11', + 12: 'md:col-span-12' + }; + + return ( +
+ + + {currentItems.map((item, rowIndex): ReactElement => ( +
onRowClick?.(item)} + > + {metadata.map(({key, label, className, fullWidth, columnSpan, format, transform}): ReactElement => { + const isNumber = !isNaN(item[key] as number); + + return ( +
+ {!fullWidth && } +
+ {transform?.(item) ?? format?.(item).toString() ?? String(item[key])} +
+
+ ); + })} +
+ ))} + {itemsPerPage && ( +
+
+ +
+
+ )} +
+ ); +} + +export {Table}; diff --git a/apps/common/hooks/usePagination.ts b/apps/common/hooks/usePagination.ts new file mode 100644 index 000000000..33ab2708b --- /dev/null +++ b/apps/common/hooks/usePagination.ts @@ -0,0 +1,40 @@ +import {useState} from 'react'; + +type TProps = { + data: T[]; + itemsPerPage: number; +} + +type TUsePaginationReturn = { + currentItems: T[]; + paginationProps: { + range: [from: number, to: number]; + pageCount: number; + numberOfItems: number; + onPageChange: (selectedItem: { + selected: number; + }) => void; + } +} + +export function usePagination({data, itemsPerPage}: TProps): TUsePaginationReturn { + const [itemOffset, set_itemOffset] = useState(0); + + const endOffset = itemOffset + itemsPerPage; + + const currentItems = data.slice(itemOffset, endOffset); + + const handlePageChange = ({selected}: {selected: number}): void => { + set_itemOffset((selected * itemsPerPage) % data.length); + }; + + return { + currentItems, + paginationProps: { + range: [endOffset - (itemsPerPage - 1), Math.min(endOffset, data.length)], + pageCount: Math.ceil(data.length / itemsPerPage), + numberOfItems: data.length, + onPageChange: handlePageChange + } + }; +} diff --git a/apps/common/icons/IconPaginationArrow.tsx b/apps/common/icons/IconPaginationArrow.tsx new file mode 100755 index 000000000..befd5a476 --- /dev/null +++ b/apps/common/icons/IconPaginationArrow.tsx @@ -0,0 +1,27 @@ +import React from 'react'; + +import type {ReactElement} from 'react'; + +function IconPaginationArrow(props: React.SVGProps): ReactElement { + return ( + + + + + + + ); +} + +export default IconPaginationArrow; diff --git a/apps/common/types/types.ts b/apps/common/types/types.ts index 8e66ff109..8c4c86dbf 100755 --- a/apps/common/types/types.ts +++ b/apps/common/types/types.ts @@ -2,6 +2,7 @@ import type {ReactElement} from 'react'; import type {TAddress, TDict} from '@yearn-finance/web-lib/types'; import type {TBalanceData} from '@yearn-finance/web-lib/types/hooks'; import type {TSolver} from '@common/schemas/yDaemonTokenListBalances'; +import type {multicall} from '@wagmi/core'; export type TClaimable = { raw: bigint, @@ -88,3 +89,5 @@ export type TMessariGraphData = { } export type TSortDirection = 'asc' | 'desc' | ''; + +export type TMulticallContract = Parameters[0]['contracts'][0]; diff --git a/apps/common/utils/wagmiUtils.ts b/apps/common/utils/wagmiUtils.ts new file mode 100644 index 000000000..41349b76e --- /dev/null +++ b/apps/common/utils/wagmiUtils.ts @@ -0,0 +1,115 @@ +import {BaseError} from 'viem'; +import {captureException} from '@sentry/nextjs'; +import {prepareWriteContract, waitForTransaction, writeContract} from '@wagmi/core'; +import {toast} from '@yearn-finance/web-lib/components/yToast'; +import {toAddress} from '@yearn-finance/web-lib/utils/address'; +import {ZERO_ADDRESS} from '@yearn-finance/web-lib/utils/constants'; +import {toBigInt} from '@yearn-finance/web-lib/utils/format.bigNumber'; +import {isEth} from '@yearn-finance/web-lib/utils/isEth'; +import {isTAddress} from '@yearn-finance/web-lib/utils/isTAddress'; +import {defaultTxStatus} from '@yearn-finance/web-lib/utils/web3/transaction'; +import {assert} from '@common/utils/assert'; + +import type {Abi, SimulateContractParameters} from 'viem'; +import type {Connector} from 'wagmi'; +import type {TAddress} from '@yearn-finance/web-lib/types'; +import type {TTxResponse} from '@yearn-finance/web-lib/utils/web3/transaction'; +import type {GetWalletClientResult, WalletClient} from '@wagmi/core'; + +export type TWagmiProviderContract = { + walletClient: GetWalletClientResult, + chainId: number, + address: TAddress, +} +export async function toWagmiProvider(connector: Connector | undefined): Promise { + assert(connector, 'Connector is not set'); + + const signer = await connector.getWalletClient(); + const chainId = await connector.getChainId(); + const {address} = signer.account; + return ({ + walletClient: signer, + chainId, + address + }); +} + +export type TWriteTransaction = { + connector: Connector | undefined; + contractAddress: TAddress | undefined; + statusHandler?: (status: typeof defaultTxStatus) => void; + onTrySomethingElse?: () => Promise; //When the abi is incorrect, ex: usdt, we may need to bypass the error and try something else +} + +export function assertAddress(addr: string | TAddress | undefined, name?: string): asserts addr is TAddress { + assert(addr, `${name || 'Address'} is not set`); + assert(isTAddress(addr), `${name || 'Address'} provided is invalid`); + assert(toAddress(addr) !== ZERO_ADDRESS, `${name || 'Address'} is 0x0`); + assert(!isEth(addr), `${name || 'Address'} is 0xE`); +} + +export function assertAddresses(addresses: (string | TAddress | undefined)[], name?: string): asserts addresses is TAddress[] { + addresses.forEach((addr): void => assertAddress(addr, name)); +} + +type TPrepareWriteContractConfig< + TAbi extends Abi | readonly unknown[] = Abi, + TFunctionName extends string = string +> = Omit, 'chain' | 'address'> & { + chainId?: number + walletClient?: WalletClient + address: TAddress | undefined +} +export async function handleTx< + TAbi extends Abi | readonly unknown[], + TFunctionName extends string +>( + args: TWriteTransaction, + props: TPrepareWriteContractConfig +): Promise { + args.statusHandler?.({...defaultTxStatus, pending: true}); + const wagmiProvider = await toWagmiProvider(args.connector); + + assertAddress(props.address, 'contractAddress'); + assertAddress(wagmiProvider.address, 'userAddress'); + try { + const config = await prepareWriteContract({ + ...wagmiProvider, + ...props as TPrepareWriteContractConfig, + address: props.address, + value: toBigInt(props.value) + }); + const {hash} = await writeContract(config.request); + const receipt = await waitForTransaction({chainId: wagmiProvider.chainId, hash}); + if (receipt.status === 'success') { + args.statusHandler?.({...defaultTxStatus, success: true}); + } else if (receipt.status === 'reverted') { + args.statusHandler?.({...defaultTxStatus, error: true}); + } + toast({type: 'success', content: 'Transaction successful!'}); + return ({isSuccessful: receipt.status === 'success', receipt}); + } catch (error) { + if (process.env.NODE_ENV === 'production') { + captureException(error); + } + + if (!(error instanceof BaseError)) { + return ({isSuccessful: false, error}); + } + + if (args.onTrySomethingElse) { + if (error.name === 'ContractFunctionExecutionError') { + return await args.onTrySomethingElse(); + } + } + + toast({type: 'error', content: error.shortMessage}); + args.statusHandler?.({...defaultTxStatus, error: true}); + console.error(error); + return ({isSuccessful: false, error}); + } finally { + setTimeout((): void => { + args.statusHandler?.({...defaultTxStatus}); + }, 3000); + } +} diff --git a/apps/veyfi/Wrapper.tsx b/apps/veyfi/Wrapper.tsx index 64dcd7512..3b866b2bc 100644 --- a/apps/veyfi/Wrapper.tsx +++ b/apps/veyfi/Wrapper.tsx @@ -4,6 +4,9 @@ import Meta from '@common/components/Meta'; import {useCurrentApp} from '@common/hooks/useCurrentApp'; import {variants} from '@common/utils/animations'; +import {GaugeContextApp} from './contexts/useGauge'; +import {OptionContextApp} from './contexts/useOption'; + import type {NextRouter} from 'next/router'; import type {ReactElement} from 'react'; @@ -14,17 +17,21 @@ export default function Wrapper({children, router}: {children: ReactElement, rou <> - - - {children} - - + + + + + {children} + + + + ); diff --git a/apps/veyfi/components/GaugesTab.tsx b/apps/veyfi/components/GaugesTab.tsx new file mode 100644 index 000000000..ff02c2333 --- /dev/null +++ b/apps/veyfi/components/GaugesTab.tsx @@ -0,0 +1,228 @@ +import {useCallback, useState} from 'react'; +import {useGauge} from '@veYFI/contexts/useGauge'; +import * as GaugeActions from '@veYFI/utils/actions/gauge'; +import {validateNetwork} from '@veYFI/utils/validations'; +import {Button} from '@yearn-finance/web-lib/components/Button'; +import {useWeb3} from '@yearn-finance/web-lib/contexts/useWeb3'; +import {allowanceKey, toAddress} from '@yearn-finance/web-lib/utils/address'; +import {formatBigNumberAsAmount, toBigInt, toNormalizedValue} from '@yearn-finance/web-lib/utils/format.bigNumber'; +import {formatAmount, formatPercent} from '@yearn-finance/web-lib/utils/format.number'; +import {isZero} from '@yearn-finance/web-lib/utils/isZero'; +import {defaultTxStatus} from '@yearn-finance/web-lib/utils/web3/transaction'; +import {ImageWithFallback} from '@common/components/ImageWithFallback'; +import {Table} from '@common/components/Table'; +import {useWallet} from '@common/contexts/useWallet'; +import {useYearn} from '@common/contexts/useYearn'; + +import type {ReactElement} from 'react'; +import type {TAddress} from '@yearn-finance/web-lib/types'; + +type TGaugeData = { + gaugeAddress: TAddress, + vaultAddress: TAddress, + decimals: number, + vaultIcon: string, + vaultName: string, + vaultApy: number, + vaultDeposited: bigint, + gaugeApy: number, + gaugeBoost: number, + gaugeStaked: bigint, + allowance: bigint, + isApproved: boolean, + actions: undefined +} + +function GaugesTab(): ReactElement { + const [selectedGauge, set_selectedGauge] = useState(''); + const [selectedAction, set_selectedAction] = useState<'stake' | 'unstake' | undefined>(); + const {provider, address, isActive, chainID} = useWeb3(); + const {gaugeAddresses, gaugesMap, positionsMap, allowancesMap, refresh: refreshGauges, isLoading: isLoadingGauges} = useGauge(); + const {vaults} = useYearn(); + const {balances, refresh: refreshBalances} = useWallet(); + const refreshData = (): unknown => Promise.all([refreshGauges(), refreshBalances()]); + const [approveAndStakeStatus, set_approveAndStakeStatus] = useState(defaultTxStatus); + const [stakeStatus, set_stakeStatus] = useState(defaultTxStatus); + const [unstakeStatus, set_unstakeStatus] = useState(defaultTxStatus); + + const userAddress = address as TAddress; + + const gaugesData = gaugeAddresses.map((address): TGaugeData => { + const gauge = gaugesMap[address]; + const vaultAddress = toAddress(gauge?.vaultAddress); + const vault = vaults[vaultAddress]; + + return { + gaugeAddress: address, + vaultAddress, + decimals: gauge?.decimals ?? 18, + vaultIcon: `${process.env.BASE_YEARN_ASSETS_URI}/1/${vaultAddress}/logo-128.png`, + vaultName: vault?.display_name ?? '', + vaultApy: vault?.apy.net_apy ?? 0, + vaultDeposited: toBigInt(formatBigNumberAsAmount(balances[vaultAddress]?.raw)), + gaugeApy: 0, // TODO: gauge apy calcs + gaugeBoost: positionsMap[address]?.boost ?? 1, + gaugeStaked: toBigInt(formatBigNumberAsAmount(positionsMap[address]?.deposit.balance)), + allowance: toBigInt(formatBigNumberAsAmount(allowancesMap[allowanceKey(1, vaultAddress, address, userAddress)])), + isApproved: toBigInt(formatBigNumberAsAmount(allowancesMap[allowanceKey(1, vaultAddress, address, userAddress)])) >= toBigInt(formatBigNumberAsAmount(balances[vaultAddress]?.raw)), + actions: undefined + }; + }); + + const {isValid: isValidNetwork} = validateNetwork({supportedNetwork: 1, walletNetwork: chainID}); + + const onApproveAndStake = useCallback(async (vaultAddress: TAddress, gaugeAddress: TAddress, amount: bigint, allowance: bigint): Promise => { + set_selectedGauge(gaugeAddress); + set_selectedAction('stake'); + const response = await GaugeActions.approveAndStake({ + connector: provider, + contractAddress: gaugeAddress, + vaultAddress, + amount, + allowance, + statusHandler: set_approveAndStakeStatus + }); + + if (response.isSuccessful) { + await refreshData(); + } + }, [provider, refreshData]); + + const onStake = useCallback(async (gaugeAddress: TAddress, amount: bigint): Promise => { + set_selectedGauge(gaugeAddress); + set_selectedAction('stake'); + + const response = await GaugeActions.stake({ + connector: provider, + contractAddress: gaugeAddress, + amount, + statusHandler: set_stakeStatus + }); + + if (response.isSuccessful) { + await refreshData(); + } + }, [provider, refreshData]); + + const onUnstake = useCallback(async (gaugeAddress: TAddress, amount: bigint): Promise => { + set_selectedGauge(gaugeAddress); + set_selectedAction('unstake'); + + const response = await GaugeActions.unstake({ + connector: provider, + contractAddress: gaugeAddress, + accountAddress: userAddress, + amount, + statusHandler: set_unstakeStatus + }); + + if (response.isSuccessful) { + await refreshData(); + } + }, [provider, refreshData, userAddress]); + + return ( +
+ ( +
+
+ +
+

{vaultName}

+
+ ) + }, + { + key: 'vaultApy', + label: 'Vault APY', + sortable: true, + format: ({vaultApy}): string => formatPercent((vaultApy) * 100, 2, 2, 500) + }, + { + key: 'vaultDeposited', + label: 'Deposited in Vault', + sortable: true, + format: ({vaultDeposited, decimals}): string => formatAmount(toNormalizedValue(vaultDeposited, decimals)) + }, + { + key: 'gaugeApy', + label: 'Gauge APY', + sortable: true + }, + { + key: 'gaugeBoost', + label: 'Boost', + sortable: true, + format: ({gaugeBoost}): string => `${gaugeBoost.toFixed(2)}x` + }, + { + key: 'gaugeStaked', + label: 'Staked in Gauge', + sortable: true, + format: ({gaugeStaked, decimals}): string => formatAmount(toNormalizedValue(gaugeStaked, decimals)) + }, + { + key: 'actions', + label: '', + columnSpan: 2, + fullWidth: true, + className: 'my-4 md:my-0', + transform: ({isApproved, vaultAddress, gaugeAddress, vaultDeposited, gaugeStaked, allowance}): ReactElement => ( +
+ + {!isApproved && ( + + )} + {isApproved && ( + + )} +
+ ) + } + ]} + data={gaugesData} + columns={9} + initialSortBy={'gaugeApy'} + /> + + ); +} + +export {GaugesTab}; diff --git a/apps/veyfi/components/RedeemTab.tsx b/apps/veyfi/components/RedeemTab.tsx new file mode 100644 index 000000000..fdacd990e --- /dev/null +++ b/apps/veyfi/components/RedeemTab.tsx @@ -0,0 +1,142 @@ +import {useCallback, useState} from 'react'; +import {useAsync} from '@react-hookz/web'; +import {useOption} from '@veYFI/contexts/useOption'; +import {redeem} from '@veYFI/utils/actions/option'; +import {VEYFI_OPTIONS_ADDRESS, VEYFI_OYFI_ADDRESS} from '@veYFI/utils/constants'; +import {validateAllowance, validateAmount, validateNetwork} from '@veYFI/utils/validations'; +import {Button} from '@yearn-finance/web-lib/components/Button'; +import {useWeb3} from '@yearn-finance/web-lib/contexts/useWeb3'; +import {useChainID} from '@yearn-finance/web-lib/hooks/useChainID'; +import {toAddress} from '@yearn-finance/web-lib/utils/address'; +import {BIG_ZERO, ETH_TOKEN_ADDRESS, YFI_ADDRESS} from '@yearn-finance/web-lib/utils/constants'; +import {formatBigNumberAsAmount, toNormalizedBN, toNormalizedValue} from '@yearn-finance/web-lib/utils/format.bigNumber'; +import {formatCounterValue} from '@yearn-finance/web-lib/utils/format.value'; +import {handleInputChangeEventValue} from '@yearn-finance/web-lib/utils/handlers/handleInputChangeEventValue'; +import {defaultTxStatus} from '@yearn-finance/web-lib/utils/web3/transaction'; +import {AmountInput} from '@common/components/AmountInput'; +import {useWallet} from '@common/contexts/useWallet'; +import {useBalance} from '@common/hooks/useBalance'; +import {useTokenPrice} from '@common/hooks/useTokenPrice'; +import {approveERC20} from '@common/utils/actions'; + +import type {ReactElement} from 'react'; +import type {TAddress} from '@yearn-finance/web-lib/types'; + +function RedeemTab(): ReactElement { + const [redeemAmount, set_redeemAmount] = useState(toNormalizedBN(0)); + const {provider, address, isActive} = useWeb3(); + const {safeChainID} = useChainID(); + const {refresh: refreshBalances} = useWallet(); + const {getRequiredEth, price: optionPrice, positions, allowances, isLoading: isLoadingOption, refresh} = useOption(); + const clearLockAmount = (): void => set_redeemAmount(toNormalizedBN(0)); + const refreshData = useCallback((): unknown => Promise.all([refresh(), refreshBalances()]), [refresh, refreshBalances]); + const onTxSuccess = useCallback((): unknown => Promise.all([refreshData(), clearLockAmount()]), [refreshData]); + const [{status, result}, fetchRequiredEth] = useAsync(getRequiredEth, BIG_ZERO); + const ethBalance = useBalance(ETH_TOKEN_ADDRESS); + const yfiPrice = useTokenPrice(YFI_ADDRESS); + const [approveRedeemStatus, set_approveRedeemStatus] = useState(defaultTxStatus); + const [redeemStatus, set_redeemStatus] = useState(defaultTxStatus); + + const userAddress = address as TAddress; + const oYFIBalance = toNormalizedBN(formatBigNumberAsAmount(positions?.balance), 18); + const ethRequired = toNormalizedValue(result, 18); + + const handleApproveRedeem = useCallback(async (): Promise => { + const response = await approveERC20({ + connector: provider, + contractAddress: VEYFI_OYFI_ADDRESS, + spenderAddress: VEYFI_OPTIONS_ADDRESS, + statusHandler: set_approveRedeemStatus, + amount: redeemAmount.raw + }); + + if (response.isSuccessful) { + await refreshData(); + } + }, [provider, redeemAmount.raw, refreshData]); + + const handleRedeem = useCallback(async (): Promise => { + const response = await redeem({ + connector: provider, + contractAddress: VEYFI_OPTIONS_ADDRESS, + statusHandler: set_redeemStatus, + accountAddress: toAddress(address), + amount: redeemAmount.raw, + ethRequired: result + + }); + + if (response.isSuccessful) { + await onTxSuccess(); + } + }, [address, onTxSuccess, provider, redeemAmount.raw, result]); + + + const {isValid: isApproved} = validateAllowance({ + tokenAddress: VEYFI_OYFI_ADDRESS, + spenderAddress: VEYFI_OPTIONS_ADDRESS, + allowances, + amount: redeemAmount.raw, + ownerAddress: userAddress, + chainID: 1 + }); + + const {isValid: isValidRedeemAmount, error: redeemAmountError} = validateAmount({ + amount: redeemAmount.normalized, + balance: oYFIBalance.normalized + }); + + const {isValid: isValidNetwork} = validateNetwork({supportedNetwork: 1, walletNetwork: safeChainID}); + + return ( +
+
+
+

+ {'Redeem'} +

+
+ +
+ + { + const amount = handleInputChangeEventValue(value, 18); + set_redeemAmount(amount); + fetchRequiredEth.execute(amount.raw); + }} + onLegendClick={(): void => set_redeemAmount(oYFIBalance)} + onMaxClick={(): void => set_redeemAmount(oYFIBalance)} + legend={formatCounterValue(redeemAmount.normalized, yfiPrice)} + error={redeemAmountError} + /> + + +
+
+
+ ); +} + +export {RedeemTab}; diff --git a/apps/veyfi/components/RewardsTab.tsx b/apps/veyfi/components/RewardsTab.tsx new file mode 100644 index 000000000..dd8e42022 --- /dev/null +++ b/apps/veyfi/components/RewardsTab.tsx @@ -0,0 +1,140 @@ +import {useCallback, useMemo, useState} from 'react'; +import {useGauge} from '@veYFI/contexts/useGauge'; +import {useOption} from '@veYFI/contexts/useOption'; +import * as GaugeActions from '@veYFI/utils/actions/gauge'; +import {VEYFI_CLAIM_REWARDS_ZAP_ADDRESS} from '@veYFI/utils/constants'; +import {validateNetwork} from '@veYFI/utils/validations'; +import {Button} from '@yearn-finance/web-lib/components/Button'; +import {useWeb3} from '@yearn-finance/web-lib/contexts/useWeb3'; +import {toAddress} from '@yearn-finance/web-lib/utils/address'; +import {BIG_ZERO} from '@yearn-finance/web-lib/utils/constants'; +import {formatBigNumberAsAmount, toBigInt, toNormalizedAmount} from '@yearn-finance/web-lib/utils/format.bigNumber'; +import {formatCounterValue} from '@yearn-finance/web-lib/utils/format.value'; +import {isZero} from '@yearn-finance/web-lib/utils/isZero'; +import {defaultTxStatus} from '@yearn-finance/web-lib/utils/web3/transaction'; +import {AmountInput} from '@common/components/AmountInput'; +import {Dropdown} from '@common/components/Dropdown'; +import {useYearn} from '@common/contexts/useYearn'; + +import type {ReactElement} from 'react'; +import type {TDropdownOption} from '@common/components/Dropdown'; + +function RewardsTab(): ReactElement { + const [selectedGauge, set_selectedGauge] = useState(); + const {provider, isActive, chainID} = useWeb3(); + const {gaugeAddresses, gaugesMap, positionsMap, refresh: refreshGauges} = useGauge(); + const {price: optionPrice} = useOption(); + const {vaults} = useYearn(); + const refreshData = useCallback((): unknown => Promise.all([refreshGauges()]), [refreshGauges]); + const [claimStatus, set_claimStatus] = useState(defaultTxStatus); + const [claimAllStatus, set_claimAllStatus] = useState(defaultTxStatus); + const selectedGaugeAddress = toAddress(selectedGauge?.id); + const selectedGaugeRewards = toBigInt(formatBigNumberAsAmount(positionsMap[selectedGaugeAddress]?.reward.balance)); + + const onClaim = useCallback(async (): Promise => { + const result = await GaugeActions.claimRewards({ + connector: provider, + contractAddress: selectedGaugeAddress, + statusHandler: set_claimStatus + }); + if (result.isSuccessful) { + refreshData(); + } + }, [provider, refreshData, selectedGaugeAddress]); + + const onClaimAll = useCallback(async (): Promise => { + const result = await GaugeActions.claimAllRewards({ + connector: provider, + contractAddress: VEYFI_CLAIM_REWARDS_ZAP_ADDRESS, + gaugeAddresses, + willLockRewards: false, + statusHandler: set_claimAllStatus + }); + if (result.isSuccessful) { + refreshData(); + } + }, [gaugeAddresses, provider, refreshData]); + + const gaugeOptions = gaugeAddresses.filter((address): boolean => toBigInt(positionsMap[address]?.reward.balance) > 0n ?? false) + .map((address): TDropdownOption => { + const gauge = gaugesMap[address]; + const vaultAddress = toAddress(gauge?.vaultAddress); + const vault = vaults[vaultAddress]; + + return { + id: address, + label: vault?.display_name ?? '', + icon: `${process.env.BASE_YEARN_ASSETS_URI}/1/${vaultAddress}/logo-128.png` + }; + }); + + const gaugesRewards = useMemo((): bigint => { + return gaugeAddresses.reduce((acc, address): bigint => { + return acc + toBigInt(formatBigNumberAsAmount(positionsMap[address]?.reward.balance)); + }, BIG_ZERO); + }, [gaugeAddresses, positionsMap]); + + const {isValid: isValidNetwork} = validateNetwork({supportedNetwork: 1, walletNetwork: chainID}); + + return ( +
+
+
+

+ {'Claim Rewards'} +

+
+ +
+ + +
+
+ +
+
+

+ {'Claim Separately'} +

+
+ +
+ + + +
+
+
+ ); +} + +export {RewardsTab}; diff --git a/apps/veyfi/components/VoteTab.tsx b/apps/veyfi/components/VoteTab.tsx new file mode 100644 index 000000000..900419ca1 --- /dev/null +++ b/apps/veyfi/components/VoteTab.tsx @@ -0,0 +1,87 @@ +import {useCallback, useState} from 'react'; +import Link from 'next/link'; +import {delegateVote} from '@veYFI/utils/actions/votingEscrow'; +import {SNAPSHOT_DELEGATE_REGISTRY_ADDRESS} from '@veYFI/utils/constants'; +import {validateAddress, validateNetwork} from '@veYFI/utils/validations'; +import {Button} from '@yearn-finance/web-lib/components/Button'; +import {useWeb3} from '@yearn-finance/web-lib/contexts/useWeb3'; +import {toAddress} from '@yearn-finance/web-lib/utils/address'; +import {defaultTxStatus} from '@yearn-finance/web-lib/utils/web3/transaction'; +import {Input} from '@common/components/Input'; + +import type {ReactElement} from 'react'; +import type {TAddress} from '@yearn-finance/web-lib/types'; + +function VoteTab(): ReactElement { + const [delegateAddress, set_delegateAddress] = useState(''); + const {provider, address, isActive, chainID} = useWeb3(); + + const [delegateVoteStatus, set_delegateVoteStatus] = useState(defaultTxStatus); + + const userAddress = address as TAddress; + + const {isValid: isValidNetwork} = validateNetwork({supportedNetwork: 1, walletNetwork: chainID}); + const {isValid: isValidDelegateAddress, error: delegateAddressError} = validateAddress({address: delegateAddress}); + + const handleExecuteDelegateVote = useCallback(async (): Promise => { + if (!userAddress || !isValidDelegateAddress) { + return; + } + + await delegateVote({ + connector: provider, + contractAddress: SNAPSHOT_DELEGATE_REGISTRY_ADDRESS, + statusHandler: set_delegateVoteStatus, + delegateAddress: toAddress(delegateAddress) + }); + }, [delegateAddress, isValidDelegateAddress, provider, userAddress]); + + return ( +
+
+
+

+ {'Vote for Gauge'} +

+
+

{'Vote to direct future YFI rewards to a particular gauge.'}

+
+

{'If you prefer your democracy on the representative side, you can delegate your vote to another address.'}

+
+
+ +
+ + + +
+ +
+ + +
+
+
+ ); +} + +export {VoteTab}; diff --git a/apps/veyfi/contexts/useGauge.tsx b/apps/veyfi/contexts/useGauge.tsx new file mode 100644 index 000000000..a3cc2453d --- /dev/null +++ b/apps/veyfi/contexts/useGauge.tsx @@ -0,0 +1,247 @@ +import React, {createContext, memo, useCallback, useContext, useEffect, useMemo} from 'react'; +import {FixedNumber} from 'ethers'; +import {useContractRead} from 'wagmi'; +import {useAsync} from '@react-hookz/web'; +import {keyBy} from '@veYFI/utils'; +import VEYFI_GAUGE_ABI from '@veYFI/utils/abi/veYFIGauge.abi'; +import VEYFI_REGISTRY_ABI from '@veYFI/utils/abi/veYFIRegistry.abi'; +import {VEYFI_REGISTRY_ADDRESS} from '@veYFI/utils/constants'; +import {erc20ABI, getContract, multicall} from '@wagmi/core'; +import {useWeb3} from '@yearn-finance/web-lib/contexts/useWeb3'; +import {allowanceKey} from '@yearn-finance/web-lib/utils/address'; + +import type {ReactElement} from 'react'; +import type {TAddress, TDict} from '@yearn-finance/web-lib/types'; +import type {TMulticallContract} from '@common/types/types'; + +export type TGauge = { + address: TAddress, + vaultAddress: TAddress, + name: string, + symbol: string, + decimals: number, + totalStaked: bigint, + // apy?: number; +} + +export type TPosition = { + balance: bigint, + underlyingBalance: bigint, +} + +export type TGaugePosition = { + address: TAddress, + deposit: TPosition, + reward: TPosition, + boost: number, +} + +export type TGaugeContext = { + gaugeAddresses: TAddress[], + gaugesMap: TDict, + positionsMap: TDict, + allowancesMap: TDict, + isLoading: boolean, + refresh: () => void, +} +const defaultProps: TGaugeContext = { + gaugeAddresses: [], + gaugesMap: {}, + positionsMap: {}, + allowancesMap: {}, + isLoading: true, + refresh: (): void => undefined +}; + +const GaugeContext = createContext(defaultProps); +export const GaugeContextApp = memo(function GaugeContextApp({children}: {children: ReactElement}): ReactElement { + const {address: userAddress, isActive} = useWeb3(); + const veYFIRegistryContract = useMemo((): {address: TAddress, abi: typeof VEYFI_REGISTRY_ABI} => ({ + address: VEYFI_REGISTRY_ADDRESS, + abi: VEYFI_REGISTRY_ABI + }), []); + const {data: vaultAddresses} = useContractRead({ + ...veYFIRegistryContract, + functionName: 'getVaults', + chainId: 1 + }); + + const gaugesFetcher = useCallback(async (): Promise => { + if (!isActive || !userAddress) { + return []; + } + + const gaugeAddressCalls = vaultAddresses?.map((vaultAddress): TMulticallContract => ({ + ...veYFIRegistryContract, + functionName: 'gauges', + args: [vaultAddress] + })); + + const gaugeAddressesResults = await multicall({contracts: gaugeAddressCalls ?? [], chainId: 1}); + + const gaugeAddresses = gaugeAddressesResults.map(({result}): unknown => result) as TAddress[]; + const gaugePromises = gaugeAddresses.map(async (address): Promise => { + // todo: update once abi is available + const veYFIGaugeContract = getContract({ + address, + abi: VEYFI_GAUGE_ABI, + chainId: 1 + }); + + // TODO: These should be migrated to wagmi + const calls: TMulticallContract[] = []; + ['asset', 'name', 'symbol', 'decimals', 'totalAssets'].forEach((functionName): void => { + calls.push({...veYFIGaugeContract, functionName}); + }); + + const results = await multicall({ + contracts: calls, + chainId: 1 + }); + + const [asset, name, symbol, decimals, totalAssets] = results.map(({result}): unknown => result) as [TAddress, string, string, number, bigint]; + + return ({ + address, + vaultAddress: asset, + name, + symbol, + decimals, + totalStaked: totalAssets + }); + }); + return Promise.all(gaugePromises); + }, []); + + const [{result: allowancesMap, status: fetchAllowancesMapStatus}, {execute: refreshAllowances}] = useAsync(async (): Promise | undefined> => { + if (!gauges || !isActive) { + return; + } + return allowancesFetcher(); + }, {}); + + const [{result: positions, status: fetchPositionsStatus}, {execute: refreshPositions}] = useAsync(async (): Promise => { + if (!gauges || !isActive) { + return; + } + return positionsFetcher(); + }, []); + + const [{result: gauges, status: fetchGaugesStatus}, {execute: refreshVotingEscrow}] = useAsync(async (): Promise => { + if (!isActive) { + return; + } + return gaugesFetcher(); + }, []); + + const refresh = useCallback((): void => { + refreshVotingEscrow(); + refreshPositions(); + refreshAllowances(); + }, [refreshAllowances, refreshPositions, refreshVotingEscrow]); + + useEffect((): void => { + refresh(); + }, [refresh]); + + const positionsFetcher = useCallback(async (): Promise => { + if (!gauges|| !isActive|| !userAddress) { + return []; + } + + const positionPromises = gauges.map(async ({address}): Promise => { + // todo: update once abi is available + const veYFIGaugeContract = getContract({ + address, + abi: VEYFI_GAUGE_ABI, + chainId: 1 + }); + + const calls: TMulticallContract[] = []; + ['balanceOf', 'earned', 'nextBoostedBalanceOf'].forEach((functionName): void => { + calls.push({...veYFIGaugeContract, functionName, args: [userAddress]}); + }); + + const results = await multicall({ + contracts: calls, + chainId: 1 + }); + + const [balance, earned, boostedBalance] = results.map(({result}): unknown => result) as bigint[]; + + const depositPosition: TPosition = { + balance, + underlyingBalance: balance + }; + + const rewardPosition: TPosition = { + balance: earned, + underlyingBalance: earned // TODO: convert to underlying + }; + + const boostRatio = balance > 0n + ? FixedNumber.from(boostedBalance).divUnsafe(FixedNumber.from(balance)).toUnsafeFloat() + : 0.1; + const boost = Math.min(1, boostRatio) * 10; + + return { + address, + deposit: depositPosition, + reward: rewardPosition, + boost + }; + }); + return Promise.all(positionPromises); + }, [gauges, isActive, userAddress]); + + const allowancesFetcher = useCallback(async (): Promise> => { + if (!gauges || !isActive || !userAddress) { + return {}; + } + + const allowanceCalls = gauges.map(({address, vaultAddress}): TMulticallContract => { + const erc20Contract = getContract({ + address: vaultAddress, + abi: erc20ABI, + chainId: 1 + }); + return { + ...erc20Contract, + abi: erc20ABI, + functionName: 'allowance', + args: [userAddress, address] + }; + }); + + const results = await multicall({ + contracts: allowanceCalls, + chainId: 1 + }); + const allowances = results.map(({result}): unknown => result) as bigint[]; + + const allowancesMap: TDict = {}; + gauges.forEach(({address, vaultAddress}, index): void => { + allowancesMap[allowanceKey(1, vaultAddress, address, userAddress)] = allowances[index]; + }); + + return allowancesMap; + }, [gauges, isActive, userAddress]); + + const contextValue = useMemo((): TGaugeContext => ({ + gaugeAddresses: gauges?.map(({address}): TAddress => address) ?? [], + gaugesMap: keyBy(gauges ?? [], 'address'), + positionsMap: keyBy(positions ?? [], 'address'), + allowancesMap: allowancesMap ?? {}, + isLoading: fetchGaugesStatus ==='loading' || fetchPositionsStatus === 'loading' || fetchAllowancesMapStatus === 'loading', + refresh + }), [allowancesMap, fetchAllowancesMapStatus, fetchGaugesStatus, fetchPositionsStatus, gauges, positions, refresh]); + + return ( + + {children} + + ); +}); + +export const useGauge = (): TGaugeContext => useContext(GaugeContext); +export default useGauge; diff --git a/apps/veyfi/contexts/useOption.tsx b/apps/veyfi/contexts/useOption.tsx new file mode 100644 index 000000000..ca037ff2e --- /dev/null +++ b/apps/veyfi/contexts/useOption.tsx @@ -0,0 +1,150 @@ +import React, {createContext, memo, useCallback, useContext, useEffect, useMemo} from 'react'; +import {ethers} from 'ethers'; +import {useAsync} from '@react-hookz/web'; +import VEYFI_OPTIONS_ABI from '@veYFI/utils/abi/veYFIOptions.abi'; +import VEYFI_OYFI_ABI from '@veYFI/utils/abi/veYFIoYFI.abi'; +import {VEYFI_OPTIONS_ADDRESS, VEYFI_OYFI_ADDRESS} from '@veYFI/utils/constants'; +import {erc20ABI, readContract} from '@wagmi/core'; +import {useWeb3} from '@yearn-finance/web-lib/contexts/useWeb3'; +import {allowanceKey} from '@yearn-finance/web-lib/utils/address'; +import {BIG_ZERO, ETH_TOKEN_ADDRESS, YFI_ADDRESS} from '@yearn-finance/web-lib/utils/constants'; +import {toBigInt, toNormalizedValue} from '@yearn-finance/web-lib/utils/format.bigNumber'; +import {useTokenPrice} from '@common/hooks/useTokenPrice'; + +import type {ReactElement} from 'react'; +import type {TDict} from '@yearn-finance/web-lib/types'; + +export type TOptionPosition = { + balance: bigint, +} + +export type TOptionContext = { + getRequiredEth: (amount: bigint) => Promise, + price: number | undefined, + positions: TOptionPosition | undefined, + allowances: TDict, + isLoading: boolean, + refresh: () => void, +} + +const defaultProps: TOptionContext = { + getRequiredEth: async (): Promise => BIG_ZERO, + price: undefined, + positions: undefined, + allowances: {}, + isLoading: true, + refresh: (): void => undefined +}; + +const OptionContext = createContext(defaultProps); +export const OptionContextApp = memo(function OptionContextApp({children}: {children: ReactElement}): ReactElement { + const {provider, address: userAddress, isActive} = useWeb3(); + const yfiPrice = useTokenPrice(YFI_ADDRESS); + const ethPrice = useTokenPrice(ETH_TOKEN_ADDRESS); + + const [{result: price, status: fetchPriceStatus}, {execute: refreshPrice}] = useAsync(async (): Promise => { + if (!isActive || provider) { + return; + } + return priceFetcher(); + }, 0); + + const [{result: positions, status: fetchPositionsStatus}, {execute: refreshPositions}] = useAsync(async (): Promise => { + if (!isActive || provider) { + return; + } + return positionsFetcher(); + }, {balance: 0n}); + + const [{result: allowances, status: fetchAllowancesStatus}, {execute: refreshAllowances}] = useAsync(async (): Promise | undefined> => { + if (!isActive || provider) { + return; + } + return allowancesFetcher(); + }, {}); + + const refresh = useCallback((): void => { + refreshPrice(); + refreshPositions(); + refreshAllowances(); + }, [refreshPrice, refreshPositions, refreshAllowances]); + + useEffect((): void => { + refresh(); + }, [refresh]); + + const getRequiredEth = useCallback(async (amount: bigint): Promise => { + // TODO: update once abi is available + return readContract({ + address: VEYFI_OPTIONS_ADDRESS, + abi: VEYFI_OPTIONS_ABI, + functionName: 'eth_required', + args: [amount], + chainId: 1 + }); + }, []); + + const priceFetcher = useCallback(async (): Promise => { + if(!ethPrice || !yfiPrice) { + return undefined; + } + const oneOption = ethers.utils.parseEther('1'); + const requiredEthPerOption = await getRequiredEth(toBigInt(oneOption.toString())); + const requiredEthValuePerOption = toNormalizedValue(requiredEthPerOption, 18) * ethPrice; + const pricePerOption = yfiPrice - requiredEthValuePerOption; + return pricePerOption; + }, [ethPrice, yfiPrice, getRequiredEth]); + + const positionsFetcher = useCallback(async (): Promise => { + if (!isActive || !userAddress) { + return undefined; + } + + // TODO: update once abi is available + return { + balance: await readContract({ + address: VEYFI_OYFI_ADDRESS, + abi: VEYFI_OYFI_ABI, + functionName: 'balanceOf', + args: [userAddress], + chainId: 1 + }) + }; + }, [isActive, userAddress]); + + const allowancesFetcher = useCallback(async (): Promise> => { + if (!isActive || !userAddress) { + return {}; + } + + const oYFIAllowanceOptions = await readContract({ + address: VEYFI_OYFI_ADDRESS, + abi: erc20ABI, + functionName: 'allowance', + args: [userAddress, VEYFI_OPTIONS_ADDRESS], + chainId: 1 + }); + + return ({ + [allowanceKey(1, VEYFI_OYFI_ADDRESS, VEYFI_OPTIONS_ADDRESS, userAddress)]: oYFIAllowanceOptions + }); + }, [isActive, userAddress]); + + const contextValue = useMemo((): TOptionContext => ({ + getRequiredEth, + price, + positions, + allowances: allowances ?? {}, + isLoading: fetchPriceStatus === 'loading' || fetchPositionsStatus === 'loading' || fetchAllowancesStatus === 'loading', + refresh + }), [allowances, fetchAllowancesStatus, fetchPositionsStatus, fetchPriceStatus, getRequiredEth, positions, price, refresh]); + + return ( + + {children} + + ); +}); + +export const useOption = (): TOptionContext => useContext(OptionContext); +export default useOption; diff --git a/apps/veyfi/utils/abi/SnapshotDelegateRegistry.abi.ts b/apps/veyfi/utils/abi/SnapshotDelegateRegistry.abi.ts new file mode 100644 index 000000000..8fc5a6324 --- /dev/null +++ b/apps/veyfi/utils/abi/SnapshotDelegateRegistry.abi.ts @@ -0,0 +1,3 @@ +const SNAPSHOT_DELEGATE_REGISTRY_ABI = [{'anonymous':false, 'inputs':[{'indexed':true, 'internalType':'address', 'name':'delegator', 'type':'address'}, {'indexed':true, 'internalType':'bytes32', 'name':'id', 'type':'bytes32'}, {'indexed':true, 'internalType':'address', 'name':'delegate', 'type':'address'}], 'name':'ClearDelegate', 'type':'event'}, {'anonymous':false, 'inputs':[{'indexed':true, 'internalType':'address', 'name':'delegator', 'type':'address'}, {'indexed':true, 'internalType':'bytes32', 'name':'id', 'type':'bytes32'}, {'indexed':true, 'internalType':'address', 'name':'delegate', 'type':'address'}], 'name':'SetDelegate', 'type':'event'}, {'inputs':[{'internalType':'bytes32', 'name':'id', 'type':'bytes32'}], 'name':'clearDelegate', 'outputs':[], 'stateMutability':'nonpayable', 'type':'function'}, {'inputs':[{'internalType':'address', 'name':'', 'type':'address'}, {'internalType':'bytes32', 'name':'', 'type':'bytes32'}], 'name':'delegation', 'outputs':[{'internalType':'address', 'name':'', 'type':'address'}], 'stateMutability':'view', 'type':'function'}, {'inputs':[{'internalType':'bytes32', 'name':'id', 'type':'bytes32'}, {'internalType':'address', 'name':'delegate', 'type':'address'}], 'name':'setDelegate', 'outputs':[], 'stateMutability':'nonpayable', 'type':'function'}] as const; + +export default SNAPSHOT_DELEGATE_REGISTRY_ABI; diff --git a/apps/veyfi/utils/abi/veYFIClaimRewardsZap.abi.ts b/apps/veyfi/utils/abi/veYFIClaimRewardsZap.abi.ts new file mode 100644 index 000000000..ff8001a7a --- /dev/null +++ b/apps/veyfi/utils/abi/veYFIClaimRewardsZap.abi.ts @@ -0,0 +1,4 @@ +// TODO: update once final version deployed +const VEYFI_CLAIM_REWARDS_ZAP_ABI = ['function claim(address[] calldata _gauges, bool _lock, bool _claimVeYfi) external']; + +export default VEYFI_CLAIM_REWARDS_ZAP_ABI; diff --git a/apps/veyfi/utils/abi/veYFIGauge.abi.ts b/apps/veyfi/utils/abi/veYFIGauge.abi.ts new file mode 100644 index 000000000..4734f1223 --- /dev/null +++ b/apps/veyfi/utils/abi/veYFIGauge.abi.ts @@ -0,0 +1,1413 @@ +// TODO: update once final version deployed +const VEYFI_GAUGE_ABI = [ + { + 'inputs': [ + { + 'internalType': 'address', + 'name': '_veYfi', + 'type': 'address' + }, + { + 'internalType': 'address', + 'name': '_oYfi', + 'type': 'address' + }, + { + 'internalType': 'address', + 'name': '_veYfiOYfiPool', + 'type': 'address' + } + ], + 'stateMutability': 'nonpayable', + 'type': 'constructor' + }, + { + 'anonymous': false, + 'inputs': [ + { + 'indexed': true, + 'internalType': 'address', + 'name': 'owner', + 'type': 'address' + }, + { + 'indexed': true, + 'internalType': 'address', + 'name': 'spender', + 'type': 'address' + }, + { + 'indexed': false, + 'internalType': 'uint256', + 'name': 'value', + 'type': 'uint256' + } + ], + 'name': 'Approval', + 'type': 'event' + }, + { + 'anonymous': false, + 'inputs': [ + { + 'indexed': false, + 'internalType': 'address', + 'name': 'account', + 'type': 'address' + }, + { + 'indexed': false, + 'internalType': 'uint256', + 'name': 'amount', + 'type': 'uint256' + } + ], + 'name': 'BoostedBalanceUpdated', + 'type': 'event' + }, + { + 'anonymous': false, + 'inputs': [ + { + 'indexed': true, + 'internalType': 'address', + 'name': 'caller', + 'type': 'address' + }, + { + 'indexed': true, + 'internalType': 'address', + 'name': 'owner', + 'type': 'address' + }, + { + 'indexed': false, + 'internalType': 'uint256', + 'name': 'assets', + 'type': 'uint256' + }, + { + 'indexed': false, + 'internalType': 'uint256', + 'name': 'shares', + 'type': 'uint256' + } + ], + 'name': 'Deposit', + 'type': 'event' + }, + { + 'anonymous': false, + 'inputs': [ + { + 'indexed': false, + 'internalType': 'uint256', + 'name': 'duration', + 'type': 'uint256' + }, + { + 'indexed': false, + 'internalType': 'uint256', + 'name': 'rewardRate', + 'type': 'uint256' + }, + { + 'indexed': false, + 'internalType': 'uint256', + 'name': 'periodFinish', + 'type': 'uint256' + } + ], + 'name': 'DurationUpdated', + 'type': 'event' + }, + { + 'anonymous': false, + 'inputs': [ + { + 'indexed': true, + 'internalType': 'address', + 'name': 'asset', + 'type': 'address' + }, + { + 'indexed': true, + 'internalType': 'address', + 'name': 'owner', + 'type': 'address' + } + ], + 'name': 'Initialize', + 'type': 'event' + }, + { + 'anonymous': false, + 'inputs': [ + { + 'indexed': false, + 'internalType': 'uint8', + 'name': 'version', + 'type': 'uint8' + } + ], + 'name': 'Initialized', + 'type': 'event' + }, + { + 'anonymous': false, + 'inputs': [ + { + 'indexed': true, + 'internalType': 'address', + 'name': 'previousOwner', + 'type': 'address' + }, + { + 'indexed': true, + 'internalType': 'address', + 'name': 'newOwner', + 'type': 'address' + } + ], + 'name': 'OwnershipTransferred', + 'type': 'event' + }, + { + 'anonymous': false, + 'inputs': [ + { + 'indexed': true, + 'internalType': 'address', + 'name': 'account', + 'type': 'address' + }, + { + 'indexed': true, + 'internalType': 'address', + 'name': 'recipient', + 'type': 'address' + } + ], + 'name': 'RecipientUpdated', + 'type': 'event' + }, + { + 'anonymous': false, + 'inputs': [ + { + 'indexed': true, + 'internalType': 'address', + 'name': 'user', + 'type': 'address' + }, + { + 'indexed': false, + 'internalType': 'uint256', + 'name': 'reward', + 'type': 'uint256' + } + ], + 'name': 'RewardPaid', + 'type': 'event' + }, + { + 'anonymous': false, + 'inputs': [ + { + 'indexed': false, + 'internalType': 'uint256', + 'name': 'currentRewards', + 'type': 'uint256' + }, + { + 'indexed': false, + 'internalType': 'uint256', + 'name': 'lastUpdateTime', + 'type': 'uint256' + }, + { + 'indexed': false, + 'internalType': 'uint256', + 'name': 'periodFinish', + 'type': 'uint256' + }, + { + 'indexed': false, + 'internalType': 'uint256', + 'name': 'rewardRate', + 'type': 'uint256' + }, + { + 'indexed': false, + 'internalType': 'uint256', + 'name': 'historicalRewards', + 'type': 'uint256' + } + ], + 'name': 'RewardsAdded', + 'type': 'event' + }, + { + 'anonymous': false, + 'inputs': [ + { + 'indexed': true, + 'internalType': 'address', + 'name': 'from', + 'type': 'address' + }, + { + 'indexed': false, + 'internalType': 'uint256', + 'name': 'amount', + 'type': 'uint256' + } + ], + 'name': 'RewardsQueued', + 'type': 'event' + }, + { + 'anonymous': false, + 'inputs': [ + { + 'indexed': true, + 'internalType': 'address', + 'name': 'token', + 'type': 'address' + }, + { + 'indexed': false, + 'internalType': 'uint256', + 'name': 'amount', + 'type': 'uint256' + } + ], + 'name': 'Sweep', + 'type': 'event' + }, + { + 'anonymous': false, + 'inputs': [ + { + 'indexed': true, + 'internalType': 'address', + 'name': 'from', + 'type': 'address' + }, + { + 'indexed': true, + 'internalType': 'address', + 'name': 'to', + 'type': 'address' + }, + { + 'indexed': false, + 'internalType': 'uint256', + 'name': 'value', + 'type': 'uint256' + } + ], + 'name': 'Transfer', + 'type': 'event' + }, + { + 'anonymous': false, + 'inputs': [ + { + 'indexed': true, + 'internalType': 'address', + 'name': 'account', + 'type': 'address' + }, + { + 'indexed': false, + 'internalType': 'uint256', + 'name': 'transfered', + 'type': 'uint256' + } + ], + 'name': 'TransferredPenalty', + 'type': 'event' + }, + { + 'anonymous': false, + 'inputs': [ + { + 'indexed': true, + 'internalType': 'address', + 'name': 'account', + 'type': 'address' + }, + { + 'indexed': false, + 'internalType': 'uint256', + 'name': 'rewardPerTokenStored', + 'type': 'uint256' + }, + { + 'indexed': false, + 'internalType': 'uint256', + 'name': 'lastUpdateTime', + 'type': 'uint256' + }, + { + 'indexed': false, + 'internalType': 'uint256', + 'name': 'rewards', + 'type': 'uint256' + }, + { + 'indexed': false, + 'internalType': 'uint256', + 'name': 'userRewardPerTokenPaid', + 'type': 'uint256' + } + ], + 'name': 'UpdatedRewards', + 'type': 'event' + }, + { + 'anonymous': false, + 'inputs': [ + { + 'indexed': true, + 'internalType': 'address', + 'name': 'caller', + 'type': 'address' + }, + { + 'indexed': true, + 'internalType': 'address', + 'name': 'receiver', + 'type': 'address' + }, + { + 'indexed': true, + 'internalType': 'address', + 'name': 'owner', + 'type': 'address' + }, + { + 'indexed': false, + 'internalType': 'uint256', + 'name': 'assets', + 'type': 'uint256' + }, + { + 'indexed': false, + 'internalType': 'uint256', + 'name': 'shares', + 'type': 'uint256' + } + ], + 'name': 'Withdraw', + 'type': 'event' + }, + { + 'inputs': [], + 'name': 'BOOSTING_FACTOR', + 'outputs': [ + { + 'internalType': 'uint256', + 'name': '', + 'type': 'uint256' + } + ], + 'stateMutability': 'view', + 'type': 'function' + }, + { + 'inputs': [], + 'name': 'BOOST_DENOMINATOR', + 'outputs': [ + { + 'internalType': 'uint256', + 'name': '', + 'type': 'uint256' + } + ], + 'stateMutability': 'view', + 'type': 'function' + }, + { + 'inputs': [], + 'name': 'PRECISION_FACTOR', + 'outputs': [ + { + 'internalType': 'uint256', + 'name': '', + 'type': 'uint256' + } + ], + 'stateMutability': 'view', + 'type': 'function' + }, + { + 'inputs': [], + 'name': 'REWARD_TOKEN', + 'outputs': [ + { + 'internalType': 'contract IERC20', + 'name': '', + 'type': 'address' + } + ], + 'stateMutability': 'view', + 'type': 'function' + }, + { + 'inputs': [], + 'name': 'VEYFI', + 'outputs': [ + { + 'internalType': 'address', + 'name': '', + 'type': 'address' + } + ], + 'stateMutability': 'view', + 'type': 'function' + }, + { + 'inputs': [], + 'name': 'VE_YFI_POOL', + 'outputs': [ + { + 'internalType': 'address', + 'name': '', + 'type': 'address' + } + ], + 'stateMutability': 'view', + 'type': 'function' + }, + { + 'inputs': [ + { + 'internalType': 'address', + 'name': 'owner', + 'type': 'address' + }, + { + 'internalType': 'address', + 'name': 'spender', + 'type': 'address' + } + ], + 'name': 'allowance', + 'outputs': [ + { + 'internalType': 'uint256', + 'name': '', + 'type': 'uint256' + } + ], + 'stateMutability': 'view', + 'type': 'function' + }, + { + 'inputs': [ + { + 'internalType': 'address', + 'name': 'spender', + 'type': 'address' + }, + { + 'internalType': 'uint256', + 'name': 'amount', + 'type': 'uint256' + } + ], + 'name': 'approve', + 'outputs': [ + { + 'internalType': 'bool', + 'name': '', + 'type': 'bool' + } + ], + 'stateMutability': 'nonpayable', + 'type': 'function' + }, + { + 'inputs': [], + 'name': 'asset', + 'outputs': [ + { + 'internalType': 'contract IERC20', + 'name': '', + 'type': 'address' + } + ], + 'stateMutability': 'view', + 'type': 'function' + }, + { + 'inputs': [ + { + 'internalType': 'address', + 'name': 'account', + 'type': 'address' + } + ], + 'name': 'balanceOf', + 'outputs': [ + { + 'internalType': 'uint256', + 'name': '', + 'type': 'uint256' + } + ], + 'stateMutability': 'view', + 'type': 'function' + }, + { + 'inputs': [ + { + 'internalType': 'address', + 'name': '_account', + 'type': 'address' + } + ], + 'name': 'boostedBalanceOf', + 'outputs': [ + { + 'internalType': 'uint256', + 'name': '', + 'type': 'uint256' + } + ], + 'stateMutability': 'view', + 'type': 'function' + }, + { + 'inputs': [ + { + 'internalType': 'uint256', + 'name': '_shares', + 'type': 'uint256' + } + ], + 'name': 'convertToAssets', + 'outputs': [ + { + 'internalType': 'uint256', + 'name': '', + 'type': 'uint256' + } + ], + 'stateMutability': 'view', + 'type': 'function' + }, + { + 'inputs': [ + { + 'internalType': 'uint256', + 'name': '_assets', + 'type': 'uint256' + } + ], + 'name': 'convertToShares', + 'outputs': [ + { + 'internalType': 'uint256', + 'name': '', + 'type': 'uint256' + } + ], + 'stateMutability': 'view', + 'type': 'function' + }, + { + 'inputs': [], + 'name': 'currentRewards', + 'outputs': [ + { + 'internalType': 'uint256', + 'name': '', + 'type': 'uint256' + } + ], + 'stateMutability': 'view', + 'type': 'function' + }, + { + 'inputs': [], + 'name': 'decimals', + 'outputs': [ + { + 'internalType': 'uint8', + 'name': '', + 'type': 'uint8' + } + ], + 'stateMutability': 'view', + 'type': 'function' + }, + { + 'inputs': [ + { + 'internalType': 'address', + 'name': 'spender', + 'type': 'address' + }, + { + 'internalType': 'uint256', + 'name': 'subtractedValue', + 'type': 'uint256' + } + ], + 'name': 'decreaseAllowance', + 'outputs': [ + { + 'internalType': 'bool', + 'name': '', + 'type': 'bool' + } + ], + 'stateMutability': 'nonpayable', + 'type': 'function' + }, + { + 'inputs': [ + { + 'internalType': 'uint256', + 'name': '_assets', + 'type': 'uint256' + } + ], + 'name': 'deposit', + 'outputs': [ + { + 'internalType': 'uint256', + 'name': '', + 'type': 'uint256' + } + ], + 'stateMutability': 'nonpayable', + 'type': 'function' + }, + { + 'inputs': [], + 'name': 'duration', + 'outputs': [ + { + 'internalType': 'uint256', + 'name': '', + 'type': 'uint256' + } + ], + 'stateMutability': 'view', + 'type': 'function' + }, + { + 'inputs': [ + { + 'internalType': 'address', + 'name': '_account', + 'type': 'address' + } + ], + 'name': 'earned', + 'outputs': [ + { + 'internalType': 'uint256', + 'name': '', + 'type': 'uint256' + } + ], + 'stateMutability': 'view', + 'type': 'function' + }, + { + 'inputs': [], + 'name': 'getReward', + 'outputs': [ + { + 'internalType': 'bool', + 'name': '', + 'type': 'bool' + } + ], + 'stateMutability': 'nonpayable', + 'type': 'function' + }, + { + 'inputs': [], + 'name': 'historicalRewards', + 'outputs': [ + { + 'internalType': 'uint256', + 'name': '', + 'type': 'uint256' + } + ], + 'stateMutability': 'view', + 'type': 'function' + }, + { + 'inputs': [ + { + 'internalType': 'address', + 'name': 'spender', + 'type': 'address' + }, + { + 'internalType': 'uint256', + 'name': 'addedValue', + 'type': 'uint256' + } + ], + 'name': 'increaseAllowance', + 'outputs': [ + { + 'internalType': 'bool', + 'name': '', + 'type': 'bool' + } + ], + 'stateMutability': 'nonpayable', + 'type': 'function' + }, + { + 'inputs': [ + { + 'internalType': 'address', + 'name': '_asset', + 'type': 'address' + }, + { + 'internalType': 'address', + 'name': '_owner', + 'type': 'address' + } + ], + 'name': 'initialize', + 'outputs': [], + 'stateMutability': 'nonpayable', + 'type': 'function' + }, + { + 'inputs': [ + { + 'internalType': 'address[]', + 'name': '_accounts', + 'type': 'address[]' + } + ], + 'name': 'kick', + 'outputs': [], + 'stateMutability': 'nonpayable', + 'type': 'function' + }, + { + 'inputs': [], + 'name': 'lastTimeRewardApplicable', + 'outputs': [ + { + 'internalType': 'uint256', + 'name': '', + 'type': 'uint256' + } + ], + 'stateMutability': 'view', + 'type': 'function' + }, + { + 'inputs': [], + 'name': 'lastUpdateTime', + 'outputs': [ + { + 'internalType': 'uint256', + 'name': '', + 'type': 'uint256' + } + ], + 'stateMutability': 'view', + 'type': 'function' + }, + { + 'inputs': [ + { + 'internalType': 'address', + 'name': '', + 'type': 'address' + } + ], + 'name': 'maxDeposit', + 'outputs': [ + { + 'internalType': 'uint256', + 'name': '', + 'type': 'uint256' + } + ], + 'stateMutability': 'view', + 'type': 'function' + }, + { + 'inputs': [ + { + 'internalType': 'address', + 'name': '', + 'type': 'address' + } + ], + 'name': 'maxMint', + 'outputs': [ + { + 'internalType': 'uint256', + 'name': '', + 'type': 'uint256' + } + ], + 'stateMutability': 'view', + 'type': 'function' + }, + { + 'inputs': [ + { + 'internalType': 'address', + 'name': '_owner', + 'type': 'address' + } + ], + 'name': 'maxRedeem', + 'outputs': [ + { + 'internalType': 'uint256', + 'name': '', + 'type': 'uint256' + } + ], + 'stateMutability': 'view', + 'type': 'function' + }, + { + 'inputs': [ + { + 'internalType': 'address', + 'name': '_owner', + 'type': 'address' + } + ], + 'name': 'maxWithdraw', + 'outputs': [ + { + 'internalType': 'uint256', + 'name': '', + 'type': 'uint256' + } + ], + 'stateMutability': 'view', + 'type': 'function' + }, + { + 'inputs': [ + { + 'internalType': 'uint256', + 'name': '_shares', + 'type': 'uint256' + }, + { + 'internalType': 'address', + 'name': '_receiver', + 'type': 'address' + } + ], + 'name': 'mint', + 'outputs': [ + { + 'internalType': 'uint256', + 'name': '', + 'type': 'uint256' + } + ], + 'stateMutability': 'nonpayable', + 'type': 'function' + }, + { + 'inputs': [], + 'name': 'name', + 'outputs': [ + { + 'internalType': 'string', + 'name': '', + 'type': 'string' + } + ], + 'stateMutability': 'view', + 'type': 'function' + }, + { + 'inputs': [ + { + 'internalType': 'address', + 'name': '_account', + 'type': 'address' + } + ], + 'name': 'nextBoostedBalanceOf', + 'outputs': [ + { + 'internalType': 'uint256', + 'name': '', + 'type': 'uint256' + } + ], + 'stateMutability': 'view', + 'type': 'function' + }, + { + 'inputs': [], + 'name': 'owner', + 'outputs': [ + { + 'internalType': 'address', + 'name': '', + 'type': 'address' + } + ], + 'stateMutability': 'view', + 'type': 'function' + }, + { + 'inputs': [], + 'name': 'periodFinish', + 'outputs': [ + { + 'internalType': 'uint256', + 'name': '', + 'type': 'uint256' + } + ], + 'stateMutability': 'view', + 'type': 'function' + }, + { + 'inputs': [ + { + 'internalType': 'uint256', + 'name': '_assets', + 'type': 'uint256' + } + ], + 'name': 'previewDeposit', + 'outputs': [ + { + 'internalType': 'uint256', + 'name': '', + 'type': 'uint256' + } + ], + 'stateMutability': 'view', + 'type': 'function' + }, + { + 'inputs': [ + { + 'internalType': 'uint256', + 'name': '_shares', + 'type': 'uint256' + } + ], + 'name': 'previewMint', + 'outputs': [ + { + 'internalType': 'uint256', + 'name': '', + 'type': 'uint256' + } + ], + 'stateMutability': 'view', + 'type': 'function' + }, + { + 'inputs': [ + { + 'internalType': 'uint256', + 'name': '_assets', + 'type': 'uint256' + } + ], + 'name': 'previewRedeem', + 'outputs': [ + { + 'internalType': 'uint256', + 'name': '', + 'type': 'uint256' + } + ], + 'stateMutability': 'view', + 'type': 'function' + }, + { + 'inputs': [ + { + 'internalType': 'uint256', + 'name': '_assets', + 'type': 'uint256' + } + ], + 'name': 'previewWithdraw', + 'outputs': [ + { + 'internalType': 'uint256', + 'name': '', + 'type': 'uint256' + } + ], + 'stateMutability': 'view', + 'type': 'function' + }, + { + 'inputs': [ + { + 'internalType': 'uint256', + 'name': '_amount', + 'type': 'uint256' + } + ], + 'name': 'queueNewRewards', + 'outputs': [ + { + 'internalType': 'bool', + 'name': '', + 'type': 'bool' + } + ], + 'stateMutability': 'nonpayable', + 'type': 'function' + }, + { + 'inputs': [], + 'name': 'queuedRewards', + 'outputs': [ + { + 'internalType': 'uint256', + 'name': '', + 'type': 'uint256' + } + ], + 'stateMutability': 'view', + 'type': 'function' + }, + { + 'inputs': [ + { + 'internalType': 'address', + 'name': '', + 'type': 'address' + } + ], + 'name': 'recipients', + 'outputs': [ + { + 'internalType': 'address', + 'name': '', + 'type': 'address' + } + ], + 'stateMutability': 'view', + 'type': 'function' + }, + { + 'inputs': [ + { + 'internalType': 'uint256', + 'name': '_assets', + 'type': 'uint256' + }, + { + 'internalType': 'address', + 'name': '_receiver', + 'type': 'address' + }, + { + 'internalType': 'address', + 'name': '_owner', + 'type': 'address' + } + ], + 'name': 'redeem', + 'outputs': [ + { + 'internalType': 'uint256', + 'name': '', + 'type': 'uint256' + } + ], + 'stateMutability': 'nonpayable', + 'type': 'function' + }, + { + 'inputs': [], + 'name': 'renounceOwnership', + 'outputs': [], + 'stateMutability': 'nonpayable', + 'type': 'function' + }, + { + 'inputs': [], + 'name': 'rewardPerToken', + 'outputs': [ + { + 'internalType': 'uint256', + 'name': '', + 'type': 'uint256' + } + ], + 'stateMutability': 'view', + 'type': 'function' + }, + { + 'inputs': [], + 'name': 'rewardPerTokenStored', + 'outputs': [ + { + 'internalType': 'uint256', + 'name': '', + 'type': 'uint256' + } + ], + 'stateMutability': 'view', + 'type': 'function' + }, + { + 'inputs': [], + 'name': 'rewardRate', + 'outputs': [ + { + 'internalType': 'uint256', + 'name': '', + 'type': 'uint256' + } + ], + 'stateMutability': 'view', + 'type': 'function' + }, + { + 'inputs': [ + { + 'internalType': 'address', + 'name': '', + 'type': 'address' + } + ], + 'name': 'rewards', + 'outputs': [ + { + 'internalType': 'uint256', + 'name': '', + 'type': 'uint256' + } + ], + 'stateMutability': 'view', + 'type': 'function' + }, + { + 'inputs': [ + { + 'internalType': 'uint256', + 'name': '_newDuration', + 'type': 'uint256' + } + ], + 'name': 'setDuration', + 'outputs': [], + 'stateMutability': 'nonpayable', + 'type': 'function' + }, + { + 'inputs': [ + { + 'internalType': 'address', + 'name': '_recipient', + 'type': 'address' + } + ], + 'name': 'setRecipient', + 'outputs': [], + 'stateMutability': 'nonpayable', + 'type': 'function' + }, + { + 'inputs': [ + { + 'internalType': 'address', + 'name': '_token', + 'type': 'address' + } + ], + 'name': 'sweep', + 'outputs': [ + { + 'internalType': 'bool', + 'name': '', + 'type': 'bool' + } + ], + 'stateMutability': 'nonpayable', + 'type': 'function' + }, + { + 'inputs': [], + 'name': 'symbol', + 'outputs': [ + { + 'internalType': 'string', + 'name': '', + 'type': 'string' + } + ], + 'stateMutability': 'view', + 'type': 'function' + }, + { + 'inputs': [], + 'name': 'totalAssets', + 'outputs': [ + { + 'internalType': 'uint256', + 'name': '', + 'type': 'uint256' + } + ], + 'stateMutability': 'view', + 'type': 'function' + }, + { + 'inputs': [], + 'name': 'totalSupply', + 'outputs': [ + { + 'internalType': 'uint256', + 'name': '', + 'type': 'uint256' + } + ], + 'stateMutability': 'view', + 'type': 'function' + }, + { + 'inputs': [ + { + 'internalType': 'address', + 'name': 'to', + 'type': 'address' + }, + { + 'internalType': 'uint256', + 'name': 'amount', + 'type': 'uint256' + } + ], + 'name': 'transfer', + 'outputs': [ + { + 'internalType': 'bool', + 'name': '', + 'type': 'bool' + } + ], + 'stateMutability': 'nonpayable', + 'type': 'function' + }, + { + 'inputs': [ + { + 'internalType': 'address', + 'name': 'from', + 'type': 'address' + }, + { + 'internalType': 'address', + 'name': 'to', + 'type': 'address' + }, + { + 'internalType': 'uint256', + 'name': 'amount', + 'type': 'uint256' + } + ], + 'name': 'transferFrom', + 'outputs': [ + { + 'internalType': 'bool', + 'name': '', + 'type': 'bool' + } + ], + 'stateMutability': 'nonpayable', + 'type': 'function' + }, + { + 'inputs': [ + { + 'internalType': 'address', + 'name': 'newOwner', + 'type': 'address' + } + ], + 'name': 'transferOwnership', + 'outputs': [], + 'stateMutability': 'nonpayable', + 'type': 'function' + }, + { + 'inputs': [ + { + 'internalType': 'address', + 'name': '', + 'type': 'address' + } + ], + 'name': 'userRewardPerTokenPaid', + 'outputs': [ + { + 'internalType': 'uint256', + 'name': '', + 'type': 'uint256' + } + ], + 'stateMutability': 'view', + 'type': 'function' + }, + { + 'inputs': [ + { + 'internalType': 'uint256', + 'name': '_assets', + 'type': 'uint256' + }, + { + 'internalType': 'address', + 'name': '_receiver', + 'type': 'address' + }, + { + 'internalType': 'address', + 'name': '_owner', + 'type': 'address' + }, + { + 'internalType': 'bool', + 'name': '_claim', + 'type': 'bool' + } + ], + 'name': 'withdraw', + 'outputs': [ + { + 'internalType': 'uint256', + 'name': '', + 'type': 'uint256' + } + ], + 'stateMutability': 'nonpayable', + 'type': 'function' + } +] as const; + +export default VEYFI_GAUGE_ABI; diff --git a/apps/veyfi/utils/abi/veYFIOptions.abi.ts b/apps/veyfi/utils/abi/veYFIOptions.abi.ts new file mode 100644 index 000000000..9e547e1b0 --- /dev/null +++ b/apps/veyfi/utils/abi/veYFIOptions.abi.ts @@ -0,0 +1,262 @@ +const VEYFI_OPTIONS_ABI = [ + { + 'anonymous': false, + 'inputs': [ + { + 'indexed': false, + 'name': 'yfi_recovered', + 'type': 'uint256' + } + ], + 'name': 'Killed', + 'type': 'event' + }, + { + 'anonymous': false, + 'inputs': [ + { + 'indexed': false, + 'name': 'token', + 'type': 'address' + }, + { + 'indexed': false, + 'name': 'amount', + 'type': 'uint256' + } + ], + 'name': 'Sweep', + 'type': 'event' + }, + { + 'anonymous': false, + 'inputs': [ + { + 'indexed': true, + 'name': 'previous_owner', + 'type': 'address' + }, + { + 'indexed': true, + 'name': 'new_owner', + 'type': 'address' + } + ], + 'name': 'OwnershipTransferStarted', + 'type': 'event' + }, + { + 'anonymous': false, + 'inputs': [ + { + 'indexed': true, + 'name': 'previous_owner', + 'type': 'address' + }, + { + 'indexed': true, + 'name': 'new_owner', + 'type': 'address' + } + ], + 'name': 'OwnershipTransferred', + 'type': 'event' + }, + { + 'anonymous': false, + 'inputs': [ + { + 'indexed': true, + 'name': 'payee', + 'type': 'address' + } + ], + 'name': 'SetPayee', + 'type': 'event' + }, + { + 'inputs': [ + { + 'name': 'yfi', + 'type': 'address' + }, + { + 'name': 'o_yfi', + 'type': 'address' + }, + { + 'name': 've_yfi', + 'type': 'address' + }, + { + 'name': 'owner', + 'type': 'address' + }, + { + 'name': 'price_feed', + 'type': 'address' + }, + { + 'name': 'curve_pool', + 'type': 'address' + } + ], + 'stateMutability': 'nonpayable', + 'type': 'constructor' + }, + { + 'inputs': [ + { + 'name': 'amount', + 'type': 'uint256' + }, + { + 'name': 'recipient', + 'type': 'address' + } + ], + 'name': 'exercise', + 'outputs': [ + { + 'name': '', + 'type': 'uint256' + } + ], + 'stateMutability': 'payable', + 'type': 'function' + }, + { + 'inputs': [ + { + 'name': 'amount', + 'type': 'uint256' + } + ], + 'name': 'eth_required', + 'outputs': [ + { + 'name': '', + 'type': 'uint256' + } + ], + 'stateMutability': 'view', + 'type': 'function' + }, + { + 'inputs': [], + 'name': 'get_latest_price', + 'outputs': [ + { + 'name': '', + 'type': 'uint256' + } + ], + 'stateMutability': 'view', + 'type': 'function' + }, + { + 'inputs': [ + { + 'name': 'new_payee', + 'type': 'address' + } + ], + 'name': 'set_payee', + 'outputs': [], + 'stateMutability': 'nonpayable', + 'type': 'function' + }, + { + 'inputs': [], + 'name': 'kill', + 'outputs': [], + 'stateMutability': 'nonpayable', + 'type': 'function' + }, + { + 'inputs': [ + { + 'name': 'token', + 'type': 'address' + } + ], + 'name': 'sweep', + 'outputs': [ + { + 'name': '', + 'type': 'uint256' + } + ], + 'stateMutability': 'nonpayable', + 'type': 'function' + }, + { + 'inputs': [ + { + 'name': 'new_owner', + 'type': 'address' + } + ], + 'name': 'transfer_ownership', + 'outputs': [], + 'stateMutability': 'nonpayable', + 'type': 'function' + }, + { + 'inputs': [], + 'name': 'accept_ownership', + 'outputs': [], + 'stateMutability': 'nonpayable', + 'type': 'function' + }, + { + 'inputs': [], + 'name': 'owner', + 'outputs': [ + { + 'name': '', + 'type': 'address' + } + ], + 'stateMutability': 'view', + 'type': 'function' + }, + { + 'inputs': [], + 'name': 'pending_owner', + 'outputs': [ + { + 'name': '', + 'type': 'address' + } + ], + 'stateMutability': 'view', + 'type': 'function' + }, + { + 'inputs': [], + 'name': 'killed', + 'outputs': [ + { + 'name': '', + 'type': 'bool' + } + ], + 'stateMutability': 'view', + 'type': 'function' + }, + { + 'inputs': [], + 'name': 'payee', + 'outputs': [ + { + 'name': '', + 'type': 'address' + } + ], + 'stateMutability': 'view', + 'type': 'function' + } +] as const; + +export default VEYFI_OPTIONS_ABI; diff --git a/apps/veyfi/utils/abi/veYFIRegistry.abi.ts b/apps/veyfi/utils/abi/veYFIRegistry.abi.ts new file mode 100644 index 000000000..d51490f31 --- /dev/null +++ b/apps/veyfi/utils/abi/veYFIRegistry.abi.ts @@ -0,0 +1,294 @@ +// TODO: update once final version deployed +const VEYFI_REGISTRY_ABI = [ + { + 'inputs': [ + { + 'internalType': 'address', + 'name': '_ve', + 'type': 'address' + }, + { + 'internalType': 'address', + 'name': '_yfi', + 'type': 'address' + }, + { + 'internalType': 'address', + 'name': '_gaugefactory', + 'type': 'address' + }, + { + 'internalType': 'address', + 'name': '_veYfiRewardPool', + 'type': 'address' + } + ], + 'stateMutability': 'nonpayable', + 'type': 'constructor' + }, + { + 'anonymous': false, + 'inputs': [ + { + 'indexed': true, + 'internalType': 'address', + 'name': 'previousOwner', + 'type': 'address' + }, + { + 'indexed': true, + 'internalType': 'address', + 'name': 'newOwner', + 'type': 'address' + } + ], + 'name': 'OwnershipTransferred', + 'type': 'event' + }, + { + 'anonymous': false, + 'inputs': [ + { + 'indexed': true, + 'internalType': 'address', + 'name': 've', + 'type': 'address' + } + ], + 'name': 'UpdatedVeToken', + 'type': 'event' + }, + { + 'anonymous': false, + 'inputs': [ + { + 'indexed': true, + 'internalType': 'address', + 'name': 'vault', + 'type': 'address' + } + ], + 'name': 'VaultAdded', + 'type': 'event' + }, + { + 'anonymous': false, + 'inputs': [ + { + 'indexed': true, + 'internalType': 'address', + 'name': 'vault', + 'type': 'address' + } + ], + 'name': 'VaultRemoved', + 'type': 'event' + }, + { + 'inputs': [ + { + 'internalType': 'address', + 'name': '_vault', + 'type': 'address' + }, + { + 'internalType': 'address', + 'name': '_owner', + 'type': 'address' + } + ], + 'name': 'addVaultToRewards', + 'outputs': [ + { + 'internalType': 'address', + 'name': '', + 'type': 'address' + } + ], + 'stateMutability': 'nonpayable', + 'type': 'function' + }, + { + 'inputs': [], + 'name': 'gaugefactory', + 'outputs': [ + { + 'internalType': 'address', + 'name': '', + 'type': 'address' + } + ], + 'stateMutability': 'view', + 'type': 'function' + }, + { + 'inputs': [ + { + 'internalType': 'address', + 'name': '', + 'type': 'address' + } + ], + 'name': 'gauges', + 'outputs': [ + { + 'internalType': 'address', + 'name': '', + 'type': 'address' + } + ], + 'stateMutability': 'view', + 'type': 'function' + }, + { + 'inputs': [], + 'name': 'getVaults', + 'outputs': [ + { + 'internalType': 'address[]', + 'name': '', + 'type': 'address[]' + } + ], + 'stateMutability': 'view', + 'type': 'function' + }, + { + 'inputs': [ + { + 'internalType': 'address', + 'name': '', + 'type': 'address' + } + ], + 'name': 'isGauge', + 'outputs': [ + { + 'internalType': 'bool', + 'name': '', + 'type': 'bool' + } + ], + 'stateMutability': 'view', + 'type': 'function' + }, + { + 'inputs': [], + 'name': 'owner', + 'outputs': [ + { + 'internalType': 'address', + 'name': '', + 'type': 'address' + } + ], + 'stateMutability': 'view', + 'type': 'function' + }, + { + 'inputs': [ + { + 'internalType': 'address', + 'name': '_vault', + 'type': 'address' + } + ], + 'name': 'removeVaultFromRewards', + 'outputs': [], + 'stateMutability': 'nonpayable', + 'type': 'function' + }, + { + 'inputs': [], + 'name': 'renounceOwnership', + 'outputs': [], + 'stateMutability': 'nonpayable', + 'type': 'function' + }, + { + 'inputs': [ + { + 'internalType': 'address', + 'name': '_veToken', + 'type': 'address' + } + ], + 'name': 'setVe', + 'outputs': [], + 'stateMutability': 'nonpayable', + 'type': 'function' + }, + { + 'inputs': [ + { + 'internalType': 'address', + 'name': 'newOwner', + 'type': 'address' + } + ], + 'name': 'transferOwnership', + 'outputs': [], + 'stateMutability': 'nonpayable', + 'type': 'function' + }, + { + 'inputs': [ + { + 'internalType': 'address', + 'name': '', + 'type': 'address' + } + ], + 'name': 'vaultForGauge', + 'outputs': [ + { + 'internalType': 'address', + 'name': '', + 'type': 'address' + } + ], + 'stateMutability': 'view', + 'type': 'function' + }, + { + 'inputs': [], + 'name': 'veToken', + 'outputs': [ + { + 'internalType': 'address', + 'name': '', + 'type': 'address' + } + ], + 'stateMutability': 'view', + 'type': 'function' + }, + { + 'inputs': [], + 'name': 'veYfiRewardPool', + 'outputs': [ + { + 'internalType': 'address', + 'name': '', + 'type': 'address' + } + ], + 'stateMutability': 'view', + 'type': 'function' + }, + { + 'inputs': [], + 'name': 'yfi', + 'outputs': [ + { + 'internalType': 'address', + 'name': '', + 'type': 'address' + } + ], + 'stateMutability': 'view', + 'type': 'function' + } +] as const; + +export default VEYFI_REGISTRY_ABI; diff --git a/apps/veyfi/utils/abi/veYFIoYFI.abi.ts b/apps/veyfi/utils/abi/veYFIoYFI.abi.ts new file mode 100644 index 000000000..889de9c26 --- /dev/null +++ b/apps/veyfi/utils/abi/veYFIoYFI.abi.ts @@ -0,0 +1,380 @@ +const VEYFI_OYFI_ABI = [ + { + 'inputs': [], + 'stateMutability': 'nonpayable', + 'type': 'constructor' + }, + { + 'anonymous': false, + 'inputs': [ + { + 'indexed': true, + 'internalType': 'address', + 'name': 'owner', + 'type': 'address' + }, + { + 'indexed': true, + 'internalType': 'address', + 'name': 'spender', + 'type': 'address' + }, + { + 'indexed': false, + 'internalType': 'uint256', + 'name': 'value', + 'type': 'uint256' + } + ], + 'name': 'Approval', + 'type': 'event' + }, + { + 'anonymous': false, + 'inputs': [ + { + 'indexed': true, + 'internalType': 'address', + 'name': 'previousOwner', + 'type': 'address' + }, + { + 'indexed': true, + 'internalType': 'address', + 'name': 'newOwner', + 'type': 'address' + } + ], + 'name': 'OwnershipTransferred', + 'type': 'event' + }, + { + 'anonymous': false, + 'inputs': [ + { + 'indexed': true, + 'internalType': 'address', + 'name': 'from', + 'type': 'address' + }, + { + 'indexed': true, + 'internalType': 'address', + 'name': 'to', + 'type': 'address' + }, + { + 'indexed': false, + 'internalType': 'uint256', + 'name': 'value', + 'type': 'uint256' + } + ], + 'name': 'Transfer', + 'type': 'event' + }, + { + 'inputs': [ + { + 'internalType': 'address', + 'name': 'owner', + 'type': 'address' + }, + { + 'internalType': 'address', + 'name': 'spender', + 'type': 'address' + } + ], + 'name': 'allowance', + 'outputs': [ + { + 'internalType': 'uint256', + 'name': '', + 'type': 'uint256' + } + ], + 'stateMutability': 'view', + 'type': 'function' + }, + { + 'inputs': [ + { + 'internalType': 'address', + 'name': 'spender', + 'type': 'address' + }, + { + 'internalType': 'uint256', + 'name': 'amount', + 'type': 'uint256' + } + ], + 'name': 'approve', + 'outputs': [ + { + 'internalType': 'bool', + 'name': '', + 'type': 'bool' + } + ], + 'stateMutability': 'nonpayable', + 'type': 'function' + }, + { + 'inputs': [ + { + 'internalType': 'address', + 'name': 'account', + 'type': 'address' + } + ], + 'name': 'balanceOf', + 'outputs': [ + { + 'internalType': 'uint256', + 'name': '', + 'type': 'uint256' + } + ], + 'stateMutability': 'view', + 'type': 'function' + }, + { + 'inputs': [ + { + 'internalType': 'uint256', + 'name': '_amount', + 'type': 'uint256' + } + ], + 'name': 'burn', + 'outputs': [], + 'stateMutability': 'nonpayable', + 'type': 'function' + }, + { + 'inputs': [ + { + 'internalType': 'address', + 'name': '_owner', + 'type': 'address' + }, + { + 'internalType': 'uint256', + 'name': '_amount', + 'type': 'uint256' + } + ], + 'name': 'burn', + 'outputs': [], + 'stateMutability': 'nonpayable', + 'type': 'function' + }, + { + 'inputs': [], + 'name': 'decimals', + 'outputs': [ + { + 'internalType': 'uint8', + 'name': '', + 'type': 'uint8' + } + ], + 'stateMutability': 'view', + 'type': 'function' + }, + { + 'inputs': [ + { + 'internalType': 'address', + 'name': 'spender', + 'type': 'address' + }, + { + 'internalType': 'uint256', + 'name': 'subtractedValue', + 'type': 'uint256' + } + ], + 'name': 'decreaseAllowance', + 'outputs': [ + { + 'internalType': 'bool', + 'name': '', + 'type': 'bool' + } + ], + 'stateMutability': 'nonpayable', + 'type': 'function' + }, + { + 'inputs': [ + { + 'internalType': 'address', + 'name': 'spender', + 'type': 'address' + }, + { + 'internalType': 'uint256', + 'name': 'addedValue', + 'type': 'uint256' + } + ], + 'name': 'increaseAllowance', + 'outputs': [ + { + 'internalType': 'bool', + 'name': '', + 'type': 'bool' + } + ], + 'stateMutability': 'nonpayable', + 'type': 'function' + }, + { + 'inputs': [ + { + 'internalType': 'address', + 'name': '_to', + 'type': 'address' + }, + { + 'internalType': 'uint256', + 'name': '_amount', + 'type': 'uint256' + } + ], + 'name': 'mint', + 'outputs': [], + 'stateMutability': 'nonpayable', + 'type': 'function' + }, + { + 'inputs': [], + 'name': 'name', + 'outputs': [ + { + 'internalType': 'string', + 'name': '', + 'type': 'string' + } + ], + 'stateMutability': 'view', + 'type': 'function' + }, + { + 'inputs': [], + 'name': 'owner', + 'outputs': [ + { + 'internalType': 'address', + 'name': '', + 'type': 'address' + } + ], + 'stateMutability': 'view', + 'type': 'function' + }, + { + 'inputs': [], + 'name': 'renounceOwnership', + 'outputs': [], + 'stateMutability': 'nonpayable', + 'type': 'function' + }, + { + 'inputs': [], + 'name': 'symbol', + 'outputs': [ + { + 'internalType': 'string', + 'name': '', + 'type': 'string' + } + ], + 'stateMutability': 'view', + 'type': 'function' + }, + { + 'inputs': [], + 'name': 'totalSupply', + 'outputs': [ + { + 'internalType': 'uint256', + 'name': '', + 'type': 'uint256' + } + ], + 'stateMutability': 'view', + 'type': 'function' + }, + { + 'inputs': [ + { + 'internalType': 'address', + 'name': 'to', + 'type': 'address' + }, + { + 'internalType': 'uint256', + 'name': 'amount', + 'type': 'uint256' + } + ], + 'name': 'transfer', + 'outputs': [ + { + 'internalType': 'bool', + 'name': '', + 'type': 'bool' + } + ], + 'stateMutability': 'nonpayable', + 'type': 'function' + }, + { + 'inputs': [ + { + 'internalType': 'address', + 'name': 'from', + 'type': 'address' + }, + { + 'internalType': 'address', + 'name': 'to', + 'type': 'address' + }, + { + 'internalType': 'uint256', + 'name': 'amount', + 'type': 'uint256' + } + ], + 'name': 'transferFrom', + 'outputs': [ + { + 'internalType': 'bool', + 'name': '', + 'type': 'bool' + } + ], + 'stateMutability': 'nonpayable', + 'type': 'function' + }, + { + 'inputs': [ + { + 'internalType': 'address', + 'name': 'newOwner', + 'type': 'address' + } + ], + 'name': 'transferOwnership', + 'outputs': [], + 'stateMutability': 'nonpayable', + 'type': 'function' + } +] as const; + +export default VEYFI_OYFI_ABI; diff --git a/apps/veyfi/utils/actions/gauge.ts b/apps/veyfi/utils/actions/gauge.ts new file mode 100644 index 000000000..354275cbc --- /dev/null +++ b/apps/veyfi/utils/actions/gauge.ts @@ -0,0 +1,98 @@ +import {assert} from '@common/utils/assert'; +import {assertAddress, assertAddresses, handleTx as handleTxWagmi} from '@common/utils/wagmiUtils'; + +import VEYFI_CLAIM_REWARDS_ZAP_ABI from '../abi/veYFIClaimRewardsZap.abi'; +import VEYFI_GAUGE_ABI from '../abi/veYFIGauge.abi'; + +import type {TAddress} from '@yearn-finance/web-lib/types'; +import type {TTxResponse} from '@yearn-finance/web-lib/utils/web3/transaction'; +import type {TWriteTransaction} from '@common/utils/wagmiUtils'; + +type TApproveAndStake = TWriteTransaction & { + vaultAddress: TAddress; + allowance: bigint; + amount: bigint; +}; +export async function approveAndStake(props: TApproveAndStake): Promise { + assertAddress(props.contractAddress); + assertAddress(props.vaultAddress); + assert(props.amount > 0n, 'Amount is 0'); + + if(!(props.allowance >= props.amount)) { + await handleTxWagmi(props, { + address: props.vaultAddress, + abi: ['function approve(address _spender, uint256 _value) external'], + functionName: 'approve', + args: [props.contractAddress, props.amount] + }); + } + + return await handleTxWagmi(props, { + address: props.contractAddress, + abi: VEYFI_GAUGE_ABI, + functionName: 'deposit', + args: [props.amount] + }); +} + +type TStake = TWriteTransaction & { + amount: bigint; +}; +export async function stake(props: TStake): Promise { + assertAddress(props.contractAddress); + assert(props.amount > 0n, 'Amount is 0'); + + return await handleTxWagmi(props, { + address: props.contractAddress, + abi: VEYFI_GAUGE_ABI, + functionName: 'deposit', + args: [props.amount] + }); +} + +type TUnstake = TWriteTransaction & { + accountAddress: TAddress; + amount: bigint; +}; +export async function unstake(props: TUnstake): Promise { + assertAddress(props.contractAddress); + assertAddress(props.accountAddress); + assert(props.amount > 0n, 'Amount is 0'); + + const willClaim = false; + + return await handleTxWagmi(props, { + address: props.contractAddress, + abi: VEYFI_GAUGE_ABI, + functionName: 'withdraw', + args: [props.amount, props.accountAddress, props.accountAddress, willClaim] + }); +} + +type TClaimRewards = TWriteTransaction; +export async function claimRewards(props: TClaimRewards): Promise { + assertAddress(props.contractAddress); + + return await handleTxWagmi(props, { + address: props.contractAddress, + abi: VEYFI_GAUGE_ABI, + functionName: 'getReward' + }); +} + +type TClaimAllRewards = TWriteTransaction & { + gaugeAddresses: TAddress[]; + willLockRewards: boolean; + claimVotingEscrow?: boolean; +}; +export async function claimAllRewards(props: TClaimAllRewards): Promise { + assertAddress(props.contractAddress); + assertAddresses(props.gaugeAddresses); + + return await handleTxWagmi(props, { + address: props.contractAddress, + abi: VEYFI_CLAIM_REWARDS_ZAP_ABI, + functionName: 'claim', + args: [props.gaugeAddresses, props.willLockRewards, props.claimVotingEscrow] + }); +} diff --git a/apps/veyfi/utils/actions/option.ts b/apps/veyfi/utils/actions/option.ts new file mode 100644 index 000000000..90f7627f7 --- /dev/null +++ b/apps/veyfi/utils/actions/option.ts @@ -0,0 +1,27 @@ +import VEYFI_OPTIONS_ABI from '@veYFI/utils/abi/veYFIOptions.abi'; +import {assert} from '@common/utils/assert'; +import {assertAddress, handleTx as handleTxWagmi} from '@common/utils/wagmiUtils'; + +import type {TAddress} from '@yearn-finance/web-lib/types'; +import type {TTxResponse} from '@yearn-finance/web-lib/utils/web3/transaction'; +import type {TWriteTransaction} from '@common/utils/wagmiUtils'; + +type TRedeem = TWriteTransaction & { + accountAddress: TAddress; + amount: bigint; + ethRequired: bigint; +}; +export async function redeem(props: TRedeem): Promise { + assertAddress(props.contractAddress); + assertAddress(props.accountAddress); + assert(props.amount > 0n, 'amount is zero'); + assert(props.ethRequired > 0n, 'ethRequired is zero'); + + return await handleTxWagmi(props, { + address: props.contractAddress, + abi: VEYFI_OPTIONS_ABI, + functionName: 'exercise', + value: props.ethRequired, + args: [props.amount, props.accountAddress] + }); +} diff --git a/apps/veyfi/utils/actions/votingEscrow.ts b/apps/veyfi/utils/actions/votingEscrow.ts new file mode 100644 index 000000000..f8dd50fdd --- /dev/null +++ b/apps/veyfi/utils/actions/votingEscrow.ts @@ -0,0 +1,114 @@ +import {stringToHex} from 'viem'; +import {prepareWriteContract} from '@wagmi/core'; +import {MAX_UINT_256} from '@yearn-finance/web-lib/utils/constants'; +import {toBigInt} from '@yearn-finance/web-lib/utils/format.bigNumber'; +import {assert} from '@common/utils/assert'; +import {assertAddresses, handleTx as handleTxWagmi} from '@common/utils/wagmiUtils'; + +import SNAPSHOT_DELEGATE_REGISTRY_ABI from '../abi/SnapshotDelegateRegistry.abi'; +import VEYFI_ABI from '../abi/veYFI.abi'; +import {YEARN_SNAPSHOT_SPACE} from '../constants'; + +import type {TAddress} from '@yearn-finance/web-lib/types'; +import type {TSeconds} from '@yearn-finance/web-lib/utils/time'; +import type {TTxResponse} from '@yearn-finance/web-lib/utils/web3/transaction'; +import type {TWriteTransaction} from '@common/utils/wagmiUtils'; + +type TApproveLock = TWriteTransaction & {votingEscrowAddress: TAddress; amount: bigint;}; +export async function approveLock(props: TApproveLock): Promise { + const {votingEscrowAddress, amount = MAX_UINT_256, contractAddress} = props; + + assertAddresses([votingEscrowAddress, contractAddress]); + assert(amount > 0n, 'Amount is 0'); + + return await handleTxWagmi(props, { + address: contractAddress, + abi: ['function approve(address _spender, uint256 _value) external'], + functionName: 'approve', + args: [votingEscrowAddress, amount] + }); +} + +type TLock = TWriteTransaction & {votingEscrowAddress: TAddress; accountAddress: TAddress; amount: bigint; time: TSeconds}; +export async function lock(props: TLock): Promise { + assertAddresses([props.votingEscrowAddress, props.accountAddress, props.contractAddress]); + assert(props.amount > 0n, 'Amount is 0'); + assert(props.time > 0, 'Time is 0'); + + return await handleTxWagmi(props, { + address: props.contractAddress, + abi: VEYFI_ABI, + functionName: 'modify_lock', + args: [props.amount, toBigInt(props.time), props.accountAddress] + }); +} + +type TIncreaseLockAmount = TWriteTransaction & {votingEscrowAddress: TAddress; accountAddress: TAddress; amount: bigint}; +export async function increaseLockAmount(props: TIncreaseLockAmount): Promise { + assertAddresses([props.votingEscrowAddress, props.accountAddress, props.contractAddress]); + assert(props.amount > 0n, 'Amount is 0'); + + return await handleTxWagmi(props, { + address: props.contractAddress, + abi: VEYFI_ABI, + functionName: 'modify_lock', + args: [props.amount, 0n, props.accountAddress] + }); +} + +type TExtendLockTime = TWriteTransaction & {votingEscrowAddress: TAddress; accountAddress: TAddress; time: TSeconds}; +export async function extendLockTime(props: TExtendLockTime): Promise { + assertAddresses([props.votingEscrowAddress, props.accountAddress, props.contractAddress]); + assert(props.time > 0, 'Time is 0'); + + return await handleTxWagmi(props, { + address: props.contractAddress, + abi: VEYFI_ABI, + functionName: 'modify_lock', + args: [0n, toBigInt(props.time), props.accountAddress] + }); +} + +type TWithdrawUnlocked = TWriteTransaction & {votingEscrowAddress: TAddress}; +export async function withdrawUnlocked(props: TWithdrawUnlocked): Promise { + assertAddresses([props.votingEscrowAddress, props.contractAddress]); + + const {result} = await prepareWriteContract({ + address: props.votingEscrowAddress, + abi: VEYFI_ABI, + functionName: 'withdraw' + }); + + if (result.penalty > 0n) { + throw new Error('Tokens are not yet unlocked'); + } + + return await handleTxWagmi(props, { + address: props.contractAddress, + abi: VEYFI_ABI, + functionName: 'withdraw' + }); +} + +type TWithdrawLocked = TWriteTransaction & {votingEscrowAddress: TAddress}; +export async function withdrawLocked(props: TWithdrawLocked): Promise { + assertAddresses([props.votingEscrowAddress, props.contractAddress]); + + return await handleTxWagmi(props, { + address: props.contractAddress, + abi: VEYFI_ABI, + functionName: 'withdraw' + }); +} + +type TDelegateVote = TWriteTransaction & {delegateAddress: TAddress}; +export async function delegateVote(props: TDelegateVote): Promise { + assertAddresses([props.delegateAddress, props.contractAddress]); + + return await handleTxWagmi(props, { + address: props.contractAddress, + abi: SNAPSHOT_DELEGATE_REGISTRY_ABI, + functionName: 'setDelegate', + args: [stringToHex(YEARN_SNAPSHOT_SPACE, {size: 32}), props.delegateAddress] + }); +} diff --git a/apps/veyfi/utils/constants.ts b/apps/veyfi/utils/constants.ts index 6dceb85e7..e5e2a1e0d 100644 --- a/apps/veyfi/utils/constants.ts +++ b/apps/veyfi/utils/constants.ts @@ -1,6 +1,17 @@ +import {toAddress} from '@yearn-finance/web-lib/utils/address'; + +import type {TAddress} from '@yearn-finance/web-lib/types'; import type {TWeeks} from '@yearn-finance/web-lib/utils/time'; +export const VEYFI_REGISTRY_ADDRESS: TAddress = toAddress(''); // TODO: update once deployed +export const VEYFI_CLAIM_REWARDS_ZAP_ADDRESS: TAddress = toAddress(''); // TODO: update once deployed +export const VEYFI_OPTIONS_ADDRESS = toAddress(''); // TODO: update once deployed +export const VEYFI_OYFI_ADDRESS = toAddress(''); // TODO: update once deployed + export const MAX_LOCK_TIME: TWeeks = 208; export const MIN_LOCK_TIME: TWeeks = 1; export const MIN_LOCK_AMOUNT: TWeeks = 1; + +export const YEARN_SNAPSHOT_SPACE = 'veyfi.eth'; +export const SNAPSHOT_DELEGATE_REGISTRY_ADDRESS = toAddress('0x469788fE6E9E9681C6ebF3bF78e7Fd26Fc015446'); diff --git a/apps/veyfi/utils/index.ts b/apps/veyfi/utils/index.ts index e77832445..6f7d28cd8 100644 --- a/apps/veyfi/utils/index.ts +++ b/apps/veyfi/utils/index.ts @@ -1,6 +1,7 @@ import {toBigInt} from '@yearn-finance/web-lib/utils/format.bigNumber'; import {roundToWeek, toSeconds, YEAR} from '@yearn-finance/web-lib/utils/time'; +import type {TDict} from '@yearn-finance/web-lib/types'; import type {TMilliseconds, TSeconds} from '@yearn-finance/web-lib/utils/time'; const MAX_LOCK: TSeconds = toSeconds(roundToWeek(YEAR * 4)); @@ -15,3 +16,26 @@ export function getVotingPower(lockAmount: bigint, unlockTime: TMilliseconds): b } return lockAmount / toBigInt(MAX_LOCK) * toBigInt(duration); } + +export const keyBy = (array: T1[], key: T2): TDict => + (array || []).reduce((r, x): TDict => ({...r, [x[key] as string]: x}), {}); + +export const isNumberable = (value: unknown): boolean => !isNaN(value as number); + +export const isString = (value: unknown): value is string => typeof value === 'string'; + +export const sort = (data: T[], by: Extract, order?: 'asc' | 'desc'): T[] => { + const compare = (a: T, b: T): number => { + const elementA = a[by]; + const elementB = b[by]; + if (isNumberable(elementA) && isNumberable(elementB)) { + return order === 'desc' ? Number(elementA) - Number(elementB) : Number(elementB) - Number(elementA); + } + if (isString(elementA) && isString(elementB)) { + return order === 'desc' ? elementA.toLowerCase().localeCompare(elementB.toLowerCase()) : elementB.toLowerCase().localeCompare(elementA.toLowerCase()); + } + return 0; + }; + + return [...data].sort(compare); +}; diff --git a/apps/veyfi/utils/validations.ts b/apps/veyfi/utils/validations.ts index 63f933d78..d68f3fc3f 100644 --- a/apps/veyfi/utils/validations.ts +++ b/apps/veyfi/utils/validations.ts @@ -1,3 +1,4 @@ +import {isAddress} from 'viem'; import {allowanceKey} from '@yearn-finance/web-lib/utils/address'; import {isZero} from '@yearn-finance/web-lib/utils/isZero'; @@ -84,3 +85,21 @@ export function validateNetwork(props: TValidateNetworkProps): TValidationRespon return {isValid: true}; } + +export type TValidateAddressProps = { + address?: string; +} + +export function validateAddress(props: TValidateAddressProps): TValidationResponse { + const {address} = props; + + if(!address) { + return {isValid: false}; + } + + if (!isAddress(address)) { + return {isValid: false, error: 'Invalid Address'}; + } + + return {isValid: true}; +} diff --git a/package.json b/package.json index b92f86a01..e0987d5e7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "yearnfi", - "version": "0.1.24", + "version": "0.1.25", "scripts": { "dev": "next", "inspect": "NODE_OPTIONS='--inspect' next", @@ -75,9 +75,9 @@ "postcss-nesting": "^12.0.0", "sass": "^1.63.3", "sharp": "^0.32.1", + "vitest": "^0.32.2", "ts-loader": "^9.4.4", "typescript": "^5.1.6", - "vitest": "^0.32.0", "webpack": "^5.88.2" }, "resolutions": { diff --git a/pages/veyfi/index.tsx b/pages/veyfi/index.tsx index 48dceec81..1d346c84a 100755 --- a/pages/veyfi/index.tsx +++ b/pages/veyfi/index.tsx @@ -1,6 +1,10 @@ import {ClaimTab} from '@veYFI/components/ClaimTab'; +import {GaugesTab} from '@veYFI/components/GaugesTab'; import {LockTab} from '@veYFI/components/LockTab'; import {ManageLockTab} from '@veYFI/components/ManageLockTab'; +import {RedeemTab} from '@veYFI/components/RedeemTab'; +import {RewardsTab} from '@veYFI/components/RewardsTab'; +import {VoteTab} from '@veYFI/components/VoteTab'; import {useVotingEscrow} from '@veYFI/contexts/useVotingEscrow'; import Wrapper from '@veYFI/Wrapper'; import {formatToNormalizedValue, toBigInt} from '@yearn-finance/web-lib/utils/format.bigNumber'; @@ -8,6 +12,7 @@ import {formatAmount} from '@yearn-finance/web-lib/utils/format.number'; import {PageProgressBar} from '@common/components/PageProgressBar'; import {SummaryData} from '@common/components/SummaryData'; import {Tabs} from '@common/components/Tabs'; +import {useFeatureFlag} from '@common/hooks/useFeatureFlag'; import {formatDateShort} from '@common/utils'; import type {NextRouter} from 'next/router'; @@ -15,6 +20,7 @@ import type {ReactElement} from 'react'; function Index(): ReactElement { const {votingEscrow, positions, isLoading} = useVotingEscrow(); + const [isGaugeVotingFeatureEnabled] = useFeatureFlag('gauge-voting'); const totalLockedYFI = formatToNormalizedValue(toBigInt(votingEscrow?.supply), 18); const yourLockedYFI = formatToNormalizedValue(toBigInt(positions?.deposit?.underlyingBalance), 18); @@ -22,12 +28,18 @@ function Index(): ReactElement { const tabs = [ {id: 'lock', label: 'Lock YFI', content: }, {id: 'manage', label: 'Manage lock', content: }, - {id: 'claim', label: 'Claim', content: } - ]; + {id: 'claim', label: 'Claim', content: }, + ...isGaugeVotingFeatureEnabled ? [ + {id: 'gauges', label: 'Stake / Unstake', content: }, + {id: 'rewards', label: 'Rewards', content: }, + {id: 'redeem', label: 'Redeem oYFI', content: }, + {id: 'vote', label: 'Vote for Gauge', content: } + ] : [] + ].filter(Boolean); return ( <> - +

{'veYFI'} diff --git a/yarn.lock b/yarn.lock index 8555355a7..a9b383b40 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2786,13 +2786,13 @@ "@babel/plugin-transform-react-jsx-source" "^7.22.5" react-refresh "^0.14.0" -"@vitest/expect@0.32.4": - version "0.32.4" - resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-0.32.4.tgz#4aa4eec78112cdbe299834b965420d4fb3afa91d" - integrity sha512-m7EPUqmGIwIeoU763N+ivkFjTzbaBn0n9evsTOcde03ugy2avPs3kZbYmw3DkcH1j5mxhMhdamJkLQ6dM1bk/A== +"@vitest/expect@0.32.2": + version "0.32.2" + resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-0.32.2.tgz#8111f6ab1ff3b203efbe3a25e8bb2d160ce4b720" + integrity sha512-6q5yzweLnyEv5Zz1fqK5u5E83LU+gOMVBDuxBl2d2Jfx1BAp5M+rZgc5mlyqdnxquyoiOXpXmFNkcGcfFnFH3Q== dependencies: - "@vitest/spy" "0.32.4" - "@vitest/utils" "0.32.4" + "@vitest/spy" "0.32.2" + "@vitest/utils" "0.32.2" chai "^4.3.7" "@vitest/expect@0.33.0": @@ -2804,14 +2804,15 @@ "@vitest/utils" "0.33.0" chai "^4.3.7" -"@vitest/runner@0.32.4": - version "0.32.4" - resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-0.32.4.tgz#2872c697994745f1b70e2bd6568236ad2d9eade6" - integrity sha512-cHOVCkiRazobgdKLnczmz2oaKK9GJOw6ZyRcaPdssO1ej+wzHVIkWiCiNacb3TTYPdzMddYkCgMjZ4r8C0JFCw== +"@vitest/runner@0.32.2": + version "0.32.2" + resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-0.32.2.tgz#18dd979ce4e8766bcc90948d11b4c8ae6ed90b89" + integrity sha512-06vEL0C1pomOEktGoLjzZw+1Fb+7RBRhmw/06WkDrd1akkT9i12su0ku+R/0QM69dfkIL/rAIDTG+CSuQVDcKw== dependencies: - "@vitest/utils" "0.32.4" + "@vitest/utils" "0.32.2" + concordance "^5.0.4" p-limit "^4.0.0" - pathe "^1.1.1" + pathe "^1.1.0" "@vitest/runner@0.33.0": version "0.33.0" @@ -2822,14 +2823,14 @@ p-limit "^4.0.0" pathe "^1.1.1" -"@vitest/snapshot@0.32.4": - version "0.32.4" - resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-0.32.4.tgz#75166b1c772d018278a7f0e79f43f3eae813f5ae" - integrity sha512-IRpyqn9t14uqsFlVI2d7DFMImGMs1Q9218of40bdQQgMePwVdmix33yMNnebXcTzDU5eiV3eUsoxxH5v0x/IQA== +"@vitest/snapshot@0.32.2": + version "0.32.2" + resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-0.32.2.tgz#500b6453e88e4c50a0aded39839352c16b519b9e" + integrity sha512-JwhpeH/PPc7GJX38vEfCy9LtRzf9F4er7i4OsAJyV7sjPwjj+AIR8cUgpMTWK4S3TiamzopcTyLsZDMuldoi5A== dependencies: magic-string "^0.30.0" - pathe "^1.1.1" - pretty-format "^29.5.0" + pathe "^1.1.0" + pretty-format "^27.5.1" "@vitest/snapshot@0.33.0": version "0.33.0" @@ -2840,12 +2841,12 @@ pathe "^1.1.1" pretty-format "^29.5.0" -"@vitest/spy@0.32.4": - version "0.32.4" - resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-0.32.4.tgz#c3212bc60c1430c3b5c39d6a384a75458b8f1e80" - integrity sha512-oA7rCOqVOOpE6rEoXuCOADX7Lla1LIa4hljI2MSccbpec54q+oifhziZIJXxlE/CvI2E+ElhBHzVu0VEvJGQKQ== +"@vitest/spy@0.32.2": + version "0.32.2" + resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-0.32.2.tgz#f3ef7afe0d34e863b90df7c959fa5af540a6aaf9" + integrity sha512-Q/ZNILJ4ca/VzQbRM8ur3Si5Sardsh1HofatG9wsJY1RfEaw0XKP8IVax2lI1qnrk9YPuG9LA2LkZ0EI/3d4ug== dependencies: - tinyspy "^2.1.1" + tinyspy "^2.1.0" "@vitest/spy@0.33.0": version "0.33.0" @@ -2854,14 +2855,14 @@ dependencies: tinyspy "^2.1.1" -"@vitest/utils@0.32.4": - version "0.32.4" - resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-0.32.4.tgz#36283e3aa3f3b1a378e19493c7b3b9107dc4ea71" - integrity sha512-Gwnl8dhd1uJ+HXrYyV0eRqfmk9ek1ASE/LWfTCuWMw+d07ogHqp4hEAV28NiecimK6UY9DpSEPh+pXBA5gtTBg== +"@vitest/utils@0.32.2": + version "0.32.2" + resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-0.32.2.tgz#809c720cafbf4b35ce651deb8570d57785e77819" + integrity sha512-lnJ0T5i03j0IJaeW73hxe2AuVnZ/y1BhhCOuIcl9LIzXnbpXJT9Lrt6brwKHXLOiA7MZ6N5hSJjt0xE1dGNCzQ== dependencies: diff-sequences "^29.4.3" loupe "^2.3.6" - pretty-format "^29.5.0" + pretty-format "^27.5.1" "@vitest/utils@0.33.0": version "0.33.0" @@ -3889,6 +3890,11 @@ bl@^4.0.3: inherits "^2.0.4" readable-stream "^3.4.0" +blueimp-md5@^2.10.0: + version "2.19.0" + resolved "https://registry.yarnpkg.com/blueimp-md5/-/blueimp-md5-2.19.0.tgz#b53feea5498dcb53dc6ec4b823adb84b729c4af0" + integrity sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w== + bn.js@5.2.1, bn.js@^4.11.0, bn.js@^4.11.8, bn.js@^4.11.9, bn.js@^5.0.0, bn.js@^5.1.1, bn.js@^5.2.0, bn.js@^5.2.1: version "5.2.1" resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.1.tgz#0bc527a6a0d18d0aa8d5b0538ce4a77dccfa7b70" @@ -4282,6 +4288,20 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== +concordance@^5.0.4: + version "5.0.4" + resolved "https://registry.yarnpkg.com/concordance/-/concordance-5.0.4.tgz#9896073261adced72f88d60e4d56f8efc4bbbbd2" + integrity sha512-OAcsnTEYu1ARJqWVGwf4zh4JDfHZEaSNlNccFmt8YjB2l/n19/PF2viLINHc57vO4FKIAFl2FWASIGZZWZ2Kxw== + dependencies: + date-time "^3.1.0" + esutils "^2.0.3" + fast-diff "^1.2.0" + js-string-escape "^1.0.1" + lodash "^4.17.15" + md5-hex "^3.0.1" + semver "^7.3.2" + well-known-symbols "^2.0.0" + convert-source-map@^1.7.0: version "1.9.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f" @@ -4448,6 +4468,13 @@ damerau-levenshtein@^1.0.8: resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7" integrity sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA== +date-time@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/date-time/-/date-time-3.1.0.tgz#0d1e934d170579f481ed8df1e2b8ff70ee845e1e" + integrity sha512-uqCUKXE5q1PNBXjPqvwhwJf9SwMoAHBgWJ6DcrnS5o+W2JOiIILl0JEdVD8SGujrNS02GGxgwAg2PN2zONgtjg== + dependencies: + time-zone "^1.0.0" + dayjs@^1.11.8, dayjs@^1.11.9: version "1.11.9" resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.9.tgz#9ca491933fadd0a60a2c19f6c237c03517d71d1a" @@ -5150,7 +5177,7 @@ estree-walker@^2.0.2: resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac" integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== -esutils@^2.0.2: +esutils@^2.0.2, esutils@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== @@ -5333,6 +5360,11 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== +fast-diff@^1.2.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.3.0.tgz#ece407fa550a64d638536cd727e129c61616e0f0" + integrity sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw== + fast-equals@^5.0.0: version "5.0.1" resolved "https://registry.yarnpkg.com/fast-equals/-/fast-equals-5.0.1.tgz#a4eefe3c5d1c0d021aeed0bc10ba5e0c12ee405d" @@ -6361,6 +6393,11 @@ js-sha3@0.8.0: resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.8.0.tgz#b9b7a5da73afad7dedd0f8c463954cbde6818840" integrity sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q== +js-string-escape@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/js-string-escape/-/js-string-escape-1.0.1.tgz#e2625badbc0d67c7533e9edc1068c587ae4137ef" + integrity sha512-Smw4xcfIQ5LVjAOuJCvN/zIodzA/BBSsluuoSykP+lUvScIi4U6RJLfwHet5cxFnCswUjISV8oAXaqaJDY3chg== + "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" @@ -6646,7 +6683,7 @@ lodash.sortby@^4.7.0: resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" integrity sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA== -lodash@^4.17.19, lodash@^4.17.2, lodash@^4.17.20, lodash@^4.17.21: +lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.2, lodash@^4.17.20, lodash@^4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -6722,6 +6759,13 @@ make-error@1.x: resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== +md5-hex@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/md5-hex/-/md5-hex-3.0.1.tgz#be3741b510591434b2784d79e556eefc2c9a8e5c" + integrity sha512-BUiRtTtV39LIJwinWBjqVsU9xhdnz7/i889V859IBFpuqGAj6LuOvHv5XLbgZ2R7ptJoJaEcxkv88/h25T7Ciw== + dependencies: + blueimp-md5 "^2.10.0" + md5.js@^1.3.4: version "1.3.5" resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f" @@ -7500,7 +7544,7 @@ pretty-bytes@^5.3.0, pretty-bytes@^5.4.1: resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb" integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg== -pretty-format@^27.0.2: +pretty-format@^27.0.2, pretty-format@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.5.1.tgz#2181879fdea51a7a5851fb39d920faa63f01d88e" integrity sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ== @@ -8088,7 +8132,7 @@ semver@^6.0.0, semver@^6.3.0, semver@^6.3.1: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.3, semver@^7.5.4: +semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.3, semver@^7.5.4: version "7.5.4" resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== @@ -8271,11 +8315,16 @@ stacktrace-parser@^0.1.10: dependencies: type-fest "^0.7.1" -std-env@^3.3.3: +std-env@^3.3.2: version "3.3.3" resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.3.3.tgz#a54f06eb245fdcfef53d56f3c0251f1d5c3d01fe" integrity sha512-Rz6yejtVyWnVjC1RFvNmYL10kgjC49EOghxWn0RFqlCHGFpQx+Xe7yW3I4ceK1SGrWIGMjD5Kbue8W/udkbMJg== +std-env@^3.3.3: + version "3.4.3" + resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.4.3.tgz#326f11db518db751c83fd58574f449b7c3060910" + integrity sha512-f9aPhy8fYBuMN+sNfakZV18U39PbalgjXG3lLB9WkaYTxijru61wb57V9wxxNthXM5Sd88ETBWi29qLAsHO52Q== + stop-iteration-iterator@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz#6a60be0b4ee757d1ed5254858ec66b10c49285e4" @@ -8675,6 +8724,11 @@ thread-stream@^0.15.1: resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== +time-zone@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/time-zone/-/time-zone-1.0.0.tgz#99c5bf55958966af6d06d83bdf3800dc82faec5d" + integrity sha512-TIsDdtKo6+XrPtiTm1ssmMngN1sAhyKnTO2kunQWqNPWIVvCm15Wmw4SWInwTVgJ5u/Tr04+8Ei9TNcw4x4ONA== + tinybench@^2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/tinybench/-/tinybench-2.5.0.tgz#4711c99bbf6f3e986f67eb722fed9cddb3a68ba5" @@ -8690,7 +8744,7 @@ tinypool@^0.6.0: resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-0.6.0.tgz#c3640b851940abe2168497bb6e43b49fafb3ba7b" integrity sha512-FdswUUo5SxRizcBc6b1GSuLpLjisa8N8qMyYoP3rl+bym+QauhtJP5bvZY1ytt8krKGmMLYIRl36HBZfeAoqhQ== -tinyspy@^2.1.1: +tinyspy@^2.1.0, tinyspy@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/tinyspy/-/tinyspy-2.1.1.tgz#9e6371b00c259e5c5b301917ca18c01d40ae558c" integrity sha512-XPJL2uSzcOyBMky6OFrusqWlzfFrXtE0hPuMgW8A2HmaqrPo4ZQHRN/V0QXN3FSjKxpsbRrFc5LI7KOwBsT1/w== @@ -9091,15 +9145,15 @@ viem@^1.5.3: isomorphic-ws "5.0.0" ws "8.12.0" -vite-node@0.32.4: - version "0.32.4" - resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-0.32.4.tgz#7b3f94af5a87c631fbc380ba662914bafbd04d80" - integrity sha512-L2gIw+dCxO0LK14QnUMoqSYpa9XRGnTTTDjW2h19Mr+GR0EFj4vx52W41gFXfMLqpA00eK4ZjOVYo1Xk//LFEw== +vite-node@0.32.2: + version "0.32.2" + resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-0.32.2.tgz#bfccdfeb708b2309ea9e5fe424951c75bb9c0096" + integrity sha512-dTQ1DCLwl2aEseov7cfQ+kDMNJpM1ebpyMMMwWzBvLbis8Nla/6c9WQcqpPssTwS6Rp/+U6KwlIj8Eapw4bLdA== dependencies: cac "^6.7.14" debug "^4.3.4" - mlly "^1.4.0" - pathe "^1.1.1" + mlly "^1.2.0" + pathe "^1.1.0" picocolors "^1.0.0" vite "^3.0.0 || ^4.0.0" @@ -9126,34 +9180,35 @@ vite-node@0.33.0: optionalDependencies: fsevents "~2.3.2" -vitest@^0.32.0: - version "0.32.4" - resolved "https://registry.yarnpkg.com/vitest/-/vitest-0.32.4.tgz#a0558ae44c2ccdc254eece0365f16c4ffc5231bb" - integrity sha512-3czFm8RnrsWwIzVDu/Ca48Y/M+qh3vOnF16czJm98Q/AN1y3B6PBsyV8Re91Ty5s7txKNjEhpgtGPcfdbh2MZg== +vitest@^0.32.2: + version "0.32.2" + resolved "https://registry.yarnpkg.com/vitest/-/vitest-0.32.2.tgz#758ce2220f609e240ac054eca7ad11a5140679ab" + integrity sha512-hU8GNNuQfwuQmqTLfiKcqEhZY72Zxb7nnN07koCUNmntNxbKQnVbeIS6sqUgR3eXSlbOpit8+/gr1KpqoMgWCQ== dependencies: "@types/chai" "^4.3.5" "@types/chai-subset" "^1.3.3" "@types/node" "*" - "@vitest/expect" "0.32.4" - "@vitest/runner" "0.32.4" - "@vitest/snapshot" "0.32.4" - "@vitest/spy" "0.32.4" - "@vitest/utils" "0.32.4" - acorn "^8.9.0" + "@vitest/expect" "0.32.2" + "@vitest/runner" "0.32.2" + "@vitest/snapshot" "0.32.2" + "@vitest/spy" "0.32.2" + "@vitest/utils" "0.32.2" + acorn "^8.8.2" acorn-walk "^8.2.0" cac "^6.7.14" chai "^4.3.7" + concordance "^5.0.4" debug "^4.3.4" local-pkg "^0.4.3" magic-string "^0.30.0" - pathe "^1.1.1" + pathe "^1.1.0" picocolors "^1.0.0" - std-env "^3.3.3" + std-env "^3.3.2" strip-literal "^1.0.1" tinybench "^2.5.0" tinypool "^0.5.0" vite "^3.0.0 || ^4.0.0" - vite-node "0.32.4" + vite-node "0.32.2" why-is-node-running "^2.2.2" vitest@^0.33.0: @@ -9281,6 +9336,11 @@ webpack@^5.88.2: watchpack "^2.4.0" webpack-sources "^3.2.3" +well-known-symbols@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/well-known-symbols/-/well-known-symbols-2.0.0.tgz#e9c7c07dbd132b7b84212c8174391ec1f9871ba5" + integrity sha512-ZMjC3ho+KXo0BfJb7JgtQ5IBuvnShdlACNkKkdsqBmYw3bPAaJfPeYUo6tLUaT5tG/Gkh7xkpBhKRQ9e7pyg9Q== + whatwg-url@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d"