From 1b03d2405d06add9a0655d9492ee5a95b356c5f1 Mon Sep 17 00:00:00 2001 From: Juan Pablo Rombola Date: Thu, 21 Sep 2023 12:50:27 -0300 Subject: [PATCH 01/38] =?UTF-8?q?=E2=9C=A8=20add=20vesting=20page?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pages/vesting.tsx | 111 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 pages/vesting.tsx diff --git a/pages/vesting.tsx b/pages/vesting.tsx new file mode 100644 index 000000000..a5920b6a6 --- /dev/null +++ b/pages/vesting.tsx @@ -0,0 +1,111 @@ +import React from 'react'; +import type { NextPage } from 'next'; + +import { usePageView } from 'hooks/useAnalytics'; +import { useTranslation } from 'react-i18next'; +import { Box, Button, Divider, Grid, Typography } from '@mui/material'; + +const Vesting: NextPage = () => { + usePageView('/vesting', 'Vesting'); + const { t } = useTranslation(); + + return ( + + + + {t('esEXA Vesting Program')} + + + + {t( + "We've created the esEXA Vesting Program to reward the Protocol’s active participants. Whenever you use the Protocol, you'll receive esEXA tokens that you can vest to earn EXA.", + )} + + + {t( + 'In just two simple steps, you can start unlocking EXA tokens while contributing to the growth and improvement of the Protocol.', + ) + ' '} + + + {t('Learn more about the esEXA Vesting Program.')} + + + + + + (palette.mode === 'light' ? '0px 3px 4px 0px rgba(97, 102, 107, 0.25)' : '')} + > + + + + {t('Step {{number}}', { number: 1 })} + + {t('Claim your esEXA Rewards')} + + + + + + + + + + {t('Step {{number}}', { number: 2 })} + + {t('Initiate Vesting Your esEXA')} + + {t( + "You'll need to deposit 10% of the total esEXA you want to vest as an EXA reserve. You can get EXA if you don’t have the required amount.", + )} + + + {/* Replace with Vesting Component */} + + Vesting Component + + + + + + + {t('Active Vesting Streams')} + + + {t( + 'Here, you can monitor all your active vesting streams, allowing you to easily track your current EXA earnings. Each vesting stream is represented by an NFT and comes with a 12-month vesting period.', + )} + + + `1px solid ${palette.grey[300]}`} + borderRadius="6px" + py={13} + px={5} + gap={1} + > + + {t('No vesting streams active yet.')} + + + {t('Start vesting your esEXA and see the streams’ details here.')} + + + + ); +}; + +export default Vesting; From ba53cd2c1ef1f38510247549e47930c0a3e0d9f3 Mon Sep 17 00:00:00 2001 From: Juan Pablo Rombola Date: Thu, 21 Sep 2023 12:56:49 -0300 Subject: [PATCH 02/38] =?UTF-8?q?=F0=9F=8C=90=20add=20vesting=20translatio?= =?UTF-8?q?ns?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- i18n/es/translation.json | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/i18n/es/translation.json b/i18n/es/translation.json index 47e2308d9..8625a1f96 100644 --- a/i18n/es/translation.json +++ b/i18n/es/translation.json @@ -507,5 +507,18 @@ "Est. Output": "Monto Estimado", "Gas fees": "Costo de la transacción", "No routes": "No hay rutas", - "Terms and Conditions": "Términos y Condiciones" + "Terms and Conditions": "Términos y Condiciones", + "esEXA Vesting Program": "Programa de Vesting de esEXA", + "We've created the esEXA Vesting Program to reward the Protocol’s active participants. Whenever you use the Protocol, you'll receive esEXA tokens that you can vest to earn EXA.": "Hemos creado el Programa de Vesting de esEXA para recompensar a los participantes activos del Protocolo. Cada vez que uses el Protocolo, recibirás tokens esEXA que puedes depositar para ganar EXA.", + "In just two simple steps, you can start unlocking EXA tokens while contributing to the growth and improvement of the Protocol.": "__STRING_NOT_TRANSLATED__", + "Learn more about the esEXA Vesting Program.": "Aprende más sobre el Programa de Vesting de esEXA.", + "Step {{number}}": "Paso {{number}}", + "Claim your esEXA Rewards": "Reclama tus recompensas esEXA", + "Claim {{amount}} esEXA": "Reclama {{amount}} esEXA", + "Initiate Vesting Your esEXA": "Inicia el Vesting de tus esEXA", + "You'll need to deposit 10% of the total esEXA you want to vest as an EXA reserve. You can get EXA if you don’t have the required amount.": "Deberás depositar el 10% del total de esEXA que deseas vestear como reserva de EXA. Puedes obtener EXA si no tienes la cantidad requerida.", + "Active Vesting Streams": "Streams de Vesting Activos", + "Here, you can monitor all your active vesting streams, allowing you to easily track your current EXA earnings. Each vesting stream is represented by an NFT and comes with a 12-month vesting period.": "Aquí puedes monitorear todos tus streams de vesting activos, lo que te permite realizar un seguimiento fácil de tus ganancias actuales de EXA. Cada stream de vesting está representado por un NFT y viene con un período de vesting de 12 meses.", + "No vesting streams active yet.": "Aún no hay streams de vesting activos.", + "Start vesting your esEXA and see the streams’ details here.": "Comienza a vestear tus esEXA y ve los detalles de los streams aquí." } From fc78513ce941733b0ae645f5e981c446b948dc6d Mon Sep 17 00:00:00 2001 From: franm Date: Fri, 22 Sep 2023 09:50:46 -0300 Subject: [PATCH 03/38] =?UTF-8?q?=F0=9F=9A=A7=20vesting=20input?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/VestingInput/index.tsx | 91 +++++++++++++++++++++++++++++++ pages/vesting.tsx | 4 +- 2 files changed, 93 insertions(+), 2 deletions(-) create mode 100644 components/VestingInput/index.tsx diff --git a/components/VestingInput/index.tsx b/components/VestingInput/index.tsx new file mode 100644 index 000000000..76659171d --- /dev/null +++ b/components/VestingInput/index.tsx @@ -0,0 +1,91 @@ +import React, { useMemo, useState } from 'react'; +import { Box, Button, Typography } from '@mui/material'; + +import { ModalBox } from 'components/common/modal/ModalBox'; + +import ModalInput from 'components/OperationsModal/ModalInput'; +import { useWeb3 } from 'hooks/useWeb3'; +import { useSwitchNetwork } from 'wagmi'; +import { useTranslation } from 'react-i18next'; +import { LoadingButton } from '@mui/lab'; +import Image from 'next/image'; +import { useEXABalance, useEXAPrice } from 'hooks/useEXA'; +import { formatEther, formatUnits, parseUnits } from 'viem'; +import formatNumber from 'utils/formatNumber'; +import { WEI_PER_ETHER } from 'utils/const'; + +function VestingInput() { + const { t } = useTranslation(); + + const { data: exaBalance } = useEXABalance(); + const EXAPrice = useEXAPrice(); + const { impersonateActive, chain: displayNetwork, isConnected } = useWeb3(); + const { isLoading: switchIsLoading } = useSwitchNetwork(); + + const [qty, setQty] = useState(''); + + const errorData = false; + + const value = useMemo(() => { + if (!qty || !EXAPrice) return; + + const parsedqty = parseUnits(qty, 18); + const usd = (parsedqty * EXAPrice) / WEI_PER_ETHER; + + return formatUnits(usd, 18); + }, [EXAPrice, qty]); + + return ( + + + + + + + + esEXA + + + + + + + {t('Available')}: {formatNumber(formatEther(exaBalance || 0n))} esEXA + + + ~${formatNumber(value || '0', 'USD')} + + + + + + + {impersonateActive ? ( + + ) : ( + + {t('Please switch to {{network}} network', { network: displayNetwork.name })} + + )} + + + ); +} +export default React.memo(VestingInput); diff --git a/pages/vesting.tsx b/pages/vesting.tsx index a5920b6a6..daa9bac98 100644 --- a/pages/vesting.tsx +++ b/pages/vesting.tsx @@ -4,6 +4,7 @@ import type { NextPage } from 'next'; import { usePageView } from 'hooks/useAnalytics'; import { useTranslation } from 'react-i18next'; import { Box, Button, Divider, Grid, Typography } from '@mui/material'; +import VestingInput from 'components/VestingInput'; const Vesting: NextPage = () => { usePageView('/vesting', 'Vesting'); @@ -69,9 +70,8 @@ const Vesting: NextPage = () => { )} - {/* Replace with Vesting Component */} - Vesting Component + From edcfec3b5470ed7e6bb0931a915d18843a7b226a Mon Sep 17 00:00:00 2001 From: Juan Pablo Rombola Date: Mon, 25 Sep 2023 10:17:04 -0300 Subject: [PATCH 04/38] =?UTF-8?q?=F0=9F=A4=A1=20add=20mock=20active=20stre?= =?UTF-8?q?ams?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/ActiveStream/index.tsx | 99 +++++++++++++++++++++++++++++++ i18n/es/translation.json | 8 ++- pages/vesting.tsx | 78 ++++++++++++++++++------ 3 files changed, 166 insertions(+), 19 deletions(-) create mode 100644 components/ActiveStream/index.tsx diff --git a/components/ActiveStream/index.tsx b/components/ActiveStream/index.tsx new file mode 100644 index 000000000..ab3d8fe2c --- /dev/null +++ b/components/ActiveStream/index.tsx @@ -0,0 +1,99 @@ +import { Box, Button, Divider, LinearProgress, Typography, linearProgressClasses, styled } from '@mui/material'; +import React, { FC, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import Image from 'next/image'; +import formatNumber from 'utils/formatNumber'; + +const StyledLinearProgress = styled(LinearProgress, { + shouldForwardProp: (prop) => prop !== 'barColor', +})<{ barColor: string }>(({ theme, barColor }) => ({ + height: 8, + borderRadius: 5, + [`&.${linearProgressClasses.colorPrimary}`]: { + backgroundColor: theme.palette.grey[100], + }, + [`& .${linearProgressClasses.bar}`]: { + borderRadius: 5, + backgroundColor: barColor, + }, +})); + +type ActiveStreamProps = { + elapsed: number; + duration: number; + vested: bigint; + claimed: bigint; + reserved: bigint; +}; + +const ActiveStream: FC = ({ elapsed, duration, vested, claimed, reserved }) => { + const { t } = useTranslation(); + + const progress = useMemo(() => { + if (elapsed >= duration || duration === 0) return 100; + return (elapsed / duration) * 100; + }, [elapsed, duration]); + + return ( + + + + + + {t('esEXA Vested')} + + + EXA + + {formatNumber(Number(vested) / 1e18)} + + + + + + + {t('Claimed EXA')} + + + EXA + + {formatNumber(Number(claimed) / 1e18)} + + + {`/ ${formatNumber(Number(vested + reserved) / 1e18)}`} + + + + + + + {t('Vesting Period')} + + + {t('{{duration}} days left', { duration })} + + + + + + + + + + + ); +}; + +export default React.memo(ActiveStream); diff --git a/i18n/es/translation.json b/i18n/es/translation.json index 8625a1f96..6bf203a67 100644 --- a/i18n/es/translation.json +++ b/i18n/es/translation.json @@ -520,5 +520,11 @@ "Active Vesting Streams": "Streams de Vesting Activos", "Here, you can monitor all your active vesting streams, allowing you to easily track your current EXA earnings. Each vesting stream is represented by an NFT and comes with a 12-month vesting period.": "Aquí puedes monitorear todos tus streams de vesting activos, lo que te permite realizar un seguimiento fácil de tus ganancias actuales de EXA. Cada stream de vesting está representado por un NFT y viene con un período de vesting de 12 meses.", "No vesting streams active yet.": "Aún no hay streams de vesting activos.", - "Start vesting your esEXA and see the streams’ details here.": "Comienza a vestear tus esEXA y ve los detalles de los streams aquí." + "Start vesting your esEXA and see the streams’ details here.": "Comienza a vestear tus esEXA y ve los detalles de los streams aquí.", + "esEXA Vested": "esEXA Vesteados", + "Claimed EXA": "EXA Reclamado", + "Vesting Period": "Período de Vesting", + "{{duration}} days left": "{{duration}} días restantes", + "View NFT": "Ver NFT", + "Claim EXA": "Reclamar EXA" } diff --git a/pages/vesting.tsx b/pages/vesting.tsx index daa9bac98..54409fa0e 100644 --- a/pages/vesting.tsx +++ b/pages/vesting.tsx @@ -5,11 +5,31 @@ import { usePageView } from 'hooks/useAnalytics'; import { useTranslation } from 'react-i18next'; import { Box, Button, Divider, Grid, Typography } from '@mui/material'; import VestingInput from 'components/VestingInput'; +import ActiveStream from 'components/ActiveStream'; const Vesting: NextPage = () => { usePageView('/vesting', 'Vesting'); const { t } = useTranslation(); + const activeStreams = [ + { + tokenId: 120, + elapsed: 30, + duration: 90, + vested: 218480000000000000000n, + claimed: 95320000000000000000n, + reserved: 21848000000000000000n, + }, + { + tokenId: 96, + elapsed: 60, + duration: 90, + vested: 80930000000000000000n, + claimed: 9450000000000000000n, + reserved: 8093000000000000000n, + }, + ]; + return ( @@ -86,24 +106,46 @@ const Vesting: NextPage = () => { )} - `1px solid ${palette.grey[300]}`} - borderRadius="6px" - py={13} - px={5} - gap={1} - > - - {t('No vesting streams active yet.')} - - - {t('Start vesting your esEXA and see the streams’ details here.')} - - + {activeStreams.length > 0 ? ( + (palette.mode === 'light' ? '0px 3px 4px 0px rgba(97, 102, 107, 0.25)' : '')} + > + {activeStreams.map(({ tokenId, elapsed, duration, vested, claimed, reserved }, index) => ( + <> + + {index !== activeStreams.length - 1 && } + + ))} + + ) : ( + `1px solid ${palette.grey[300]}`} + borderRadius="6px" + py={13} + px={5} + gap={1} + > + + {t('No vesting streams active yet.')} + + + {t('Start vesting your esEXA and see the streams’ details here.')} + + + )} ); }; From 7a5d8b8a8a10b9f536f2205794a9efce656b6418 Mon Sep 17 00:00:00 2001 From: franm Date: Thu, 28 Sep 2023 17:29:07 -0300 Subject: [PATCH 05/38] =?UTF-8?q?=F0=9F=94=A7=20add=20esEXA=20contract?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- wagmi.config.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/wagmi.config.ts b/wagmi.config.ts index 499637c26..b3e146065 100644 --- a/wagmi.config.ts +++ b/wagmi.config.ts @@ -22,6 +22,7 @@ import SablierV2LockupLinear from '@exactly/protocol/deployments/goerli/SablierV import SablierV2NFTDescriptor from '@exactly/protocol/deployments/goerli/SablierV2NFTDescriptor.json' assert { type: 'json' }; import ExtraFinanceLendingABI from './abi/extraFinanceLending.json' assert { type: 'json' }; import DelegateRegistryABI from './abi/DelegateRegistry.json' assert { type: 'json' }; +import EscrowedEXA from '@exactly/protocol/deployments/goerli/EscrowedEXA.json' assert { type: 'json' }; import { Abi } from 'viem'; @@ -49,6 +50,7 @@ export default defineConfig({ { name: 'SablierV2NFTDescriptor', abi: SablierV2NFTDescriptor.abi as Abi }, { name: 'ExtraFinanceLending', abi: ExtraFinanceLendingABI as Abi }, { name: 'DelegateRegistry', abi: DelegateRegistryABI as Abi }, + { name: 'EscrowedEXA', abi: EscrowedEXA.abi as Abi }, ], plugins: [ react({ From 1d5a0c85d720c994294e1abbdc804ad506857f97 Mon Sep 17 00:00:00 2001 From: franm Date: Thu, 28 Sep 2023 17:29:52 -0300 Subject: [PATCH 06/38] =?UTF-8?q?=F0=9F=94=A7=20add=20sablier=20subgraph?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/networkData.json | 9 ++++++--- hooks/useGraphClient.ts | 6 ++++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/config/networkData.json b/config/networkData.json index 11c9c7d70..4a2e52e3b 100644 --- a/config/networkData.json +++ b/config/networkData.json @@ -1,14 +1,17 @@ { "1": { "etherscan": "https://etherscan.io", - "subgraph": "https://gateway.thegraph.com/api/3bd03f49a36caaa5ed4efc5a27c5425d/subgraphs/id/As6Xz6GCvbW8B9Xb7Rx2LqQeJcL3FcUyD8Tk95L8rG5d" + "subgraph": "https://gateway.thegraph.com/api/3bd03f49a36caaa5ed4efc5a27c5425d/subgraphs/id/As6Xz6GCvbW8B9Xb7Rx2LqQeJcL3FcUyD8Tk95L8rG5d", + "sablierSubgraph": "https://api.thegraph.com/subgraphs/name/sablier-labs/sablier-v2" }, "5": { "etherscan": "https://goerli.etherscan.io", - "subgraph": "https://api.thegraph.com/subgraphs/name/exactly/goerli" + "subgraph": "https://api.thegraph.com/subgraphs/name/exactly/goerli", + "sablierSubgraph": "https://api.thegraph.com/subgraphs/name/sablier-labs/sablier-v2-goerli" }, "10": { "etherscan": "https://optimistic.etherscan.io", - "subgraph": "https://api.thegraph.com/subgraphs/name/exactly/optimism" + "subgraph": "https://api.thegraph.com/subgraphs/name/exactly/optimism", + "sablierSubgraph": "https://api.thegraph.com/subgraphs/name/sablier-labs/sablier-v2-optimism" } } diff --git a/hooks/useGraphClient.ts b/hooks/useGraphClient.ts index d8c817076..ab8735243 100644 --- a/hooks/useGraphClient.ts +++ b/hooks/useGraphClient.ts @@ -10,8 +10,10 @@ export default function useGraphClient() { const { setIndexerError } = useGlobalError(); return useCallback( - async (query: string): Promise => { - const subgraphUrl = networkData[String(chain.id) as keyof typeof networkData]?.subgraph; + async (query: string, sablier: boolean = false): Promise => { + const subgraphType = sablier ? 'sablierSubgraph' : 'subgraph'; + + const subgraphUrl = networkData[String(chain.id) as keyof typeof networkData]?.[subgraphType]; if (!subgraphUrl) return undefined; try { From 0f2551d55bb4175b29fac89812aa5072ef9a433e Mon Sep 17 00:00:00 2001 From: franm Date: Thu, 28 Sep 2023 17:31:32 -0300 Subject: [PATCH 07/38] =?UTF-8?q?=F0=9F=9A=A7=20esEXA:=20stream=20progress?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/ActiveStream/index.tsx | 51 ++++++++++++++++++++++++------- hooks/useEscrowedEXA.ts | 49 +++++++++++++++++++++++++++++ pages/vesting.tsx | 34 +++++---------------- queries/getStreams.ts | 21 +++++++++++++ 4 files changed, 118 insertions(+), 37 deletions(-) create mode 100644 hooks/useEscrowedEXA.ts create mode 100644 queries/getStreams.ts diff --git a/components/ActiveStream/index.tsx b/components/ActiveStream/index.tsx index ab3d8fe2c..9edad6a7b 100644 --- a/components/ActiveStream/index.tsx +++ b/components/ActiveStream/index.tsx @@ -19,21 +19,50 @@ const StyledLinearProgress = styled(LinearProgress, { })); type ActiveStreamProps = { - elapsed: number; - duration: number; - vested: bigint; - claimed: bigint; - reserved: bigint; + depositAmount: bigint; + withdrawnAmount: bigint; + startTime: number; + endTime: number; }; -const ActiveStream: FC = ({ elapsed, duration, vested, claimed, reserved }) => { +const ActiveStream: FC = ({ depositAmount, withdrawnAmount, startTime, endTime }) => { const { t } = useTranslation(); + const elapsed = useMemo(() => { + const now = Math.floor(Date.now() / 1000); + return now - startTime; + }, [startTime]); + + const duration = useMemo(() => { + return endTime - startTime; + }, [startTime, endTime]); + const progress = useMemo(() => { - if (elapsed >= duration || duration === 0) return 100; + if (elapsed >= duration) return 100; return (elapsed / duration) * 100; }, [elapsed, duration]); + const timeLeft = useMemo(() => { + const now = Math.floor(Date.now() / 1000); + const secondsLeft = endTime - now; + const daysLeft = Math.floor(secondsLeft / 86_400); + + if (daysLeft > 1) { + return t('{{daysLeft}} days left', { daysLeft }); + } + + if (daysLeft === 1) { + return t('{{daysLeft}} day left', { daysLeft }); + } + + if (daysLeft === 0) { + const minutesLeft = Math.floor(secondsLeft / 60); + return t('{{minutesLeft}} minutes left', { minutesLeft }); + } + }, [endTime, t]); + + const reserved = (depositAmount * 20000000000000000000n) / 100000000000000000000n; + return ( @@ -51,7 +80,7 @@ const ActiveStream: FC = ({ elapsed, duration, vested, claime style={{ maxWidth: '100%', height: 'auto' }} /> - {formatNumber(Number(vested) / 1e18)} + {formatNumber(Number(depositAmount) / 1e18)} @@ -69,10 +98,10 @@ const ActiveStream: FC = ({ elapsed, duration, vested, claime style={{ maxWidth: '100%', height: 'auto' }} /> - {formatNumber(Number(claimed) / 1e18)} + {formatNumber(Number(withdrawnAmount) / 1e18)} - {`/ ${formatNumber(Number(vested + reserved) / 1e18)}`} + {`/ ${formatNumber(Number(depositAmount + reserved) / 1e18)}`} @@ -82,7 +111,7 @@ const ActiveStream: FC = ({ elapsed, duration, vested, claime {t('Vesting Period')} - {t('{{duration}} days left', { duration })} + {timeLeft} diff --git a/hooks/useEscrowedEXA.ts b/hooks/useEscrowedEXA.ts new file mode 100644 index 000000000..2aa69d1a3 --- /dev/null +++ b/hooks/useEscrowedEXA.ts @@ -0,0 +1,49 @@ +import { useCallback, useEffect, useState } from 'react'; +import { escrowedExaABI } from 'types/abi'; +import useContract from './useContract'; +import { useEXA } from './useEXA'; +import useGraphClient from './useGraphClient'; +import { useWeb3 } from './useWeb3'; +import { getStreams } from 'queries/getStreams'; +import { Address } from 'viem'; + +export const useEscrowedEXA = () => { + return useContract('EscrowedEXA', escrowedExaABI); +}; + +type Stream = { + id: string; + tokenId: number; + chainId: number; + recipient: Address; + startTime: number; + endTime: number; + depositAmount: bigint; + withdrawnAmount: bigint; + canceled: boolean; +}; + +export default function useUpdateStreams() { + const EXA = useEXA(); + const { walletAddress } = useWeb3(); + const request = useGraphClient(); + + const [activeStreams, setActiveStreams] = useState([]); + const [loading, setLoading] = useState(true); + + const fetchStreams = useCallback(async () => { + if (EXA && walletAddress) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const data = await request(getStreams(EXA.address.toLowerCase(), walletAddress, false), true); + + setActiveStreams(data.streams); + setLoading(false); + } + }, [EXA, request, walletAddress]); + + useEffect(() => { + fetchStreams(); + }, [fetchStreams]); + + return { activeStreams, loading }; +} diff --git a/pages/vesting.tsx b/pages/vesting.tsx index 54409fa0e..1d9efe525 100644 --- a/pages/vesting.tsx +++ b/pages/vesting.tsx @@ -6,29 +6,12 @@ import { useTranslation } from 'react-i18next'; import { Box, Button, Divider, Grid, Typography } from '@mui/material'; import VestingInput from 'components/VestingInput'; import ActiveStream from 'components/ActiveStream'; +import useUpdateStreams from 'hooks/useEscrowedEXA'; const Vesting: NextPage = () => { usePageView('/vesting', 'Vesting'); const { t } = useTranslation(); - - const activeStreams = [ - { - tokenId: 120, - elapsed: 30, - duration: 90, - vested: 218480000000000000000n, - claimed: 95320000000000000000n, - reserved: 21848000000000000000n, - }, - { - tokenId: 96, - elapsed: 60, - duration: 90, - vested: 80930000000000000000n, - claimed: 9450000000000000000n, - reserved: 8093000000000000000n, - }, - ]; + const { activeStreams } = useUpdateStreams(); return ( @@ -112,15 +95,14 @@ const Vesting: NextPage = () => { bgcolor="components.bg" boxShadow={({ palette }) => (palette.mode === 'light' ? '0px 3px 4px 0px rgba(97, 102, 107, 0.25)' : '')} > - {activeStreams.map(({ tokenId, elapsed, duration, vested, claimed, reserved }, index) => ( + {activeStreams.map(({ id, tokenId, depositAmount, withdrawnAmount, startTime, endTime }, index) => ( <> {index !== activeStreams.length - 1 && } diff --git a/queries/getStreams.ts b/queries/getStreams.ts new file mode 100644 index 000000000..e07bcdf6a --- /dev/null +++ b/queries/getStreams.ts @@ -0,0 +1,21 @@ +export function getStreams(assetAddress: string, address: string, canceled: boolean) { + return ` + { + streams(orderBy: timestamp, orderDirection: asc, where:{ asset:"${assetAddress}", recipient: "${address}", canceled: ${canceled}}) { + id + tokenId + chainId + recipient + startTime + endTime + depositAmount + withdrawnAmount + canceled + asset { + symbol + address + } + } + } + `; +} From 1ca1bce4de4246f175a2e6f600ac5d3319e4bc2a Mon Sep 17 00:00:00 2001 From: Jorge Galat Date: Tue, 3 Oct 2023 18:14:39 -0300 Subject: [PATCH 08/38] =?UTF-8?q?=F0=9F=A7=AA=20e2e:=20add=20skeleton=20te?= =?UTF-8?q?sts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- e2e/fixture/base.ts | 13 +++- e2e/fixture/graph.ts | 29 +++++++++ e2e/fixture/time.ts | 29 +++++++++ e2e/specs/8-vesting/esEXA.spec.ts | 78 ++++++++++++++++++++++ e2e/utils/contracts.ts | 38 +++++++++-- e2e/utils/tenderly.ts | 105 +++++++++++++++++++++++++----- hooks/useEscrowedEXA.ts | 11 ++-- pages/vesting.tsx | 4 +- queries/getStreams.ts | 5 -- types/contracts.ts | 4 ++ 10 files changed, 283 insertions(+), 33 deletions(-) create mode 100644 e2e/fixture/graph.ts create mode 100644 e2e/fixture/time.ts create mode 100644 e2e/specs/8-vesting/esEXA.spec.ts diff --git a/e2e/fixture/base.ts b/e2e/fixture/base.ts index 0704d5c0b..a6be777a3 100644 --- a/e2e/fixture/base.ts +++ b/e2e/fixture/base.ts @@ -12,7 +12,9 @@ import { privateKeyToAccount, generatePrivateKey } from 'viem/accounts'; import { optimism, type Chain } from 'viem/chains'; import { type Tenderly, tenderly } from '../utils/tenderly'; -import actions, { Actions } from './actions'; +import actions, { type Actions } from './actions'; +import graph, { type Graph } from './graph'; +import time, { type Time } from './time'; type MarketView = 'simple' | 'advanced'; @@ -36,6 +38,11 @@ const defaultTestParams = { options: defaultOptions, } as const; +type Web2 = { + graph: Graph; + time: Time; +}; + type Web3 = { account: Account; publicClient: PublicClient; @@ -44,6 +51,7 @@ type Web3 = { }; type TestProps = { + web2: Web2; web3: Web3; setup: Actions; }; @@ -57,6 +65,9 @@ declare global { const base = (params: TestParams = defaultTestParams) => test.extend({ bypassCSP: true, + web2: async ({ page }, use) => { + await use({ graph: graph(page), time: time(page) }); + }, web3: async ({ page }, use) => { const { privateKey, options } = { privateKey: params.privateKey ?? defaultTestParams.privateKey, diff --git a/e2e/fixture/graph.ts b/e2e/fixture/graph.ts new file mode 100644 index 000000000..02c0b7361 --- /dev/null +++ b/e2e/fixture/graph.ts @@ -0,0 +1,29 @@ +import { type Page } from '@playwright/test'; +import { type Address } from 'viem'; + +function graph(page: Page) { + type Stream = { + id: string; + tokenId: string; + recipient: Address; + startTime: string; + endTime: string; + depositAmount: string; + withdrawnAmount: string; + canceled: boolean; + }; + + const streams = async (body: Stream[]) => { + await page.route(/sablier-labs\/sablier-v2-optimism$/, async (route) => { + await route.fulfill({ json: { data: { streams: body } } }); + }); + }; + + return { + streams, + }; +} + +export type Graph = ReturnType; + +export default graph; diff --git a/e2e/fixture/time.ts b/e2e/fixture/time.ts new file mode 100644 index 000000000..ea4812d1c --- /dev/null +++ b/e2e/fixture/time.ts @@ -0,0 +1,29 @@ +import { type Page } from '@playwright/test'; + +function time(page: Page) { + const now = async (timestamp: number) => { + const fake = new Date(Math.floor(timestamp) * 1_000).valueOf(); + await page.addInitScript(`{ + Date = class extends Date { + constructor(...args) { + if (args.length === 0) { + super(${fake}); + } else { + super(...args); + } + } + } + const __DateNowOffset = ${fake} - Date.now(); + const __DateNow = Date.now; + Date.now = () => __DateNow() + __DateNowOffset; + }`); + }; + + return { + now, + }; +} + +export type Time = ReturnType; + +export default time; diff --git a/e2e/specs/8-vesting/esEXA.spec.ts b/e2e/specs/8-vesting/esEXA.spec.ts new file mode 100644 index 000000000..6b84fa13d --- /dev/null +++ b/e2e/specs/8-vesting/esEXA.spec.ts @@ -0,0 +1,78 @@ +import { expect } from '@playwright/test'; +import { parseEther } from 'viem'; + +import base from '../../fixture/base'; +import _app from '../../common/app'; +import { escrowedEXA, sablierV2LockupLinear } from '../../utils/contracts'; + +const test = base(); + +test.describe.configure({ mode: 'serial' }); + +test('Vesting esEXA', async ({ page, web2, web3 }) => { + await web3.fork.setBalance(web3.account.address, { + esEXA: 100, + EXA: 10, + }); + + const app = _app({ test, page }); + const esEXA = await escrowedEXA({ publicClient: web3.publicClient }); + const sablier = await sablierV2LockupLinear({ publicClient: web3.publicClient }); + const stream = await sablier.read.nextStreamId(); + + await page.goto('/vesting'); + + await test.step('Vest esEXA', async () => { + // FIXME: Add test + await page.waitForTimeout(15_000); + }); + + const now = Date.now() / 1_000; + const period = await esEXA.read.vestingPeriod(); + + await web2.time.now(now + period / 2); + await web3.fork.increaseTime(now + period / 2); + + await web2.graph.streams([ + { + id: `0xb923abdca17aed90eb5ec5e407bd37164f632bfd-10-${stream}`, + tokenId: String(stream), + recipient: web3.account.address, + startTime: String(now), + endTime: String(now + period), + depositAmount: String(parseEther('100')), + withdrawnAmount: '0', + canceled: false, + }, + ]); + + await app.reload(); + + await test.step('Withdraw EXA half-way', async () => { + // FIXME: Add test + await page.waitForTimeout(15_000); + }); + + await web2.time.now(now + period * 2); + await web3.fork.increaseTime(now + period * 2); + + await web2.graph.streams([ + { + id: `0xb923abdca17aed90eb5ec5e407bd37164f632bfd-10-${stream}`, + tokenId: String(stream), + recipient: web3.account.address, + startTime: String(now), + endTime: String(now + period), + depositAmount: String(parseEther('100')), + withdrawnAmount: String(parseEther('50')), + canceled: false, + }, + ]); + + await app.reload(); + + await test.step('Withdraw EXA from depleted stream', async () => { + // FIXME: Add test + await page.waitForTimeout(15_000); + }); +}); diff --git a/e2e/utils/contracts.ts b/e2e/utils/contracts.ts index 3fdc0c808..115dd4822 100644 --- a/e2e/utils/contracts.ts +++ b/e2e/utils/contracts.ts @@ -3,11 +3,31 @@ import auditorContract from '@exactly/protocol/deployments/optimism/Auditor.json import marketETHRouter from '@exactly/protocol/deployments/optimism/MarketETHRouter.json' assert { type: 'json' }; import debtManagerContract from '@exactly/protocol/deployments/optimism/DebtManager.json' assert { type: 'json' }; import permit2Contract from '@exactly/protocol/deployments/optimism/Permit2.json' assert { type: 'json' }; +import sablierV2LockupLinearContract from '@exactly/protocol/deployments/optimism/SablierV2LockupLinear.json' assert { type: 'json' }; +import escrowedEXAContract from '@exactly/protocol/deployments/optimism/EscrowedEXA.json' assert { type: 'json' }; -import { Auditor, Market, MarketETHRouter, ERC20, DebtManager, Permit2 } from '../../types/contracts'; -import { auditorABI, marketABI, marketEthRouterABI, erc20ABI, debtManagerABI, permit2ABI } from '../../types/abi'; +import type { + Auditor, + Market, + MarketETHRouter, + ERC20, + DebtManager, + Permit2, + SablierV2LockupLinear, + EscrowedEXA, +} from '../../types/contracts'; +import { + auditorABI, + marketABI, + marketEthRouterABI, + erc20ABI, + debtManagerABI, + permit2ABI, + sablierV2LockupLinearABI, + escrowedExaABI, +} from '../../types/abi'; -const ERC20TokenSymbols = ['WETH', 'USDC', 'OP'] as const; +const ERC20TokenSymbols = ['WETH', 'USDC', 'OP', 'esEXA', 'EXA'] as const; export type ERC20TokenSymbol = (typeof ERC20TokenSymbols)[number]; export type Coin = ERC20TokenSymbol | 'ETH'; @@ -19,7 +39,7 @@ type Clients = { export const erc20 = async (symbol: ERC20TokenSymbol, clients: Clients = {}): Promise => { const { default: { address }, - } = await import(`@exactly/protocol/deployments/optimism/${symbol}.json`, { + } = await import(`@exactly/protocol/deployments/optimism/${symbol === 'esEXA' ? 'EscrowedEXA' : symbol}.json`, { assert: { type: 'json' }, }); if (!isAddress(address)) throw new Error('Invalid address'); @@ -55,3 +75,13 @@ export const permit2 = async (clients: Clients = {}): Promise => { if (!isAddress(permit2Contract.address)) throw new Error('Invalid address'); return getContract({ address: permit2Contract.address, abi: permit2ABI, ...clients }); }; + +export const sablierV2LockupLinear = async (clients: Clients = {}): Promise => { + if (!isAddress(sablierV2LockupLinearContract.address)) throw new Error('Invalid address'); + return getContract({ address: sablierV2LockupLinearContract.address, abi: sablierV2LockupLinearABI, ...clients }); +}; + +export const escrowedEXA = async (clients: Clients = {}): Promise => { + if (!isAddress(escrowedEXAContract.address)) throw new Error('Invalid address'); + return getContract({ address: escrowedEXAContract.address, abi: escrowedExaABI, ...clients }); +}; diff --git a/e2e/utils/tenderly.ts b/e2e/utils/tenderly.ts index fe1956fbc..3a32927f7 100644 --- a/e2e/utils/tenderly.ts +++ b/e2e/utils/tenderly.ts @@ -7,14 +7,18 @@ import { toHex, encodeFunctionData, pad, + concat, trim, createWalletClient, isAddress, + keccak256, } from 'viem'; import { Chain, optimism } from 'viem/chains'; -import type { Coin, ERC20TokenSymbol } from './contracts'; +import type { Coin } from './contracts'; import { erc20 } from './contracts'; +import type { ERC20 } from '../../types/contracts'; +import { escrowedExaABI } from '../../types/abi'; const { TENDERLY_USER, TENDERLY_PROJECT, TENDERLY_ACCESS_KEY } = process.env; @@ -56,6 +60,7 @@ export type Balance = { export type Tenderly = { url: () => string; setBalance: (address: Address, balance: Balance) => Promise; + increaseTime: (timestamp: number) => Promise; deleteFork: () => Promise; }; @@ -73,12 +78,61 @@ export const tenderly = async ({ chain = optimism }: { chain: Chain }): Promise< return await walletClient.request({ method: 'tenderly_setBalance' as any, params }); }; - const setERC20TokenBalance = async (address: Address, symbol: ERC20TokenSymbol, amount: number) => { - const tokenContract = await erc20(symbol, { publicClient }); - const tokenAmount = parseUnits(String(amount), await tokenContract.read.decimals()); + const setEXATokenBalance = async (token: ERC20, address: Address, amount: bigint) => { + const symbol = await token.read.symbol(); + + switch (symbol) { + case 'EXA': { + const timelock = '0x92024C4bDa9DA602b711B9AbB610d072018eb58b'; + await walletClient.request({ + method: 'eth_sendTransaction', + params: [ + { + from: timelock, + to: token.address, + data: encodeFunctionData({ + abi: token.abi, + functionName: 'transfer', + args: [address, amount], + }), + }, + ], + }); + + break; + } + case 'esEXA': { + const exa = await erc20('EXA', { publicClient, walletClient }); + await setEXATokenBalance(exa, address, amount); + await exa.write.approve([token.address, amount], { account: address, chain }); + await walletClient.writeContract({ + account: address, + address: token.address, + abi: escrowedExaABI, + functionName: 'mint', + args: [amount, address], + }); + + break; + } + default: + throw new Error('Invalid token'); + } + }; + + const setERC20TokenBalance = async (token: ERC20, address: Address, amount: bigint) => { + const index = (await token.read.symbol()) === 'WETH' ? '0x3' : '0x0'; + const slot = keccak256(concat([pad(address), pad(index)])); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const params = [token.address, slot, pad(toHex(amount))] as any; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + await walletClient.request({ method: 'tenderly_setStorageAt' as any, params }); + }; + const setOwnedERC20TokenBalance = async (token: ERC20, address: Address, amount: bigint) => { let owner = await publicClient.readContract({ - address: tokenContract.address, + address: token.address, abi: [ { stateMutability: 'view', @@ -91,8 +145,8 @@ export const tenderly = async ({ chain = optimism }: { chain: Chain }): Promise< functionName: 'owner', }); - if (symbol === 'USDC') { - const l2Bridge = await publicClient.getStorageAt({ address: tokenContract.address, slot: '0x6' }); + if ((await token.read.symbol()) === 'USDC') { + const l2Bridge = await publicClient.getStorageAt({ address: token.address, slot: '0x6' }); if (!l2Bridge) throw new Error('L2 bridge not found'); owner = pad(trim(l2Bridge), { size: 20 }); } @@ -108,7 +162,7 @@ export const tenderly = async ({ chain = optimism }: { chain: Chain }): Promise< params: [ { from, - to: tokenContract.address, + to: token.address, data: encodeFunctionData({ abi: [ { @@ -123,7 +177,7 @@ export const tenderly = async ({ chain = optimism }: { chain: Chain }): Promise< }, ], functionName: 'mint', - args: [address, tokenAmount], + args: [address, amount], }), }, ], @@ -134,17 +188,38 @@ export const tenderly = async ({ chain = optimism }: { chain: Chain }): Promise< url: () => url, setBalance: async (address: Address, balance: Balance) => { for (const symbol of Object.keys(balance) as Array) { - const amount = balance[symbol]; - if (!amount) return; + const _amount = balance[symbol]; + if (!_amount) return; + + if (symbol === 'ETH') { + await setNativeBalance(address, _amount); + return; + } + + const token = await erc20(symbol, { publicClient }); + const amount = parseUnits(String(_amount), await token.read.decimals()); + switch (symbol) { - case 'ETH': - await setNativeBalance(address, amount); + case 'USDC': + case 'OP': + await setOwnedERC20TokenBalance(token, address, amount); + break; + + case 'WETH': + await setERC20TokenBalance(token, address, amount); + break; + + case 'EXA': + case 'esEXA': + await setEXATokenBalance(token, address, amount); break; - default: - await setERC20TokenBalance(address, symbol, amount); } } }, + increaseTime: async (timestamp: number) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + await walletClient.request({ method: 'evm_increaseTime' as any, params: [toHex(Math.floor(timestamp))] }); + }, deleteFork: async () => deleteFork(forkId), }; }; diff --git a/hooks/useEscrowedEXA.ts b/hooks/useEscrowedEXA.ts index 2aa69d1a3..cb5aeb3d4 100644 --- a/hooks/useEscrowedEXA.ts +++ b/hooks/useEscrowedEXA.ts @@ -13,13 +13,12 @@ export const useEscrowedEXA = () => { type Stream = { id: string; - tokenId: number; - chainId: number; + tokenId: string; recipient: Address; - startTime: number; - endTime: number; - depositAmount: bigint; - withdrawnAmount: bigint; + startTime: string; + endTime: string; + depositAmount: string; + withdrawnAmount: string; canceled: boolean; }; diff --git a/pages/vesting.tsx b/pages/vesting.tsx index 1d9efe525..bc6279683 100644 --- a/pages/vesting.tsx +++ b/pages/vesting.tsx @@ -101,8 +101,8 @@ const Vesting: NextPage = () => { key={id} depositAmount={BigInt(depositAmount)} withdrawnAmount={BigInt(withdrawnAmount)} - startTime={startTime} - endTime={endTime} + startTime={Number(startTime)} + endTime={Number(endTime)} /> {index !== activeStreams.length - 1 && } diff --git a/queries/getStreams.ts b/queries/getStreams.ts index e07bcdf6a..8ba105349 100644 --- a/queries/getStreams.ts +++ b/queries/getStreams.ts @@ -4,17 +4,12 @@ export function getStreams(assetAddress: string, address: string, canceled: bool streams(orderBy: timestamp, orderDirection: asc, where:{ asset:"${assetAddress}", recipient: "${address}", canceled: ${canceled}}) { id tokenId - chainId recipient startTime endTime depositAmount withdrawnAmount canceled - asset { - symbol - address - } } } `; diff --git a/types/contracts.ts b/types/contracts.ts index 445de08cf..29c46919b 100644 --- a/types/contracts.ts +++ b/types/contracts.ts @@ -12,6 +12,8 @@ import { previewerABI, rewardsControllerABI, permit2ABI, + sablierV2LockupLinearABI, + escrowedExaABI, } from './abi'; export type ContractType = ReturnType>; @@ -27,3 +29,5 @@ export type RewardsController = ContractType; export type DebtManager = ContractType; export type Permit2 = ContractType; export type DebtPreviewer = ContractType; +export type SablierV2LockupLinear = ContractType; +export type EscrowedEXA = ContractType; From e5e3e03a17f6b9f706f18837d6c13de5a8f0f78d Mon Sep 17 00:00:00 2001 From: franm Date: Tue, 3 Oct 2023 18:46:47 -0300 Subject: [PATCH 09/38] =?UTF-8?q?=E2=9C=A8vesting:=20claim=20and=20claim?= =?UTF-8?q?=20all?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/ActiveStream/index.tsx | 162 +++++++++++++++++++++++++++--- pages/vesting.tsx | 75 +++++++++++++- 2 files changed, 219 insertions(+), 18 deletions(-) diff --git a/components/ActiveStream/index.tsx b/components/ActiveStream/index.tsx index 9edad6a7b..2756a2f2d 100644 --- a/components/ActiveStream/index.tsx +++ b/components/ActiveStream/index.tsx @@ -1,13 +1,19 @@ import { Box, Button, Divider, LinearProgress, Typography, linearProgressClasses, styled } from '@mui/material'; -import React, { FC, useMemo } from 'react'; +import React, { FC, useCallback, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import Image from 'next/image'; import formatNumber from 'utils/formatNumber'; +import { toPercentage } from 'utils/utils'; +import { useWeb3 } from 'hooks/useWeb3'; +import { useNetwork, useSwitchNetwork } from 'wagmi'; +import { useEscrowedEXA } from 'hooks/useEscrowedEXA'; +import { waitForTransaction } from '@wagmi/core'; +import { LoadingButton } from '@mui/lab'; const StyledLinearProgress = styled(LinearProgress, { shouldForwardProp: (prop) => prop !== 'barColor', })<{ barColor: string }>(({ theme, barColor }) => ({ - height: 8, + height: 6, borderRadius: 5, [`&.${linearProgressClasses.colorPrimary}`]: { backgroundColor: theme.palette.grey[100], @@ -18,15 +24,68 @@ const StyledLinearProgress = styled(LinearProgress, { }, })); +const CustomProgressBar: React.FC<{ value: number }> = ({ value }) => { + return ( + + + 0% + + + + + + {toPercentage(value / 100, value === 100 ? 0 : 2)} + + + + + 100% + + + ); +}; + type ActiveStreamProps = { + tokenId: number; depositAmount: bigint; withdrawnAmount: bigint; startTime: number; endTime: number; }; -const ActiveStream: FC = ({ depositAmount, withdrawnAmount, startTime, endTime }) => { +const ActiveStream: FC = ({ tokenId, depositAmount, withdrawnAmount, startTime, endTime }) => { const { t } = useTranslation(); + const { impersonateActive, chain: displayNetwork, opts } = useWeb3(); + const { chain } = useNetwork(); + const { switchNetwork, isLoading: switchIsLoading } = useSwitchNetwork(); + const escrowedEXA = useEscrowedEXA(); + const [loading, setLoading] = useState(false); + + const handleClick = useCallback(async () => { + if (!escrowedEXA || !opts) return; + setLoading(true); + try { + const tx = await escrowedEXA.write.withdrawMax([[BigInt(tokenId)]], opts); + await waitForTransaction({ hash: tx }); + } catch { + // if request fails, don't do anything + } finally { + setLoading(false); + } + }, [escrowedEXA, opts, tokenId]); const elapsed = useMemo(() => { const now = Math.floor(Date.now() / 1000); @@ -59,6 +118,10 @@ const ActiveStream: FC = ({ depositAmount, withdrawnAmount, s const minutesLeft = Math.floor(secondsLeft / 60); return t('{{minutesLeft}} minutes left', { minutesLeft }); } + + if (daysLeft < 0) { + return t('Completed'); + } }, [endTime, t]); const reserved = (depositAmount * 20000000000000000000n) / 100000000000000000000n; @@ -82,12 +145,28 @@ const ActiveStream: FC = ({ depositAmount, withdrawnAmount, s {formatNumber(Number(depositAmount) / 1e18)} + (mode === 'light' ? '#EEEEEE' : '#2A2A2A')} + px={0.5} + borderRadius="2px" + alignItems="center" + // onClick={onClick} + sx={{ cursor: 'pointer' }} + > + {`${t('View NFT').toUpperCase()}`} + - {t('Claimed EXA')} + {t('Reserved EXA')} = ({ depositAmount, withdrawnAmount, s {formatNumber(Number(withdrawnAmount) / 1e18)} - {`/ ${formatNumber(Number(depositAmount + reserved) / 1e18)}`} + {`/ ${formatNumber(Number(reserved) / 1e18)}`} + (mode === 'light' ? '#EEEEEE' : '#2A2A2A')} + px={0.5} + borderRadius="2px" + alignItems="center" + // onClick={onClick} + sx={{ cursor: 'pointer' }} + > + {`${t('Whitdraw').toUpperCase()}`} + - {t('Vesting Period')} - - - {timeLeft} + {t('Claimed EXA')} + + EXA + + {formatNumber(Number(withdrawnAmount) / 1e18)} + + + {`/ ${formatNumber(Number(depositAmount) / 1e18)}`} + + - - - + + + {impersonateActive ? ( + + ) : chain && chain.id !== displayNetwork.id ? ( + switchNetwork?.(displayNetwork.id)} + variant="contained" + loading={switchIsLoading} + > + {t('Please switch to {{network}} network', { network: displayNetwork.name })} + + ) : ( + <> + + {progress === 100 ? t('Claim & Whitdraw EXA') : t('Claim EXA')} + + + )} + + + + + + {t('Vesting Period')}: + + + {timeLeft} + + - ); }; diff --git a/pages/vesting.tsx b/pages/vesting.tsx index bc6279683..e8be8f8db 100644 --- a/pages/vesting.tsx +++ b/pages/vesting.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useCallback, useState } from 'react'; import type { NextPage } from 'next'; import { usePageView } from 'hooks/useAnalytics'; @@ -6,12 +6,35 @@ import { useTranslation } from 'react-i18next'; import { Box, Button, Divider, Grid, Typography } from '@mui/material'; import VestingInput from 'components/VestingInput'; import ActiveStream from 'components/ActiveStream'; -import useUpdateStreams from 'hooks/useEscrowedEXA'; +import useUpdateStreams, { useEscrowedEXA } from 'hooks/useEscrowedEXA'; +import { useWeb3 } from 'hooks/useWeb3'; +import { useNetwork, useSwitchNetwork } from 'wagmi'; +import { LoadingButton } from '@mui/lab'; +import { waitForTransaction } from '@wagmi/core'; const Vesting: NextPage = () => { usePageView('/vesting', 'Vesting'); const { t } = useTranslation(); - const { activeStreams } = useUpdateStreams(); + const { impersonateActive, chain: displayNetwork, opts } = useWeb3(); + const { chain } = useNetwork(); + const { activeStreams, loading: streamsLoading } = useUpdateStreams(); + const { switchNetwork, isLoading: switchIsLoading } = useSwitchNetwork(); + + const [loading, setLoading] = useState(false); + const escrowedEXA = useEscrowedEXA(); + + const handleClick = useCallback(async () => { + if (!activeStreams || !escrowedEXA || !opts) return; + setLoading(true); + try { + const tx = await escrowedEXA.write.withdrawMax([activeStreams.map(({ tokenId }) => BigInt(tokenId))], opts); + await waitForTransaction({ hash: tx }); + } catch { + // if request fails, don't do anything + } finally { + setLoading(false); + } + }, [activeStreams, escrowedEXA, opts]); return ( @@ -89,16 +112,56 @@ const Vesting: NextPage = () => { )} - {activeStreams.length > 0 ? ( + + {streamsLoading && loading...} + + {activeStreams.length > 0 && !streamsLoading && ( (palette.mode === 'light' ? '0px 3px 4px 0px rgba(97, 102, 107, 0.25)' : '')} > + + + + + + + {t('You can claim all streams at once.')} + + + + + {impersonateActive ? ( + + ) : chain && chain.id !== displayNetwork.id ? ( + switchNetwork?.(displayNetwork.id)} + variant="contained" + loading={switchIsLoading} + > + {t('Please switch to {{network}} network', { network: displayNetwork.name })} + + ) : ( + <> + + {t('Claim All')} + + + )} + + + + + {activeStreams.map(({ id, tokenId, depositAmount, withdrawnAmount, startTime, endTime }, index) => ( <> { ))} - ) : ( + )} + + {activeStreams.length === 0 && !streamsLoading && ( Date: Wed, 4 Oct 2023 11:12:15 -0300 Subject: [PATCH 10/38] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20vesting:=20minor=20c?= =?UTF-8?q?alcs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/ActiveStream/index.tsx | 101 ++++++++++++++++++------------ components/VestingInput/index.tsx | 98 ++++++++++++++++++----------- hooks/useEscrowedEXA.ts | 42 ++++++++++++- pages/vesting.tsx | 31 ++++----- queries/getStreams.ts | 1 + 5 files changed, 178 insertions(+), 95 deletions(-) diff --git a/components/ActiveStream/index.tsx b/components/ActiveStream/index.tsx index 2756a2f2d..540a5111a 100644 --- a/components/ActiveStream/index.tsx +++ b/components/ActiveStream/index.tsx @@ -1,14 +1,24 @@ -import { Box, Button, Divider, LinearProgress, Typography, linearProgressClasses, styled } from '@mui/material'; +import { + Box, + Button, + Divider, + LinearProgress, + Skeleton, + Typography, + linearProgressClasses, + styled, +} from '@mui/material'; import React, { FC, useCallback, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; +import { waitForTransaction } from '@wagmi/core'; +import { LoadingButton } from '@mui/lab'; import Image from 'next/image'; import formatNumber from 'utils/formatNumber'; import { toPercentage } from 'utils/utils'; import { useWeb3 } from 'hooks/useWeb3'; import { useNetwork, useSwitchNetwork } from 'wagmi'; -import { useEscrowedEXA } from 'hooks/useEscrowedEXA'; -import { waitForTransaction } from '@wagmi/core'; -import { LoadingButton } from '@mui/lab'; +import { useEscrowedEXA, useEscrowedEXAReserves } from 'hooks/useEscrowedEXA'; +import { useSablierV2LockupLinearWithdrawableAmountOf } from 'hooks/useSablier'; const StyledLinearProgress = styled(LinearProgress, { shouldForwardProp: (prop) => prop !== 'barColor', @@ -64,13 +74,25 @@ type ActiveStreamProps = { withdrawnAmount: bigint; startTime: number; endTime: number; + cancellable: boolean; }; -const ActiveStream: FC = ({ tokenId, depositAmount, withdrawnAmount, startTime, endTime }) => { +const ActiveStream: FC = ({ + tokenId, + depositAmount, + withdrawnAmount, + startTime, + endTime, + cancellable, +}) => { const { t } = useTranslation(); const { impersonateActive, chain: displayNetwork, opts } = useWeb3(); const { chain } = useNetwork(); const { switchNetwork, isLoading: switchIsLoading } = useSwitchNetwork(); + const { data: reserve, isLoading: reserveIsLoading } = useEscrowedEXAReserves(BigInt(tokenId)); + const { data: withdrawable, isLoading: withdrawableIsLoading } = useSablierV2LockupLinearWithdrawableAmountOf( + BigInt(tokenId), + ); const escrowedEXA = useEscrowedEXA(); const [loading, setLoading] = useState(false); @@ -124,8 +146,6 @@ const ActiveStream: FC = ({ tokenId, depositAmount, withdrawn } }, [endTime, t]); - const reserved = (depositAmount * 20000000000000000000n) / 100000000000000000000n; - return ( @@ -154,12 +174,9 @@ const ActiveStream: FC = ({ tokenId, depositAmount, withdrawn // onClick={onClick} sx={{ cursor: 'pointer' }} > - {`${t('View NFT').toUpperCase()}`} + + {t('View NFT')} + @@ -176,34 +193,34 @@ const ActiveStream: FC = ({ tokenId, depositAmount, withdrawn height={20} style={{ maxWidth: '100%', height: 'auto' }} /> - - {formatNumber(Number(withdrawnAmount) / 1e18)} - - - {`/ ${formatNumber(Number(reserved) / 1e18)}`} - - (mode === 'light' ? '#EEEEEE' : '#2A2A2A')} - px={0.5} - borderRadius="2px" - alignItems="center" - // onClick={onClick} - sx={{ cursor: 'pointer' }} - > - {`${t('Whitdraw').toUpperCase()}`} - + {reserveIsLoading ? ( + + ) : ( + + {formatNumber(Number(reserve ?? 0n) / 1e18)} + + )} + {cancellable && ( + (mode === 'light' ? '#EEEEEE' : '#2A2A2A')} + px={0.5} + borderRadius="2px" + alignItems="center" + // onClick={onClick} + sx={{ cursor: 'pointer' }} + > + + {t('Whitdraw')} + + + )} - {t('Claimed EXA')} + {t('Claimable EXA')} = ({ tokenId, depositAmount, withdrawn height={20} style={{ maxWidth: '100%', height: 'auto' }} /> - - {formatNumber(Number(withdrawnAmount) / 1e18)} - + {withdrawableIsLoading ? ( + + ) : ( + + {formatNumber(Number(withdrawable) / 1e18)} + + )} - {`/ ${formatNumber(Number(depositAmount) / 1e18)}`} + / {formatNumber(Number(depositAmount - withdrawnAmount) / 1e18)} diff --git a/components/VestingInput/index.tsx b/components/VestingInput/index.tsx index 76659171d..56cd8c18d 100644 --- a/components/VestingInput/index.tsx +++ b/components/VestingInput/index.tsx @@ -1,5 +1,5 @@ -import React, { useMemo, useState } from 'react'; -import { Box, Button, Typography } from '@mui/material'; +import React, { useCallback, useMemo, useState } from 'react'; +import { Box, Button, Skeleton, Typography } from '@mui/material'; import { ModalBox } from 'components/common/modal/ModalBox'; @@ -10,14 +10,17 @@ import { useTranslation } from 'react-i18next'; import { LoadingButton } from '@mui/lab'; import Image from 'next/image'; import { useEXABalance, useEXAPrice } from 'hooks/useEXA'; -import { formatEther, formatUnits, parseUnits } from 'viem'; +import { useEscrowedEXABalance, useEscrowedEXAReserveRatio } from 'hooks/useEscrowedEXA'; +import { formatEther, parseEther } from 'viem'; import formatNumber from 'utils/formatNumber'; import { WEI_PER_ETHER } from 'utils/const'; function VestingInput() { const { t } = useTranslation(); + const { data: balance, isLoading: balanceIsLoading } = useEscrowedEXABalance(); const { data: exaBalance } = useEXABalance(); + const { data: reserveRatio } = useEscrowedEXAReserveRatio(); const EXAPrice = useEXAPrice(); const { impersonateActive, chain: displayNetwork, isConnected } = useWeb3(); const { isLoading: switchIsLoading } = useSwitchNetwork(); @@ -29,50 +32,69 @@ function VestingInput() { const value = useMemo(() => { if (!qty || !EXAPrice) return; - const parsedqty = parseUnits(qty, 18); + const parsedqty = parseEther(qty); const usd = (parsedqty * EXAPrice) / WEI_PER_ETHER; - return formatUnits(usd, 18); + return formatEther(usd); }, [EXAPrice, qty]); + const reserve = useMemo(() => { + if (reserveRatio === undefined || !qty) return; + const parsed = parseEther(qty); + return formatEther((parsed * reserveRatio) / WEI_PER_ETHER); + }, [reserveRatio, qty]); + + const submit = useCallback(() => { + if (reserveRatio === undefined) return; + }, [reserveRatio]); + return ( - - - - - - - esEXA + + + + + + + + esEXA + + + + + + {isConnected ? ( + balanceIsLoading ? ( + + ) : ( + + {t('Available')}: {formatNumber(formatEther(balance || 0n))} esEXA + + ) + ) : null} + + ~${formatNumber(value || '0', 'USD')} - - - - {t('Available')}: {formatNumber(formatEther(exaBalance || 0n))} esEXA - - - ~${formatNumber(value || '0', 'USD')} - - - - + + {reserve ? formatNumber(reserve) : null} + {impersonateActive ? ( diff --git a/hooks/useEscrowedEXA.ts b/hooks/useEscrowedEXA.ts index cb5aeb3d4..cee8a1464 100644 --- a/hooks/useEscrowedEXA.ts +++ b/hooks/useEscrowedEXA.ts @@ -1,11 +1,11 @@ import { useCallback, useEffect, useState } from 'react'; -import { escrowedExaABI } from 'types/abi'; +import { Address, zeroAddress } from 'viem'; +import { escrowedExaABI, useEscrowedExaBalanceOf, useEscrowedExaReserveRatio, useEscrowedExaReserves } from 'types/abi'; import useContract from './useContract'; import { useEXA } from './useEXA'; import useGraphClient from './useGraphClient'; import { useWeb3 } from './useWeb3'; import { getStreams } from 'queries/getStreams'; -import { Address } from 'viem'; export const useEscrowedEXA = () => { return useContract('EscrowedEXA', escrowedExaABI); @@ -20,9 +20,10 @@ type Stream = { depositAmount: string; withdrawnAmount: string; canceled: boolean; + cancelable: boolean; }; -export default function useUpdateStreams() { +export function useUpdateStreams() { const EXA = useEXA(); const { walletAddress } = useWeb3(); const request = useGraphClient(); @@ -46,3 +47,38 @@ export default function useUpdateStreams() { return { activeStreams, loading }; } + +export const useEscrowedEXAReserves = (stream: bigint) => { + const { chain } = useWeb3(); + const esEXA = useEscrowedEXA(); + + return useEscrowedExaReserves({ + chainId: chain.id, + address: esEXA?.address, + args: [stream], + staleTime: 30_000, + }); +}; + +export const useEscrowedEXABalance = () => { + const { chain, walletAddress } = useWeb3(); + const esEXA = useEscrowedEXA(); + + return useEscrowedExaBalanceOf({ + chainId: chain.id, + address: esEXA?.address, + args: [walletAddress ?? zeroAddress], + staleTime: 30_000, + }); +}; + +export const useEscrowedEXAReserveRatio = () => { + const { chain } = useWeb3(); + const esEXA = useEscrowedEXA(); + + return useEscrowedExaReserveRatio({ + chainId: chain.id, + address: esEXA?.address, + staleTime: 30_000, + }); +}; diff --git a/pages/vesting.tsx b/pages/vesting.tsx index e8be8f8db..f10cb64a3 100644 --- a/pages/vesting.tsx +++ b/pages/vesting.tsx @@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next'; import { Box, Button, Divider, Grid, Typography } from '@mui/material'; import VestingInput from 'components/VestingInput'; import ActiveStream from 'components/ActiveStream'; -import useUpdateStreams, { useEscrowedEXA } from 'hooks/useEscrowedEXA'; +import { useUpdateStreams, useEscrowedEXA } from 'hooks/useEscrowedEXA'; import { useWeb3 } from 'hooks/useWeb3'; import { useNetwork, useSwitchNetwork } from 'wagmi'; import { LoadingButton } from '@mui/lab'; @@ -157,19 +157,22 @@ const Vesting: NextPage = () => { - {activeStreams.map(({ id, tokenId, depositAmount, withdrawnAmount, startTime, endTime }, index) => ( - <> - - {index !== activeStreams.length - 1 && } - - ))} + {activeStreams.map( + ({ id, tokenId, depositAmount, withdrawnAmount, startTime, endTime, cancelable }, index) => ( + <> + + {index !== activeStreams.length - 1 && } + + ), + )} )} diff --git a/queries/getStreams.ts b/queries/getStreams.ts index 8ba105349..07b9bba9b 100644 --- a/queries/getStreams.ts +++ b/queries/getStreams.ts @@ -10,6 +10,7 @@ export function getStreams(assetAddress: string, address: string, canceled: bool depositAmount withdrawnAmount canceled + cancelable } } `; From baa0caadcb6df8243f6b0d0e3bb668abdef975ce Mon Sep 17 00:00:00 2001 From: Jorge Galat Date: Wed, 4 Oct 2023 12:14:25 -0300 Subject: [PATCH 11/38] =?UTF-8?q?=E2=9C=A8vesting:=20add=20reserver=20rati?= =?UTF-8?q?o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/VestingInput/index.tsx | 70 +++++++++++++++++++++++++++---- 1 file changed, 62 insertions(+), 8 deletions(-) diff --git a/components/VestingInput/index.tsx b/components/VestingInput/index.tsx index 56cd8c18d..6c08b482d 100644 --- a/components/VestingInput/index.tsx +++ b/components/VestingInput/index.tsx @@ -6,7 +6,7 @@ import { ModalBox } from 'components/common/modal/ModalBox'; import ModalInput from 'components/OperationsModal/ModalInput'; import { useWeb3 } from 'hooks/useWeb3'; import { useSwitchNetwork } from 'wagmi'; -import { useTranslation } from 'react-i18next'; +import { useTranslation, Trans } from 'react-i18next'; import { LoadingButton } from '@mui/lab'; import Image from 'next/image'; import { useEXABalance, useEXAPrice } from 'hooks/useEXA'; @@ -14,6 +14,8 @@ import { useEscrowedEXABalance, useEscrowedEXAReserveRatio } from 'hooks/useEscr import { formatEther, parseEther } from 'viem'; import formatNumber from 'utils/formatNumber'; import { WEI_PER_ETHER } from 'utils/const'; +import { toPercentage } from 'utils/utils'; +import Link from 'next/link'; function VestingInput() { const { t } = useTranslation(); @@ -38,11 +40,12 @@ function VestingInput() { return formatEther(usd); }, [EXAPrice, qty]); - const reserve = useMemo(() => { - if (reserveRatio === undefined || !qty) return; + const [reserve, moreThanBalance] = useMemo(() => { + if (reserveRatio === undefined || exaBalance === undefined || !qty) return [undefined, false]; const parsed = parseEther(qty); - return formatEther((parsed * reserveRatio) / WEI_PER_ETHER); - }, [reserveRatio, qty]); + const _reserve = (parsed * reserveRatio) / WEI_PER_ETHER; + return [formatEther(_reserve), _reserve > exaBalance]; + }, [reserveRatio, qty, exaBalance]); const submit = useCallback(() => { if (reserveRatio === undefined) return; @@ -51,10 +54,21 @@ function VestingInput() { return ( - + - + esEXA @@ -93,7 +107,47 @@ function VestingInput() { - {reserve ? formatNumber(reserve) : null} + {reserve ? ( + + {moreThanBalance ? ( + + , + }} + /> + + ) : ( + <> + + {t('{{number}} Reserve', { number: toPercentage(Number(reserveRatio) / 1e18, 0) })} + + + + + EXA + + + {formatNumber(reserve)} + + + + )} + + ) : null} From 7e8ebe49c32d86ddc7e8a4d3929174f0c8819745 Mon Sep 17 00:00:00 2001 From: Jorge Galat Date: Wed, 4 Oct 2023 12:43:23 -0300 Subject: [PATCH 12/38] =?UTF-8?q?=E2=9C=A8=20vesting:=20add=20submit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/VestingInput/index.tsx | 119 ++++++++++++++++++++++++++---- 1 file changed, 106 insertions(+), 13 deletions(-) diff --git a/components/VestingInput/index.tsx b/components/VestingInput/index.tsx index 6c08b482d..ee8547499 100644 --- a/components/VestingInput/index.tsx +++ b/components/VestingInput/index.tsx @@ -1,37 +1,54 @@ import React, { useCallback, useMemo, useState } from 'react'; import { Box, Button, Skeleton, Typography } from '@mui/material'; +import dayjs from 'dayjs'; +import { splitSignature } from '@ethersproject/bytes'; +import { type Hex, formatEther, parseEther } from 'viem'; +import { waitForTransaction } from '@wagmi/core'; +import { escrowedExaABI } from 'types/abi'; +import { AbiParametersToPrimitiveTypes, ExtractAbiFunction, ExtractAbiFunctionNames } from 'abitype'; import { ModalBox } from 'components/common/modal/ModalBox'; import ModalInput from 'components/OperationsModal/ModalInput'; import { useWeb3 } from 'hooks/useWeb3'; -import { useSwitchNetwork } from 'wagmi'; +import { useNetwork, useSignTypedData, useSwitchNetwork } from 'wagmi'; import { useTranslation, Trans } from 'react-i18next'; import { LoadingButton } from '@mui/lab'; import Image from 'next/image'; -import { useEXABalance, useEXAPrice } from 'hooks/useEXA'; -import { useEscrowedEXABalance, useEscrowedEXAReserveRatio } from 'hooks/useEscrowedEXA'; -import { formatEther, parseEther } from 'viem'; +import { useEXA, useEXABalance, useEXAPrice } from 'hooks/useEXA'; +import { useEscrowedEXA, useEscrowedEXABalance, useEscrowedEXAReserveRatio } from 'hooks/useEscrowedEXA'; import formatNumber from 'utils/formatNumber'; import { WEI_PER_ETHER } from 'utils/const'; import { toPercentage } from 'utils/utils'; import Link from 'next/link'; +import useIsContract from 'hooks/useIsContract'; +import { gasLimit } from 'utils/gas'; + +type Params> = AbiParametersToPrimitiveTypes< + ExtractAbiFunction['inputs'] +>; function VestingInput() { const { t } = useTranslation(); + const { chain } = useNetwork(); + const exa = useEXA(); + const escrowedEXA = useEscrowedEXA(); const { data: balance, isLoading: balanceIsLoading } = useEscrowedEXABalance(); const { data: exaBalance } = useEXABalance(); const { data: reserveRatio } = useEscrowedEXAReserveRatio(); const EXAPrice = useEXAPrice(); - const { impersonateActive, chain: displayNetwork, isConnected } = useWeb3(); - const { isLoading: switchIsLoading } = useSwitchNetwork(); + const { impersonateActive, chain: displayNetwork, isConnected, opts, walletAddress } = useWeb3(); + const { isLoading: switchIsLoading, switchNetwork } = useSwitchNetwork(); + const isContract = useIsContract(); + const { signTypedDataAsync } = useSignTypedData(); + const [isLoading, setIsLoading] = useState(false); const [qty, setQty] = useState(''); const errorData = false; - const value = useMemo(() => { + const usdValue = useMemo(() => { if (!qty || !EXAPrice) return; const parsedqty = parseEther(qty); @@ -47,9 +64,76 @@ function VestingInput() { return [formatEther(_reserve), _reserve > exaBalance]; }, [reserveRatio, qty, exaBalance]); - const submit = useCallback(() => { - if (reserveRatio === undefined) return; - }, [reserveRatio]); + const sign = useCallback(async () => { + if (!walletAddress || reserveRatio === undefined || !exa || !escrowedEXA) return; + + const deadline = BigInt(dayjs().unix() + 3_600); + const _qty = parseEther(qty); + const value = (_qty * reserveRatio) / WEI_PER_ETHER; + + const nonce = await exa.read.nonces([walletAddress], opts); + const name = await exa.read.name(opts); + + const { v, r, s } = await signTypedDataAsync({ + primaryType: 'Permit', + domain: { + name, + version: '1', + chainId: displayNetwork.id, + verifyingContract: exa.address, + }, + types: { + Permit: [ + { name: 'owner', type: 'address' }, + { name: 'spender', type: 'address' }, + { name: 'value', type: 'uint256' }, + { name: 'nonce', type: 'uint256' }, + { name: 'deadline', type: 'uint256' }, + ], + }, + message: { + owner: walletAddress, + spender: escrowedEXA.address, + value, + nonce, + deadline, + }, + }).then(splitSignature); + + return { + value, + deadline, + ...{ v, r: r as Hex, s: s as Hex }, + } as const; + }, [displayNetwork.id, escrowedEXA, exa, opts, qty, reserveRatio, signTypedDataAsync, walletAddress]); + + const submit = useCallback(async () => { + if (!walletAddress || reserve === undefined || !escrowedEXA || !opts || !qty) return; + + setIsLoading(true); + try { + const amount = parseEther(qty); + + let args: Params<'vest'> = [amount, walletAddress] as const; + + if (await isContract(walletAddress)) { + // TODO + return; + } + + const p = await sign(); + if (!p) return; + args = [...args, p] as const; + const gas = await escrowedEXA.estimateGas.vest(args, opts); + const hash = await escrowedEXA.write.vest(args, { ...opts, gasLimit: gasLimit(gas) }); + + await waitForTransaction({ hash }); + } catch (e) { + // TODO + } finally { + setIsLoading(false); + } + }, [walletAddress, reserve, escrowedEXA, qty, isContract, sign, opts]); return ( @@ -102,7 +186,7 @@ function VestingInput() { ) ) : null} - ~${formatNumber(value || '0', 'USD')} + ~${formatNumber(usdValue || '0', 'USD')} @@ -155,10 +239,19 @@ function VestingInput() { - ) : ( - + ) : displayNetwork.id !== chain?.id ? ( + switchNetwork?.(displayNetwork.id)} + > {t('Please switch to {{network}} network', { network: displayNetwork.name })} + ) : ( + + {t('Vest esEXA')} + )} From ac4fedcfec700c96c0d8937eac4bd6b2371c5536 Mon Sep 17 00:00:00 2001 From: Jorge Galat Date: Wed, 4 Oct 2023 18:20:00 -0300 Subject: [PATCH 13/38] =?UTF-8?q?=E2=9C=85=20vesting:=20add=20e2e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/ActiveStream/index.tsx | 26 +-- .../OperationsModal/ModalInput/index.tsx | 5 +- components/VestingInput/index.tsx | 19 +- e2e/page/vesting.ts | 135 +++++++++++++++ e2e/specs/7-leverage/usdc.spec.ts | 2 +- e2e/specs/8-vesting/esEXA.spec.ts | 163 ++++++++++++++++-- e2e/utils/tenderly.ts | 4 +- pages/vesting.tsx | 8 +- 8 files changed, 326 insertions(+), 36 deletions(-) create mode 100644 e2e/page/vesting.ts diff --git a/components/ActiveStream/index.tsx b/components/ActiveStream/index.tsx index 540a5111a..7715f8e6c 100644 --- a/components/ActiveStream/index.tsx +++ b/components/ActiveStream/index.tsx @@ -34,7 +34,7 @@ const StyledLinearProgress = styled(LinearProgress, { }, })); -const CustomProgressBar: React.FC<{ value: number }> = ({ value }) => { +const CustomProgressBar: React.FC<{ value: number; 'data-testid'?: string }> = ({ value, 'data-testid': testId }) => { return ( @@ -56,7 +56,7 @@ const CustomProgressBar: React.FC<{ value: number }> = ({ value }) => { borderRadius: '4px', }} > - + {toPercentage(value / 100, value === 100 ? 0 : 2)} @@ -147,7 +147,7 @@ const ActiveStream: FC = ({ }, [endTime, t]); return ( - + @@ -162,7 +162,7 @@ const ActiveStream: FC = ({ height={20} style={{ maxWidth: '100%', height: 'auto' }} /> - + {formatNumber(Number(depositAmount) / 1e18)} = ({ {reserveIsLoading ? ( ) : ( - + {formatNumber(Number(reserve ?? 0n) / 1e18)} )} @@ -233,11 +233,11 @@ const ActiveStream: FC = ({ {withdrawableIsLoading ? ( ) : ( - + {formatNumber(Number(withdrawable) / 1e18)} )} - + / {formatNumber(Number(depositAmount - withdrawnAmount) / 1e18)} @@ -260,7 +260,13 @@ const ActiveStream: FC = ({ ) : ( <> - + {progress === 100 ? t('Claim & Whitdraw EXA') : t('Claim EXA')} @@ -272,11 +278,11 @@ const ActiveStream: FC = ({ {t('Vesting Period')}: - + {timeLeft} - + ); diff --git a/components/OperationsModal/ModalInput/index.tsx b/components/OperationsModal/ModalInput/index.tsx index d969980ff..4b8180076 100644 --- a/components/OperationsModal/ModalInput/index.tsx +++ b/components/OperationsModal/ModalInput/index.tsx @@ -5,6 +5,7 @@ import { NumericFormat } from 'react-number-format'; type CustomProps = { decimals: number; onValueChange: (value: string) => void; + 'data-testid'?: string; }; const NumberFormatCustom = React.forwardRef( @@ -32,7 +33,7 @@ const NumberFormatCustom = React.forwardRef ); }, @@ -57,6 +58,7 @@ function ModalInput({ maxWidth, align = 'right', disabled = false, + 'data-testid': testId = 'modal-input', ...props }: Props) { return ( @@ -70,6 +72,7 @@ function ModalInput({ style: { padding: 0, textAlign: align }, onValueChange: onValueChange, decimals: decimals, + 'data-testid': testId, }} disabled={disabled} value={value} diff --git a/components/VestingInput/index.tsx b/components/VestingInput/index.tsx index ee8547499..859217b1c 100644 --- a/components/VestingInput/index.tsx +++ b/components/VestingInput/index.tsx @@ -165,6 +165,7 @@ function VestingInput() { align="right" maxWidth="100%" sx={{ paddingTop: 0, fontSize: 21 }} + data-testid="vesting-input" /> {isConnected ? ( balanceIsLoading ? ( - + ) : ( - + {t('Available')}: {formatNumber(formatEther(balance || 0n))} esEXA ) @@ -206,7 +207,7 @@ function VestingInput() { }} > {moreThanBalance ? ( - + ) : ( <> - + {t('{{number}} Reserve', { number: toPercentage(Number(reserveRatio) / 1e18, 0) })} @@ -224,7 +225,7 @@ function VestingInput() { EXA - + {formatNumber(reserve)} @@ -249,7 +250,13 @@ function VestingInput() { {t('Please switch to {{network}} network', { network: displayNetwork.name })} ) : ( - + {t('Vest esEXA')} )} diff --git a/e2e/page/vesting.ts b/e2e/page/vesting.ts new file mode 100644 index 000000000..599b190ec --- /dev/null +++ b/e2e/page/vesting.ts @@ -0,0 +1,135 @@ +import { Page, expect } from '@playwright/test'; + +export default function (page: Page) { + const waitForPageToBeReady = async () => { + await page.waitForFunction(() => document.querySelectorAll('.MuiSkeleton-root').length === 0, null, { + timeout: 30_000, + polling: 1_000, + }); + }; + + const checkBalanceAvailable = async (balance: string | RegExp) => { + await expect(page.getByTestId('vesting-balance')).toContainText(balance); + }; + + const checkReserveNeeded = async (ratio: string | RegExp, reserve: string | RegExp) => { + await expect(page.getByTestId('vesting-reserve-ratio')).toContainText(ratio); + await expect(page.getByTestId('vesting-reserve')).toContainText(reserve); + }; + + const input = async (text: string) => { + const inp = page.getByTestId('vesting-input'); + await expect(inp).toBeVisible(); + await inp.fill(text); + }; + + const checkError = async (error: string | RegExp) => { + const container = page.getByTestId('vesting-error'); + await expect(container).toBeVisible(); + await expect(container).toHaveText(error); + }; + + const waitForSubmitToBeReady = async () => { + await page.waitForFunction( + () => { + const button = document.querySelector('[data-testid="vesting-submit"]'); + if (!button) return true; + return !button.classList.contains('MuiLoadingButton-loading'); + }, + null, + { + timeout: 30_000, + polling: 1_000, + }, + ); + }; + + const submit = async () => { + const button = page.getByTestId('vesting-submit'); + await expect(button).not.toBeDisabled(); + await button.click(); + }; + + const waitForVestTransaction = async () => { + await waitForSubmitToBeReady(); + }; + + type Stream = { + id: number; + vested: string | RegExp; + reserved: string | RegExp; + withdrawable: string | RegExp; + left: string | RegExp; + progress: string | RegExp; + }; + + const checkStream = async (stream: Stream) => { + const { id, ...props } = stream; + const row = page.getByTestId(`vesting-stream-${id}`); + await expect(row).toBeVisible(); + + for (const key in props) { + const el = page.getByTestId(`vesting-stream-${id}-${key}`); + await expect(el).toBeVisible(); + await expect(el).toContainText(props[key]); + } + }; + + const claimStream = async (streamId: number) => { + const button = page.getByTestId(`vesting-stream-${streamId}-claim`); + await expect(button).not.toBeDisabled(); + await button.click(); + }; + + const waitForClaimStreamTransaction = async (streamId: number) => { + await page.waitForFunction( + (id) => { + const button = document.querySelector(`[data-testid="vesting-stream-${id}-claim"]`); + if (!button) return true; + return !button.classList.contains('MuiLoadingButton-loading'); + }, + streamId, + { + timeout: 30_000, + polling: 1_000, + }, + ); + }; + + const claimAllStreams = async () => { + const button = page.getByTestId('vesting-claim-all'); + await expect(button).not.toBeDisabled(); + await button.click(); + }; + + const waitForClaimAllTransaction = async () => { + await page.waitForFunction( + () => { + const button = document.querySelector('[data-testid="vesting-claim-all"]'); + if (!button) return true; + return !button.classList.contains('MuiLoadingButton-loading'); + }, + null, + { + timeout: 30_000, + polling: 1_000, + }, + ); + }; + + return { + waitForPageToBeReady, + checkBalanceAvailable, + checkReserveNeeded, + input, + checkError, + waitForSubmitToBeReady, + submit, + waitForVestTransaction, + checkStream, + claimStream, + waitForClaimStreamTransaction, + claimAllStreams, + waitForClaimAllTransaction, + }; +} diff --git a/e2e/specs/7-leverage/usdc.spec.ts b/e2e/specs/7-leverage/usdc.spec.ts index ae71cd2fb..097038b28 100644 --- a/e2e/specs/7-leverage/usdc.spec.ts +++ b/e2e/specs/7-leverage/usdc.spec.ts @@ -28,7 +28,7 @@ test('USDC leverage', async ({ page, web3, setup }) => { await setup.enterMarket('USDC'); await setup.deposit({ symbol: 'USDC', amount: '10000', receiver: web3.account.address }); - await usdc.write.approve([p2.address, 2n ** 256n - 1n], { account: web3.account.address, chain }); + await usdc.write.approve([p2.address, 2n ** 256n - 1n], { account: web3.account, chain }); await app.reload(); diff --git a/e2e/specs/8-vesting/esEXA.spec.ts b/e2e/specs/8-vesting/esEXA.spec.ts index 6b84fa13d..d3d4a9ebb 100644 --- a/e2e/specs/8-vesting/esEXA.spec.ts +++ b/e2e/specs/8-vesting/esEXA.spec.ts @@ -1,37 +1,64 @@ -import { expect } from '@playwright/test'; import { parseEther } from 'viem'; -import base from '../../fixture/base'; +import base, { chain } from '../../fixture/base'; import _app from '../../common/app'; -import { escrowedEXA, sablierV2LockupLinear } from '../../utils/contracts'; +import _balance from '../../common/balance'; +import _allowance from '../../common/allowance'; +import _vesting from '../../page/vesting'; +import { escrowedEXA, sablierV2LockupLinear, erc20 } from '../../utils/contracts'; const test = base(); test.describe.configure({ mode: 'serial' }); -test('Vesting esEXA', async ({ page, web2, web3 }) => { +test('Vesting esEXA & Claiming EXA', async ({ page, web2, web3 }) => { await web3.fork.setBalance(web3.account.address, { + ETH: 1, esEXA: 100, - EXA: 10, + EXA: 20, }); - const app = _app({ test, page }); const esEXA = await escrowedEXA({ publicClient: web3.publicClient }); const sablier = await sablierV2LockupLinear({ publicClient: web3.publicClient }); const stream = await sablier.read.nextStreamId(); + const period = await esEXA.read.vestingPeriod(); + + const app = _app({ test, page }); + const balance = _balance({ test, page, publicClient: web3.publicClient }); + const allowance = _allowance({ test, page, publicClient: web3.publicClient }); + const vesting = _vesting(page); await page.goto('/vesting'); + await vesting.waitForPageToBeReady(); await test.step('Vest esEXA', async () => { - // FIXME: Add test - await page.waitForTimeout(15_000); + await vesting.checkBalanceAvailable('100.00'); + + await vesting.input('1000'); + await vesting.checkError('Not enough EXA for reserve. Get EXA.'); + + await vesting.input('100'); + await vesting.checkReserveNeeded('20%', '20'); + + await vesting.waitForSubmitToBeReady(); + await vesting.submit(); + await vesting.waitForVestTransaction(); + + await balance.check({ address: web3.account.address, symbol: 'esEXA', amount: '0' }); + await balance.check({ address: web3.account.address, symbol: 'EXA', amount: '0' }); + await allowance.check({ + address: web3.account.address, + type: 'erc20', + symbol: 'EXA', + spender: esEXA.address, + less: '0', + }); }); const now = Date.now() / 1_000; - const period = await esEXA.read.vestingPeriod(); + await web3.fork.increaseTime(period / 2); await web2.time.now(now + period / 2); - await web3.fork.increaseTime(now + period / 2); await web2.graph.streams([ { @@ -49,12 +76,24 @@ test('Vesting esEXA', async ({ page, web2, web3 }) => { await app.reload(); await test.step('Withdraw EXA half-way', async () => { - // FIXME: Add test - await page.waitForTimeout(15_000); + const id = Number(stream); + await vesting.checkStream({ + id, + vested: '100.00', + reserved: '20.00', + withdrawable: '50.00', + left: '100.00', + progress: '50.00%', + }); + + await vesting.claimStream(id); + await vesting.waitForClaimStreamTransaction(id); + + await balance.check({ address: web3.account.address, symbol: 'EXA', amount: '50', delta: '0.001' }); }); - await web2.time.now(now + period * 2); - await web3.fork.increaseTime(now + period * 2); + await web3.fork.increaseTime(period / 2); + await web2.time.now(now + period); await web2.graph.streams([ { @@ -72,7 +111,99 @@ test('Vesting esEXA', async ({ page, web2, web3 }) => { await app.reload(); await test.step('Withdraw EXA from depleted stream', async () => { - // FIXME: Add test - await page.waitForTimeout(15_000); + const id = Number(stream); + await vesting.checkStream({ + id, + vested: '100.00', + reserved: '20.00', + withdrawable: '50.00', + left: '50.00', + progress: '100%', + }); + + await vesting.claimStream(id); + await vesting.waitForClaimStreamTransaction(id); + + await balance.check({ address: web3.account.address, symbol: 'EXA', amount: '120' }); + }); +}); + +test('Claiming multiple streams', async ({ page, web2, web3 }) => { + await web3.fork.setBalance(web3.account.address, { + ETH: 1, + esEXA: 100, + EXA: 20, + }); + + const exa = await erc20('EXA', { walletClient: web3.walletClient }); + const esEXA = await escrowedEXA({ publicClient: web3.publicClient, walletClient: web3.walletClient }); + const sablier = await sablierV2LockupLinear({ publicClient: web3.publicClient }); + const stream = await sablier.read.nextStreamId(); + const period = await esEXA.read.vestingPeriod(); + + await exa.write.approve([esEXA.address, 2n ** 256n - 1n], { account: web3.account, chain }); + await esEXA.write.vest([parseEther('50'), web3.account.address], { account: web3.account, chain }); + await esEXA.write.vest([parseEther('50'), web3.account.address], { account: web3.account, chain }); + + const [stream0, stream1] = [stream, stream + 1n]; + + const balance = _balance({ test, page, publicClient: web3.publicClient }); + const vesting = _vesting(page); + + const now = Date.now() / 1_000; + + await web3.fork.increaseTime(period * 2); + await web2.time.now(now + period * 2); + + await web2.graph.streams([ + { + id: `0xb923abdca17aed90eb5ec5e407bd37164f632bfd-10-${stream0}`, + tokenId: String(stream0), + recipient: web3.account.address, + startTime: String(now), + endTime: String(now + period), + depositAmount: String(parseEther('50')), + withdrawnAmount: '0', + canceled: false, + }, + { + id: `0xb923abdca17aed90eb5ec5e407bd37164f632bfd-10-${stream1}`, + tokenId: String(stream1), + recipient: web3.account.address, + startTime: String(now), + endTime: String(now + period), + depositAmount: String(parseEther('50')), + withdrawnAmount: '0', + canceled: false, + }, + ]); + + await page.goto('/vesting'); + await vesting.waitForPageToBeReady(); + + await test.step('Withdraw all depleted streams', async () => { + const [id0, id1] = [Number(stream0), Number(stream1)]; + await vesting.checkStream({ + id: id0, + vested: '50.00', + reserved: '10.00', + withdrawable: '50.00', + left: '50.00', + progress: '100%', + }); + + await vesting.checkStream({ + id: id1, + vested: '50.00', + reserved: '10.00', + withdrawable: '50.00', + left: '50.00', + progress: '100%', + }); + + await vesting.claimAllStreams(); + await vesting.waitForClaimAllTransaction(); + + await balance.check({ address: web3.account.address, symbol: 'EXA', amount: '120' }); }); }); diff --git a/e2e/utils/tenderly.ts b/e2e/utils/tenderly.ts index 3a32927f7..a4bb927f3 100644 --- a/e2e/utils/tenderly.ts +++ b/e2e/utils/tenderly.ts @@ -193,7 +193,7 @@ export const tenderly = async ({ chain = optimism }: { chain: Chain }): Promise< if (symbol === 'ETH') { await setNativeBalance(address, _amount); - return; + continue; } const token = await erc20(symbol, { publicClient }); @@ -219,6 +219,8 @@ export const tenderly = async ({ chain = optimism }: { chain: Chain }): Promise< increaseTime: async (timestamp: number) => { // eslint-disable-next-line @typescript-eslint/no-explicit-any await walletClient.request({ method: 'evm_increaseTime' as any, params: [toHex(Math.floor(timestamp))] }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + await walletClient.request({ method: 'evm_increaseBlocks' as any, params: [toHex(1)] }); }, deleteFork: async () => deleteFork(forkId), }; diff --git a/pages/vesting.tsx b/pages/vesting.tsx index f10cb64a3..bc7aadb53 100644 --- a/pages/vesting.tsx +++ b/pages/vesting.tsx @@ -147,7 +147,13 @@ const Vesting: NextPage = () => { ) : ( <> - + {t('Claim All')} From 71f310bbdf45747e572aee2a1d8ecebeed8408c3 Mon Sep 17 00:00:00 2001 From: Jorge Galat Date: Thu, 5 Oct 2023 10:22:37 -0300 Subject: [PATCH 14/38] =?UTF-8?q?=E2=9C=A8=20vesting:=20add=20contract=20f?= =?UTF-8?q?low?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/VestingInput/index.tsx | 39 +++++++++++++++++++++++-------- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/components/VestingInput/index.tsx b/components/VestingInput/index.tsx index 859217b1c..97eb20286 100644 --- a/components/VestingInput/index.tsx +++ b/components/VestingInput/index.tsx @@ -23,6 +23,7 @@ import { toPercentage } from 'utils/utils'; import Link from 'next/link'; import useIsContract from 'hooks/useIsContract'; import { gasLimit } from 'utils/gas'; +import { Transaction } from 'types/Transaction'; type Params> = AbiParametersToPrimitiveTypes< ExtractAbiFunction['inputs'] @@ -43,6 +44,7 @@ function VestingInput() { const isContract = useIsContract(); const { signTypedDataAsync } = useSignTypedData(); const [isLoading, setIsLoading] = useState(false); + const [tx, setTx] = useState(); const [qty, setQty] = useState(''); @@ -108,32 +110,49 @@ function VestingInput() { }, [displayNetwork.id, escrowedEXA, exa, opts, qty, reserveRatio, signTypedDataAsync, walletAddress]); const submit = useCallback(async () => { - if (!walletAddress || reserve === undefined || !escrowedEXA || !opts || !qty) return; + if (!walletAddress || reserveRatio === undefined || !escrowedEXA || !exa || !opts || !qty) return; setIsLoading(true); + let hash; try { const amount = parseEther(qty); + const res = (amount * reserveRatio) / WEI_PER_ETHER; let args: Params<'vest'> = [amount, walletAddress] as const; if (await isContract(walletAddress)) { - // TODO - return; + const allowance = await exa.read.allowance([walletAddress, escrowedEXA.address]); + + if (allowance < res) { + const approve = [escrowedEXA.address, res] as const; + const gas = await exa.estimateGas.approve(approve, opts); + const approveHash = await exa.write.approve(approve, { ...opts, gasLimit: gasLimit(gas) }); + await waitForTransaction({ hash: approveHash }); + } + + const gas = await escrowedEXA.estimateGas.vest(args, opts); + hash = await escrowedEXA.write.vest(args, { ...opts, gasLimit: gasLimit(gas) }); + } else { + const p = await sign(); + if (!p) return; + args = [...args, p] as const; + const gas = await escrowedEXA.estimateGas.vest(args, opts); + hash = await escrowedEXA.write.vest(args, { ...opts, gasLimit: gasLimit(gas) }); } - const p = await sign(); - if (!p) return; - args = [...args, p] as const; - const gas = await escrowedEXA.estimateGas.vest(args, opts); - const hash = await escrowedEXA.write.vest(args, { ...opts, gasLimit: gasLimit(gas) }); + setTx({ status: 'processing', hash }); + + const { status, transactionHash } = await waitForTransaction({ hash }); + + setTx({ status: status ? 'success' : 'error', hash: transactionHash }); await waitForTransaction({ hash }); } catch (e) { - // TODO + if (hash) setTx({ status: 'error', hash }); } finally { setIsLoading(false); } - }, [walletAddress, reserve, escrowedEXA, qty, isContract, sign, opts]); + }, [walletAddress, reserveRatio, escrowedEXA, exa, opts, qty, isContract, sign]); return ( From 853b92b3e13ac06b04b6e161f49927f8ac3dc715 Mon Sep 17 00:00:00 2001 From: Jorge Galat Date: Thu, 5 Oct 2023 10:35:48 -0300 Subject: [PATCH 15/38] =?UTF-8?q?=E2=9C=A8=20vesting:=20add=20tx=20modal?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/VestingInput/index.tsx | 110 +++++++++++++++++++++++++++++- 1 file changed, 107 insertions(+), 3 deletions(-) diff --git a/components/VestingInput/index.tsx b/components/VestingInput/index.tsx index 97eb20286..027fb7b98 100644 --- a/components/VestingInput/index.tsx +++ b/components/VestingInput/index.tsx @@ -1,11 +1,27 @@ -import React, { useCallback, useMemo, useState } from 'react'; -import { Box, Button, Skeleton, Typography } from '@mui/material'; +import React, { useCallback, useMemo, useRef, useState } from 'react'; +import { + Box, + Button, + Dialog, + DialogContent, + IconButton, + Paper, + PaperProps, + Skeleton, + Slide, + Typography, + useMediaQuery, + useTheme, +} from '@mui/material'; +import { TransitionProps } from '@mui/material/transitions'; import dayjs from 'dayjs'; import { splitSignature } from '@ethersproject/bytes'; import { type Hex, formatEther, parseEther } from 'viem'; import { waitForTransaction } from '@wagmi/core'; import { escrowedExaABI } from 'types/abi'; import { AbiParametersToPrimitiveTypes, ExtractAbiFunction, ExtractAbiFunctionNames } from 'abitype'; +import Draggable from 'react-draggable'; +import CloseIcon from '@mui/icons-material/Close'; import { ModalBox } from 'components/common/modal/ModalBox'; @@ -24,11 +40,93 @@ import Link from 'next/link'; import useIsContract from 'hooks/useIsContract'; import { gasLimit } from 'utils/gas'; import { Transaction } from 'types/Transaction'; +import LoadingTransaction from 'components/common/modal/Loading'; type Params> = AbiParametersToPrimitiveTypes< ExtractAbiFunction['inputs'] >; +function PaperComponent(props: PaperProps | undefined) { + const ref = useRef(null); + return ( + + + + ); +} + +const Transition = React.forwardRef(function Transition( + props: TransitionProps & { + children: React.ReactElement; + }, + ref: React.Ref, +) { + return ; +}); + +function LoadingModal({ tx, onClose }: { tx: Transaction; onClose: () => void }) { + const { t } = useTranslation(); + const { breakpoints, spacing, palette } = useTheme(); + const isMobile = useMediaQuery(breakpoints.down('sm')); + const loadingTx = useMemo(() => tx && (tx.status === 'loading' || tx.status === 'processing'), [tx]); + + return ( + + {!loadingTx && ( + + + + )} + + + + + + + ); +} + function VestingInput() { const { t } = useTranslation(); @@ -144,7 +242,7 @@ function VestingInput() { const { status, transactionHash } = await waitForTransaction({ hash }); - setTx({ status: status ? 'success' : 'error', hash: transactionHash }); + setTx({ status: status === 'success' ? 'success' : 'error', hash: transactionHash }); await waitForTransaction({ hash }); } catch (e) { @@ -154,8 +252,14 @@ function VestingInput() { } }, [walletAddress, reserveRatio, escrowedEXA, exa, opts, qty, isContract, sign]); + const onClose = useCallback(() => { + setTx(undefined); + setQty(''); + }, []); + return ( + {tx && } Date: Thu, 5 Oct 2023 10:56:27 -0300 Subject: [PATCH 16/38] =?UTF-8?q?=E2=9C=85vesting:=20update=20test=20for?= =?UTF-8?q?=20loading=20tx?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/VestingInput/index.tsx | 9 +++++++-- e2e/page/vesting.ts | 31 ++++++++++++++++++++++++++++++- e2e/specs/8-vesting/esEXA.spec.ts | 11 +++++++---- hooks/useEscrowedEXA.ts | 3 ++- pages/vesting.tsx | 4 ++-- 5 files changed, 48 insertions(+), 10 deletions(-) diff --git a/components/VestingInput/index.tsx b/components/VestingInput/index.tsx index 027fb7b98..a8b6c6818 100644 --- a/components/VestingInput/index.tsx +++ b/components/VestingInput/index.tsx @@ -127,7 +127,11 @@ function LoadingModal({ tx, onClose }: { tx: Transaction; onClose: () => void }) ); } -function VestingInput() { +type Props = { + refetch: () => void; +}; + +function VestingInput({ refetch }: Props) { const { t } = useTranslation(); const { chain } = useNetwork(); @@ -255,7 +259,8 @@ function VestingInput() { const onClose = useCallback(() => { setTx(undefined); setQty(''); - }, []); + refetch(); + }, [refetch]); return ( diff --git a/e2e/page/vesting.ts b/e2e/page/vesting.ts index 599b190ec..5ded7db4e 100644 --- a/e2e/page/vesting.ts +++ b/e2e/page/vesting.ts @@ -51,7 +51,34 @@ export default function (page: Page) { }; const waitForVestTransaction = async () => { - await waitForSubmitToBeReady(); + const modal = page.getByTestId('vesting-vest-modal'); + await expect(modal).toBeVisible(); + + const status = page.getByTestId('transaction-status'); + await expect(status).toBeVisible({ timeout: 30_000 }); + + await page.waitForFunction( + (message) => { + const text = document.querySelector('[data-testid="transaction-status"]'); + if (!text) return false; + return text.textContent !== message; + }, + 'Processing transaction...', + { timeout: 30_000, polling: 1_000 }, + ); + }; + + const closeVestTransaction = async () => { + const close = page.getByTestId('vesting-vest-modal-close'); + await expect(close).toBeVisible(); + await close.click(); + }; + + const checkVestTransactionStatus = async (target: 'success' | 'error', summary: string) => { + const status = page.getByTestId('transaction-status'); + + await expect(status).toHaveText(`Transaction ${target === 'success' ? 'completed' : target}`); + await expect(page.getByTestId('transaction-summary')).toHaveText(summary); }; type Stream = { @@ -126,6 +153,8 @@ export default function (page: Page) { waitForSubmitToBeReady, submit, waitForVestTransaction, + checkVestTransactionStatus, + closeVestTransaction, checkStream, claimStream, waitForClaimStreamTransaction, diff --git a/e2e/specs/8-vesting/esEXA.spec.ts b/e2e/specs/8-vesting/esEXA.spec.ts index d3d4a9ebb..1de74f55f 100644 --- a/e2e/specs/8-vesting/esEXA.spec.ts +++ b/e2e/specs/8-vesting/esEXA.spec.ts @@ -44,6 +44,9 @@ test('Vesting esEXA & Claiming EXA', async ({ page, web2, web3 }) => { await vesting.submit(); await vesting.waitForVestTransaction(); + await vesting.checkVestTransactionStatus('success', 'Your esEXA has been vested'); + await vesting.closeVestTransaction(); + await balance.check({ address: web3.account.address, symbol: 'esEXA', amount: '0' }); await balance.check({ address: web3.account.address, symbol: 'EXA', amount: '0' }); await allowance.check({ @@ -81,9 +84,9 @@ test('Vesting esEXA & Claiming EXA', async ({ page, web2, web3 }) => { id, vested: '100.00', reserved: '20.00', - withdrawable: '50.00', + withdrawable: /50\.0|49\.9/, left: '100.00', - progress: '50.00%', + progress: /50\.00%|50\.01%/, }); await vesting.claimStream(id); @@ -116,8 +119,8 @@ test('Vesting esEXA & Claiming EXA', async ({ page, web2, web3 }) => { id, vested: '100.00', reserved: '20.00', - withdrawable: '50.00', - left: '50.00', + withdrawable: /50\.0|49\.9/, + left: /50\.0|49\.9/, progress: '100%', }); diff --git a/hooks/useEscrowedEXA.ts b/hooks/useEscrowedEXA.ts index cee8a1464..1bdf27612 100644 --- a/hooks/useEscrowedEXA.ts +++ b/hooks/useEscrowedEXA.ts @@ -33,6 +33,7 @@ export function useUpdateStreams() { const fetchStreams = useCallback(async () => { if (EXA && walletAddress) { + setLoading(true); // eslint-disable-next-line @typescript-eslint/no-explicit-any const data = await request(getStreams(EXA.address.toLowerCase(), walletAddress, false), true); @@ -45,7 +46,7 @@ export function useUpdateStreams() { fetchStreams(); }, [fetchStreams]); - return { activeStreams, loading }; + return { activeStreams, loading, refetch: fetchStreams }; } export const useEscrowedEXAReserves = (stream: bigint) => { diff --git a/pages/vesting.tsx b/pages/vesting.tsx index bc7aadb53..7408ed7b7 100644 --- a/pages/vesting.tsx +++ b/pages/vesting.tsx @@ -17,7 +17,7 @@ const Vesting: NextPage = () => { const { t } = useTranslation(); const { impersonateActive, chain: displayNetwork, opts } = useWeb3(); const { chain } = useNetwork(); - const { activeStreams, loading: streamsLoading } = useUpdateStreams(); + const { activeStreams, loading: streamsLoading, refetch } = useUpdateStreams(); const { switchNetwork, isLoading: switchIsLoading } = useSwitchNetwork(); const [loading, setLoading] = useState(false); @@ -97,7 +97,7 @@ const Vesting: NextPage = () => { - + From 4f9b6fe0fe3d873c666e6bb96cb4dff9c3f245b7 Mon Sep 17 00:00:00 2001 From: Jorge Galat Date: Thu, 5 Oct 2023 10:58:07 -0300 Subject: [PATCH 17/38] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20vesting:=20add=20ref?= =?UTF-8?q?etch=20on=20tx=20finish?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/ActiveStream/index.tsx | 5 ++++- pages/vesting.tsx | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/components/ActiveStream/index.tsx b/components/ActiveStream/index.tsx index 7715f8e6c..f6b57e332 100644 --- a/components/ActiveStream/index.tsx +++ b/components/ActiveStream/index.tsx @@ -75,6 +75,7 @@ type ActiveStreamProps = { startTime: number; endTime: number; cancellable: boolean; + refetch: () => void; }; const ActiveStream: FC = ({ @@ -84,6 +85,7 @@ const ActiveStream: FC = ({ startTime, endTime, cancellable, + refetch, }) => { const { t } = useTranslation(); const { impersonateActive, chain: displayNetwork, opts } = useWeb3(); @@ -106,8 +108,9 @@ const ActiveStream: FC = ({ // if request fails, don't do anything } finally { setLoading(false); + refetch(); } - }, [escrowedEXA, opts, tokenId]); + }, [escrowedEXA, opts, refetch, tokenId]); const elapsed = useMemo(() => { const now = Math.floor(Date.now() / 1000); diff --git a/pages/vesting.tsx b/pages/vesting.tsx index 7408ed7b7..6a676af09 100644 --- a/pages/vesting.tsx +++ b/pages/vesting.tsx @@ -33,8 +33,9 @@ const Vesting: NextPage = () => { // if request fails, don't do anything } finally { setLoading(false); + refetch(); } - }, [activeStreams, escrowedEXA, opts]); + }, [activeStreams, escrowedEXA, opts, refetch]); return ( @@ -174,6 +175,7 @@ const Vesting: NextPage = () => { startTime={Number(startTime)} endTime={Number(endTime)} cancellable={cancelable} + refetch={refetch} /> {index !== activeStreams.length - 1 && } From 6939de9623910665de85cd9b32d62c3e80c110e6 Mon Sep 17 00:00:00 2001 From: Jorge Galat Date: Thu, 5 Oct 2023 11:06:49 -0300 Subject: [PATCH 18/38] =?UTF-8?q?=E2=9C=A8=20vesting:=20add=20rewards=20cl?= =?UTF-8?q?aim?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pages/vesting.tsx | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/pages/vesting.tsx b/pages/vesting.tsx index 6a676af09..4903b3a03 100644 --- a/pages/vesting.tsx +++ b/pages/vesting.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useState } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import type { NextPage } from 'next'; import { usePageView } from 'hooks/useAnalytics'; @@ -11,6 +11,10 @@ import { useWeb3 } from 'hooks/useWeb3'; import { useNetwork, useSwitchNetwork } from 'wagmi'; import { LoadingButton } from '@mui/lab'; import { waitForTransaction } from '@wagmi/core'; +import useRewards from 'hooks/useRewards'; +import { useModal } from 'contexts/ModalContext'; +import formatNumber from 'utils/formatNumber'; +import { formatEther } from 'viem'; const Vesting: NextPage = () => { usePageView('/vesting', 'Vesting'); @@ -23,7 +27,14 @@ const Vesting: NextPage = () => { const [loading, setLoading] = useState(false); const escrowedEXA = useEscrowedEXA(); - const handleClick = useCallback(async () => { + const { rewards } = useRewards(); + const { open: openRewards } = useModal('rewards'); + + const unclaimedTokens = useMemo(() => { + return rewards['esEXA']?.amount || 0n; + }, [rewards]); + + const handleClaimAll = useCallback(async () => { if (!activeStreams || !escrowedEXA || !opts) return; setLoading(true); try { @@ -79,8 +90,10 @@ const Vesting: NextPage = () => { {t('Claim your esEXA Rewards')} - @@ -151,7 +164,7 @@ const Vesting: NextPage = () => { From dfaf7e80f67f295d52fe7f9efa02bfccbe51c44e Mon Sep 17 00:00:00 2001 From: franm Date: Thu, 5 Oct 2023 17:55:15 -0300 Subject: [PATCH 19/38] =?UTF-8?q?=F0=9F=92=84vesting:=20add=20mobile=20and?= =?UTF-8?q?=20withdraw=20reserve?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/ActiveStream/index.tsx | 335 ++++++++++++++++++++++-------- pages/vesting.tsx | 74 +++---- 2 files changed, 288 insertions(+), 121 deletions(-) diff --git a/components/ActiveStream/index.tsx b/components/ActiveStream/index.tsx index f6b57e332..7a1d5108b 100644 --- a/components/ActiveStream/index.tsx +++ b/components/ActiveStream/index.tsx @@ -1,14 +1,24 @@ import { Box, Button, + Dialog, + DialogContent, + DialogTitle, Divider, + Grid, + IconButton, LinearProgress, + Paper, + PaperProps, Skeleton, + Slide, Typography, linearProgressClasses, styled, + useMediaQuery, + useTheme, } from '@mui/material'; -import React, { FC, useCallback, useMemo, useState } from 'react'; +import React, { FC, ReactNode, useCallback, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { waitForTransaction } from '@wagmi/core'; import { LoadingButton } from '@mui/lab'; @@ -19,6 +29,9 @@ import { useWeb3 } from 'hooks/useWeb3'; import { useNetwork, useSwitchNetwork } from 'wagmi'; import { useEscrowedEXA, useEscrowedEXAReserves } from 'hooks/useEscrowedEXA'; import { useSablierV2LockupLinearWithdrawableAmountOf } from 'hooks/useSablier'; +import Draggable from 'react-draggable'; +import CloseIcon from '@mui/icons-material/Close'; +import { TransitionProps } from '@mui/material/transitions'; const StyledLinearProgress = styled(LinearProgress, { shouldForwardProp: (prop) => prop !== 'barColor', @@ -78,6 +91,65 @@ type ActiveStreamProps = { refetch: () => void; }; +function PaperComponent(props: PaperProps | undefined) { + const ref = useRef(null); + return ( + + + + ); +} + +const Transition = React.forwardRef(function Transition( + props: TransitionProps & { + children: React.ReactElement; + }, + ref: React.Ref, +) { + return ; +}); + +function Modal({ open, onClose, content }: { open: boolean; onClose: () => void; content: ReactNode }) { + const { breakpoints } = useTheme(); + const isMobile = useMediaQuery(breakpoints.down('sm')); + + return ( + + + + + {content} + + ); +} + const ActiveStream: FC = ({ tokenId, depositAmount, @@ -90,6 +162,7 @@ const ActiveStream: FC = ({ const { t } = useTranslation(); const { impersonateActive, chain: displayNetwork, opts } = useWeb3(); const { chain } = useNetwork(); + const { spacing } = useTheme(); const { switchNetwork, isLoading: switchIsLoading } = useSwitchNetwork(); const { data: reserve, isLoading: reserveIsLoading } = useEscrowedEXAReserves(BigInt(tokenId)); const { data: withdrawable, isLoading: withdrawableIsLoading } = useSablierV2LockupLinearWithdrawableAmountOf( @@ -97,6 +170,91 @@ const ActiveStream: FC = ({ ); const escrowedEXA = useEscrowedEXA(); const [loading, setLoading] = useState(false); + const [modalOpen, setModalOpen] = useState(false); + const [modalContent, setModalContent] = useState(null); + const { breakpoints } = useTheme(); + const isMobile = useMediaQuery(breakpoints.down('sm')); + + const WhitdrawAndCancel = () => { + return ( + <> + + + {t('Whitdraw Reserved EXA')} + + + + + + {t( + 'When you withdraw the reserved EXA, the associated vesting stream will be cancelled automatically. You’ll be able to claim the earned EXA and will get back all remaining esEXA.', + )} + + + {impersonateActive ? ( + + ) : chain && chain.id !== displayNetwork.id ? ( + switchNetwork?.(displayNetwork.id)} + variant="contained" + loading={switchIsLoading} + > + {t('Please switch to {{network}} network', { network: displayNetwork.name })} + + ) : ( + <> + + {t('Whitdraw and Cancel Stream')} + + + )} + + + + + ); + }; + + const onClose = useCallback(() => { + setModalOpen(false); + }, []); + + function handleContent(contentComponent: ReactNode) { + setModalContent(contentComponent); + setModalOpen(true); + } + + const handleWithdraw = useCallback(async () => { + if (!escrowedEXA || !opts) return; + setLoading(true); + try { + const tx = await escrowedEXA.write.cancel([[BigInt(tokenId)]], opts); + await waitForTransaction({ hash: tx }); + } catch (e) { + // if request fails, don't do anything + } finally { + setLoading(false); + refetch(); + } + }, [escrowedEXA, opts, refetch, tokenId]); const handleClick = useCallback(async () => { if (!escrowedEXA || !opts) return; @@ -140,8 +298,8 @@ const ActiveStream: FC = ({ } if (daysLeft === 0) { - const minutesLeft = Math.floor(secondsLeft / 60); - return t('{{minutesLeft}} minutes left', { minutesLeft }); + const hoursLeft = Math.floor(secondsLeft / 60 / 60); + return t('{{hoursLeft}} hours left', { hoursLeft }); } if (daysLeft < 0) { @@ -150,14 +308,14 @@ const ActiveStream: FC = ({ }, [endTime, t]); return ( - - - - - - {t('esEXA Vested')} - - + + + + + {t('esEXA Vested')} + + + EXA = ({ {formatNumber(Number(depositAmount) / 1e18)} - (mode === 'light' ? '#EEEEEE' : '#2A2A2A')} - px={0.5} - borderRadius="2px" - alignItems="center" - // onClick={onClick} - sx={{ cursor: 'pointer' }} - > - - {t('View NFT')} - - + + (mode === 'light' ? '#EEEEEE' : '#2A2A2A')} + px={0.5} + borderRadius="2px" + alignItems="center" + onClick={() => handleContent(WhitdrawAndCancel())} + sx={{ cursor: 'pointer' }} + > + + {t('View NFT')} + - - - - {t('Reserved EXA')} - - + + + + + {t('Reserved EXA')} + + + EXA = ({ {formatNumber(Number(reserve ?? 0n) / 1e18)} )} - {cancellable && ( - (mode === 'light' ? '#EEEEEE' : '#2A2A2A')} - px={0.5} - borderRadius="2px" - alignItems="center" - // onClick={onClick} - sx={{ cursor: 'pointer' }} - > - - {t('Whitdraw')} - - - )} + {cancellable && ( + (mode === 'light' ? '#EEEEEE' : '#2A2A2A')} + px={0.5} + borderRadius="2px" + alignItems="center" + onClick={() => (progress === 100 ? handleClick() : handleContent(WhitdrawAndCancel()))} + sx={{ cursor: 'pointer' }} + > + + {t('Whitdraw')} + + + )} - - - - {t('Claimable EXA')} - - + {modalOpen && modalContent && } + + + + + {t('Claimable EXA')} + + + EXA = ({ / {formatNumber(Number(depositAmount - withdrawnAmount) / 1e18)} + + {impersonateActive ? ( + + ) : chain && chain.id !== displayNetwork.id ? ( + switchNetwork?.(displayNetwork.id)} + variant="contained" + loading={switchIsLoading} + > + {t('Please switch to {{network}} network', { network: displayNetwork.name })} + + ) : ( + <> + + {progress === 100 ? t('Claim & Whitdraw EXA') : t('Claim EXA')} + + + )} + - + + - - {impersonateActive ? ( - - ) : chain && chain.id !== displayNetwork.id ? ( - switchNetwork?.(displayNetwork.id)} - variant="contained" - loading={switchIsLoading} - > - {t('Please switch to {{network}} network', { network: displayNetwork.name })} - - ) : ( - <> - - {progress === 100 ? t('Claim & Whitdraw EXA') : t('Claim EXA')} - - - )} - - - {t('Vesting Period')}: - - - {timeLeft} + {t('Progress')}: + {!isMobile && ( + + {timeLeft} + + )} diff --git a/pages/vesting.tsx b/pages/vesting.tsx index 4903b3a03..bbe9ae1a9 100644 --- a/pages/vesting.tsx +++ b/pages/vesting.tsx @@ -83,13 +83,13 @@ const Vesting: NextPage = () => { boxShadow={({ palette }) => (palette.mode === 'light' ? '0px 3px 4px 0px rgba(97, 102, 107, 0.25)' : '')} > - + {t('Step {{number}}', { number: 1 })} {t('Claim your esEXA Rewards')} - + - ) : chain && chain.id !== displayNetwork.id ? ( - switchNetwork?.(displayNetwork.id)} - variant="contained" - loading={switchIsLoading} - > - {t('Please switch to {{network}} network', { network: displayNetwork.name })} - - ) : ( - <> - - {t('Claim All')} - - - )} - + + + + {impersonateActive ? ( + + ) : chain && chain.id !== displayNetwork.id ? ( + switchNetwork?.(displayNetwork.id)} + variant="contained" + loading={switchIsLoading} + > + {t('Please switch to {{network}} network', { network: displayNetwork.name })} + + ) : ( + <> + + {t('Claim All')} + + + )} + + + From 3568197b6d7415997fb85cdcf8635fcb021d8044 Mon Sep 17 00:00:00 2001 From: franm Date: Mon, 9 Oct 2023 11:27:26 -0300 Subject: [PATCH 20/38] =?UTF-8?q?=F0=9F=92=84vesting:=20style=20updates?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/ActiveStream/index.tsx | 25 +++++++++++++++---------- pages/vesting.tsx | 6 +++--- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/components/ActiveStream/index.tsx b/components/ActiveStream/index.tsx index 7a1d5108b..8752026d1 100644 --- a/components/ActiveStream/index.tsx +++ b/components/ActiveStream/index.tsx @@ -123,9 +123,8 @@ function Modal({ open, onClose, content }: { open: boolean; onClose: () => void; sx: { borderRadius: 1, minWidth: '375px', - maxWidth: '488px !important', + width: '100%', - overflowY: 'hidden !important', }, }} TransitionComponent={isMobile ? Transition : undefined} @@ -183,22 +182,28 @@ const ActiveStream: FC = ({ cursor: { xs: '', sm: 'move' }, }} > - - {t('Whitdraw Reserved EXA')} - + + + {t('Whitdraw Reserved EXA')} + + - - + + {t( 'When you withdraw the reserved EXA, the associated vesting stream will be cancelled automatically. You’ll be able to claim the earned EXA and will get back all remaining esEXA.', )} - - + + {impersonateActive ? ( - ) : chain && chain.id !== displayNetwork.id ? ( - switchNetwork?.(displayNetwork.id)} - variant="contained" - loading={switchIsLoading} - > - {t('Please switch to {{network}} network', { network: displayNetwork.name })} - - ) : ( - <> - - {progress === 100 ? t('Claim & Whitdraw EXA') : t('Claim EXA')} - - - )} - + + {impersonateActive ? ( + + ) : chain && chain.id !== displayNetwork.id ? ( + switchNetwork?.(displayNetwork.id)} + variant="contained" + loading={switchIsLoading} + > + {t('Please switch to {{network}} network', { network: displayNetwork.name })} + + ) : ( + <> + + {t('Whitdraw EXA')} + + + )} + diff --git a/hooks/useEscrowedEXA.ts b/hooks/useEscrowedEXA.ts index 1bdf27612..6fd8dd4c0 100644 --- a/hooks/useEscrowedEXA.ts +++ b/hooks/useEscrowedEXA.ts @@ -25,6 +25,7 @@ type Stream = { export function useUpdateStreams() { const EXA = useEXA(); + const esEXA = useEscrowedEXA(); const { walletAddress } = useWeb3(); const request = useGraphClient(); @@ -32,15 +33,28 @@ export function useUpdateStreams() { const [loading, setLoading] = useState(true); const fetchStreams = useCallback(async () => { - if (EXA && walletAddress) { + if (EXA && walletAddress && esEXA) { setLoading(true); // eslint-disable-next-line @typescript-eslint/no-explicit-any - const data = await request(getStreams(EXA.address.toLowerCase(), walletAddress, false), true); + const data = await request( + getStreams(EXA.address.toLowerCase(), walletAddress, esEXA.address.toLowerCase(), false), + true, + ); + + const filteredStreams = data.streams.filter( + (stream: { startTime: string; duration: string; endTime: string; intactAmount: string }) => { + const startTime = Number(stream.startTime); + const endTime = Number(stream.endTime); + const duration = Number(stream.duration); + const intactAmount = BigInt(stream.intactAmount); + return startTime + duration === endTime && intactAmount > BigInt(0); + }, + ); - setActiveStreams(data.streams); + setActiveStreams(filteredStreams); setLoading(false); } - }, [EXA, request, walletAddress]); + }, [EXA, esEXA, request, walletAddress]); useEffect(() => { fetchStreams(); diff --git a/pages/vesting.tsx b/pages/vesting.tsx index 7a660de78..7c11a3b7b 100644 --- a/pages/vesting.tsx +++ b/pages/vesting.tsx @@ -135,50 +135,52 @@ const Vesting: NextPage = () => { bgcolor="components.bg" boxShadow={({ palette }) => (palette.mode === 'light' ? '0px 3px 4px 0px rgba(97, 102, 107, 0.25)' : '')} > - - - - - - - {t('You can claim all streams at once.')} - - - - - {impersonateActive ? ( - - ) : chain && chain.id !== displayNetwork.id ? ( - switchNetwork?.(displayNetwork.id)} - variant="contained" - loading={switchIsLoading} - > - {t('Please switch to {{network}} network', { network: displayNetwork.name })} - - ) : ( - <> + {activeStreams.length > 1 && ( + + + + + + + {t('You can claim all streams at once.')} + + + + + {impersonateActive ? ( + + ) : chain && chain.id !== displayNetwork.id ? ( switchNetwork?.(displayNetwork.id)} variant="contained" - onClick={handleClaimAll} - loading={loading} - data-testid="vesting-claim-all" + loading={switchIsLoading} > - {t('Claim All')} + {t('Please switch to {{network}} network', { network: displayNetwork.name })} - - )} - + ) : ( + <> + + {t('Claim All')} + + + )} + + - + + - - + )} {activeStreams.map( ({ id, tokenId, depositAmount, withdrawnAmount, startTime, endTime, cancelable }, index) => ( <> diff --git a/queries/getStreams.ts b/queries/getStreams.ts index 07b9bba9b..40163bc39 100644 --- a/queries/getStreams.ts +++ b/queries/getStreams.ts @@ -1,16 +1,18 @@ -export function getStreams(assetAddress: string, address: string, canceled: boolean) { +export function getStreams(assetAddress: string, address: string, sender: string, canceled: boolean) { return ` { - streams(orderBy: timestamp, orderDirection: asc, where:{ asset:"${assetAddress}", recipient: "${address}", canceled: ${canceled}}) { + streams(orderBy: timestamp, orderDirection: asc, where:{ asset:"${assetAddress}", recipient: "${address}", sender: "${sender}", canceled: ${canceled}}) { id tokenId recipient startTime endTime + duration depositAmount withdrawnAmount canceled cancelable + intactAmount } } `; From a2e52f0f7243615d53b6ce8190d59e5e64d82806 Mon Sep 17 00:00:00 2001 From: franm Date: Wed, 11 Oct 2023 15:32:51 -0300 Subject: [PATCH 23/38] =?UTF-8?q?=F0=9F=92=84vesting:=20add=20skeleton?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/VestingInput/index.tsx | 9 +++++++-- hooks/useEscrowedEXA.ts | 4 ++-- pages/vesting.tsx | 4 ++-- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/components/VestingInput/index.tsx b/components/VestingInput/index.tsx index a8b6c6818..47877028b 100644 --- a/components/VestingInput/index.tsx +++ b/components/VestingInput/index.tsx @@ -168,6 +168,10 @@ function VestingInput({ refetch }: Props) { return [formatEther(_reserve), _reserve > exaBalance]; }, [reserveRatio, qty, exaBalance]); + const insuficentFunds = useMemo(() => { + return parseEther(qty) > (balance || 0n) || !qty || moreThanBalance; + }, [balance, moreThanBalance, qty]); + const sign = useCallback(async () => { if (!walletAddress || reserveRatio === undefined || !exa || !escrowedEXA) return; @@ -273,7 +277,7 @@ function VestingInput({ refetch }: Props) { p: 1, px: 2, alignItems: 'center', - zIndex: 420, + zIndex: 69, position: 'relative', backgroundColor: 'components.bg', }} @@ -384,8 +388,9 @@ function VestingInput({ refetch }: Props) { loading={isLoading} onClick={submit} data-testid="vesting-submit" + disabled={insuficentFunds} > - {t('Vest esEXA')} + {insuficentFunds ? t('Insuficent esEXA balance') : t('Vest esEXA')} )} diff --git a/hooks/useEscrowedEXA.ts b/hooks/useEscrowedEXA.ts index 6fd8dd4c0..45265c4cc 100644 --- a/hooks/useEscrowedEXA.ts +++ b/hooks/useEscrowedEXA.ts @@ -33,11 +33,11 @@ export function useUpdateStreams() { const [loading, setLoading] = useState(true); const fetchStreams = useCallback(async () => { - if (EXA && walletAddress && esEXA) { + if (EXA && esEXA) { setLoading(true); // eslint-disable-next-line @typescript-eslint/no-explicit-any const data = await request( - getStreams(EXA.address.toLowerCase(), walletAddress, esEXA.address.toLowerCase(), false), + getStreams(EXA.address.toLowerCase(), walletAddress || zeroAddress, esEXA.address.toLowerCase(), false), true, ); diff --git a/pages/vesting.tsx b/pages/vesting.tsx index 7c11a3b7b..41ad178da 100644 --- a/pages/vesting.tsx +++ b/pages/vesting.tsx @@ -3,7 +3,7 @@ import type { NextPage } from 'next'; import { usePageView } from 'hooks/useAnalytics'; import { useTranslation } from 'react-i18next'; -import { Box, Button, Divider, Grid, Typography } from '@mui/material'; +import { Box, Button, Divider, Grid, Skeleton, Typography } from '@mui/material'; import VestingInput from 'components/VestingInput'; import ActiveStream from 'components/ActiveStream'; import { useUpdateStreams, useEscrowedEXA } from 'hooks/useEscrowedEXA'; @@ -127,7 +127,7 @@ const Vesting: NextPage = () => { - {streamsLoading && loading...} + {streamsLoading && } {activeStreams.length > 0 && !streamsLoading && ( Date: Tue, 17 Oct 2023 16:17:12 -0300 Subject: [PATCH 24/38] =?UTF-8?q?=F0=9F=92=84update=20esEXA=20image?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/ActiveStream/index.tsx | 2 +- public/img/assets/esEXA.svg | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 public/img/assets/esEXA.svg diff --git a/components/ActiveStream/index.tsx b/components/ActiveStream/index.tsx index 781020ca1..1fa7b415d 100644 --- a/components/ActiveStream/index.tsx +++ b/components/ActiveStream/index.tsx @@ -346,7 +346,7 @@ const ActiveStream: FC = ({ EXA + + + + + + + + + + + + + + From c018f26c4e77d5b819b149d9bf289896ac6c34eb Mon Sep 17 00:00:00 2001 From: franm Date: Tue, 17 Oct 2023 16:18:02 -0300 Subject: [PATCH 25/38] =?UTF-8?q?=F0=9F=9A=B8=20vesting:=20add=20vesting?= =?UTF-8?q?=20period?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/VestingInput/index.tsx | 25 ++++++++++++++++++++----- hooks/useEscrowedEXA.ts | 19 ++++++++++++++++++- 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/components/VestingInput/index.tsx b/components/VestingInput/index.tsx index 47877028b..9dc5d2317 100644 --- a/components/VestingInput/index.tsx +++ b/components/VestingInput/index.tsx @@ -32,7 +32,12 @@ import { useTranslation, Trans } from 'react-i18next'; import { LoadingButton } from '@mui/lab'; import Image from 'next/image'; import { useEXA, useEXABalance, useEXAPrice } from 'hooks/useEXA'; -import { useEscrowedEXA, useEscrowedEXABalance, useEscrowedEXAReserveRatio } from 'hooks/useEscrowedEXA'; +import { + useEscrowedEXA, + useEscrowedEXABalance, + useEscrowedEXAReserveRatio, + useEscrowedEXAVestingPeriod, +} from 'hooks/useEscrowedEXA'; import formatNumber from 'utils/formatNumber'; import { WEI_PER_ETHER } from 'utils/const'; import { toPercentage } from 'utils/utils'; @@ -140,6 +145,7 @@ function VestingInput({ refetch }: Props) { const { data: balance, isLoading: balanceIsLoading } = useEscrowedEXABalance(); const { data: exaBalance } = useEXABalance(); const { data: reserveRatio } = useEscrowedEXAReserveRatio(); + const { data: vestingPeriod } = useEscrowedEXAVestingPeriod(); const EXAPrice = useEXAPrice(); const { impersonateActive, chain: displayNetwork, isConnected, opts, walletAddress } = useWeb3(); const { isLoading: switchIsLoading, switchNetwork } = useSwitchNetwork(); @@ -216,7 +222,16 @@ function VestingInput({ refetch }: Props) { }, [displayNetwork.id, escrowedEXA, exa, opts, qty, reserveRatio, signTypedDataAsync, walletAddress]); const submit = useCallback(async () => { - if (!walletAddress || reserveRatio === undefined || !escrowedEXA || !exa || !opts || !qty) return; + if ( + !walletAddress || + reserveRatio === undefined || + vestingPeriod === undefined || + !escrowedEXA || + !exa || + !opts || + !qty + ) + return; setIsLoading(true); let hash; @@ -224,7 +239,7 @@ function VestingInput({ refetch }: Props) { const amount = parseEther(qty); const res = (amount * reserveRatio) / WEI_PER_ETHER; - let args: Params<'vest'> = [amount, walletAddress] as const; + let args: Params<'vest'> = [amount, walletAddress, reserveRatio, BigInt(vestingPeriod)] as const; if (await isContract(walletAddress)) { const allowance = await exa.read.allowance([walletAddress, escrowedEXA.address]); @@ -258,7 +273,7 @@ function VestingInput({ refetch }: Props) { } finally { setIsLoading(false); } - }, [walletAddress, reserveRatio, escrowedEXA, exa, opts, qty, isContract, sign]); + }, [walletAddress, reserveRatio, vestingPeriod, escrowedEXA, exa, opts, qty, isContract, sign]); const onClose = useCallback(() => { setTx(undefined); @@ -285,7 +300,7 @@ function VestingInput({ refetch }: Props) { - + esEXA diff --git a/hooks/useEscrowedEXA.ts b/hooks/useEscrowedEXA.ts index 45265c4cc..1e45d12ee 100644 --- a/hooks/useEscrowedEXA.ts +++ b/hooks/useEscrowedEXA.ts @@ -1,6 +1,12 @@ import { useCallback, useEffect, useState } from 'react'; import { Address, zeroAddress } from 'viem'; -import { escrowedExaABI, useEscrowedExaBalanceOf, useEscrowedExaReserveRatio, useEscrowedExaReserves } from 'types/abi'; +import { + escrowedExaABI, + useEscrowedExaBalanceOf, + useEscrowedExaReserveRatio, + useEscrowedExaReserves, + useEscrowedExaVestingPeriod, +} from 'types/abi'; import useContract from './useContract'; import { useEXA } from './useEXA'; import useGraphClient from './useGraphClient'; @@ -97,3 +103,14 @@ export const useEscrowedEXAReserveRatio = () => { staleTime: 30_000, }); }; + +export const useEscrowedEXAVestingPeriod = () => { + const { chain } = useWeb3(); + const esEXA = useEscrowedEXA(); + + return useEscrowedExaVestingPeriod({ + chainId: chain.id, + address: esEXA?.address, + staleTime: 30_000, + }); +}; From bde393fd386ed831c72d028edff4c83f064c8bd0 Mon Sep 17 00:00:00 2001 From: franm Date: Tue, 17 Oct 2023 16:40:19 -0300 Subject: [PATCH 26/38] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20protocol=20deploy=20?= =?UTF-8?q?branch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7abe45e60..9031d7ae3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "dependencies": { "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", - "@exactly/protocol": "^0.2.16", + "@exactly/protocol": "exactly/protocol#deploy", "@mui/icons-material": "^5.14.3", "@mui/lab": "^5.0.0-alpha.138", "@mui/material": "^5.14.3", @@ -1081,13 +1081,13 @@ }, "node_modules/@exactly/protocol": { "version": "0.2.16", - "resolved": "https://registry.npmjs.org/@exactly/protocol/-/protocol-0.2.16.tgz", - "integrity": "sha512-Ph1GMRqboazlUKm7sfaRDeai72NowA+/euFHOQkWL06/i+cwEBSVW8XtOPGMuHy1tGxwnwUiWHYBB+e/SgCtvQ==", + "resolved": "git+ssh://git@github.com/exactly/protocol.git#d0b23ed426d2f7fae36ea7ec90d59aacdeac8c48", "hasInstallScript": true, + "license": "BUSL-1.1", "dependencies": { "@openzeppelin/contracts": "4.9.2", "@openzeppelin/contracts-upgradeable": "4.9.2", - "solmate": "github:transmissions11/solmate#v7" + "solmate": "transmissions11/solmate#v7" }, "engines": { "node": ">=18" diff --git a/package.json b/package.json index 59a996fb9..0e9b99523 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "dependencies": { "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", - "@exactly/protocol": "^0.2.16", + "@exactly/protocol": "exactly/protocol#deploy", "@mui/icons-material": "^5.14.3", "@mui/lab": "^5.0.0-alpha.138", "@mui/material": "^5.14.3", From 4976f644c5ae1a502eebfcbf1e308a6b90eb799e Mon Sep 17 00:00:00 2001 From: Jorge Galat Date: Tue, 17 Oct 2023 17:39:25 -0300 Subject: [PATCH 27/38] =?UTF-8?q?=E2=9C=85=20e2e:=20add=20cancel=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/ActiveStream/index.tsx | 43 +++++++++------- e2e/fixture/graph.ts | 3 ++ e2e/page/vesting.ts | 27 ++++++++++ e2e/specs/8-vesting/esEXA.spec.ts | 83 +++++++++++++++++++++++++++++-- 4 files changed, 132 insertions(+), 24 deletions(-) diff --git a/components/ActiveStream/index.tsx b/components/ActiveStream/index.tsx index 1fa7b415d..2124fc987 100644 --- a/components/ActiveStream/index.tsx +++ b/components/ActiveStream/index.tsx @@ -18,7 +18,7 @@ import { useMediaQuery, useTheme, } from '@mui/material'; -import React, { FC, ReactNode, useCallback, useMemo, useRef, useState } from 'react'; +import React, { FC, type PropsWithChildren, useCallback, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { waitForTransaction } from '@wagmi/core'; import { LoadingButton } from '@mui/lab'; @@ -99,7 +99,7 @@ const Transition = React.forwardRef(function Transition( return ; }); -function Modal({ open, onClose, content }: { open: boolean; onClose: () => void; content: ReactNode }) { +function Modal({ open, onClose, children }: PropsWithChildren<{ open: boolean; onClose: () => void }>) { const { breakpoints } = useTheme(); const isMobile = useMediaQuery(breakpoints.down('sm')); @@ -117,7 +117,7 @@ function Modal({ open, onClose, content }: { open: boolean; onClose: () => void; }} TransitionComponent={isMobile ? Transition : undefined} fullScreen={isMobile} - sx={isMobile ? { top: 'auto' } : { backdropFilter: content ? 'blur(1.5px)' : '' }} + sx={isMobile ? { top: 'auto' } : { backdropFilter: 'blur(1.5px)' }} > void; > - {content} + {children} ); } @@ -168,12 +168,12 @@ const ActiveStream: FC = ({ const escrowedEXA = useEscrowedEXA(); const [loading, setLoading] = useState(false); const [modalOpen, setModalOpen] = useState(false); - const [modalContent, setModalContent] = useState(null); + const [modalContent, setModalContent] = useState<'nft' | 'cancel'>('nft'); const { breakpoints } = useTheme(); const isMobile = useMediaQuery(breakpoints.down('sm')); const { data: nft } = useSablierV2NftDescriptorTokenUri(BigInt(tokenId)); - const WhitdrawAndCancel = () => { + const WithdrawAndCancel = () => { return ( <> = ({ }} > - {t('Whitdraw Reserved EXA')} + {t('Withdraw Reserved EXA')} @@ -222,11 +222,11 @@ const ActiveStream: FC = ({ fullWidth variant="contained" color="error" - onClick={handleWithdraw} + onClick={handleCancel} loading={loading} - data-testid={`vesting-stream-${tokenId}-claim`} + data-testid={`vesting-stream-${tokenId}-cancel-submit`} > - {t('Whitdraw and Cancel Stream')} + {t('Withdraw and Cancel Stream')} )} @@ -266,12 +266,12 @@ const ActiveStream: FC = ({ setModalOpen(false); }, []); - function handleContent(contentComponent: ReactNode) { - setModalContent(contentComponent); + const handleContent = useCallback((content: 'nft' | 'cancel') => { setModalOpen(true); - } + setModalContent(content); + }, []); - const handleWithdraw = useCallback(async () => { + const handleCancel = useCallback(async () => { if (!escrowedEXA || !opts) return; setLoading(true); try { @@ -363,7 +363,7 @@ const ActiveStream: FC = ({ borderRadius="2px" alignItems="center" onClick={() => { - handleContent(NFT()); + handleContent('nft'); }} sx={{ cursor: 'pointer' }} > @@ -402,16 +402,21 @@ const ActiveStream: FC = ({ px={0.5} borderRadius="2px" alignItems="center" - onClick={() => (progress === 100 ? handleClick() : handleContent(WhitdrawAndCancel()))} + data-testid={ + progress === 100 ? `vesting-stream-${tokenId}-withdraw` : `vesting-stream-${tokenId}-cancel` + } + onClick={() => (progress === 100 ? handleClick() : handleContent('cancel'))} sx={{ cursor: 'pointer' }} > - {progress === 100 ? t('Whitdraw') : t('Cancel')} + {progress === 100 ? t('Withdraw') : t('Cancel')} )} - {modalOpen && modalContent && } + + {modalContent === 'nft' ? : } + {!isMobile && } @@ -463,7 +468,7 @@ const ActiveStream: FC = ({ loading={loading} data-testid={`vesting-stream-${tokenId}-claim`} > - {t('Whitdraw EXA')} + {t('Withdraw EXA')} )} diff --git a/e2e/fixture/graph.ts b/e2e/fixture/graph.ts index 02c0b7361..d983268a0 100644 --- a/e2e/fixture/graph.ts +++ b/e2e/fixture/graph.ts @@ -11,6 +11,9 @@ function graph(page: Page) { depositAmount: string; withdrawnAmount: string; canceled: boolean; + cancelable: boolean; + intactAmount: string; + duration: string; }; const streams = async (body: Stream[]) => { diff --git a/e2e/page/vesting.ts b/e2e/page/vesting.ts index 5ded7db4e..cc6a669e8 100644 --- a/e2e/page/vesting.ts +++ b/e2e/page/vesting.ts @@ -144,6 +144,31 @@ export default function (page: Page) { ); }; + const cancelStream = async (streamId: number) => { + const button = page.getByTestId(`vesting-stream-${streamId}-cancel`); + await expect(button).not.toBeDisabled(); + await button.click(); + + const confirm = page.getByTestId(`vesting-stream-${streamId}-cancel-submit`); + await expect(confirm).not.toBeDisabled(); + await confirm.click(); + }; + + const waitForStreamCancelTransaction = async (streamId: number) => { + await page.waitForFunction( + (id: number) => { + const button = document.querySelector(`[data-testid="vesting-stream-${id}-cancel-submit"]`); + if (!button) return true; + return !button.classList.contains('MuiLoadingButton-loading'); + }, + streamId, + { + timeout: 30_000, + polling: 1_000, + }, + ); + }; + return { waitForPageToBeReady, checkBalanceAvailable, @@ -160,5 +185,7 @@ export default function (page: Page) { waitForClaimStreamTransaction, claimAllStreams, waitForClaimAllTransaction, + cancelStream, + waitForStreamCancelTransaction, }; } diff --git a/e2e/specs/8-vesting/esEXA.spec.ts b/e2e/specs/8-vesting/esEXA.spec.ts index 1de74f55f..7cd8d6f0d 100644 --- a/e2e/specs/8-vesting/esEXA.spec.ts +++ b/e2e/specs/8-vesting/esEXA.spec.ts @@ -58,10 +58,11 @@ test('Vesting esEXA & Claiming EXA', async ({ page, web2, web3 }) => { }); }); - const now = Date.now() / 1_000; + const now = Math.ceil(Date.now() / 1_000); + const half = Math.ceil(period / 2); - await web3.fork.increaseTime(period / 2); - await web2.time.now(now + period / 2); + await web3.fork.increaseTime(half); + await web2.time.now(now + half); await web2.graph.streams([ { @@ -70,8 +71,11 @@ test('Vesting esEXA & Claiming EXA', async ({ page, web2, web3 }) => { recipient: web3.account.address, startTime: String(now), endTime: String(now + period), + duration: String(period), depositAmount: String(parseEther('100')), withdrawnAmount: '0', + intactAmount: String(parseEther('100')), + cancelable: true, canceled: false, }, ]); @@ -95,7 +99,7 @@ test('Vesting esEXA & Claiming EXA', async ({ page, web2, web3 }) => { await balance.check({ address: web3.account.address, symbol: 'EXA', amount: '50', delta: '0.001' }); }); - await web3.fork.increaseTime(period / 2); + await web3.fork.increaseTime(half); await web2.time.now(now + period); await web2.graph.streams([ @@ -105,8 +109,11 @@ test('Vesting esEXA & Claiming EXA', async ({ page, web2, web3 }) => { recipient: web3.account.address, startTime: String(now), endTime: String(now + period), + duration: String(period), depositAmount: String(parseEther('100')), withdrawnAmount: String(parseEther('50')), + intactAmount: String(parseEther('50')), + cancelable: true, canceled: false, }, ]); @@ -153,7 +160,7 @@ test('Claiming multiple streams', async ({ page, web2, web3 }) => { const balance = _balance({ test, page, publicClient: web3.publicClient }); const vesting = _vesting(page); - const now = Date.now() / 1_000; + const now = Math.ceil(Date.now() / 1_000); await web3.fork.increaseTime(period * 2); await web2.time.now(now + period * 2); @@ -165,9 +172,12 @@ test('Claiming multiple streams', async ({ page, web2, web3 }) => { recipient: web3.account.address, startTime: String(now), endTime: String(now + period), + duration: String(period), depositAmount: String(parseEther('50')), + intactAmount: String(parseEther('50')), withdrawnAmount: '0', canceled: false, + cancelable: true, }, { id: `0xb923abdca17aed90eb5ec5e407bd37164f632bfd-10-${stream1}`, @@ -175,9 +185,12 @@ test('Claiming multiple streams', async ({ page, web2, web3 }) => { recipient: web3.account.address, startTime: String(now), endTime: String(now + period), + duration: String(period), depositAmount: String(parseEther('50')), + intactAmount: String(parseEther('50')), withdrawnAmount: '0', canceled: false, + cancelable: true, }, ]); @@ -186,6 +199,7 @@ test('Claiming multiple streams', async ({ page, web2, web3 }) => { await test.step('Withdraw all depleted streams', async () => { const [id0, id1] = [Number(stream0), Number(stream1)]; + await vesting.checkStream({ id: id0, vested: '50.00', @@ -210,3 +224,62 @@ test('Claiming multiple streams', async ({ page, web2, web3 }) => { await balance.check({ address: web3.account.address, symbol: 'EXA', amount: '120' }); }); }); + +test('Stream cancellation', async ({ page, web2, web3 }) => { + await web3.fork.setBalance(web3.account.address, { + ETH: 1, + esEXA: 100, + EXA: 20, + }); + + const exa = await erc20('EXA', { walletClient: web3.walletClient }); + const esEXA = await escrowedEXA({ publicClient: web3.publicClient, walletClient: web3.walletClient }); + const sablier = await sablierV2LockupLinear({ publicClient: web3.publicClient }); + const stream = await sablier.read.nextStreamId(); + const period = await esEXA.read.vestingPeriod(); + + await exa.write.approve([esEXA.address, 2n ** 256n - 1n], { account: web3.account, chain }); + await esEXA.write.vest([parseEther('100'), web3.account.address], { account: web3.account, chain }); + + const balance = _balance({ test, page, publicClient: web3.publicClient }); + const vesting = _vesting(page); + + const now = Math.ceil(Date.now() / 1_000); + + await web2.graph.streams([ + { + id: `0xb923abdca17aed90eb5ec5e407bd37164f632bfd-10-${stream}`, + tokenId: String(stream), + recipient: web3.account.address, + startTime: String(now), + endTime: String(now + period), + duration: String(period), + depositAmount: String(parseEther('100')), + withdrawnAmount: '0', + intactAmount: String(parseEther('100')), + cancelable: true, + canceled: false, + }, + ]); + + await page.goto('/vesting'); + await vesting.waitForPageToBeReady(); + + await test.step('Cancel stream', async () => { + const id = Number(stream); + await vesting.checkStream({ + id, + vested: '100.00', + reserved: '20.00', + withdrawable: '0.00', + left: '100.00', + progress: '0%', + }); + + await vesting.cancelStream(id); + await vesting.waitForStreamCancelTransaction(id); + + await balance.check({ address: web3.account.address, symbol: 'EXA', amount: '20', delta: '0.001' }); + await balance.check({ address: web3.account.address, symbol: 'esEXA', amount: '100', delta: '0.001' }); + }); +}); From e44e01d8ac2d94f6d278ef36d08e67046fb8b987 Mon Sep 17 00:00:00 2001 From: Jorge Galat Date: Wed, 18 Oct 2023 10:56:43 -0300 Subject: [PATCH 28/38] =?UTF-8?q?=F0=9F=8C=90=20vesting:=20add=20translati?= =?UTF-8?q?ons?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/VestingInput/index.tsx | 6 +++--- i18n/es/translation.json | 23 +++++++++++++++++++---- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/components/VestingInput/index.tsx b/components/VestingInput/index.tsx index 9dc5d2317..b3105dff6 100644 --- a/components/VestingInput/index.tsx +++ b/components/VestingInput/index.tsx @@ -174,7 +174,7 @@ function VestingInput({ refetch }: Props) { return [formatEther(_reserve), _reserve > exaBalance]; }, [reserveRatio, qty, exaBalance]); - const insuficentFunds = useMemo(() => { + const insufficientFunds = useMemo(() => { return parseEther(qty) > (balance || 0n) || !qty || moreThanBalance; }, [balance, moreThanBalance, qty]); @@ -403,9 +403,9 @@ function VestingInput({ refetch }: Props) { loading={isLoading} onClick={submit} data-testid="vesting-submit" - disabled={insuficentFunds} + disabled={insufficientFunds} > - {insuficentFunds ? t('Insuficent esEXA balance') : t('Vest esEXA')} + {insufficientFunds ? t('Insufficient esEXA balance') : t('Vest esEXA')} )} diff --git a/i18n/es/translation.json b/i18n/es/translation.json index 6bf203a67..4fd8a8e1b 100644 --- a/i18n/es/translation.json +++ b/i18n/es/translation.json @@ -522,9 +522,24 @@ "No vesting streams active yet.": "Aún no hay streams de vesting activos.", "Start vesting your esEXA and see the streams’ details here.": "Comienza a vestear tus esEXA y ve los detalles de los streams aquí.", "esEXA Vested": "esEXA Vesteados", - "Claimed EXA": "EXA Reclamado", - "Vesting Period": "Período de Vesting", - "{{duration}} days left": "{{duration}} días restantes", "View NFT": "Ver NFT", - "Claim EXA": "Reclamar EXA" + "Withdraw Reserved EXA": "Retirar EXA Reservado", + "When you withdraw the reserved EXA, the associated vesting stream will be cancelled automatically. You’ll be able to claim the earned EXA and will get back all remaining esEXA.": "Cuando retires el EXA reservado, el flujo de consolidación asociado se cancelará automáticamente. Podrás reclamar el EXA ganado y recuperarás todos los esEXA restantes.", + "Withdraw and Cancel Stream": "Retirar y Cancelar Stream", + "{{daysLeft}} days left": "{{daysLeft}} días restantes", + "{{daysLeft}} day left": "{{daysLeft}} día restante", + "{{hoursLeft}} hours left": "{{hoursLeft}} horas restantes", + "Reserved EXA": "EXA Reservado", + "Cancel": "Cancelar", + "Claimable EXA": "EXA Reclamable", + "Withdraw EXA": "Retirar EXA", + "Progress": "Progreso", + "You are vesting your esEXA": "Estás consolidando tus esEXA.", + "Your esEXA has been vested": "Tu esEXA ha sido consolidado.", + "{{number}} Reserve": "{{number}} Reserva", + "Insufficient esEXA balance": "Balance esEXA insuficiente", + "Vest esEXA": "Consolidar esEXA", + "No esEXA to claim": "No hay esEXA que reclamar.", + "You can claim all streams at once.": "Puedes reclamar todos los streams a la vez.", + "Claim All": "Reclamar Todos" } From 59b4d94e73586af8302870f9fcb79958bf656c0a Mon Sep 17 00:00:00 2001 From: franm Date: Wed, 18 Oct 2023 13:45:32 -0300 Subject: [PATCH 29/38] =?UTF-8?q?=F0=9F=92=84=20vesting:=20fix=20styles?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/ActiveStream/index.tsx | 2 +- .../DashboardHeader/UserRewards/index.tsx | 10 ++++++---- styles/theme.ts | 2 ++ 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/components/ActiveStream/index.tsx b/components/ActiveStream/index.tsx index 2124fc987..9735d6f7f 100644 --- a/components/ActiveStream/index.tsx +++ b/components/ActiveStream/index.tsx @@ -69,7 +69,7 @@ const CustomProgressBar: React.FC<{ value: number; 'data-testid'?: string }> = ( borderRadius: '4px', }} > - + {toPercentage(value / 100, value === 100 ? 0 : 2)} diff --git a/components/newDashboard/DashboardSummary/DashboardHeader/UserRewards/index.tsx b/components/newDashboard/DashboardSummary/DashboardHeader/UserRewards/index.tsx index a01e445bb..41bd5b7dc 100644 --- a/components/newDashboard/DashboardSummary/DashboardHeader/UserRewards/index.tsx +++ b/components/newDashboard/DashboardSummary/DashboardHeader/UserRewards/index.tsx @@ -17,9 +17,10 @@ type RewardProps = { amount: string; amountInUSD?: string; xsDirection?: 'row' | 'column'; + dense: boolean; }; -const Reward: FC = ({ assetSymbol, amount, amountInUSD, xsDirection = 'column' }) => { +const Reward: FC = ({ assetSymbol, amount, amountInUSD, xsDirection = 'column', dense }) => { const { breakpoints } = useTheme(); const isMobile = useMediaQuery(breakpoints.down('lg')); @@ -29,14 +30,14 @@ const Reward: FC = ({ assetSymbol, amount, amountInUSD, xsDirection {assetSymbol} - {amountInUSD ? `$${amountInUSD}` : amount} + {amountInUSD ? `$${amountInUSD}` : amount} ); @@ -111,6 +112,7 @@ const UserRewards = () => { amount={amount} amountInUSD={amountInUSD} xsDirection={rewards.length > 1 ? 'column' : 'row'} + dense={rewards.length > 2} /> )) diff --git a/styles/theme.ts b/styles/theme.ts index 0e6a3b87e..0200af3b1 100644 --- a/styles/theme.ts +++ b/styles/theme.ts @@ -135,6 +135,7 @@ export const lightTheme = createTheme({ '100': '#d4d4fa', '200': '#a6a6f4', }, + secondary: { main: '#fafafa' }, grey: { '50': '#fafafa', '100': '#F9FAFB', @@ -369,6 +370,7 @@ export const darkTheme = createTheme({ '100': '#9a9a9a', '200': '#62666A', }, + secondary: { main: '#0E0E0E' }, grey: { '900': '#fafafa', '700': '#F9FAFB', From b08273b953c5ecc4d7245d028fa9b37cb78c3d60 Mon Sep 17 00:00:00 2001 From: franm Date: Wed, 18 Oct 2023 13:46:31 -0300 Subject: [PATCH 30/38] =?UTF-8?q?=F0=9F=93=88vesting:=20add=20analytics?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/VestingInput/index.tsx | 33 ++++++++++++++++++++---- components/vesting/utils.ts | 5 ++++ hooks/useAnalytics.ts | 43 +++++++++++++++++++++++++++---- types/Vest.ts | 8 ++++++ 4 files changed, 79 insertions(+), 10 deletions(-) create mode 100644 components/vesting/utils.ts create mode 100644 types/Vest.ts diff --git a/components/VestingInput/index.tsx b/components/VestingInput/index.tsx index b3105dff6..1d13aa4dc 100644 --- a/components/VestingInput/index.tsx +++ b/components/VestingInput/index.tsx @@ -46,6 +46,7 @@ import useIsContract from 'hooks/useIsContract'; import { gasLimit } from 'utils/gas'; import { Transaction } from 'types/Transaction'; import LoadingTransaction from 'components/common/modal/Loading'; +import useAnalytics from 'hooks/useAnalytics'; type Params> = AbiParametersToPrimitiveTypes< ExtractAbiFunction['inputs'] @@ -153,6 +154,7 @@ function VestingInput({ refetch }: Props) { const { signTypedDataAsync } = useSignTypedData(); const [isLoading, setIsLoading] = useState(false); const [tx, setTx] = useState(); + const { transaction } = useAnalytics(); const [qty, setQty] = useState(''); @@ -234,11 +236,17 @@ function VestingInput({ refetch }: Props) { return; setIsLoading(true); + const amount = parseEther(qty); + const res = (amount * reserveRatio) / WEI_PER_ETHER; + + const vestInput = { + chainId: displayNetwork?.id, + amount: amount, + reserve: res, + }; + transaction.addToCart('vest', vestInput); let hash; try { - const amount = parseEther(qty); - const res = (amount * reserveRatio) / WEI_PER_ETHER; - let args: Params<'vest'> = [amount, walletAddress, reserveRatio, BigInt(vestingPeriod)] as const; if (await isContract(walletAddress)) { @@ -261,19 +269,34 @@ function VestingInput({ refetch }: Props) { hash = await escrowedEXA.write.vest(args, { ...opts, gasLimit: gasLimit(gas) }); } + transaction.beginCheckout('vest', vestInput); + setTx({ status: 'processing', hash }); const { status, transactionHash } = await waitForTransaction({ hash }); setTx({ status: status === 'success' ? 'success' : 'error', hash: transactionHash }); - await waitForTransaction({ hash }); + if (status) transaction.purchase('vest', vestInput); } catch (e) { + transaction.removeFromCart('vest', vestInput); if (hash) setTx({ status: 'error', hash }); } finally { setIsLoading(false); } - }, [walletAddress, reserveRatio, vestingPeriod, escrowedEXA, exa, opts, qty, isContract, sign]); + }, [ + walletAddress, + reserveRatio, + vestingPeriod, + escrowedEXA, + exa, + opts, + qty, + displayNetwork?.id, + transaction, + isContract, + sign, + ]); const onClose = useCallback(() => { setTx(undefined); diff --git a/components/vesting/utils.ts b/components/vesting/utils.ts new file mode 100644 index 000000000..55856307a --- /dev/null +++ b/components/vesting/utils.ts @@ -0,0 +1,5 @@ +import { VestInput } from 'types/Vest'; + +export function isVestInput(input: unknown): input is VestInput { + return typeof input === 'object' && input !== null && 'chainId' in input && 'amount' in input && 'reserve' in input; +} diff --git a/hooks/useAnalytics.ts b/hooks/useAnalytics.ts index cbe3097db..51f7b3aff 100644 --- a/hooks/useAnalytics.ts +++ b/hooks/useAnalytics.ts @@ -16,8 +16,19 @@ import { formatUnits } from 'viem'; import { isBridgeInput } from 'components/BridgeContent/utils'; import { BridgeInput } from 'types/Bridge'; import { Operation } from 'types/Operation'; - -type ItemVariant = 'operation' | 'approve' | 'enterMarket' | 'exitMarket' | 'claimAll' | 'claim' | 'roll' | 'bridge'; +import { VestInput } from 'types/Vest'; +import { isVestInput } from 'components/vesting/utils'; + +type ItemVariant = + | 'operation' + | 'approve' + | 'enterMarket' + | 'exitMarket' + | 'claimAll' + | 'claim' + | 'roll' + | 'bridge' + | 'vest'; type TrackItem = { eventName: string; variant: ItemVariant }; type OperationInput = { operation: Operation; symbol: string; qty: string }; @@ -116,7 +127,19 @@ function useAnalyticsContext(assetSymbol?: string) { }; }, []); - return { appContext: useSnapshot(appContext), itemContext, rolloverContext, bridgeContext }; + const vestContext = useCallback((input: VestInput) => { + const operationLabel = 'vest'; + + return { + item_id: operationLabel, + item_name: operationLabel, + symbol: input.destinationToken, + quantity: input.destinationToken, + ...input, + }; + }, []); + + return { appContext: useSnapshot(appContext), itemContext, rolloverContext, bridgeContext, vestContext }; } export function usePageView(pathname: string, title: string) { @@ -139,7 +162,7 @@ export default function useAnalytics({ rewards, operationInput, }: { symbol?: string; rewards?: Rewards; operationInput?: OperationInput } = {}) { - const { appContext, itemContext, rolloverContext, bridgeContext } = useAnalyticsContext(symbol); + const { appContext, itemContext, rolloverContext, bridgeContext, vestContext } = useAnalyticsContext(symbol); const { isDisable } = useActionButton(); const trackWithContext = useCallback( @@ -315,6 +338,10 @@ export default function useAnalytics({ (eventName: string, input: BridgeInput) => trackWithContext(eventName, { items: [bridgeContext(input)] }), [bridgeContext, trackWithContext], ); + const trackVest = useCallback( + (eventName: string, input: VestInput) => trackWithContext(eventName, { items: [vestContext(input)] }), + [trackWithContext, vestContext], + ); const buildTransactionTrack = useCallback( (variant: ItemVariant = 'operation', eventName: string, input?: object) => { @@ -331,12 +358,18 @@ export default function useAnalytics({ } break; } + case 'vest': { + if (isVestInput(input)) { + trackVest(eventName, input); + } + break; + } default: { trackItem({ eventName, variant }); } } }, - [trackBridge, trackItem, trackRollover], + [trackBridge, trackItem, trackRollover, trackVest], ); const addToCart = useCallback( diff --git a/types/Vest.ts b/types/Vest.ts new file mode 100644 index 000000000..33252ff3e --- /dev/null +++ b/types/Vest.ts @@ -0,0 +1,8 @@ +export type VestInput = { + sourceChainId?: number; + sourceToken: string; + destinationChainId?: number; + destinationToken: string; + sourceAmount: string; + destinationAmount: string; +}; From 28a0dafe1c8f6eead2c0415937064e9f89b9f92e Mon Sep 17 00:00:00 2001 From: franm Date: Wed, 18 Oct 2023 18:19:06 -0300 Subject: [PATCH 31/38] =?UTF-8?q?=F0=9F=9A=B8=20vesting:=20add=20max=20but?= =?UTF-8?q?ton?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/VestingInput/index.tsx | 35 ++++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/components/VestingInput/index.tsx b/components/VestingInput/index.tsx index 1d13aa4dc..b9659d8bb 100644 --- a/components/VestingInput/index.tsx +++ b/components/VestingInput/index.tsx @@ -298,6 +298,12 @@ function VestingInput({ refetch }: Props) { sign, ]); + const onMax = useCallback(() => { + if (balance) { + setQty(formatEther(balance || 0n)); + } + }, [balance]); + const onClose = useCallback(() => { setTx(undefined); setQty(''); @@ -351,9 +357,32 @@ function VestingInput({ refetch }: Props) { balanceIsLoading ? ( ) : ( - - {t('Available')}: {formatNumber(formatEther(balance || 0n))} esEXA - + + + {t('Available')}: {formatNumber(formatEther(balance || 0n))} + + + ) ) : null} From 24c3e4e68bb2c040b7826f9cdeb9f49f23029c93 Mon Sep 17 00:00:00 2001 From: franm Date: Wed, 18 Oct 2023 18:19:50 -0300 Subject: [PATCH 32/38] =?UTF-8?q?=F0=9F=92=84vesting:=20update=20styles?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/ActiveStream/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/ActiveStream/index.tsx b/components/ActiveStream/index.tsx index 9735d6f7f..6312729b3 100644 --- a/components/ActiveStream/index.tsx +++ b/components/ActiveStream/index.tsx @@ -39,7 +39,7 @@ const StyledLinearProgress = styled(LinearProgress, { height: 6, borderRadius: 5, [`&.${linearProgressClasses.colorPrimary}`]: { - backgroundColor: theme.palette.grey[100], + backgroundColor: theme.palette.grey[theme.palette.mode === 'light' ? 100 : 200], }, [`& .${linearProgressClasses.bar}`]: { borderRadius: 5, From fc1fcc73295c5076f686296688dcb0d286752d68 Mon Sep 17 00:00:00 2001 From: franm Date: Wed, 18 Oct 2023 18:20:24 -0300 Subject: [PATCH 33/38] =?UTF-8?q?=F0=9F=92=AC=20vesting:=20change=20vestin?= =?UTF-8?q?g=20text?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- i18n/es/translation.json | 9 ++++----- pages/vesting.tsx | 31 +++++++++++++++---------------- 2 files changed, 19 insertions(+), 21 deletions(-) diff --git a/i18n/es/translation.json b/i18n/es/translation.json index 4fd8a8e1b..883830ffe 100644 --- a/i18n/es/translation.json +++ b/i18n/es/translation.json @@ -509,14 +509,10 @@ "No routes": "No hay rutas", "Terms and Conditions": "Términos y Condiciones", "esEXA Vesting Program": "Programa de Vesting de esEXA", - "We've created the esEXA Vesting Program to reward the Protocol’s active participants. Whenever you use the Protocol, you'll receive esEXA tokens that you can vest to earn EXA.": "Hemos creado el Programa de Vesting de esEXA para recompensar a los participantes activos del Protocolo. Cada vez que uses el Protocolo, recibirás tokens esEXA que puedes depositar para ganar EXA.", - "In just two simple steps, you can start unlocking EXA tokens while contributing to the growth and improvement of the Protocol.": "__STRING_NOT_TRANSLATED__", "Learn more about the esEXA Vesting Program.": "Aprende más sobre el Programa de Vesting de esEXA.", "Step {{number}}": "Paso {{number}}", "Claim your esEXA Rewards": "Reclama tus recompensas esEXA", "Claim {{amount}} esEXA": "Reclama {{amount}} esEXA", - "Initiate Vesting Your esEXA": "Inicia el Vesting de tus esEXA", - "You'll need to deposit 10% of the total esEXA you want to vest as an EXA reserve. You can get EXA if you don’t have the required amount.": "Deberás depositar el 10% del total de esEXA que deseas vestear como reserva de EXA. Puedes obtener EXA si no tienes la cantidad requerida.", "Active Vesting Streams": "Streams de Vesting Activos", "Here, you can monitor all your active vesting streams, allowing you to easily track your current EXA earnings. Each vesting stream is represented by an NFT and comes with a 12-month vesting period.": "Aquí puedes monitorear todos tus streams de vesting activos, lo que te permite realizar un seguimiento fácil de tus ganancias actuales de EXA. Cada stream de vesting está representado por un NFT y viene con un período de vesting de 12 meses.", "No vesting streams active yet.": "Aún no hay streams de vesting activos.", @@ -541,5 +537,8 @@ "Vest esEXA": "Consolidar esEXA", "No esEXA to claim": "No hay esEXA que reclamar.", "You can claim all streams at once.": "Puedes reclamar todos los streams a la vez.", - "Claim All": "Reclamar Todos" + "Claim All": "Reclamar Todos", + "The esEXA program provides rewards equivalent to EXA with a linear vesting period, ensuring that the Exactly protocol remains sustainable and rewarding for long-term community members.": "El programa esEXA proporciona recompensas equivalentes a EXA con un período de consolidación lineal, asegurando que el protocolo Exactly siga siendo sostenible y gratificante para los miembros de la comunidad a largo plazo.", + "Initiate the vesting of your esEXA": "Inicia la consolidación de tus esEXA", + "You must deposit 10% of the total esEXA you want to vest as an EXA reserve.": "Debes depositar el 10% del total de esEXA que deseas consolidar como reserva de EXA." } diff --git a/pages/vesting.tsx b/pages/vesting.tsx index 41ad178da..6374946ba 100644 --- a/pages/vesting.tsx +++ b/pages/vesting.tsx @@ -2,8 +2,8 @@ import React, { useCallback, useMemo, useState } from 'react'; import type { NextPage } from 'next'; import { usePageView } from 'hooks/useAnalytics'; -import { useTranslation } from 'react-i18next'; -import { Box, Button, Divider, Grid, Skeleton, Typography } from '@mui/material'; +import { Trans, useTranslation } from 'react-i18next'; +import { Box, Button, Divider, Grid, Link, Skeleton, Typography } from '@mui/material'; import VestingInput from 'components/VestingInput'; import ActiveStream from 'components/ActiveStream'; import { useUpdateStreams, useEscrowedEXA } from 'hooks/useEscrowedEXA'; @@ -57,18 +57,13 @@ const Vesting: NextPage = () => { {t( - "We've created the esEXA Vesting Program to reward the Protocol’s active participants. Whenever you use the Protocol, you'll receive esEXA tokens that you can vest to earn EXA.", + 'The esEXA program provides rewards equivalent to EXA with a linear vesting period, ensuring that the Exactly protocol remains sustainable and rewarding for long-term community members.', )} - - {t( - 'In just two simple steps, you can start unlocking EXA tokens while contributing to the growth and improvement of the Protocol.', - ) + ' '} - - - {t('Learn more about the esEXA Vesting Program.')} - - + + + {t('Learn more about the esEXA Vesting Program.')} + @@ -103,11 +98,15 @@ const Vesting: NextPage = () => { {t('Step {{number}}', { number: 2 })} - {t('Initiate Vesting Your esEXA')} + {t('Initiate the vesting of your esEXA')} + {t('You must deposit 10% of the total esEXA you want to vest as an EXA reserve.')} - {t( - "You'll need to deposit 10% of the total esEXA you want to vest as an EXA reserve. You can get EXA if you don’t have the required amount.", - )} + , + }} + /> From 59c983545eded2f110cf10b6259689fe57006037 Mon Sep 17 00:00:00 2001 From: franm Date: Thu, 19 Oct 2023 16:32:14 -0300 Subject: [PATCH 34/38] =?UTF-8?q?=F0=9F=8E=A8vesting:=20improve=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/ActiveStream/index.tsx | 258 +++++++++++++++++------------- components/VestingInput/index.tsx | 16 +- hooks/useEscrowedEXA.ts | 23 ++- i18n/es/translation.json | 4 +- pages/strategies.tsx | 16 ++ 5 files changed, 185 insertions(+), 132 deletions(-) diff --git a/components/ActiveStream/index.tsx b/components/ActiveStream/index.tsx index 6312729b3..1e144f2a8 100644 --- a/components/ActiveStream/index.tsx +++ b/components/ActiveStream/index.tsx @@ -18,7 +18,7 @@ import { useMediaQuery, useTheme, } from '@mui/material'; -import React, { FC, type PropsWithChildren, useCallback, useMemo, useRef, useState } from 'react'; +import React, { FC, useCallback, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { waitForTransaction } from '@wagmi/core'; import { LoadingButton } from '@mui/lab'; @@ -99,9 +99,15 @@ const Transition = React.forwardRef(function Transition( return ; }); -function Modal({ open, onClose, children }: PropsWithChildren<{ open: boolean; onClose: () => void }>) { +const NFT: React.FC<{ tokenId: number; open: boolean; onClose: () => void }> = ({ tokenId, open, onClose }) => { const { breakpoints } = useTheme(); const isMobile = useMediaQuery(breakpoints.down('sm')); + const { data: nft } = useSablierV2NftDescriptorTokenUri(BigInt(tokenId)); + const { spacing } = useTheme(); + + const b64 = nft?.split(',')[1] ?? ''; + const json = atob(b64) || '{}'; + const { image, name } = JSON.parse(json); return ( - {children} + + {name} + ); -} +}; + +const WithdrawAndCancel: React.FC<{ + tokenId: number; + open: boolean; + onClose: () => void; + cancel: () => void; + loading: boolean; +}> = ({ tokenId, open, onClose, cancel, loading }) => { + const { spacing } = useTheme(); + const { chain } = useNetwork(); + const { impersonateActive, chain: displayNetwork } = useWeb3(); + const { switchNetwork, isLoading: switchIsLoading } = useSwitchNetwork(); + const { t } = useTranslation(); + + const { breakpoints } = useTheme(); + const isMobile = useMediaQuery(breakpoints.down('sm')); + return ( + + + + + + + + + {t('Withdraw Reserved EXA')} + + + + + + + {t( + 'When you withdraw the reserved EXA, the associated vesting stream will be cancelled automatically. You’ll be able to claim the earned EXA and will get back all remaining esEXA.', + )} + + + {impersonateActive ? ( + + ) : chain && chain.id !== displayNetwork.id ? ( + switchNetwork?.(displayNetwork.id)} + variant="contained" + loading={switchIsLoading} + > + {t('Please switch to {{network}} network', { network: displayNetwork.name })} + + ) : ( + + {t('Withdraw and Cancel Stream')} + + )} + + + + + ); +}; type ActiveStreamProps = { tokenId: number; @@ -159,7 +284,6 @@ const ActiveStream: FC = ({ const { t } = useTranslation(); const { impersonateActive, chain: displayNetwork, opts } = useWeb3(); const { chain } = useNetwork(); - const { spacing } = useTheme(); const { switchNetwork, isLoading: switchIsLoading } = useSwitchNetwork(); const { data: reserve, isLoading: reserveIsLoading } = useEscrowedEXAReserves(BigInt(tokenId)); const { data: withdrawable, isLoading: withdrawableIsLoading } = useSablierV2LockupLinearWithdrawableAmountOf( @@ -167,111 +291,20 @@ const ActiveStream: FC = ({ ); const escrowedEXA = useEscrowedEXA(); const [loading, setLoading] = useState(false); - const [modalOpen, setModalOpen] = useState(false); - const [modalContent, setModalContent] = useState<'nft' | 'cancel'>('nft'); + const [NFTModalOpen, setNFTModalOpen] = useState(false); + const [CancelModalOpen, setCancelModalOpen] = useState(false); const { breakpoints } = useTheme(); const isMobile = useMediaQuery(breakpoints.down('sm')); - const { data: nft } = useSablierV2NftDescriptorTokenUri(BigInt(tokenId)); - - const WithdrawAndCancel = () => { - return ( - <> - - - - {t('Withdraw Reserved EXA')} - - - - - - - {t( - 'When you withdraw the reserved EXA, the associated vesting stream will be cancelled automatically. You’ll be able to claim the earned EXA and will get back all remaining esEXA.', - )} - - - {impersonateActive ? ( - - ) : chain && chain.id !== displayNetwork.id ? ( - switchNetwork?.(displayNetwork.id)} - variant="contained" - loading={switchIsLoading} - > - {t('Please switch to {{network}} network', { network: displayNetwork.name })} - - ) : ( - <> - - {t('Withdraw and Cancel Stream')} - - - )} - - - - - ); - }; - - const NFT = () => { - const b64 = nft?.split(',')[1] ?? ''; - const json = atob(b64) || '{}'; - const { image, name } = JSON.parse(json); - - return ( - - {name} - - ); - }; - const onClose = useCallback(() => { - setModalOpen(false); + const closeNFTModal = useCallback(() => { + setNFTModalOpen(false); }, []); - const handleContent = useCallback((content: 'nft' | 'cancel') => { - setModalOpen(true); - setModalContent(content); + const closeCancelModal = useCallback(() => { + setCancelModalOpen(false); }, []); - const handleCancel = useCallback(async () => { + const cancel = useCallback(async () => { if (!escrowedEXA || !opts) return; setLoading(true); try { @@ -285,7 +318,7 @@ const ActiveStream: FC = ({ } }, [escrowedEXA, opts, refetch, tokenId]); - const handleClick = useCallback(async () => { + const withdraw = useCallback(async () => { if (!escrowedEXA || !opts) return; setLoading(true); try { @@ -363,7 +396,7 @@ const ActiveStream: FC = ({ borderRadius="2px" alignItems="center" onClick={() => { - handleContent('nft'); + setNFTModalOpen(true); }} sx={{ cursor: 'pointer' }} > @@ -405,7 +438,7 @@ const ActiveStream: FC = ({ data-testid={ progress === 100 ? `vesting-stream-${tokenId}-withdraw` : `vesting-stream-${tokenId}-cancel` } - onClick={() => (progress === 100 ? handleClick() : handleContent('cancel'))} + onClick={() => (progress === 100 ? withdraw() : setCancelModalOpen(true))} sx={{ cursor: 'pointer' }} > @@ -414,9 +447,14 @@ const ActiveStream: FC = ({ )} - - {modalContent === 'nft' ? : } - + + {!isMobile && } @@ -464,7 +502,7 @@ const ActiveStream: FC = ({ diff --git a/components/VestingInput/index.tsx b/components/VestingInput/index.tsx index b9659d8bb..168e79001 100644 --- a/components/VestingInput/index.tsx +++ b/components/VestingInput/index.tsx @@ -158,13 +158,11 @@ function VestingInput({ refetch }: Props) { const [qty, setQty] = useState(''); - const errorData = false; - const usdValue = useMemo(() => { if (!qty || !EXAPrice) return; - const parsedqty = parseEther(qty); - const usd = (parsedqty * EXAPrice) / WEI_PER_ETHER; + const parsedQty = parseEther(qty); + const usd = (parsedQty * EXAPrice) / WEI_PER_ETHER; return formatEther(usd); }, [EXAPrice, qty]); @@ -241,7 +239,7 @@ function VestingInput({ refetch }: Props) { const vestInput = { chainId: displayNetwork?.id, - amount: amount, + amount, reserve: res, }; transaction.addToCart('vest', vestInput); @@ -298,9 +296,9 @@ function VestingInput({ refetch }: Props) { sign, ]); - const onMax = useCallback(() => { + const setMaxBalance = useCallback(() => { if (balance) { - setQty(formatEther(balance || 0n)); + setQty(formatEther(balance)); } }, [balance]); @@ -368,7 +366,7 @@ function VestingInput({ refetch }: Props) { {t('Available')}: {formatNumber(formatEther(balance || 0n))} ), }, + { + title: 'esEXA', + description: t('Unlock your EXA rewards for being an active participant in the Protocol'), + tags: [ + { prefix: t('EARN'), text: 'EXA' }, + { text: t('Basic'), size: 'small' as const }, + ], + button: ( + + + + ), + isNew: true, + }, { chainId: optimism.id, title: t('Get EXA'), From e834bf07378fd20e4a82471b32248af58547950d Mon Sep 17 00:00:00 2001 From: franm Date: Thu, 19 Oct 2023 17:33:58 -0300 Subject: [PATCH 35/38] =?UTF-8?q?=F0=9F=92=ACvesting:=20fix=20reserve=20va?= =?UTF-8?q?lue?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- i18n/es/translation.json | 4 ++-- pages/vesting.tsx | 10 ++++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/i18n/es/translation.json b/i18n/es/translation.json index a26a502c7..04fc44fd0 100644 --- a/i18n/es/translation.json +++ b/i18n/es/translation.json @@ -540,7 +540,7 @@ "Claim All": "Reclamar Todos", "The esEXA program provides rewards equivalent to EXA with a linear vesting period, ensuring that the Exactly protocol remains sustainable and rewarding for long-term community members.": "El programa esEXA proporciona recompensas equivalentes a EXA con un período de consolidación lineal, asegurando que el protocolo Exactly siga siendo sostenible y gratificante para los miembros de la comunidad a largo plazo.", "Initiate the vesting of your esEXA": "Inicia la consolidación de tus esEXA", - "You must deposit 10% of the total esEXA you want to vest as an EXA reserve.": "Debes depositar el 10% del total de esEXA que deseas consolidar como reserva de EXA.", "Unlock your EXA rewards for being an active participant in the Protocol": "Desbloquea tus recompensas EXA por ser un participante activo en el Protocolo", - "EARN": "GANA" + "EARN": "GANA", + "You must deposit {{reserveRatio}} of the total esEXA you want to vest as an EXA reserve.": "Debes depositar {{reserveRatio}} del total de esEXA que deseas consolidar como reserva de EXA." } diff --git a/pages/vesting.tsx b/pages/vesting.tsx index 6374946ba..172bc643c 100644 --- a/pages/vesting.tsx +++ b/pages/vesting.tsx @@ -6,7 +6,7 @@ import { Trans, useTranslation } from 'react-i18next'; import { Box, Button, Divider, Grid, Link, Skeleton, Typography } from '@mui/material'; import VestingInput from 'components/VestingInput'; import ActiveStream from 'components/ActiveStream'; -import { useUpdateStreams, useEscrowedEXA } from 'hooks/useEscrowedEXA'; +import { useUpdateStreams, useEscrowedEXA, useEscrowedEXAReserveRatio } from 'hooks/useEscrowedEXA'; import { useWeb3 } from 'hooks/useWeb3'; import { useNetwork, useSwitchNetwork } from 'wagmi'; import { LoadingButton } from '@mui/lab'; @@ -15,6 +15,7 @@ import useRewards from 'hooks/useRewards'; import { useModal } from 'contexts/ModalContext'; import formatNumber from 'utils/formatNumber'; import { formatEther } from 'viem'; +import { toPercentage } from 'utils/utils'; const Vesting: NextPage = () => { usePageView('/vesting', 'Vesting'); @@ -23,6 +24,7 @@ const Vesting: NextPage = () => { const { chain } = useNetwork(); const { activeStreams, loading: streamsLoading, refetch } = useUpdateStreams(); const { switchNetwork, isLoading: switchIsLoading } = useSwitchNetwork(); + const { data: reserveRatio } = useEscrowedEXAReserveRatio(); const [loading, setLoading] = useState(false); const escrowedEXA = useEscrowedEXA(); @@ -99,7 +101,11 @@ const Vesting: NextPage = () => { {t('Step {{number}}', { number: 2 })} {t('Initiate the vesting of your esEXA')} - {t('You must deposit 10% of the total esEXA you want to vest as an EXA reserve.')} + + {t('You must deposit {{reserveRatio}} of the total esEXA you want to vest as an EXA reserve.', { + reserveRatio: toPercentage(Number(reserveRatio) / 1e18, 0), + })} + Date: Fri, 20 Oct 2023 12:23:25 -0300 Subject: [PATCH 36/38] =?UTF-8?q?=F0=9F=92=AC=20update=20security=20hub?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- i18n/es/translation.json | 3 ++- pages/security/periphery.tsx | 24 ++++++++++++++++++++---- pages/strategies.tsx | 32 ++++++++++++++++---------------- 3 files changed, 38 insertions(+), 21 deletions(-) diff --git a/i18n/es/translation.json b/i18n/es/translation.json index 04fc44fd0..d59828247 100644 --- a/i18n/es/translation.json +++ b/i18n/es/translation.json @@ -542,5 +542,6 @@ "Initiate the vesting of your esEXA": "Inicia la consolidación de tus esEXA", "Unlock your EXA rewards for being an active participant in the Protocol": "Desbloquea tus recompensas EXA por ser un participante activo en el Protocolo", "EARN": "GANA", - "You must deposit {{reserveRatio}} of the total esEXA you want to vest as an EXA reserve.": "Debes depositar {{reserveRatio}} del total de esEXA que deseas consolidar como reserva de EXA." + "You must deposit {{reserveRatio}} of the total esEXA you want to vest as an EXA reserve.": "Debes depositar {{reserveRatio}} del total de esEXA que deseas consolidar como reserva de EXA.", + "The EscrowedEXA contract is an ERC-20 token that allows anyone to mint esEXA tokens in exchange for EXA tokens.": "El contrato EscrowedEXA es un token ERC-20 que permite a cualquiera acuñar tokens esEXA a cambio de tokens EXA." } diff --git a/pages/security/periphery.tsx b/pages/security/periphery.tsx index 49f4f2892..35e8f3976 100644 --- a/pages/security/periphery.tsx +++ b/pages/security/periphery.tsx @@ -51,10 +51,10 @@ const Security: NextPage = () => { reports: ['ABDK'], information: [`24 ${t('lines')} (20 ${t('lines of code')}), 674 bytes`], proxy: async () => { - return [{ name: '', address: await getContractAddress('EXA_Proxy') }]; + return [{ name: '', address: await getContractAddress('Airdrop_Proxy') }]; }, implementation: async () => { - return [{ name: '', address: await getContractAddress('EXA_Implementation') }]; + return [{ name: '', address: await getContractAddress('Airdrop_Implementation') }]; }, codeLink: 'https://github.com/exactly/protocol/blob/main/contracts/periphery/EXA.sol', }, @@ -67,13 +67,29 @@ const Security: NextPage = () => { reports: ['ABDK'], information: [`79 ${t('lines')} (65 ${t('lines of code')}), 2.25 kb`], proxy: async () => { - return [{ name: '', address: await getContractAddress('Airdrop_Proxy') }]; + return [{ name: '', address: await getContractAddress('EXA_Proxy') }]; }, implementation: async () => { - return [{ name: '', address: await getContractAddress('Airdrop_Implementation') }]; + return [{ name: '', address: await getContractAddress('EXA_Implementation') }]; }, codeLink: 'https://github.com/exactly/protocol/blob/main/contracts/periphery/Airdrop.sol', }, + { + name: 'EscrowedEXA.sol', + audited: true, + description: t( + 'The EscrowedEXA contract is an ERC-20 token that allows anyone to mint esEXA tokens in exchange for EXA tokens.', + ), + reports: ['ABDK', 'OpenZeppelin'], + information: [`279 ${t('lines')} (242 ${t('lines of code')}), 10.5 kb`], + proxy: async () => { + return [{ name: '', address: await getContractAddress('EscrowedEXA_Proxy') }]; + }, + implementation: async () => { + return [{ name: '', address: await getContractAddress('EscrowedEXA_Implementation') }]; + }, + codeLink: 'https://github.com/exactly/protocol/blob/main/contracts/periphery/EscrowedEXA.sol', + }, ] : []), ], diff --git a/pages/strategies.tsx b/pages/strategies.tsx index 08281c6e6..bd9fdbb86 100644 --- a/pages/strategies.tsx +++ b/pages/strategies.tsx @@ -161,6 +161,22 @@ const Strategies: NextPage = () => { const exactlyStrategies = useMemo( () => [ + { + title: 'esEXA', + description: t('Unlock your EXA rewards for being an active participant in the Protocol'), + tags: [ + { prefix: t('EARN'), text: 'EXA' }, + { text: t('Basic'), size: 'small' as const }, + ], + button: ( + + + + ), + isNew: true, + }, { title: t('Maximize your yield'), description: t( @@ -204,22 +220,6 @@ const Strategies: NextPage = () => { ), }, - { - title: 'esEXA', - description: t('Unlock your EXA rewards for being an active participant in the Protocol'), - tags: [ - { prefix: t('EARN'), text: 'EXA' }, - { text: t('Basic'), size: 'small' as const }, - ], - button: ( - - - - ), - isNew: true, - }, { chainId: optimism.id, title: t('Get EXA'), From ad2b1543d7e7b60ac1d820c506815b82e8f82c6f Mon Sep 17 00:00:00 2001 From: Jorge Galat Date: Fri, 20 Oct 2023 13:38:36 -0300 Subject: [PATCH 37/38] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20chore:=20upgrade=20@?= =?UTF-8?q?exactly/protocol=200.2.17?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- hooks/useEscrowedEXA.ts | 2 +- package-lock.json | 10 +++++----- package.json | 2 +- pages/api/circulating-exa.ts | 2 +- pages/security/periphery.tsx | 2 +- wagmi.config.ts | 2 +- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/hooks/useEscrowedEXA.ts b/hooks/useEscrowedEXA.ts index 387ad7277..3331c71a5 100644 --- a/hooks/useEscrowedEXA.ts +++ b/hooks/useEscrowedEXA.ts @@ -14,7 +14,7 @@ import { useWeb3 } from './useWeb3'; import { getStreams } from 'queries/getStreams'; export const useEscrowedEXA = () => { - return useContract('EscrowedEXA', escrowedExaABI); + return useContract('esEXA', escrowedExaABI); }; type Stream = { diff --git a/package-lock.json b/package-lock.json index 9031d7ae3..f3b10a6fc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "dependencies": { "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", - "@exactly/protocol": "exactly/protocol#deploy", + "@exactly/protocol": "^0.2.17", "@mui/icons-material": "^5.14.3", "@mui/lab": "^5.0.0-alpha.138", "@mui/material": "^5.14.3", @@ -1080,14 +1080,14 @@ } }, "node_modules/@exactly/protocol": { - "version": "0.2.16", - "resolved": "git+ssh://git@github.com/exactly/protocol.git#d0b23ed426d2f7fae36ea7ec90d59aacdeac8c48", + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/@exactly/protocol/-/protocol-0.2.17.tgz", + "integrity": "sha512-esNIHSAZjk1aCR5tWLLYuKOEdMjAc5HG4EE3iTzT+ge8hGGQVtbtxq0fLwTYFZq+I0b2OTWYKOoNDukLv86XcQ==", "hasInstallScript": true, - "license": "BUSL-1.1", "dependencies": { "@openzeppelin/contracts": "4.9.2", "@openzeppelin/contracts-upgradeable": "4.9.2", - "solmate": "transmissions11/solmate#v7" + "solmate": "github:transmissions11/solmate#v7" }, "engines": { "node": ">=18" diff --git a/package.json b/package.json index 0e9b99523..bcb28c9b4 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "dependencies": { "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", - "@exactly/protocol": "exactly/protocol#deploy", + "@exactly/protocol": "^0.2.17", "@mui/icons-material": "^5.14.3", "@mui/lab": "^5.0.0-alpha.138", "@mui/material": "^5.14.3", diff --git a/pages/api/circulating-exa.ts b/pages/api/circulating-exa.ts index 73d64707c..53ae0a62d 100644 --- a/pages/api/circulating-exa.ts +++ b/pages/api/circulating-exa.ts @@ -4,7 +4,7 @@ import { optimism } from 'viem/chains'; import { address as sablierV2LockupLinear } from '@exactly/protocol/deployments/optimism/SablierV2LockupLinear.json'; import { address as timelockController } from '@exactly/protocol/deployments/optimism/TimelockController.json'; import { address as rewardsController } from '@exactly/protocol/deployments/optimism/RewardsController.json'; -import { address as escrowedEXA } from '@exactly/protocol/deployments/optimism/EscrowedEXA.json'; +import { address as escrowedEXA } from '@exactly/protocol/deployments/optimism/esEXA.json'; import { address as airdrop } from '@exactly/protocol/deployments/optimism/Airdrop.json'; import { address as exaAddress } from '@exactly/protocol/deployments/optimism/EXA.json'; import { exaABI } from '../../types/abi'; diff --git a/pages/security/periphery.tsx b/pages/security/periphery.tsx index 35e8f3976..59fd9a3d7 100644 --- a/pages/security/periphery.tsx +++ b/pages/security/periphery.tsx @@ -75,7 +75,7 @@ const Security: NextPage = () => { codeLink: 'https://github.com/exactly/protocol/blob/main/contracts/periphery/Airdrop.sol', }, { - name: 'EscrowedEXA.sol', + name: 'esEXA.sol', audited: true, description: t( 'The EscrowedEXA contract is an ERC-20 token that allows anyone to mint esEXA tokens in exchange for EXA tokens.', diff --git a/wagmi.config.ts b/wagmi.config.ts index b3e146065..133bb8414 100644 --- a/wagmi.config.ts +++ b/wagmi.config.ts @@ -22,7 +22,7 @@ import SablierV2LockupLinear from '@exactly/protocol/deployments/goerli/SablierV import SablierV2NFTDescriptor from '@exactly/protocol/deployments/goerli/SablierV2NFTDescriptor.json' assert { type: 'json' }; import ExtraFinanceLendingABI from './abi/extraFinanceLending.json' assert { type: 'json' }; import DelegateRegistryABI from './abi/DelegateRegistry.json' assert { type: 'json' }; -import EscrowedEXA from '@exactly/protocol/deployments/goerli/EscrowedEXA.json' assert { type: 'json' }; +import EscrowedEXA from '@exactly/protocol/deployments/goerli/esEXA.json' assert { type: 'json' }; import { Abi } from 'viem'; From 43f98da06be4e662efde533f875e869423366041 Mon Sep 17 00:00:00 2001 From: Jorge Galat Date: Fri, 20 Oct 2023 13:38:53 -0300 Subject: [PATCH 38/38] =?UTF-8?q?=E2=9C=85=20e2e:=20fix=20vesting=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- e2e/specs/8-vesting/esEXA.spec.ts | 41 ++++++++++++++++++++----------- e2e/utils/contracts.ts | 4 +-- pages/vesting.tsx | 2 +- playwright.config.ts | 2 +- 4 files changed, 30 insertions(+), 19 deletions(-) diff --git a/e2e/specs/8-vesting/esEXA.spec.ts b/e2e/specs/8-vesting/esEXA.spec.ts index 7cd8d6f0d..d6800f3d8 100644 --- a/e2e/specs/8-vesting/esEXA.spec.ts +++ b/e2e/specs/8-vesting/esEXA.spec.ts @@ -15,7 +15,7 @@ test('Vesting esEXA & Claiming EXA', async ({ page, web2, web3 }) => { await web3.fork.setBalance(web3.account.address, { ETH: 1, esEXA: 100, - EXA: 20, + EXA: 15, }); const esEXA = await escrowedEXA({ publicClient: web3.publicClient }); @@ -38,7 +38,7 @@ test('Vesting esEXA & Claiming EXA', async ({ page, web2, web3 }) => { await vesting.checkError('Not enough EXA for reserve. Get EXA.'); await vesting.input('100'); - await vesting.checkReserveNeeded('20%', '20'); + await vesting.checkReserveNeeded('15%', '15'); await vesting.waitForSubmitToBeReady(); await vesting.submit(); @@ -87,7 +87,7 @@ test('Vesting esEXA & Claiming EXA', async ({ page, web2, web3 }) => { await vesting.checkStream({ id, vested: '100.00', - reserved: '20.00', + reserved: '15.00', withdrawable: /50\.0|49\.9/, left: '100.00', progress: /50\.00%|50\.01%/, @@ -125,7 +125,7 @@ test('Vesting esEXA & Claiming EXA', async ({ page, web2, web3 }) => { await vesting.checkStream({ id, vested: '100.00', - reserved: '20.00', + reserved: '15.00', withdrawable: /50\.0|49\.9/, left: /50\.0|49\.9/, progress: '100%', @@ -134,7 +134,7 @@ test('Vesting esEXA & Claiming EXA', async ({ page, web2, web3 }) => { await vesting.claimStream(id); await vesting.waitForClaimStreamTransaction(id); - await balance.check({ address: web3.account.address, symbol: 'EXA', amount: '120' }); + await balance.check({ address: web3.account.address, symbol: 'EXA', amount: '115' }); }); }); @@ -142,7 +142,7 @@ test('Claiming multiple streams', async ({ page, web2, web3 }) => { await web3.fork.setBalance(web3.account.address, { ETH: 1, esEXA: 100, - EXA: 20, + EXA: 15, }); const exa = await erc20('EXA', { walletClient: web3.walletClient }); @@ -150,10 +150,17 @@ test('Claiming multiple streams', async ({ page, web2, web3 }) => { const sablier = await sablierV2LockupLinear({ publicClient: web3.publicClient }); const stream = await sablier.read.nextStreamId(); const period = await esEXA.read.vestingPeriod(); + const reserveRatio = await esEXA.read.reserveRatio(); await exa.write.approve([esEXA.address, 2n ** 256n - 1n], { account: web3.account, chain }); - await esEXA.write.vest([parseEther('50'), web3.account.address], { account: web3.account, chain }); - await esEXA.write.vest([parseEther('50'), web3.account.address], { account: web3.account, chain }); + await esEXA.write.vest([parseEther('50'), web3.account.address, reserveRatio, BigInt(period)], { + account: web3.account, + chain, + }); + await esEXA.write.vest([parseEther('50'), web3.account.address, reserveRatio, BigInt(period)], { + account: web3.account, + chain, + }); const [stream0, stream1] = [stream, stream + 1n]; @@ -203,7 +210,7 @@ test('Claiming multiple streams', async ({ page, web2, web3 }) => { await vesting.checkStream({ id: id0, vested: '50.00', - reserved: '10.00', + reserved: '7.50', withdrawable: '50.00', left: '50.00', progress: '100%', @@ -212,7 +219,7 @@ test('Claiming multiple streams', async ({ page, web2, web3 }) => { await vesting.checkStream({ id: id1, vested: '50.00', - reserved: '10.00', + reserved: '7.50', withdrawable: '50.00', left: '50.00', progress: '100%', @@ -221,7 +228,7 @@ test('Claiming multiple streams', async ({ page, web2, web3 }) => { await vesting.claimAllStreams(); await vesting.waitForClaimAllTransaction(); - await balance.check({ address: web3.account.address, symbol: 'EXA', amount: '120' }); + await balance.check({ address: web3.account.address, symbol: 'EXA', amount: '115' }); }); }); @@ -229,7 +236,7 @@ test('Stream cancellation', async ({ page, web2, web3 }) => { await web3.fork.setBalance(web3.account.address, { ETH: 1, esEXA: 100, - EXA: 20, + EXA: 15, }); const exa = await erc20('EXA', { walletClient: web3.walletClient }); @@ -237,9 +244,13 @@ test('Stream cancellation', async ({ page, web2, web3 }) => { const sablier = await sablierV2LockupLinear({ publicClient: web3.publicClient }); const stream = await sablier.read.nextStreamId(); const period = await esEXA.read.vestingPeriod(); + const reserveRatio = await esEXA.read.reserveRatio(); await exa.write.approve([esEXA.address, 2n ** 256n - 1n], { account: web3.account, chain }); - await esEXA.write.vest([parseEther('100'), web3.account.address], { account: web3.account, chain }); + await esEXA.write.vest([parseEther('100'), web3.account.address, reserveRatio, BigInt(period)], { + account: web3.account, + chain, + }); const balance = _balance({ test, page, publicClient: web3.publicClient }); const vesting = _vesting(page); @@ -270,7 +281,7 @@ test('Stream cancellation', async ({ page, web2, web3 }) => { await vesting.checkStream({ id, vested: '100.00', - reserved: '20.00', + reserved: '15.00', withdrawable: '0.00', left: '100.00', progress: '0%', @@ -279,7 +290,7 @@ test('Stream cancellation', async ({ page, web2, web3 }) => { await vesting.cancelStream(id); await vesting.waitForStreamCancelTransaction(id); - await balance.check({ address: web3.account.address, symbol: 'EXA', amount: '20', delta: '0.001' }); + await balance.check({ address: web3.account.address, symbol: 'EXA', amount: '15', delta: '0.001' }); await balance.check({ address: web3.account.address, symbol: 'esEXA', amount: '100', delta: '0.001' }); }); }); diff --git a/e2e/utils/contracts.ts b/e2e/utils/contracts.ts index 115dd4822..7cb939f26 100644 --- a/e2e/utils/contracts.ts +++ b/e2e/utils/contracts.ts @@ -4,7 +4,7 @@ import marketETHRouter from '@exactly/protocol/deployments/optimism/MarketETHRou import debtManagerContract from '@exactly/protocol/deployments/optimism/DebtManager.json' assert { type: 'json' }; import permit2Contract from '@exactly/protocol/deployments/optimism/Permit2.json' assert { type: 'json' }; import sablierV2LockupLinearContract from '@exactly/protocol/deployments/optimism/SablierV2LockupLinear.json' assert { type: 'json' }; -import escrowedEXAContract from '@exactly/protocol/deployments/optimism/EscrowedEXA.json' assert { type: 'json' }; +import escrowedEXAContract from '@exactly/protocol/deployments/optimism/esEXA.json' assert { type: 'json' }; import type { Auditor, @@ -39,7 +39,7 @@ type Clients = { export const erc20 = async (symbol: ERC20TokenSymbol, clients: Clients = {}): Promise => { const { default: { address }, - } = await import(`@exactly/protocol/deployments/optimism/${symbol === 'esEXA' ? 'EscrowedEXA' : symbol}.json`, { + } = await import(`@exactly/protocol/deployments/optimism/${symbol}.json`, { assert: { type: 'json' }, }); if (!isAddress(address)) throw new Error('Invalid address'); diff --git a/pages/vesting.tsx b/pages/vesting.tsx index 172bc643c..e1baf0118 100644 --- a/pages/vesting.tsx +++ b/pages/vesting.tsx @@ -103,7 +103,7 @@ const Vesting: NextPage = () => { {t('Initiate the vesting of your esEXA')} {t('You must deposit {{reserveRatio}} of the total esEXA you want to vest as an EXA reserve.', { - reserveRatio: toPercentage(Number(reserveRatio) / 1e18, 0), + reserveRatio: toPercentage(reserveRatio !== undefined ? Number(reserveRatio) / 1e18 : 0.15, 0), })} diff --git a/playwright.config.ts b/playwright.config.ts index 937b5f7f7..7b74189c0 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -6,7 +6,7 @@ dotenv.config(); const config: PlaywrightTestConfig = { testDir: './e2e/specs', testMatch: [/.*spec\.ts/], - timeout: 180_000, + timeout: process.env.CI ? 180_000 : 600_000, expect: { timeout: 30_000, },