diff --git a/apps/govern/common-util/functions/balance.ts b/apps/govern/common-util/functions/balance.ts index d31b3dc4..dd2a3bbd 100644 --- a/apps/govern/common-util/functions/balance.ts +++ b/apps/govern/common-util/functions/balance.ts @@ -1,20 +1,34 @@ +import isNil from 'lodash/isNil'; + +/** + * Return formatted number with appropriate suffix + */ +export const formatWeiNumber = ({ + value, + minimumFractionDigits = 0, + maximumFractionDigits = 2, +}: { + value: number | string | undefined; + minimumFractionDigits?: number; + maximumFractionDigits?: number; +}) => { + if (isNil(value) || Number(value) === 0) return '0'; + + return new Intl.NumberFormat('en', { + notation: 'compact', + minimumFractionDigits, + maximumFractionDigits, + }).format(Number(value)); +}; + /** - * @param {number} balanceInWei - * @returns formatted balance with appropriate suffix + * Converts a number to a comma separated format + * eg: 1000000 => 1,000,000, 12345.67 => 12,345.67 */ -export const formatWeiBalance = (balanceInWei: number | string) => { - const formatNumberWithSuffix = (number: number) => { - if (number >= 1e9) { - return `${Math.floor((number / 1e9) * 10) / 10}B`; - } - if (number >= 1e6) { - return `${Math.floor((number / 1e6) * 10) / 10}M`; - } - if (number >= 1e3) { - return `${Math.floor((number / 1e3) * 10) / 10}k`; - } - return Math.floor(number * 10) / 10; - }; +export const getCommaSeparatedNumber = (value: number | string | undefined) => { + if (isNil(value) || Number(value) === 0) return '0'; - return formatNumberWithSuffix(parseFloat(`${balanceInWei}`)); + return new Intl.NumberFormat('en', { + maximumFractionDigits: 2, + }).format(Number(value)); }; diff --git a/apps/govern/common-util/functions/index.ts b/apps/govern/common-util/functions/index.ts index ed7f47e7..ca91ccfb 100644 --- a/apps/govern/common-util/functions/index.ts +++ b/apps/govern/common-util/functions/index.ts @@ -3,3 +3,4 @@ export * from './frontend-library'; export * from './requests'; export * from './web3'; export * from './balance'; +export * from './time'; diff --git a/apps/govern/common-util/functions/requests.ts b/apps/govern/common-util/functions/requests.ts index 228b6cc5..2e448b3a 100644 --- a/apps/govern/common-util/functions/requests.ts +++ b/apps/govern/common-util/functions/requests.ts @@ -1,19 +1,20 @@ import { readContract, readContracts } from '@wagmi/core'; -import { AbiFunction } from 'viem'; +import { ethers } from 'ethers'; +import { AbiFunction, TransactionReceipt } from 'viem'; import { Address } from 'viem'; import { mainnet } from 'viem/chains'; import { sendTransaction } from '@autonolas/frontend-library'; import { STAKING_FACTORY, VE_OLAS } from 'libs/util-contracts/src/lib/abiAndAddresses'; -import { getEstimatedGasLimit } from 'libs/util-functions/src'; +import { getEstimatedGasLimit, notifyError } from 'libs/util-functions/src'; import { SUPPORTED_CHAINS, wagmiConfig } from 'common-util/config/wagmi'; import { RPC_URLS } from 'common-util/constants/rpcs'; import { getAddressFromBytes32 } from './addresses'; import { getUnixNextWeekStartTimestamp } from './time'; -import { getVoteWeightingContract } from './web3'; +import { getOlasContract, getVeOlasContract, getVoteWeightingContract } from './web3'; type VoteForNomineeWeightsParams = { account: Address | undefined; @@ -113,3 +114,177 @@ export const checkLockExpired = async (account: Address) => { return result ? nextWeek >= (result as number) : false; }; + +/** + * Approve amount of OLAS to be used + */ +export const approveOlasByOwner = ({ account, amount }: { account: Address; amount: bigint }) => + new Promise((resolve, reject) => { + const contract = getOlasContract(); + const spender = (VE_OLAS.addresses as Record)[mainnet.id]; + const fn = contract.methods.approve(spender, amount).send({ from: account }); + + sendTransaction(fn, account, { + supportedChains: SUPPORTED_CHAINS, + rpcUrls: RPC_URLS, + }) + .then((response) => { + resolve(response); + }) + .catch((e) => { + window.console.log('Error occurred on approving OLAS by owner'); + reject(e); + }); + }); + +/** + * Check if `Approve` button can be clicked; `allowance` should be greater than or equal to the amount + */ +export const hasSufficientTokensRequest = ({ + account, + amount, +}: { + account: Address; + amount: number; +}) => + new Promise((resolve, reject) => { + const contract = getOlasContract(); + const spender = (VE_OLAS.addresses as Record)[mainnet.id]; + + contract.methods + .allowance(account, spender) + .call() + .then((response: bigint) => { + const responseInBg = ethers.toBigInt(response); + + // Resolve false if the response amount is zero + if (responseInBg === ethers.toBigInt(0)) { + resolve(false); + return; + } + + const amountBN = ethers.parseUnits(`${amount}`); + + // check if the allowance is greater than or equal to the amount input + resolve(responseInBg >= amountBN); + }) + .catch((e: Error) => { + window.console.log('Error occurred on calling `allowance` method'); + reject(e); + }); + }); + +/** + * Create lock for veOLAS + */ +export const createLockRequest = async ({ + account, + amount, + unlockTime, +}: { + account: Address; + amount: string; + unlockTime: number; +}) => { + const contract = getVeOlasContract(); + + try { + const createLockFn = contract.methods.createLock(amount, unlockTime); + const estimatedGas = await getEstimatedGasLimit(createLockFn, account); + const fn = createLockFn.send({ from: account, gasLimit: estimatedGas }); + + const response = await sendTransaction(fn, account, { + supportedChains: SUPPORTED_CHAINS, + rpcUrls: RPC_URLS, + }); + + return (response as TransactionReceipt)?.transactionHash; + } catch (error) { + window.console.log('Error occurred on creating lock for veOLAS'); + throw error; + } +}; + +/** + * Increase Olas amount locked without modifying the lock time + */ +export const updateIncreaseAmount = async ({ + account, + amount, +}: { + account: Address; + amount: string; +}) => { + const contract = getVeOlasContract(); + + try { + const increaseAmountFn = contract.methods.increaseAmount(amount); + const estimatedGas = await getEstimatedGasLimit(increaseAmountFn, account); + const fn = increaseAmountFn.send({ from: account, gasLimit: estimatedGas }); + + const response = await sendTransaction(fn, account, { + supportedChains: SUPPORTED_CHAINS, + rpcUrls: RPC_URLS, + }); + + return (response as TransactionReceipt)?.transactionHash; + } catch (e) { + window.console.log('Error occurred on increasing amount with estimated gas'); + throw e; + } +}; + +/** + * Increase the unlock time without modifying the amount + */ +export const updateIncreaseUnlockTime = async ({ + account, + time, +}: { + account: Address; + time: number; +}) => { + const contract = getVeOlasContract(); + + try { + const increaseUnlockTimeFn = contract.methods.increaseUnlockTime(time); + const estimatedGas = await getEstimatedGasLimit(increaseUnlockTimeFn, account); + const fn = increaseUnlockTimeFn.send({ + from: account, + gasLimit: estimatedGas, + }); + + const response = await sendTransaction(fn, account, { + supportedChains: SUPPORTED_CHAINS, + rpcUrls: RPC_URLS, + }); + + return (response as TransactionReceipt)?.transactionHash; + } catch (error) { + window.console.log('Error occurred on increasing unlock time'); + throw error; + } +}; + +/** + * Withdraw VeOlas + */ +export const withdrawVeolasRequest = async ({ account }: { account: Address }) => { + const contract = getVeOlasContract(); + + try { + const withdrawFn = contract.methods.withdraw(); + const estimatedGas = await getEstimatedGasLimit(withdrawFn, account); + const fn = withdrawFn.send({ from: account, gasLimit: estimatedGas }); + + const response = await sendTransaction(fn, account, { + supportedChains: SUPPORTED_CHAINS, + rpcUrls: RPC_URLS, + }); + + return (response as TransactionReceipt)?.transactionHash; + } catch (error) { + window.console.log('Error occurred on withdrawing veOlas'); + throw error; + } +}; diff --git a/apps/govern/common-util/functions/time.ts b/apps/govern/common-util/functions/time.ts index b2629f4b..0f2f9b34 100644 --- a/apps/govern/common-util/functions/time.ts +++ b/apps/govern/common-util/functions/time.ts @@ -1,3 +1,5 @@ +import { NA } from 'libs/util-constants/src'; + // Returns the closest Thursday in the future // which is the start of the next week by Unix time export const getUnixNextWeekStartTimestamp = () => { @@ -25,3 +27,78 @@ export const getUnixWeekStartTimestamp = () => { return result.getTime() / 1000; }; + +const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; + +/** + * Get formatted date from milliseconds + * example, 1678320000000 => Mar 09 '23 + */ +export function getFormattedDate(ms: number): string { + if (ms == 0) return NA; + + let date = new Date(ms); + let month = MONTHS[date.getMonth()]; + let day = date.getUTCDate(); + let year = date.getUTCFullYear() % 100; // Get last two digits of year + + return ( + month + + ' ' + + (day < 10 ? '0' : '') + + day.toString() + + " '" + + (year < 10 ? '0' : '') + + year.toString() + ); +} + +/** + * Get formatted date from milliseconds including time + * example, 1678320000000 => Mar 09 '2023 16:00 + */ +export function getFullFormattedDate(ms: number): string { + if (ms == 0) return NA; + + let date = new Date(ms); + let month = MONTHS[date.getMonth()]; + let day = date.getUTCDate(); + let year = date.getUTCFullYear(); + let hours = date.getUTCHours(); + let minutes = date.getUTCMinutes(); + + return ( + month + + ' ' + + (day < 10 ? '0' : '') + + day.toString() + + ' ' + + year.toString() + + ', ' + + (hours < 10 ? '0' : '') + + hours.toString() + + ':' + + (minutes < 10 ? '0' : '') + + minutes.toString() + ); +} + +export const dateInMs = (time: number) => { + if (!time) return 0; + return Math.round(new Date(time).getTime()); +}; + +/* + * Returns the remaining time in seconds between unlockTime and the current time. + * Steps: + * 1. Convert unlockTime to a timestamp + * 2. Get the current time as a timestamp + * 3. Calculate the difference between the future timestamp and the current timestamp, convert to seconds. + */ +export const getRemainingTimeInSeconds = (unlockTime?: number) => { + if (!unlockTime) return 0; + + const futureDateInTimeStamp = new Date(unlockTime).getTime(); + const todayDateInTimeStamp = new Date().getTime(); + return Math.round((futureDateInTimeStamp - todayDateInTimeStamp) / 1000); +}; diff --git a/apps/govern/common-util/functions/web3.ts b/apps/govern/common-util/functions/web3.ts index 3c8c52fe..3362c14a 100644 --- a/apps/govern/common-util/functions/web3.ts +++ b/apps/govern/common-util/functions/web3.ts @@ -1,7 +1,8 @@ +import { mainnet } from 'viem/chains'; import Web3 from 'web3'; import { AbiItem } from 'web3-utils'; -import { VOTE_WEIGHTING } from 'libs/util-contracts/src/lib/abiAndAddresses'; +import { OLAS, VE_OLAS, VOTE_WEIGHTING } from 'libs/util-contracts/src/lib/abiAndAddresses'; import { getChainId, getProvider } from 'common-util/functions/frontend-library'; @@ -26,9 +27,22 @@ const getContract = (abi: AbiItem[], contractAddress: string, chainId?: number) }; export const getVoteWeightingContract = () => { - const { chainId } = getWeb3Details(); const abi = VOTE_WEIGHTING.abi as AbiItem[]; - const address = (VOTE_WEIGHTING.addresses as Record)[chainId as number]; + const address = (VOTE_WEIGHTING.addresses as Record)[mainnet.id]; + const contract = getContract(abi, address); + return contract; +}; + +export const getOlasContract = () => { + const abi = OLAS.abi as AbiItem[]; + const address = (OLAS.addresses as Record)[mainnet.id]; + const contract = getContract(abi, address); + return contract; +}; + +export const getVeOlasContract = () => { + const abi = VE_OLAS.abi as AbiItem[]; + const address = (VE_OLAS.addresses as Record)[mainnet.id]; const contract = getContract(abi, address); return contract; }; diff --git a/apps/govern/components/Contracts/ContractsList.tsx b/apps/govern/components/Contracts/ContractsList.tsx index 4c0249bb..78ea8cc2 100644 --- a/apps/govern/components/Contracts/ContractsList.tsx +++ b/apps/govern/components/Contracts/ContractsList.tsx @@ -7,7 +7,7 @@ import { useAccount } from 'wagmi'; import { CHAIN_NAMES } from 'libs/util-constants/src'; -import { formatWeiBalance } from 'common-util/functions/balance'; +import { formatWeiNumber } from 'common-util/functions/balance'; import { NextWeekTooltip } from 'components/NextWeekTooltip'; import { useVotingPower } from 'hooks/useVotingPower'; import { useAppSelector } from 'store/index'; @@ -59,7 +59,9 @@ const getColumns = ({ render: (currentWeight) => ( {`${currentWeight?.percentage.toFixed(2)}%`} - {`${formatWeiBalance(currentWeight?.value)} veOLAS`} + {`${formatWeiNumber({ + value: currentWeight?.value, + })} veOLAS`} ), }, @@ -70,7 +72,7 @@ const getColumns = ({ render: (nextWeight) => ( {`${nextWeight?.percentage.toFixed(2)}%`} - {`${formatWeiBalance(nextWeight?.value)} veOLAS`} + {`${formatWeiNumber({ value: nextWeight?.value })} veOLAS`} ), }, diff --git a/apps/govern/components/Layout/Balance.tsx b/apps/govern/components/Layout/Balance.tsx index a94ae862..5a8f24f0 100644 --- a/apps/govern/components/Layout/Balance.tsx +++ b/apps/govern/components/Layout/Balance.tsx @@ -5,7 +5,7 @@ import { useAccount } from 'wagmi'; import { COLOR } from 'libs/ui-theme/src/lib/ui-theme'; import { MEMBER_URL, UNICODE_SYMBOLS } from 'libs/util-constants/src'; -import { formatWeiBalance } from 'common-util/functions'; +import { formatWeiNumber } from 'common-util/functions'; import { useVotingPower } from 'hooks/index'; const { Text, Paragraph } = Typography; @@ -35,7 +35,7 @@ export const Balance = () => { Your voting power: - {formatWeiBalance(data)} veOLAS + {formatWeiNumber({ value: data })} veOLAS ); diff --git a/apps/govern/components/Layout/Menu.tsx b/apps/govern/components/Layout/Menu.tsx index ccd01329..b070112c 100644 --- a/apps/govern/components/Layout/Menu.tsx +++ b/apps/govern/components/Layout/Menu.tsx @@ -11,6 +11,7 @@ interface MenuItem { const items: MenuItem[] = [ { label: 'Staking Contracts', key: 'contracts', path: '/contracts' }, { label: 'Proposals', key: 'proposals', path: '/proposals' }, + { label: 'veOLAS', key: 'veolas', path: '/veolas' }, { label: 'Docs', key: 'docs', path: '/docs' }, ]; diff --git a/apps/govern/components/VeOlas/ApproveOlasModal.tsx b/apps/govern/components/VeOlas/ApproveOlasModal.tsx new file mode 100644 index 00000000..bf861d40 --- /dev/null +++ b/apps/govern/components/VeOlas/ApproveOlasModal.tsx @@ -0,0 +1,87 @@ +import { Alert, Button, Divider, Flex, Form, Modal } from 'antd'; +import { ethers } from 'ethers'; +import { useState } from 'react'; +import { useAccount } from 'wagmi'; + +import { notifyError, notifySuccess } from 'libs/util-functions/src'; + +import { + approveOlasByOwner, + createLockRequest, + dateInMs, + getRemainingTimeInSeconds, + hasSufficientTokensRequest, +} from 'common-util/functions'; +import { useFetchBalances } from 'hooks/useFetchBalances'; + +import { MaxButton } from './MaxButton'; +import { OlasAmountInput } from './OlasAmountInput'; +import { ProjectedVeOlas } from './ProjectedVeOlas'; +import { UnlockTimeInput } from './UnlockTimeInput'; + +type CreateLockModalProps = { + isModalVisible: boolean; + setIsModalVisible: (value: boolean) => void; + amountInEth: bigint; + onApprove: () => void; +}; + +export const ApproveOlasModal = ({ + isModalVisible, + setIsModalVisible, + amountInEth, + onApprove, +}: CreateLockModalProps) => { + const { address } = useAccount(); + + const [isApproving, setIsApproving] = useState(false); + + const handleApprove = async () => { + if (!address) return; + + try { + setIsApproving(true); + const amountBN = ethers.parseUnits(`${amountInEth}`, 'ether'); + + await approveOlasByOwner({ + account: address, + amount: amountBN, + }); + + onApprove(); + } catch (error) { + console.error(error); + notifyError(); + } finally { + setIsApproving(false); + setIsModalVisible(false); + } + }; + + return ( + setIsModalVisible(false)} + > + + + + + + + ); +}; diff --git a/apps/govern/components/VeOlas/CreateLockModal.tsx b/apps/govern/components/VeOlas/CreateLockModal.tsx new file mode 100644 index 00000000..9118f33c --- /dev/null +++ b/apps/govern/components/VeOlas/CreateLockModal.tsx @@ -0,0 +1,166 @@ +import { Alert, Button, Divider, Flex, Form, Modal } from 'antd'; +import { ethers } from 'ethers'; +import { useState } from 'react'; + +import { notifyError, notifySuccess } from 'libs/util-functions/src'; + +import { + createLockRequest, + dateInMs, + getRemainingTimeInSeconds, + hasSufficientTokensRequest, +} from 'common-util/functions'; +import { useFetchBalances } from 'hooks/useFetchBalances'; + +import { ApproveOlasModal } from './ApproveOlasModal'; +import { MaxButton } from './MaxButton'; +import { OlasAmountInput } from './OlasAmountInput'; +import { ProjectedVeOlas } from './ProjectedVeOlas'; +import { UnlockTimeInput } from './UnlockTimeInput'; + +type CreateLockModalProps = { + isModalVisible: boolean; + setIsModalVisible: (value: boolean) => void; +}; + +type FormValues = { + amount: number; + unlockTime: number; +}; + +export const CreateLockModal = ({ isModalVisible, setIsModalVisible }: CreateLockModalProps) => { + const [form] = Form.useForm(); + const { account, lockedEnd, olasBalance, veOlasBalance, refetch } = useFetchBalances(); + + const [isLoading, setIsLoading] = useState(false); + const [isApproveModalVisible, setIsApproveModalVisible] = useState(false); + + const amountInEth = Form.useWatch('amount', form); + const unlockTime = dateInMs(Form.useWatch('unlockTime', form)); + + const handleClose = () => { + setIsModalVisible(false); + }; + + const onCreateLock = async () => { + if (!account) return; + + setIsLoading(true); + + const txHash = await createLockRequest({ + amount: ethers.parseUnits(`${amountInEth}`, 18).toString(), + unlockTime: getRemainingTimeInSeconds(unlockTime), + account, + }); + + notifySuccess('Lock created successfully!', `Transaction Hash: ${txHash}`); + + // once the lock is created, refetch the data + refetch(); + + handleClose(); + setIsLoading(false); + }; + + const onFinish = async ({ amount }: FormValues) => { + if (!account) return; + + try { + setIsLoading(true); + + const hasSufficientTokens = await hasSufficientTokensRequest({ + account, + amount, + }); + + if (!hasSufficientTokens) { + setIsLoading(false); + setIsApproveModalVisible(true); + return; + } + + await onCreateLock(); + } catch (error) { + window.console.error(error); + notifyError(); + } finally { + setIsLoading(false); + } + }; + + const cannotCreateLock = veOlasBalance !== undefined && Number(veOlasBalance) !== 0; + + return ( + +
+
+ + { + form.setFieldsValue({ amount: olasBalance }); + form.validateFields(['amount']); + }} + /> +
+ + + + + + + + + + + + + + + + + + {!account && ( + + )} + + {cannotCreateLock && ( + + )} + + +
+ ); +}; diff --git a/apps/govern/components/VeOlas/IncreaseLockModal/IncreaseAmount.tsx b/apps/govern/components/VeOlas/IncreaseLockModal/IncreaseAmount.tsx new file mode 100644 index 00000000..e7159655 --- /dev/null +++ b/apps/govern/components/VeOlas/IncreaseLockModal/IncreaseAmount.tsx @@ -0,0 +1,144 @@ +import { Button, Divider, Flex, Form, Modal } from 'antd'; +import { ethers } from 'ethers'; +import { useState } from 'react'; + +import { notifyError, notifySuccess } from 'libs/util-functions/src'; + +import { hasSufficientTokensRequest, updateIncreaseAmount } from 'common-util/functions'; +import { useFetchBalances } from 'hooks/useFetchBalances'; + +import { ApproveOlasModal } from '../ApproveOlasModal'; +import { MaxButton } from '../MaxButton'; +import { OlasAmountInput } from '../OlasAmountInput'; +import { ProjectedVeOlas } from '../ProjectedVeOlas'; +import { useVeolasComponents } from '../useVeolasComponents'; + +type IncreaseAmountProps = { + closeModal: () => void; +}; + +type FormValues = { + amount: number; +}; + +export const IncreaseAmount = ({ closeModal }: IncreaseAmountProps) => { + const [form] = Form.useForm(); + const { account, lockedEnd, olasBalance, veOlasBalance, refetch } = useFetchBalances(); + const { getLockedAmountComponent } = useVeolasComponents(); + + const [isLoading, setIsLoading] = useState(false); + const [isApproveModalVisible, setIsApproveModalVisible] = useState(false); + + const amountInEth = Form.useWatch('amount', form); + + const onIncreaseAmount = async () => { + if (!account) return; + + setIsLoading(true); + + const txHash = await updateIncreaseAmount({ + amount: ethers.parseUnits(`${amountInEth}`, 18).toString(), + account, + }); + + notifySuccess('Amount increased successfully!', `Transaction Hash: ${txHash}`); + + // once the amount is increased, refetch the data + refetch(); + + closeModal(); + setIsLoading(false); + }; + + const onFinish = async ({ amount }: FormValues) => { + if (!account) return; + if (!veOlasBalance) return; + + try { + setIsLoading(true); + + await form.validateFields(); + const hasSufficientTokens = await hasSufficientTokensRequest({ + account, + amount, + }); + + if (!hasSufficientTokens) { + setIsLoading(false); + setIsApproveModalVisible(true); + return; + } + + await onIncreaseAmount(); + } catch (error) { + window.console.error(error); + notifyError(); + } finally { + setIsLoading(false); + } + }; + + /** + * can increase amount only if the mapped amount is zero (ie. no lock exists) + * or if the user has some olas tokens. + */ + const cannotIncreaseAmount = Number(veOlasBalance) === 0 || Number(olasBalance) === 0 || !account; + + return ( + <> +
+ {getLockedAmountComponent()} + + + +
+ + { + form.setFieldsValue({ amount: olasBalance }); + form.validateFields(['amount']); + }} + /> +
+ + + + + + + + + + + + + + + + + + ); +}; diff --git a/apps/govern/components/VeOlas/IncreaseLockModal/IncreaseUnlockTime.tsx b/apps/govern/components/VeOlas/IncreaseLockModal/IncreaseUnlockTime.tsx new file mode 100644 index 00000000..bbbee2fb --- /dev/null +++ b/apps/govern/components/VeOlas/IncreaseLockModal/IncreaseUnlockTime.tsx @@ -0,0 +1,111 @@ +import { Button, Divider, Flex, Form } from 'antd'; +import { useState } from 'react'; + +import { notifyError, notifySuccess } from 'libs/util-functions/src'; + +import { + dateInMs, + getRemainingTimeInSeconds, + updateIncreaseUnlockTime, +} from 'common-util/functions'; +import { useFetchBalances } from 'hooks/useFetchBalances'; + +import { ProjectedVeOlas } from '../ProjectedVeOlas'; +import { UnlockTimeInput } from '../UnlockTimeInput'; +import { useVeolasComponents } from '../useVeolasComponents'; + +type IncreaseUnlockTimeProps = { + closeModal: () => void; +}; + +type FormValues = { + unlockTime: number; +}; + +export const IncreaseUnlockTime = ({ closeModal }: IncreaseUnlockTimeProps) => { + const [form] = Form.useForm(); + const { account, lockedEnd, olasBalance, veOlasBalance, refetch } = useFetchBalances(); + const { getUnlockTimeComponent } = useVeolasComponents(); + + const [isLoading, setIsLoading] = useState(false); + + const unlockTime = dateInMs(Form.useWatch('unlockTime', form)); + + const onFinish = async ({ unlockTime }: FormValues) => { + if (!account) return; + if (!veOlasBalance) return; + + try { + setIsLoading(true); + + const txHash = await updateIncreaseUnlockTime({ + time: getRemainingTimeInSeconds(unlockTime), + account, + }); + + notifySuccess('Unlock time increased successfully!', `Transaction Hash: ${txHash}`); + + // once the unlockTime is increased, refetch the data + refetch(); + + // close the modal after successful locking & loading state + closeModal(); + } catch (error) { + window.console.error(error); + notifyError(); + } finally { + setIsLoading(false); + } + }; + + /** + * can increase amount only if the mapped amount is zero (ie. no lock exists) + * or if the user has some olas tokens. + */ + const cannotIncreaseAmount = Number(veOlasBalance) === 0 || Number(olasBalance) === 0 || !account; + + return ( +
+ {getUnlockTimeComponent()} + + + +
+ +
+ + + + + + + + + + + + + + + ); +}; diff --git a/apps/govern/components/VeOlas/IncreaseLockModal/index.tsx b/apps/govern/components/VeOlas/IncreaseLockModal/index.tsx new file mode 100644 index 00000000..2fa4e7ee --- /dev/null +++ b/apps/govern/components/VeOlas/IncreaseLockModal/index.tsx @@ -0,0 +1,44 @@ +import { Modal, Segmented } from 'antd'; +import { useState } from 'react'; + +import { IncreaseAmount } from './IncreaseAmount'; +import { IncreaseUnlockTime } from './IncreaseUnlockTime'; + +const TABS = { by_olas_amount: 'By OLAS Amount', by_lock_duration: 'By Lock Duration' }; +const TABS_OPTIONS = Object.values(TABS); + +type IncreaseLockModalProps = { + isModalVisible: boolean; + setIsModalVisible: (value: boolean) => void; +}; + +export const IncreaseLockModal = ({ + isModalVisible, + setIsModalVisible, +}: IncreaseLockModalProps) => { + const [selectedTab, setSelectedTab] = useState(TABS_OPTIONS[0]); + const handleClose = () => { + setIsModalVisible(false); + }; + + return ( + + + + {selectedTab == TABS.by_olas_amount && } + {selectedTab == TABS.by_lock_duration && } + + ); +}; diff --git a/apps/govern/components/VeOlas/InfoCard.tsx b/apps/govern/components/VeOlas/InfoCard.tsx new file mode 100644 index 00000000..98a389dc --- /dev/null +++ b/apps/govern/components/VeOlas/InfoCard.tsx @@ -0,0 +1,65 @@ +import { Skeleton, Tooltip, Typography } from 'antd'; +import styled from 'styled-components'; +import { useAccount } from 'wagmi'; + +import { COLOR } from 'libs/ui-theme/src'; + +const { Title } = Typography; + +const InfoCardContainer = styled.div` + padding: 16px 0; + h5 { + font-weight: normal; + } +`; + +const ValueText = styled.div` + font-size: 30px; + font-style: normal; + font-weight: 700; + line-height: 38px; + letter-spacing: -0.9px; + + /* ellipsis */ + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +`; + +type InfoCard = { + isLoading: boolean; + title?: string; + value: string; + tooltipValue?: string; +}; + +const Shimmer = ({ active = true }) => ; + +export const InfoCard = ({ isLoading, title, value, tooltipValue }: InfoCard) => { + const { isConnected } = useAccount(); + + return ( + + {title && ( + + {title} + + )} + + + {isLoading ? ( + + ) : ( + + {isConnected ? value : '--'} + + )} + + + ); +}; diff --git a/apps/govern/components/VeOlas/MaxButton.tsx b/apps/govern/components/VeOlas/MaxButton.tsx new file mode 100644 index 00000000..ccc55d30 --- /dev/null +++ b/apps/govern/components/VeOlas/MaxButton.tsx @@ -0,0 +1,19 @@ +import { Button, Typography } from 'antd'; + +import { getCommaSeparatedNumber } from 'common-util/functions'; + +const { Text } = Typography; + +type MaxButtonProps = { olasBalance?: string; onMaxClick: () => void }; + +export const MaxButton = ({ olasBalance, onMaxClick }: MaxButtonProps) => { + return ( + + Balance:  + {getCommaSeparatedNumber(olasBalance || 0)} OLAS  + + + ); +}; diff --git a/apps/govern/components/VeOlas/OlasAmountInput.tsx b/apps/govern/components/VeOlas/OlasAmountInput.tsx new file mode 100644 index 00000000..2f76187b --- /dev/null +++ b/apps/govern/components/VeOlas/OlasAmountInput.tsx @@ -0,0 +1,36 @@ +import { Form, InputNumber, Typography } from 'antd'; + +const { Text } = Typography; + +type OlasAmountInputProps = { + olasBalance?: string; +}; +/** + * @returns Amount Input + */ +export const OlasAmountInput = ({ olasBalance }: OlasAmountInputProps) => { + return ( + OLAS amount to lock} + rules={[ + { required: true, message: 'Amount is required' }, + () => ({ + validator(_, value) { + if (value === '' || value === null) return Promise.resolve(); + if (value <= 0) { + return Promise.reject(new Error('Please input a valid amount')); + } + if (olasBalance && value > Number(olasBalance)) { + return Promise.reject(new Error('Amount cannot be greater than the balance')); + } + return Promise.resolve(); + }, + }), + ]} + > + + + ); +}; diff --git a/apps/govern/components/VeOlas/ProjectedVeOlas.tsx b/apps/govern/components/VeOlas/ProjectedVeOlas.tsx new file mode 100644 index 00000000..f6c78e39 --- /dev/null +++ b/apps/govern/components/VeOlas/ProjectedVeOlas.tsx @@ -0,0 +1,41 @@ +import { getCommaSeparatedNumber } from 'common-util/functions'; + +import { InfoCard } from './InfoCard'; + +const SECONDS_IN_A_YEAR = 31536000; + +type ProjectedVeOlasProps = { + olasAmount?: number; + unlockTime?: number; +}; + +export const ProjectedVeOlas = ({ olasAmount, unlockTime }: ProjectedVeOlasProps) => { + /** + * @returns projected veOLAS amount as per the formula. + * formula = veOLAS = OLAS * lockDuration / maxLockDuration + */ + const getProjectedVeOlas = () => { + if (!olasAmount) return 0; + if (!unlockTime) return 0; + + const maxLockDuration = SECONDS_IN_A_YEAR * 4; + const todayDateMs = new Date().getTime(); + const lockDuration = (unlockTime - todayDateMs) / 1000; + + const projectedVeOlas = (olasAmount * lockDuration) / maxLockDuration; + + if (!projectedVeOlas || lockDuration < 0) { + return 0; + } + + return getCommaSeparatedNumber(projectedVeOlas.toFixed(2).toString()); + }; + + return ( + + ); +}; diff --git a/apps/govern/components/VeOlas/UnlockTimeInput.tsx b/apps/govern/components/VeOlas/UnlockTimeInput.tsx new file mode 100644 index 00000000..3fb220a0 --- /dev/null +++ b/apps/govern/components/VeOlas/UnlockTimeInput.tsx @@ -0,0 +1,62 @@ +import { DatePicker, DatePickerProps, Form, Typography } from 'antd'; +import range from 'lodash/range'; +import React from 'react'; + +const { Text } = Typography; + +type UnlockTimeInputProps = { + startDate?: number; +}; + +// Helper function to add days to a date +const addDays = (date: Date, days: number) => { + const result = new Date(date); + result.setDate(result.getDate() + days); + return result; +}; + +export const UnlockTimeInput = ({ startDate }: UnlockTimeInputProps) => { + const tempStartDate = startDate ? new Date(startDate) : new Date(); + + // Function to disable specific dates + const disableDateForUnlockTime: DatePickerProps['disabledDate'] = (current) => { + const currentDate = current.toDate(); + const today = new Date(); + const sevenDaysFromTempStartDate = addDays(tempStartDate, 6); + const fourYearsFromToday = addDays(today, 4 * 365); + + const pastDate = currentDate < sevenDaysFromTempStartDate; + const notSameDayInFuture = currentDate.getDay() !== today.getDay(); + const futureDate = currentDate > fourYearsFromToday; + + return pastDate || notSameDayInFuture || futureDate; + }; + + return ( + Unlock date and time} + rules={[{ required: true, message: 'Unlock Time is required' }]} + tooltip="The date should be minimum 1 week and maximum 4 years" + className="mb-4" + > + { + const now = new Date(); + const currentHour = now.getHours(); + const currentMinute = now.getMinutes(); + + return { + disabledHours: () => range(0, currentHour), + disabledMinutes: () => range(0, currentMinute), + }; + }} + format="MM/DD/YYYY HH:mm" + className="full-width" + showTime={{ format: 'HH:mm' }} + size="large" + /> + + ); +}; diff --git a/apps/govern/components/VeOlas/VeOlasManage.tsx b/apps/govern/components/VeOlas/VeOlasManage.tsx new file mode 100644 index 00000000..15581e2b --- /dev/null +++ b/apps/govern/components/VeOlas/VeOlasManage.tsx @@ -0,0 +1,68 @@ +import { Button, Col, Row } from 'antd'; +import { useState } from 'react'; + +import { notifySuccess } from '@autonolas/frontend-library'; + +import { notifyError } from 'libs/util-functions/src'; + +import { withdrawVeolasRequest } from 'common-util/functions'; +import { useFetchBalances } from 'hooks/index'; + +import { useVeolasComponents } from './useVeolasComponents'; + +export const VeOlasManage = () => { + const { isLoading, canWithdrawVeolas, account, refetch } = useFetchBalances(); + const { + getBalanceComponent, + getVotingPowerComponent, + getVotingPercentComponent, + getLockedAmountComponent, + getUnlockTimeComponent, + getUnlockedAmountComponent, + } = useVeolasComponents(); + + const [isWithdrawLoading, setIsWithdrawLoading] = useState(false); + + const onWithdraw = async () => { + if (!account) return; + + try { + setIsWithdrawLoading(true); + await withdrawVeolasRequest({ account }); + notifySuccess('Claimed successfully'); + + refetch(); + } catch (error) { + window.console.error(error); + notifyError(); + } finally { + setIsWithdrawLoading(false); + } + }; + + return ( + <> + + {getBalanceComponent()} + + {getVotingPowerComponent()} + + {getVotingPercentComponent()} + + {getLockedAmountComponent()} + + + {getUnlockTimeComponent()} + + {!isLoading && canWithdrawVeolas && ( + + )} + + + {getUnlockedAmountComponent()} + + + ); +}; diff --git a/apps/govern/components/VeOlas/index.tsx b/apps/govern/components/VeOlas/index.tsx new file mode 100644 index 00000000..0445c29c --- /dev/null +++ b/apps/govern/components/VeOlas/index.tsx @@ -0,0 +1,75 @@ +import { Alert, Button, Card, Space, Typography } from 'antd'; +import { useState } from 'react'; +import styled from 'styled-components'; + +import { useFetchBalances } from 'hooks/index'; + +import { CreateLockModal } from './CreateLockModal'; +import { IncreaseLockModal } from './IncreaseLockModal'; +import { VeOlasManage } from './VeOlasManage'; + +const { Paragraph } = Typography; + +const StyledMain = styled.main` + display: flex; + flex-direction: column; + max-width: 946px; + margin: 0 auto; +`; + +const Title = styled.h1` + font-size: 24px; + margin: 0 0 8px; +`; + +export const VeOlasPage = () => { + const { isLoading, canWithdrawVeolas, canIncreaseAmountOrUnlock } = useFetchBalances(); + + const [isCreateLockModalVisible, setIsCreateLockModalVisible] = useState(false); + const [isIncreaseModalVisible, setIsIncreaseModalVisible] = useState(false); + + return ( + + + veOLAS + + veOLAS gives you voting power in Olas governance. Lock OLAS for longer periods to get more + veOLAS. + + + + + {canWithdrawVeolas && ( + + )} + + + + + + + + + ); +}; diff --git a/apps/govern/components/VeOlas/useVeolasComponents.tsx b/apps/govern/components/VeOlas/useVeolasComponents.tsx new file mode 100644 index 00000000..29480d53 --- /dev/null +++ b/apps/govern/components/VeOlas/useVeolasComponents.tsx @@ -0,0 +1,107 @@ +import { + formatWeiNumber, + getCommaSeparatedNumber, + getFormattedDate, + getFullFormattedDate, +} from 'common-util/functions'; +import { useFetchBalances } from 'hooks/useFetchBalances'; + +import { InfoCard } from './InfoCard'; + +const getTotalVotesPercentage = ( + votingPower: string | undefined, + totalSupply: string | undefined, +) => { + if (votingPower && totalSupply) { + const votingPowerInPercentage = ((Number(votingPower) / Number(totalSupply)) * 100).toFixed(2); + return formatWeiNumber({ value: votingPowerInPercentage }); + } + + return null; +}; + +/** + * This hook is used to get the components + */ +export const useVeolasComponents = () => { + const { + isLoading, + olasBalance, + veOlasBalance, + votingPower, + totalSupply, + lockedEnd, + canWithdrawVeolas, + } = useFetchBalances(); + + const getBalanceComponent = () => ( + + ); + + const getVotingPowerComponent = () => ( + + ); + + const getVotingPercentComponent = () => ( + + ); + + const getLockedAmountComponent = () => ( + + ); + + const getUnlockTimeComponent = () => ( + + ); + + // unlocked OLAS = balanceOf(amount) of veOlas contract + const getUnlockedAmountComponent = () => { + // if the user has no locked OLAS, then don't show the component + if (!canWithdrawVeolas) return null; + return ( + + ); + }; + + return { + getBalanceComponent, + getVotingPowerComponent, + getVotingPercentComponent, + getLockedAmountComponent, + getUnlockTimeComponent, + getUnlockedAmountComponent, + }; +}; diff --git a/apps/govern/hooks/index.ts b/apps/govern/hooks/index.ts index adf03d78..0076b892 100644 --- a/apps/govern/hooks/index.ts +++ b/apps/govern/hooks/index.ts @@ -1,4 +1,5 @@ export * from './useFetchStakingContractsList'; export * from './useFetchUserVotes'; +export * from './useFetchBalances'; export * from './useVotingPower'; export * from './useContractParams'; diff --git a/apps/govern/hooks/useFetchBalances.ts b/apps/govern/hooks/useFetchBalances.ts new file mode 100644 index 00000000..1e82e2a1 --- /dev/null +++ b/apps/govern/hooks/useFetchBalances.ts @@ -0,0 +1,107 @@ +import { ethers } from 'ethers'; +import { useMemo } from 'react'; +import { Address } from 'viem'; +import { mainnet } from 'viem/chains'; +import { useAccount, useBlock, useReadContracts } from 'wagmi'; + +import { OLAS, VE_OLAS } from 'libs/util-contracts/src/lib/abiAndAddresses'; + +import { LATEST_BLOCK_KEY } from 'common-util/constants/scopeKeys'; + +import { useVotingPower } from './useVotingPower'; + +const getContracts = (account: Address) => [ + { + address: OLAS.addresses[mainnet.id], + abi: OLAS.abi, + chainId: mainnet.id, + functionName: 'balanceOf', + args: [account], + }, + { + address: VE_OLAS.addresses[mainnet.id], + abi: VE_OLAS.abi, + chainId: mainnet.id, + functionName: 'totalSupplyLocked', + }, + { + address: VE_OLAS.addresses[mainnet.id], + abi: VE_OLAS.abi, + chainId: mainnet.id, + functionName: 'mapLockedBalances', + args: [account], + }, +]; + +export const useFetchBalances = () => { + const { address: account } = useAccount(); + const { + data: votingPower, + isFetching: isVotingPowerFetching, + refetch: refetchVotingPower, + } = useVotingPower(account); + + const { + data: block, + isFetching: isBlockFetching, + refetch: refetchBlock, + } = useBlock({ + blockTag: 'latest', + scopeKey: LATEST_BLOCK_KEY, + }); + + const { + data: balanceData, + isFetching: isBalanceFetching, + refetch: refetchBalances, + } = useReadContracts({ + contracts: getContracts(account || '0x'), + query: { + enabled: !!account, + select: (data) => { + const [olasBalanceData, totalSupplyLockedData, mapLockedBalancesData] = data; + const [veOlasBalance, lockedEnd] = mapLockedBalancesData.result as bigint[]; + + return { + olasBalance: ethers.formatUnits(olasBalanceData.result as bigint, 18), + veOlasBalance: ethers.formatUnits(veOlasBalance as bigint, 18), + totalSupplyLocked: ethers.formatUnits(totalSupplyLockedData.result as bigint, 18), + lockedEnd: Number(lockedEnd) * 1000, + }; + }, + }, + }); + + const canWithdrawVeolas = useMemo(() => { + if (balanceData === undefined) return false; + if (block === undefined) return false; + + return Number(balanceData.veOlasBalance) > 0 && balanceData.lockedEnd <= block.timestamp; + }, []); + + const refetch = () => { + return new Promise(async (resolve, reject) => { + try { + await refetchVotingPower(); + await refetchBlock(); + await refetchBalances(); + resolve(); + } catch (error) { + reject(error); + } + }); + }; + + return { + isLoading: isVotingPowerFetching || isBalanceFetching || isBlockFetching, + account, + votingPower, + totalSupply: balanceData?.totalSupplyLocked, + olasBalance: balanceData?.olasBalance, + veOlasBalance: balanceData?.veOlasBalance, + lockedEnd: balanceData?.lockedEnd, + canWithdrawVeolas, + canIncreaseAmountOrUnlock: balanceData ? Number(balanceData.veOlasBalance) > 0 : false, + refetch, + }; +}; diff --git a/apps/govern/hooks/useVotingPower.ts b/apps/govern/hooks/useVotingPower.ts index 049fbf53..fa11c415 100644 --- a/apps/govern/hooks/useVotingPower.ts +++ b/apps/govern/hooks/useVotingPower.ts @@ -6,7 +6,7 @@ import { useReadContract } from 'wagmi'; import { VE_OLAS } from 'libs/util-contracts/src/lib/abiAndAddresses'; export const useVotingPower = (account: Address | undefined) => { - const { data, isFetching } = useReadContract({ + const { data, isFetching, refetch } = useReadContract({ address: (VE_OLAS.addresses as Record)[mainnet.id], abi: VE_OLAS.abi, functionName: 'getVotes', @@ -18,5 +18,5 @@ export const useVotingPower = (account: Address | undefined) => { }, }); - return { data, isFetching }; + return { data, isFetching, refetch }; }; diff --git a/apps/govern/pages/veolas.tsx b/apps/govern/pages/veolas.tsx new file mode 100644 index 00000000..08219922 --- /dev/null +++ b/apps/govern/pages/veolas.tsx @@ -0,0 +1,3 @@ +import { VeOlasPage } from '../components/VeOlas'; + +export default VeOlasPage; diff --git a/libs/ui-theme/src/lib/GlobalStyles.tsx b/libs/ui-theme/src/lib/GlobalStyles.tsx index 082a6b14..ff03a500 100644 --- a/libs/ui-theme/src/lib/GlobalStyles.tsx +++ b/libs/ui-theme/src/lib/GlobalStyles.tsx @@ -106,6 +106,10 @@ export const GlobalStyles = createGlobalStyle` padding-top: 48px; } + .block { + display: block; + } + .text-start { text-align: start; } diff --git a/libs/ui-theme/src/lib/ThemeConfig.tsx b/libs/ui-theme/src/lib/ThemeConfig.tsx index c08f086a..89773073 100644 --- a/libs/ui-theme/src/lib/ThemeConfig.tsx +++ b/libs/ui-theme/src/lib/ThemeConfig.tsx @@ -85,6 +85,9 @@ export const THEME_CONFIG: ThemeConfig = { Collapse: { colorBorder: COLOR.BORDER_GREY_2, }, + Modal: { + titleFontSize: 24, + }, }, }; diff --git a/libs/util-constants/src/lib/symbols.ts b/libs/util-constants/src/lib/symbols.ts index 37cd357a..bb53f5d3 100644 --- a/libs/util-constants/src/lib/symbols.ts +++ b/libs/util-constants/src/lib/symbols.ts @@ -1,3 +1,5 @@ export const UNICODE_SYMBOLS = { EXTERNAL_LINK: '↗', }; + +export const NA = 'n/a'; diff --git a/libs/util-contracts/src/lib/abiAndAddresses/index.js b/libs/util-contracts/src/lib/abiAndAddresses/index.js index 5ed75cdb..89826df8 100644 --- a/libs/util-contracts/src/lib/abiAndAddresses/index.js +++ b/libs/util-contracts/src/lib/abiAndAddresses/index.js @@ -15,3 +15,4 @@ export * from './stakingFactory'; export * from './veOlas'; export * from './stakingToken'; export * from './stakingVerifier'; +export * from './olas'; diff --git a/libs/util-contracts/src/lib/abiAndAddresses/olas.ts b/libs/util-contracts/src/lib/abiAndAddresses/olas.ts new file mode 100644 index 00000000..a5394f35 --- /dev/null +++ b/libs/util-contracts/src/lib/abiAndAddresses/olas.ts @@ -0,0 +1,575 @@ +import { Contract } from './types'; + +export const OLAS: Contract = { + contractName: 'OLAS', + addresses: { + 1: '0x0001A500A6B18995B03f44bb040A5fFc28E45CB0', + 5: '0xEdfc28215B1Eb6eb0be426f1f529cf691A5C2400', + }, + abi: [ + { + inputs: [], + stateMutability: 'nonpayable', + type: 'constructor', + }, + { + inputs: [ + { + internalType: 'address', + name: 'sender', + type: 'address', + }, + { + internalType: 'address', + name: 'manager', + type: 'address', + }, + ], + name: 'ManagerOnly', + type: 'error', + }, + { + inputs: [], + name: 'ZeroAddress', + type: 'error', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'owner', + type: 'address', + }, + { + indexed: true, + internalType: 'address', + name: 'spender', + type: 'address', + }, + { + indexed: false, + internalType: 'uint256', + name: 'amount', + type: 'uint256', + }, + ], + name: 'Approval', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'minter', + type: 'address', + }, + ], + name: 'MinterUpdated', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'owner', + type: 'address', + }, + ], + name: 'OwnerUpdated', + 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: 'amount', + type: 'uint256', + }, + ], + name: 'Transfer', + type: 'event', + }, + { + inputs: [], + name: 'DOMAIN_SEPARATOR', + outputs: [ + { + internalType: 'bytes32', + name: '', + type: 'bytes32', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + internalType: 'address', + name: '', + type: 'address', + }, + { + internalType: 'address', + name: '', + 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: '', + 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: 'newMinter', + type: 'address', + }, + ], + name: 'changeMinter', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + internalType: 'address', + name: 'newOwner', + type: 'address', + }, + ], + name: 'changeOwner', + 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: 'amount', + type: 'uint256', + }, + ], + name: 'decreaseAllowance', + outputs: [ + { + internalType: 'bool', + name: '', + type: 'bool', + }, + ], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + internalType: 'address', + name: 'spender', + type: 'address', + }, + { + internalType: 'uint256', + name: 'amount', + type: 'uint256', + }, + ], + name: 'increaseAllowance', + outputs: [ + { + internalType: 'bool', + name: '', + type: 'bool', + }, + ], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + internalType: 'uint256', + name: 'amount', + type: 'uint256', + }, + ], + name: 'inflationControl', + outputs: [ + { + internalType: 'bool', + name: '', + type: 'bool', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'inflationRemainder', + outputs: [ + { + internalType: 'uint256', + name: 'remainder', + type: 'uint256', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'maxMintCapFraction', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + internalType: 'address', + name: 'account', + type: 'address', + }, + { + internalType: 'uint256', + name: 'amount', + type: 'uint256', + }, + ], + name: 'mint', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'minter', + outputs: [ + { + internalType: 'address', + name: '', + type: 'address', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'name', + outputs: [ + { + internalType: 'string', + name: '', + type: 'string', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + internalType: 'address', + name: '', + type: 'address', + }, + ], + name: 'nonces', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'oneYear', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'owner', + outputs: [ + { + internalType: 'address', + name: '', + type: 'address', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + internalType: 'address', + name: 'owner', + type: 'address', + }, + { + internalType: 'address', + name: 'spender', + type: 'address', + }, + { + internalType: 'uint256', + name: 'value', + type: 'uint256', + }, + { + internalType: 'uint256', + name: 'deadline', + type: 'uint256', + }, + { + internalType: 'uint8', + name: 'v', + type: 'uint8', + }, + { + internalType: 'bytes32', + name: 'r', + type: 'bytes32', + }, + { + internalType: 'bytes32', + name: 's', + type: 'bytes32', + }, + ], + name: 'permit', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'symbol', + outputs: [ + { + internalType: 'string', + name: '', + type: 'string', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'tenYearSupplyCap', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'timeLaunch', + 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', + }, + ], +}; diff --git a/libs/util-contracts/src/lib/abiAndAddresses/types.ts b/libs/util-contracts/src/lib/abiAndAddresses/types.ts new file mode 100644 index 00000000..0f1e2843 --- /dev/null +++ b/libs/util-contracts/src/lib/abiAndAddresses/types.ts @@ -0,0 +1,7 @@ +import { Abi, Address } from 'viem'; + +export type Contract = { + contractName: string; + addresses: Record; + abi: Abi; +}; diff --git a/libs/util-contracts/src/lib/abiAndAddresses/veOlas.js b/libs/util-contracts/src/lib/abiAndAddresses/veOlas.ts similarity index 99% rename from libs/util-contracts/src/lib/abiAndAddresses/veOlas.js rename to libs/util-contracts/src/lib/abiAndAddresses/veOlas.ts index 332a04c8..c9b78b01 100644 --- a/libs/util-contracts/src/lib/abiAndAddresses/veOlas.js +++ b/libs/util-contracts/src/lib/abiAndAddresses/veOlas.ts @@ -1,4 +1,6 @@ -export const VE_OLAS = { +import { Contract } from './types'; + +export const VE_OLAS: Contract = { contractName: 'veOLAS', addresses: { 1: '0x7e01A500805f8A52Fad229b3015AD130A332B7b3',