From 4c9bc09ab08fffce04f752d929b8ee21c28b96b1 Mon Sep 17 00:00:00 2001 From: Usame Algan Date: Fri, 25 Oct 2024 16:10:39 +0200 Subject: [PATCH 01/33] feat: Add view for SAP redeem --- pages/claim-sap.tsx | 9 ++ src/components/Claim/index.tsx | 5 +- src/components/ClaimSAP/index.tsx | 108 ++++++++++++++++++++++ src/components/ClaimSAP/styles.module.css | 14 +++ src/components/PageLayout/index.tsx | 8 +- src/config/constants.ts | 2 + src/config/routes.ts | 1 + src/hooks/useTaggedAllocations.ts | 35 ++++++- src/utils/claim.ts | 34 +++++++ src/utils/vesting.ts | 4 + 10 files changed, 216 insertions(+), 4 deletions(-) create mode 100644 pages/claim-sap.tsx create mode 100644 src/components/ClaimSAP/index.tsx create mode 100644 src/components/ClaimSAP/styles.module.css diff --git a/pages/claim-sap.tsx b/pages/claim-sap.tsx new file mode 100644 index 00000000..a3bb0740 --- /dev/null +++ b/pages/claim-sap.tsx @@ -0,0 +1,9 @@ +import type { NextPage } from 'next' + +import ClaimSAPOverview from '@/components/ClaimSAP' + +const ClaimSAPPage: NextPage = () => { + return +} + +export default ClaimSAPPage diff --git a/src/components/Claim/index.tsx b/src/components/Claim/index.tsx index e7b3d65d..1a141a99 100644 --- a/src/components/Claim/index.tsx +++ b/src/components/Claim/index.tsx @@ -10,6 +10,7 @@ import { TextField, CircularProgress, InputAdornment, + Link, } from '@mui/material' import { useState, type ReactElement, ChangeEvent } from 'react' @@ -191,9 +192,9 @@ const ClaimOverview = (): ReactElement => { - Claim your tokens as rewards! + Token claiming for the Safe Activity Rewards Program live! - You get more tokens if you are active in activity program. + Claim now diff --git a/src/components/ClaimSAP/index.tsx b/src/components/ClaimSAP/index.tsx new file mode 100644 index 00000000..4dd0527f --- /dev/null +++ b/src/components/ClaimSAP/index.tsx @@ -0,0 +1,108 @@ +import { Grid, Typography, Button, Paper, Box, Stack } from '@mui/material' +import { type ReactElement } from 'react' + +import PaperContainer from '../PaperContainer' + +import { useSafeTokenAllocation } from '@/hooks/useSafeTokenAllocation' +import { useTaggedAllocations } from '@/hooks/useTaggedAllocations' +import { getVestingTypes } from '@/utils/vesting' +import { formatEther } from 'ethers/lib/utils' +import { createSAPClaimTxs } from '@/utils/claim' +import { useSafeAppsSDK } from '@safe-global/safe-apps-react-sdk' + +import css from './styles.module.css' +import { Odometer } from '@/components/Odometer' +import { formatDate } from '@/utils/date' + +const getDecimalLength = (amount: string) => { + const length = Number(amount).toFixed(2).length + + if (length > 10) { + return 0 + } + + if (length > 9) { + return 1 + } + + return 2 +} + +const ClaimSAPOverview = (): ReactElement => { + const { sdk } = useSafeAppsSDK() + + // Allocation, vesting and voting power + const { data: allocation } = useSafeTokenAllocation() + const { sapBoosted, sapUnboosted, totalSAP } = useTaggedAllocations() + const { sapBoostedVesting } = getVestingTypes(allocation?.vestingData ?? []) + + const startDate = sapBoostedVesting && new Date(sapBoostedVesting.startDate * 1000) + const startDateFormatted = startDate && formatDate(startDate, true) + + const decimals = getDecimalLength(totalSAP.inVesting) + + const onRedeem = async () => { + const txs = createSAPClaimTxs({ + vestingData: allocation?.vestingData ?? [], + sapBoostedClaimable: sapBoosted.inVesting, + sapUnboostedClaimable: sapUnboosted.inVesting, + }) + + try { + await sdk.txs.send({ txs }) + } catch (error) { + console.error(error) + } + } + + return ( + + + Claim SAFE tokens from the Safe Activity Rewards program + + + + + + + + Your total allocation + + + SAFE + + + + + + + + + Here are the next steps: + + + Your allocation will be available to claim at {startDateFormatted} + + + You will need to redeem the airdrop before then. + + + + + + + + + + ) +} + +export default ClaimSAPOverview diff --git a/src/components/ClaimSAP/styles.module.css b/src/components/ClaimSAP/styles.module.css new file mode 100644 index 00000000..667f6998 --- /dev/null +++ b/src/components/ClaimSAP/styles.module.css @@ -0,0 +1,14 @@ +.maxButton { + border: 0; + background: none; + color: var(--mui-palette-primary-main); + padding: 0; + font-size: 16px; + font-weight: bold; + cursor: pointer; +} + +.input :global .MuiInputBase-input { + padding: 12px 14px 12px 0; + font-weight: 700; +} diff --git a/src/components/PageLayout/index.tsx b/src/components/PageLayout/index.tsx index c9875c40..29cde64a 100644 --- a/src/components/PageLayout/index.tsx +++ b/src/components/PageLayout/index.tsx @@ -12,7 +12,13 @@ import { AppRoutes } from '@/config/routes' import { useRouter } from 'next/router' import { NAVIGATION_EVENTS } from '@/analytics/navigation' -const RoutesWithNavigation = [AppRoutes.index, AppRoutes.points, AppRoutes.activity, AppRoutes.governance] +const RoutesWithNavigation = [ + AppRoutes.index, + AppRoutes.points, + AppRoutes.activity, + AppRoutes.governance, + AppRoutes.claimSap, +] export const PageLayout = ({ children, diff --git a/src/config/constants.ts b/src/config/constants.ts index 519eaefb..a049b955 100644 --- a/src/config/constants.ts +++ b/src/config/constants.ts @@ -78,6 +78,8 @@ export const AIRDROP_TAGS = { SEP5: 'user_v2', ECOSYSTEM: 'ecosystem', INVESTOR: 'investor', + SAP_BOOSTED: 'sap_boosted', + SAP_UNBOOSTED: 'sap_unboosted', } as const // Delegation diff --git a/src/config/routes.ts b/src/config/routes.ts index b536663b..aad51dd9 100644 --- a/src/config/routes.ts +++ b/src/config/routes.ts @@ -7,6 +7,7 @@ export const AppRoutes = { governance: '/governance', delegate: '/delegate', claim: '/claim', + claimSap: '/claim-sap', activity: '/activity', activityProgram: '/activity-program', } diff --git a/src/hooks/useTaggedAllocations.ts b/src/hooks/useTaggedAllocations.ts index 7821baa8..916bb410 100644 --- a/src/hooks/useTaggedAllocations.ts +++ b/src/hooks/useTaggedAllocations.ts @@ -30,22 +30,38 @@ export const useTaggedAllocations = (): { claimable: string inVesting: string } + sapBoosted: { + claimable: string + inVesting: string + } + sapUnboosted: { + claimable: string + inVesting: string + } total: { claimable: string inVesting: string allocation: string } + totalSAP: { + claimable: string + inVesting: string + allocation: string + } } => { const { data: allocation } = useSafeTokenAllocation() // Get vesting types - const { userVesting, sep5Vesting, ecosystemVesting, investorVesting } = getVestingTypes(allocation?.vestingData || []) + const { userVesting, sep5Vesting, ecosystemVesting, investorVesting, sapBoostedVesting, sapUnboostedVesting } = + getVestingTypes(allocation?.vestingData || []) // Calculate claimable vs. vested amounts for each vesting type const [sep5Claimable, sep5InVesting] = useAmounts(sep5Vesting) const [userClaimable, userInVesting] = useAmounts(userVesting) const [ecosystemClaimable, ecosystemInVesting] = useAmounts(ecosystemVesting) const [investorClaimable, investorInVesting] = useAmounts(investorVesting) + const [sapBoostedClaimable, sapBoostedInVesting] = useAmounts(sapBoostedVesting) + const [sapUnboostedClaimable, sapUnboostedInVesting] = useAmounts(sapUnboostedVesting) // Calculate total of claimable vs. vested amounts const totalAmountClaimable = getTotal(sep5Claimable, userClaimable, ecosystemClaimable, investorClaimable) @@ -59,6 +75,10 @@ export const useTaggedAllocations = (): { investorVesting?.amount || '0', ) + const totalSAPClaimable = getTotal(sapBoostedClaimable, sapUnboostedClaimable) + const totalSAPInVesting = getTotal(sapBoostedInVesting, sapUnboostedInVesting) + const totalSAP = getTotal(sapBoostedVesting?.amount || '0', sapUnboostedVesting?.amount || '0') + return { sep5: { claimable: sep5Claimable, @@ -76,10 +96,23 @@ export const useTaggedAllocations = (): { claimable: investorClaimable, inVesting: investorInVesting, }, + sapBoosted: { + claimable: sapBoostedClaimable, + inVesting: sapBoostedInVesting, + }, + sapUnboosted: { + claimable: sapUnboostedClaimable, + inVesting: sapUnboostedInVesting, + }, total: { claimable: totalAmountClaimable, inVesting: totalAmountInVesting, allocation: totalAllocation, }, + totalSAP: { + claimable: totalSAPClaimable, + inVesting: totalSAPInVesting, + allocation: totalSAP, + }, } } diff --git a/src/utils/claim.ts b/src/utils/claim.ts index 591189e8..8613e6d3 100644 --- a/src/utils/claim.ts +++ b/src/utils/claim.ts @@ -187,3 +187,37 @@ export const createClaimTxs = ({ return txs } + +export const createSAPClaimTxs = ({ + vestingData, + sapBoostedClaimable, + sapUnboostedClaimable, +}: { + vestingData: Vesting[] + sapBoostedClaimable: string + sapUnboostedClaimable: string +}): BaseTransaction[] => { + const txs: BaseTransaction[] = [] + + const { sapBoostedVesting, sapUnboostedVesting } = getVestingTypes(vestingData) + + if (sapBoostedVesting && BigNumber.from(sapBoostedClaimable).gt(0)) { + const redeemTx = createRedeemTx({ + vestingClaim: sapBoostedVesting, + airdropAddress: sapBoostedVesting.contract, + }) + + txs.push(redeemTx) + } + + if (sapUnboostedVesting && BigNumber.from(sapUnboostedClaimable).gt(0)) { + const redeemTx = createRedeemTx({ + vestingClaim: sapUnboostedVesting, + airdropAddress: sapUnboostedVesting.contract, + }) + + txs.push(redeemTx) + } + + return txs +} diff --git a/src/utils/vesting.ts b/src/utils/vesting.ts index 49a1d272..21fbd4b7 100644 --- a/src/utils/vesting.ts +++ b/src/utils/vesting.ts @@ -50,11 +50,15 @@ export const getVestingTypes = (vestingData: Vesting[]) => { const sep5Vesting = vestingData?.find((vesting) => vesting.tag === AIRDROP_TAGS.SEP5) ?? null const ecosystemVesting = vestingData?.find((vesting) => vesting.tag === AIRDROP_TAGS.ECOSYSTEM) ?? null const investorVesting = vestingData?.find((vesting) => vesting.tag === AIRDROP_TAGS.INVESTOR) ?? null + const sapBoostedVesting = vestingData?.find((vesting) => vesting.tag === AIRDROP_TAGS.SAP_BOOSTED) ?? null + const sapUnboostedVesting = vestingData?.find((vesting) => vesting.tag === AIRDROP_TAGS.SAP_UNBOOSTED) ?? null return { userVesting, sep5Vesting, ecosystemVesting, investorVesting, + sapBoostedVesting, + sapUnboostedVesting, } } From 06345080cf3303fa93e0a1b95accaa5e8499d9b6 Mon Sep 17 00:00:00 2001 From: Usame Algan Date: Thu, 31 Oct 2024 12:56:32 +0100 Subject: [PATCH 02/33] feat: Update activity points page design --- package.json | 1 + src/components/Points/index.tsx | 207 +++++++++++++++----------- src/components/TokenLocking/index.tsx | 2 +- yarn.lock | 30 +++- 4 files changed, 151 insertions(+), 89 deletions(-) diff --git a/package.json b/package.json index aaa710df..f63719e9 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "@emotion/react": "^11.10.5", "@emotion/server": "^11.10.0", "@emotion/styled": "^11.10.5", + "@fingerprintjs/fingerprintjs-pro-react": "^2.6.3", "@mui/icons-material": "^5.15.15", "@mui/material": "^5.11.5", "@mui/x-date-pickers": "^7.1.1", diff --git a/src/components/Points/index.tsx b/src/components/Points/index.tsx index 31f63ea6..c0c2809c 100644 --- a/src/components/Points/index.tsx +++ b/src/components/Points/index.tsx @@ -1,114 +1,147 @@ -import { useCampaignInfo } from '@/hooks/useCampaigns' -import { Grid, Typography, Stack, Box, SvgIcon, Divider, useMediaQuery } from '@mui/material' -import { useMemo, useState } from 'react' +import { Grid, Typography, Stack, Box, Divider, Button } from '@mui/material' import { ExternalLink } from '../ExternalLink' import PaperContainer from '../PaperContainer' import SafePassDisclaimer from '../SafePassDisclaimer' -import { ActivityPointsFeed } from './ActivityPointsFeed' -import { CampaignInfo } from './CampaignInfo' -import { CampaignLeaderboard } from './CampaignLeaderboard' -import CampaignTabs from './CampaignTabs' -import StarIcon from '@/public/images/leaderboard-title-star.svg' import css from './styles.module.css' -import { TotalPoints } from './TotalPoints' -import { useTheme } from '@mui/material/styles' import { useGlobalCampaignId } from '@/hooks/useGlobalCampaignId' -import { CampaignTitle } from './CampaignTitle' -import { CampaignPromo } from './CampaignPromo' +import { useOwnCampaignRank } from '@/hooks/useLeaderboard' +import PointsCounter from '@/components/PointsCounter' +import SafeToken from '@/public/images/token.svg' const Points = () => { const globalCampaignId = useGlobalCampaignId() - - const [selectedCampaignId, setSelectedCampaignId] = useState(globalCampaignId) - const campaign = useCampaignInfo(selectedCampaignId) - - const isGlobalCampaign = useMemo( - () => globalCampaignId === selectedCampaignId, - [globalCampaignId, selectedCampaignId], - ) - const theme = useTheme() - const isSmallScreen = useMediaQuery(theme.breakpoints.down('lg')) + const { data: globalRank } = useOwnCampaignRank(globalCampaignId) return ( - - - {'Get rewards with Safe{Pass}'} - - {'What is Safe{Pass}'} - + <> + + + {'Get your Safe{Pass} rewards'} + + {'What is Safe{Pass}'} + + - - - {isSmallScreen && } + + - - {!isSmallScreen && } - - - Points activity feed - + + + Rewards Await! πŸ† + - {!isSmallScreen && ( - - Earn points by engaging with Safe and our campaign partners. Depending on the campaign, you'll - be rewarded for various activities suggested by our partners. You can also earn points for regular - Safe activities. + + You've made it to the finish line! The rewards program has officially ended, but the points + you've earned are just the beginning. It’s time to get SAFE tokens your way. + + + + + + + + + + + + Reward tokens + + + + + SAFE - )} + + Available from 15.01.2026 + + + + + + + + + + + Your score calculation + + Your total rewarded tokens were calculated based on your total points. - - - - - - - {campaign && ( - - )} - {isGlobalCampaign && ( - - In this view you can see the points total from all currently active campaigns and regular Safe - activities. - - )} - - - - + + + + + + {globalRank && ( + <> + + + + Leaderboard score + + + + + + + + + Total points + + + + )} + + + + + + + + + + + How to claim? + + Description text. + + - + + + When can I get tokens? + + Description text. + - - - - - {!isSmallScreen && } - - - + - + - + ) } diff --git a/src/components/TokenLocking/index.tsx b/src/components/TokenLocking/index.tsx index b8984fa1..42148fcf 100644 --- a/src/components/TokenLocking/index.tsx +++ b/src/components/TokenLocking/index.tsx @@ -22,7 +22,7 @@ const TokenLocking = () => { return ( - {'Get rewards with Safe{Pass}'} + {'Get your Safe{Pass} rewards'} {'What is Safe{Pass}'} diff --git a/yarn.lock b/yarn.lock index e8ab3091..3a30b463 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1895,6 +1895,29 @@ "@ethersproject/properties" "^5.7.0" "@ethersproject/strings" "^5.7.0" +"@fingerprintjs/fingerprintjs-pro-react@^2.6.3": + version "2.6.3" + resolved "https://registry.yarnpkg.com/@fingerprintjs/fingerprintjs-pro-react/-/fingerprintjs-pro-react-2.6.3.tgz#f04e043514d141e41b9397648e7e567f84395415" + integrity sha512-/axCq/cfjZkIM+WFZM/05FQvqtNfdKbIFKU6b2yrwPKlgT8BqWkAq8XvFX6JCPlq8/udVLJjFEDCK+1JQh1L6g== + dependencies: + "@fingerprintjs/fingerprintjs-pro-spa" "^1.3.1" + fast-deep-equal "3.1.3" + +"@fingerprintjs/fingerprintjs-pro-spa@^1.3.1": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@fingerprintjs/fingerprintjs-pro-spa/-/fingerprintjs-pro-spa-1.3.2.tgz#80654b554ec5e260a0627162f88918a1fd5bab21" + integrity sha512-s1YGsx1XQLmjU+av4UrUHNxyzwPHyZRB0GXJQFOJK8ZHCYc2SNukxnJmZA++bNBa8twU3wW+QgSJhA4Prjnd0g== + dependencies: + "@fingerprintjs/fingerprintjs-pro" "^3.11.0" + tslib "^2.7.0" + +"@fingerprintjs/fingerprintjs-pro@^3.11.0": + version "3.11.2" + resolved "https://registry.yarnpkg.com/@fingerprintjs/fingerprintjs-pro/-/fingerprintjs-pro-3.11.2.tgz#22e0df80904390c1f66dc9d2f12bd190ba02c458" + integrity sha512-KRRCDAwSEbSUgbZYD0k70uWdo/nv62YaQEz012/Uaa1XzgvHbrcvIaDVSVZr2Wmv34d1jg+szpp4m3Mcstmq3g== + dependencies: + tslib "^2.4.1" + "@floating-ui/core@^1.0.0": version "1.6.0" resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.6.0.tgz#fa41b87812a16bf123122bf945946bae3fdf7fc1" @@ -6277,7 +6300,7 @@ ext@^1.1.2: dependencies: type "^2.7.2" -fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: +fast-deep-equal@3.1.3, fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== @@ -10022,6 +10045,11 @@ tslib@^2.3.1: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.3.tgz#24944ba2d990940e6e982c4bea147aba80209913" integrity sha512-mSxlJJwl3BMEQCUNnxXBU9jP4JBktcEGhURcPR6VQVlnP0FdDEsIaz0C35dXNGLyRfrATNofF0F5p2KPxQgB+w== +tslib@^2.4.1, tslib@^2.7.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.0.tgz#d124c86c3c05a40a91e6fdea4021bd31d377971b" + integrity sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA== + tsort@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/tsort/-/tsort-0.0.1.tgz#e2280f5e817f8bf4275657fd0f9aebd44f5a2786" From e3ab335f5653ecf182355f06d1981e4039f2fc87 Mon Sep 17 00:00:00 2001 From: Usame Algan Date: Thu, 31 Oct 2024 16:49:37 +0100 Subject: [PATCH 03/33] fix: Update design for activities page --- public/images/spotlight.svg | 20 +--- public/images/star.svg | 2 +- public/images/star1.svg | 5 + src/components/Points/index.tsx | 192 +++++++++++++++++++------------- 4 files changed, 124 insertions(+), 95 deletions(-) create mode 100644 public/images/star1.svg diff --git a/public/images/spotlight.svg b/public/images/spotlight.svg index 8e08f700..b77ebfe2 100644 --- a/public/images/spotlight.svg +++ b/public/images/spotlight.svg @@ -1,18 +1,4 @@ - - - - - - - - - - - - - - - - - + + + diff --git a/public/images/star.svg b/public/images/star.svg index f5ce86af..11dcb5f2 100644 --- a/public/images/star.svg +++ b/public/images/star.svg @@ -1,3 +1,3 @@ - + diff --git a/public/images/star1.svg b/public/images/star1.svg new file mode 100644 index 00000000..687247d4 --- /dev/null +++ b/public/images/star1.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/components/Points/index.tsx b/src/components/Points/index.tsx index c0c2809c..3e8a42b3 100644 --- a/src/components/Points/index.tsx +++ b/src/components/Points/index.tsx @@ -1,4 +1,4 @@ -import { Grid, Typography, Stack, Box, Divider, Button } from '@mui/material' +import { Grid, Typography, Stack, Box, Button, SvgIcon } from '@mui/material' import { ExternalLink } from '../ExternalLink' import PaperContainer from '../PaperContainer' import SafePassDisclaimer from '../SafePassDisclaimer' @@ -7,10 +7,18 @@ import { useGlobalCampaignId } from '@/hooks/useGlobalCampaignId' import { useOwnCampaignRank } from '@/hooks/useLeaderboard' import PointsCounter from '@/components/PointsCounter' import SafeToken from '@/public/images/token.svg' +import { useTaggedAllocations } from '@/hooks/useTaggedAllocations' +import { formatEther } from 'ethers/lib/utils' +import Spotlight from '@/public/images/spotlight.svg' +import Star from '@/public/images/star.svg' +import Star1 from '@/public/images/star1.svg' +import { useCampaignsPaginated } from '@/hooks/useCampaigns' const Points = () => { + const { data: campaigns = [] } = useCampaignsPaginated() const globalCampaignId = useGlobalCampaignId() const { data: globalRank } = useOwnCampaignRank(globalCampaignId) + const { sapBoosted, sapUnboosted, totalSAP } = useTaggedAllocations() return ( <> @@ -23,7 +31,7 @@ const Points = () => { className={css.pageTitle} display="flex" flexDirection="row" - alignItems="center" + alignItems="flex-end" > {'Get your Safe{Pass} rewards'} @@ -32,89 +40,119 @@ const Points = () => { - - - - - - Rewards Await! πŸ† - + + + + + + + + Rewards Await! πŸ† + - - You've made it to the finish line! The rewards program has officially ended, but the points - you've earned are just the beginning. It’s time to get SAFE tokens your way. - + + You've made it to the finish line! The rewards program has officially ended, but the points + you've earned are just the beginning. It’s time to get SAFE tokens your way. + - - - - + + + - - - - - Reward tokens - - - - - SAFE - + + + + + Reward tokens + + + + + + + + + + SAFE + + + Available from 15.01.2026 + - Available from 15.01.2026 - - - + + + - - - - - - Your score calculation - - Your total rewarded tokens were calculated based on your total points. - - - - - - - {globalRank && ( - <> - - - - Leaderboard score - - + {globalRank && ( + + + + + + + Campaigns completed + + + + - + + + + + + Leaderboard score + + + + - - - - Total points - - - - )} - - + + + + + + Total points + + + + - + )} @@ -123,7 +161,7 @@ const Points = () => { How to claim? - Description text. + Description text. @@ -133,7 +171,7 @@ const Points = () => { When can I get tokens? - Description text. + Description text. From 47709cbb7e54faf068abc3416a334627b2b28ebc Mon Sep 17 00:00:00 2001 From: Usame Algan Date: Thu, 31 Oct 2024 17:29:33 +0100 Subject: [PATCH 04/33] Add cool mode --- src/components/Points/index.tsx | 26 ++-- src/hooks/useCoolMode.ts | 220 ++++++++++++++++++++++++++++++++ 2 files changed, 235 insertions(+), 11 deletions(-) create mode 100644 src/hooks/useCoolMode.ts diff --git a/src/components/Points/index.tsx b/src/components/Points/index.tsx index 3e8a42b3..497515f7 100644 --- a/src/components/Points/index.tsx +++ b/src/components/Points/index.tsx @@ -13,12 +13,14 @@ import Spotlight from '@/public/images/spotlight.svg' import Star from '@/public/images/star.svg' import Star1 from '@/public/images/star1.svg' import { useCampaignsPaginated } from '@/hooks/useCampaigns' +import { useCoolMode } from '@/hooks/useCoolMode' const Points = () => { const { data: campaigns = [] } = useCampaignsPaginated() const globalCampaignId = useGlobalCampaignId() const { data: globalRank } = useOwnCampaignRank(globalCampaignId) const { sapBoosted, sapUnboosted, totalSAP } = useTaggedAllocations() + const particlesRef = useCoolMode('./images/token.svg') return ( <> @@ -62,17 +64,19 @@ const Points = () => { you've earned are just the beginning. It’s time to get SAFE tokens your way. - + + + diff --git a/src/hooks/useCoolMode.ts b/src/hooks/useCoolMode.ts new file mode 100644 index 00000000..8e01cd19 --- /dev/null +++ b/src/hooks/useCoolMode.ts @@ -0,0 +1,220 @@ +import { useEffect, useRef } from 'react' + +interface Particle { + direction: number + element: HTMLElement + left: number + size: number + speedHorz: number + speedUp: number + spinSpeed: number + spinVal: number + top: number +} + +// Taken from https://github.com/rainbow-me/rainbowkit/blob/main/site/lib/useCoolMode.ts +export const useCoolMode = (imageUrl: string, disabled?: boolean, ignoreCoolModeDocsDemo?: boolean) => { + const ref = useRef(null) + const resolvedImageUrl = imageUrl + + useEffect(() => { + if (ref.current && resolvedImageUrl) { + return makeElementCool(ref.current, resolvedImageUrl, disabled || false, ignoreCoolModeDocsDemo || false) + } + }, [resolvedImageUrl, disabled, ignoreCoolModeDocsDemo]) + + return ref +} + +const getContainer = () => { + const id = '_rk_site_coolMode' + const existingContainer = document.getElementById(id) + + if (existingContainer) { + return existingContainer + } + + const container = document.createElement('div') + container.setAttribute('id', id) + container.setAttribute( + 'style', + [ + 'overflow:hidden', + 'position:fixed', + 'height:100%', + 'top:0', + 'left:0', + 'right:0', + 'bottom:0', + 'pointer-events:none', + 'z-index:2147483647', + ].join(';'), + ) + + document.body.appendChild(container) + + return container +} + +let instanceCounter = 0 + +function makeElementCool( + element: HTMLElement, + imageUrl: string, + disabled: boolean, + ignoreCoolModeDocsDemo: boolean, +): () => void { + instanceCounter++ + + const sizes = [15, 20, 25, 35, 45] + const limit = 35 + + let particles: Particle[] = [] + let autoAddParticle = false + let mouseX = 0 + let mouseY = 0 + + const container = getContainer() + + function createParticle() { + const size = sizes[Math.floor(Math.random() * sizes.length)] + const speedHorz = Math.random() * 10 + const speedUp = Math.random() * 25 + const spinVal = Math.random() * 360 + const spinSpeed = Math.random() * 35 * (Math.random() <= 0.5 ? -1 : 1) + const top = mouseY - size / 2 + const left = mouseX - size / 2 + const direction = Math.random() <= 0.5 ? -1 : 1 + + const particle = document.createElement('div') + particle.innerHTML = `` + particle.setAttribute( + 'style', + [ + 'position:absolute', + 'will-change:transform', + `top:${top}px`, + `left:${left}px`, + `transform:rotate(${spinVal}deg)`, + ].join(';'), + ) + + container.appendChild(particle) + + particles.push({ + direction, + element: particle, + left, + size, + speedHorz, + speedUp, + spinSpeed, + spinVal, + top, + }) + } + + function updateParticles() { + for (const p of particles) { + p.left = p.left - p.speedHorz * p.direction + p.top = p.top - p.speedUp + p.speedUp = Math.min(p.size, p.speedUp - 1) + p.spinVal = p.spinVal + p.spinSpeed + + if (p.top >= Math.max(window.innerHeight, document.body.clientHeight) + p.size) { + particles = particles.filter((o) => o !== p) + p.element.remove() + } + + p.element.setAttribute( + 'style', + [ + 'position:absolute', + 'will-change:transform', + `top:${p.top}px`, + `left:${p.left}px`, + `transform:rotate(${p.spinVal}deg)`, + ].join(';'), + ) + } + } + + let animationFrame: number | undefined + + function loop() { + if (autoAddParticle && particles.length < limit) { + createParticle() + } + + updateParticles() + animationFrame = requestAnimationFrame(loop) + } + + loop() + + const isTouchInteraction = + 'ontouchstart' in window || + // @ts-expect-error + navigator.msMaxTouchPoints + + const tap = isTouchInteraction ? 'touchstart' : 'mousedown' + const tapEnd = isTouchInteraction ? 'touchend' : 'mouseup' + const move = isTouchInteraction ? 'touchmove' : 'mousemove' + + const updateMousePosition = (e: MouseEvent | TouchEvent) => { + if ('touches' in e) { + mouseX = e.touches?.[0].clientX + mouseY = e.touches?.[0].clientY + } else { + mouseX = e.clientX + mouseY = e.clientY + } + } + + const tapHandler = (e: MouseEvent | TouchEvent) => { + if (disabled) return + + // HACK: sorry, bit short on time here to find a more elegant solution + // but if this option is true, we don't wanna show particles if users + // are tapping on the buttons in #cool-mode-demo which is an `id` + // added to the CoolMode.tsx Component - otherwise we get double emoji showers! + if (ignoreCoolModeDocsDemo) { + const element = document.getElementById('cool-mode-demo') + if (element?.contains(e.target as any)) return + } + + updateMousePosition(e) + autoAddParticle = true + } + + const disableAutoAddParticle = () => { + autoAddParticle = false + } + + element.addEventListener(move, updateMousePosition, { passive: true }) + element.addEventListener(tap, tapHandler, { passive: true }) + element.addEventListener(tapEnd, disableAutoAddParticle, { passive: true }) + element.addEventListener('mouseleave', disableAutoAddParticle, { + passive: true, + }) + + return () => { + element.removeEventListener(move, updateMousePosition) + element.removeEventListener(tap, tapHandler) + element.removeEventListener(tapEnd, disableAutoAddParticle) + element.removeEventListener('mouseleave', disableAutoAddParticle) + + // Cancel animation loop once animations are done + const interval = setInterval(() => { + if (animationFrame && particles.length === 0) { + cancelAnimationFrame(animationFrame) + clearInterval(interval) + + // Clean up container if this is the last instance + if (--instanceCounter === 0) { + container.remove() + } + } + }, 500) + } +} From e652a5fede274b14dda60d0793a4552bd9cc7ce5 Mon Sep 17 00:00:00 2001 From: Usame Algan Date: Fri, 8 Nov 2024 14:14:49 +0100 Subject: [PATCH 05/33] feat: Add Fingerprint logic and TODOS --- src/components/Points/index.tsx | 77 ++++++++++++++++++++++++++++++--- src/config/constants.ts | 3 ++ src/hooks/useUnsealedResult.ts | 40 +++++++++++++++++ 3 files changed, 115 insertions(+), 5 deletions(-) create mode 100644 src/hooks/useUnsealedResult.ts diff --git a/src/components/Points/index.tsx b/src/components/Points/index.tsx index 497515f7..7993c6b9 100644 --- a/src/components/Points/index.tsx +++ b/src/components/Points/index.tsx @@ -14,14 +14,64 @@ import Star from '@/public/images/star.svg' import Star1 from '@/public/images/star1.svg' import { useCampaignsPaginated } from '@/hooks/useCampaigns' import { useCoolMode } from '@/hooks/useCoolMode' +import { createSAPClaimTxs } from '@/utils/claim' +import { useSafeTokenAllocation } from '@/hooks/useSafeTokenAllocation' +import { useSafeAppsSDK } from '@safe-global/safe-apps-react-sdk' +import { FingerprintJSPro } from '@fingerprintjs/fingerprintjs-pro-react' +import { useEffect, useState } from 'react' +import { FINGERPRINT_KEY, SAP_LOCK_DATE } from '@/config/constants' +import useUnsealedResult, { SealedRequest } from '@/hooks/useUnsealedResult' const Points = () => { + const [sealedResult, setSealedResult] = useState() + const eligibility = useUnsealedResult(sealedResult) + const { sdk } = useSafeAppsSDK() const { data: campaigns = [] } = useCampaignsPaginated() const globalCampaignId = useGlobalCampaignId() const { data: globalRank } = useOwnCampaignRank(globalCampaignId) + const { data: allocation } = useSafeTokenAllocation() const { sapBoosted, sapUnboosted, totalSAP } = useTaggedAllocations() const particlesRef = useCoolMode('./images/token.svg') + useEffect(() => { + const fpPromise = FingerprintJSPro.load({ + apiKey: FINGERPRINT_KEY, + region: 'eu', + }) + + fpPromise + .then((fp) => fp.get({ extendedResult: true })) + .then((fingerprint) => { + setSealedResult({ requestId: fingerprint.requestId, sealedResult: fingerprint.sealedResult }) + }) + .catch((err) => console.error('Failed to fetch sealed client results: ', err)) + }, []) + + const startClaiming = async () => { + // Something went wrong with fetching the eligibility for this user so we don't let them redeem + if (!eligibility) return + + if (eligibility.isVpn) { + // TODO: User has a VPN, show them an error and ask to turn it off for the claim process + } + + if (!eligibility.isAllowed) { + // TODO: Only redeem from the unboosted contract + } + + const txs = createSAPClaimTxs({ + vestingData: allocation?.vestingData ?? [], + sapBoostedClaimable: sapBoosted.inVesting, + sapUnboostedClaimable: sapUnboosted.inVesting, + }) + + try { + await sdk.txs.send({ txs }) + } catch (error) { + console.error(error) + } + } + return ( <> @@ -73,6 +123,7 @@ const Points = () => { color: 'text.primary', '&:hover': { backgroundColor: 'static.main' }, }} + onClick={startClaiming} > Start claiming @@ -112,7 +163,7 @@ const Points = () => { SAFE - Available from 15.01.2026 + Available from 01.01.2025 @@ -161,21 +212,37 @@ const Points = () => { - + How to claim? - Description text. + + Press the Start claiming button on this page to redeem your tokens. Make sure you do this before{' '} + {SAP_LOCK_DATE} + + + + Learn more + + - + When can I get tokens? - Description text. + + Tokens are locked until {SAP_LOCK_DATE} and can be claimed after this date. Make sure to redeem the + tokens before this date. Otherwise they cannot be claimed anymore. + + + + Learn more + + diff --git a/src/config/constants.ts b/src/config/constants.ts index a049b955..62c86709 100644 --- a/src/config/constants.ts +++ b/src/config/constants.ts @@ -3,6 +3,7 @@ import { BigNumber } from 'ethers' // General export const IS_PRODUCTION = process.env.NEXT_PUBLIC_IS_PRODUCTION === 'true' export const INFURA_TOKEN = process.env.NEXT_PUBLIC_INFURA_TOKEN || '' +export const FINGERPRINT_KEY = process.env.NEXT_PUBLIC_FINGERPRINT_KEY || '' export const LS_NAMESPACE = 'SAFE__' export const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' @@ -73,6 +74,8 @@ export const VESTING_URL = `${CLAIMING_DATA_URL}/allocations` export const SEP5_EXPIRATION_DATE = '27.10.2023' export const SEP5_EXPIRATION = `${SEP5_EXPIRATION_DATE} 10:00 UTC` +export const SAP_LOCK_DATE = '01.01.2026' // TBD + export const AIRDROP_TAGS = { USER: 'user', SEP5: 'user_v2', diff --git a/src/hooks/useUnsealedResult.ts b/src/hooks/useUnsealedResult.ts new file mode 100644 index 00000000..24b30b24 --- /dev/null +++ b/src/hooks/useUnsealedResult.ts @@ -0,0 +1,40 @@ +import useSWR from 'swr' +import { GATEWAY_URL } from '@/config/constants' + +export type SealedRequest = { + requestId: string + sealedResult?: string +} + +type Eligibility = { + requestId: string + isAllowed: boolean + isVpn: boolean +} + +const fetchUnsealedResult = async (sealedRequest: SealedRequest) => { + return fetch(`${GATEWAY_URL}/v1/community/eligibility`, { + method: 'POST', + body: JSON.stringify({ requestId: sealedRequest.requestId, sealedResult: sealedRequest.sealedResult }), + }).then((resp) => { + if (resp.ok) { + return resp.json() as Promise + } else { + throw Error('Error fetching eligibility') + } + }) +} + +const useUnsealedResult = (sealedRequest?: SealedRequest) => { + const QUERY_KEY = 'unsealed-result' + + const { data } = useSWR(sealedRequest ? QUERY_KEY : null, async () => { + if (!sealedRequest) return + + return await fetchUnsealedResult(sealedRequest) + }) + + return data +} + +export default useUnsealedResult From 2028ecb580421f89d8e4f03ca866f239916d9bd0 Mon Sep 17 00:00:00 2001 From: Usame Algan Date: Fri, 8 Nov 2024 15:45:12 +0100 Subject: [PATCH 06/33] chore: Add console.log --- src/components/Points/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/Points/index.tsx b/src/components/Points/index.tsx index 7993c6b9..96ace6b5 100644 --- a/src/components/Points/index.tsx +++ b/src/components/Points/index.tsx @@ -48,6 +48,7 @@ const Points = () => { }, []) const startClaiming = async () => { + console.log({ eligibility }) // Something went wrong with fetching the eligibility for this user so we don't let them redeem if (!eligibility) return From 573665b8ee0991676be089faa4faa1e42d3f08a7 Mon Sep 17 00:00:00 2001 From: Usame Algan Date: Mon, 11 Nov 2024 21:09:58 +0700 Subject: [PATCH 07/33] fix: Update fetch call for elibility --- src/hooks/useUnsealedResult.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/hooks/useUnsealedResult.ts b/src/hooks/useUnsealedResult.ts index 24b30b24..2c64adfd 100644 --- a/src/hooks/useUnsealedResult.ts +++ b/src/hooks/useUnsealedResult.ts @@ -15,7 +15,13 @@ type Eligibility = { const fetchUnsealedResult = async (sealedRequest: SealedRequest) => { return fetch(`${GATEWAY_URL}/v1/community/eligibility`, { method: 'POST', - body: JSON.stringify({ requestId: sealedRequest.requestId, sealedResult: sealedRequest.sealedResult }), + body: JSON.stringify({ + requestId: sealedRequest.requestId, + sealedData: sealedRequest.sealedResult, + }), + headers: { + 'Content-Type': 'application/json', + }, }).then((resp) => { if (resp.ok) { return resp.json() as Promise From f991a0c98c6f2ddccc0c9f31bdab4080de7cc491 Mon Sep 17 00:00:00 2001 From: Usame Algan Date: Thu, 21 Nov 2024 14:34:46 +0100 Subject: [PATCH 08/33] feat: Implement eligibility checks --- src/components/Points/index.tsx | 134 +++++++++++++++++------------- src/hooks/useTaggedAllocations.ts | 13 ++- src/hooks/useUnsealedResult.ts | 4 +- 3 files changed, 87 insertions(+), 64 deletions(-) diff --git a/src/components/Points/index.tsx b/src/components/Points/index.tsx index 96ace6b5..ad6fdbef 100644 --- a/src/components/Points/index.tsx +++ b/src/components/Points/index.tsx @@ -1,4 +1,4 @@ -import { Grid, Typography, Stack, Box, Button, SvgIcon } from '@mui/material' +import { Grid, Typography, Stack, Box, Button, SvgIcon, Alert } from '@mui/material' import { ExternalLink } from '../ExternalLink' import PaperContainer from '../PaperContainer' import SafePassDisclaimer from '../SafePassDisclaimer' @@ -24,13 +24,13 @@ import useUnsealedResult, { SealedRequest } from '@/hooks/useUnsealedResult' const Points = () => { const [sealedResult, setSealedResult] = useState() - const eligibility = useUnsealedResult(sealedResult) + const { data: eligibility, isLoading } = useUnsealedResult(sealedResult) const { sdk } = useSafeAppsSDK() const { data: campaigns = [] } = useCampaignsPaginated() const globalCampaignId = useGlobalCampaignId() const { data: globalRank } = useOwnCampaignRank(globalCampaignId) const { data: allocation } = useSafeTokenAllocation() - const { sapBoosted, sapUnboosted, totalSAP } = useTaggedAllocations() + const { sapBoosted, sapUnboosted, totalSAP } = useTaggedAllocations(eligibility?.isAllowed) const particlesRef = useCoolMode('./images/token.svg') useEffect(() => { @@ -48,21 +48,12 @@ const Points = () => { }, []) const startClaiming = async () => { - console.log({ eligibility }) // Something went wrong with fetching the eligibility for this user so we don't let them redeem if (!eligibility) return - if (eligibility.isVpn) { - // TODO: User has a VPN, show them an error and ask to turn it off for the claim process - } - - if (!eligibility.isAllowed) { - // TODO: Only redeem from the unboosted contract - } - const txs = createSAPClaimTxs({ vestingData: allocation?.vestingData ?? [], - sapBoostedClaimable: sapBoosted.inVesting, + sapBoostedClaimable: eligibility.isAllowed ? sapBoosted.inVesting : '0', sapUnboostedClaimable: sapUnboosted.inVesting, }) @@ -73,6 +64,8 @@ const Points = () => { } } + const loading = !sealedResult || isLoading + return ( <> @@ -115,58 +108,83 @@ const Points = () => { you've earned are just the beginning. It’s time to get SAFE tokens your way. - - - + {eligibility?.isVpn ? ( + + We detected that you are using a VPN. Please turn it off in order to start the claiming process. + + + + + ) : !loading ? ( + + + + ) : null} - - - - Reward tokens - - - - - - - - - - SAFE + {!eligibility?.isVpn && !loading && ( + + + + Reward tokens + + + + + + + + + + SAFE + + + Available from {SAP_LOCK_DATE} - Available from 01.01.2025 - + )} diff --git a/src/hooks/useTaggedAllocations.ts b/src/hooks/useTaggedAllocations.ts index 916bb410..0c710812 100644 --- a/src/hooks/useTaggedAllocations.ts +++ b/src/hooks/useTaggedAllocations.ts @@ -13,7 +13,9 @@ const getTotal = (...amounts: string[]) => { return total.toString() } -export const useTaggedAllocations = (): { +export const useTaggedAllocations = ( + isEligibleForBoostedSAP?: boolean, +): { sep5: { claimable: string inVesting: string @@ -75,9 +77,12 @@ export const useTaggedAllocations = (): { investorVesting?.amount || '0', ) - const totalSAPClaimable = getTotal(sapBoostedClaimable, sapUnboostedClaimable) - const totalSAPInVesting = getTotal(sapBoostedInVesting, sapUnboostedInVesting) - const totalSAP = getTotal(sapBoostedVesting?.amount || '0', sapUnboostedVesting?.amount || '0') + const totalSAPClaimable = getTotal(isEligibleForBoostedSAP ? sapBoostedClaimable : '0', sapUnboostedClaimable) + const totalSAPInVesting = getTotal(isEligibleForBoostedSAP ? sapBoostedInVesting : '0', sapUnboostedInVesting) + const totalSAP = getTotal( + isEligibleForBoostedSAP ? sapBoostedVesting?.amount || '0' : '0', + sapUnboostedVesting?.amount || '0', + ) return { sep5: { diff --git a/src/hooks/useUnsealedResult.ts b/src/hooks/useUnsealedResult.ts index 2c64adfd..4711a8aa 100644 --- a/src/hooks/useUnsealedResult.ts +++ b/src/hooks/useUnsealedResult.ts @@ -34,13 +34,13 @@ const fetchUnsealedResult = async (sealedRequest: SealedRequest) => { const useUnsealedResult = (sealedRequest?: SealedRequest) => { const QUERY_KEY = 'unsealed-result' - const { data } = useSWR(sealedRequest ? QUERY_KEY : null, async () => { + const { data, isLoading } = useSWR(sealedRequest ? QUERY_KEY : null, async () => { if (!sealedRequest) return return await fetchUnsealedResult(sealedRequest) }) - return data + return { data, isLoading } } export default useUnsealedResult From c19fe0fd8d1e7982be7f4f12653f2765b3838ce3 Mon Sep 17 00:00:00 2001 From: Usame Algan Date: Thu, 21 Nov 2024 15:03:34 +0100 Subject: [PATCH 09/33] chore: Add FP Key to build step --- .github/workflows/build/action.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build/action.yml b/.github/workflows/build/action.yml index b8b1c23c..600b7931 100644 --- a/.github/workflows/build/action.yml +++ b/.github/workflows/build/action.yml @@ -21,3 +21,4 @@ runs: NEXT_PUBLIC_IS_PRODUCTION: ${{ inputs.is-production }} NEXT_PUBLIC_WC_BRIDGE: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_WC_BRIDGE }} NEXT_PUBLIC_WC_PROJECT_ID: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_WC_PROJECT_ID }} + NEXT_PUBLIC_FINGERPRINT_KEY: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_FINGERPRINT_KEY }} From 0904fc7b888b08e5d34c460178536bb84979a5c4 Mon Sep 17 00:00:00 2001 From: Usame Algan Date: Thu, 21 Nov 2024 15:26:52 +0100 Subject: [PATCH 10/33] fix: Remove unused sap page --- pages/claim-sap.tsx | 9 -- src/components/Claim/index.tsx | 27 ------ src/components/ClaimSAP/index.tsx | 108 ---------------------- src/components/ClaimSAP/styles.module.css | 14 --- src/components/PageLayout/index.tsx | 8 +- src/config/routes.ts | 1 - 6 files changed, 1 insertion(+), 166 deletions(-) delete mode 100644 pages/claim-sap.tsx delete mode 100644 src/components/ClaimSAP/index.tsx delete mode 100644 src/components/ClaimSAP/styles.module.css diff --git a/pages/claim-sap.tsx b/pages/claim-sap.tsx deleted file mode 100644 index a3bb0740..00000000 --- a/pages/claim-sap.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import type { NextPage } from 'next' - -import ClaimSAPOverview from '@/components/ClaimSAP' - -const ClaimSAPPage: NextPage = () => { - return -} - -export default ClaimSAPPage diff --git a/src/components/Claim/index.tsx b/src/components/Claim/index.tsx index 1a141a99..173931be 100644 --- a/src/components/Claim/index.tsx +++ b/src/components/Claim/index.tsx @@ -2,21 +2,17 @@ import { Grid, Typography, Button, - Paper, Box, Stack, - SvgIcon, Divider, TextField, CircularProgress, InputAdornment, - Link, } from '@mui/material' import { useState, type ReactElement, ChangeEvent } from 'react' import PaperContainer from '../PaperContainer' -import StarIcon from '@/public/images/star.svg' import { maxDecimals, minMaxValue, mustBeFloat } from '@/utils/validation' import { useIsTokenPaused } from '@/hooks/useIsTokenPaused' import { useSafeTokenAllocation } from '@/hooks/useSafeTokenAllocation' @@ -176,29 +172,6 @@ const ClaimOverview = (): ReactElement => { - palette.background.default, - color: ({ palette }) => palette.text.primary, - position: 'relative', - height: '100%', - display: 'flex', - flexDirection: 'column', - gap: 1, - }} - > - - - - - Token claiming for the Safe Activity Rewards Program live! - - Claim now - - - - diff --git a/src/components/ClaimSAP/index.tsx b/src/components/ClaimSAP/index.tsx deleted file mode 100644 index 4dd0527f..00000000 --- a/src/components/ClaimSAP/index.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import { Grid, Typography, Button, Paper, Box, Stack } from '@mui/material' -import { type ReactElement } from 'react' - -import PaperContainer from '../PaperContainer' - -import { useSafeTokenAllocation } from '@/hooks/useSafeTokenAllocation' -import { useTaggedAllocations } from '@/hooks/useTaggedAllocations' -import { getVestingTypes } from '@/utils/vesting' -import { formatEther } from 'ethers/lib/utils' -import { createSAPClaimTxs } from '@/utils/claim' -import { useSafeAppsSDK } from '@safe-global/safe-apps-react-sdk' - -import css from './styles.module.css' -import { Odometer } from '@/components/Odometer' -import { formatDate } from '@/utils/date' - -const getDecimalLength = (amount: string) => { - const length = Number(amount).toFixed(2).length - - if (length > 10) { - return 0 - } - - if (length > 9) { - return 1 - } - - return 2 -} - -const ClaimSAPOverview = (): ReactElement => { - const { sdk } = useSafeAppsSDK() - - // Allocation, vesting and voting power - const { data: allocation } = useSafeTokenAllocation() - const { sapBoosted, sapUnboosted, totalSAP } = useTaggedAllocations() - const { sapBoostedVesting } = getVestingTypes(allocation?.vestingData ?? []) - - const startDate = sapBoostedVesting && new Date(sapBoostedVesting.startDate * 1000) - const startDateFormatted = startDate && formatDate(startDate, true) - - const decimals = getDecimalLength(totalSAP.inVesting) - - const onRedeem = async () => { - const txs = createSAPClaimTxs({ - vestingData: allocation?.vestingData ?? [], - sapBoostedClaimable: sapBoosted.inVesting, - sapUnboostedClaimable: sapUnboosted.inVesting, - }) - - try { - await sdk.txs.send({ txs }) - } catch (error) { - console.error(error) - } - } - - return ( - - - Claim SAFE tokens from the Safe Activity Rewards program - - - - - - - - Your total allocation - - - SAFE - - - - - - - - - Here are the next steps: - - - Your allocation will be available to claim at {startDateFormatted} - - - You will need to redeem the airdrop before then. - - - - - - - - - - ) -} - -export default ClaimSAPOverview diff --git a/src/components/ClaimSAP/styles.module.css b/src/components/ClaimSAP/styles.module.css deleted file mode 100644 index 667f6998..00000000 --- a/src/components/ClaimSAP/styles.module.css +++ /dev/null @@ -1,14 +0,0 @@ -.maxButton { - border: 0; - background: none; - color: var(--mui-palette-primary-main); - padding: 0; - font-size: 16px; - font-weight: bold; - cursor: pointer; -} - -.input :global .MuiInputBase-input { - padding: 12px 14px 12px 0; - font-weight: 700; -} diff --git a/src/components/PageLayout/index.tsx b/src/components/PageLayout/index.tsx index 29cde64a..c9875c40 100644 --- a/src/components/PageLayout/index.tsx +++ b/src/components/PageLayout/index.tsx @@ -12,13 +12,7 @@ import { AppRoutes } from '@/config/routes' import { useRouter } from 'next/router' import { NAVIGATION_EVENTS } from '@/analytics/navigation' -const RoutesWithNavigation = [ - AppRoutes.index, - AppRoutes.points, - AppRoutes.activity, - AppRoutes.governance, - AppRoutes.claimSap, -] +const RoutesWithNavigation = [AppRoutes.index, AppRoutes.points, AppRoutes.activity, AppRoutes.governance] export const PageLayout = ({ children, diff --git a/src/config/routes.ts b/src/config/routes.ts index aad51dd9..b536663b 100644 --- a/src/config/routes.ts +++ b/src/config/routes.ts @@ -7,7 +7,6 @@ export const AppRoutes = { governance: '/governance', delegate: '/delegate', claim: '/claim', - claimSap: '/claim-sap', activity: '/activity', activityProgram: '/activity-program', } From ffc34db0a3aed9315bb3554bb8512fd73f0fdcfd Mon Sep 17 00:00:00 2001 From: Usame Algan Date: Fri, 22 Nov 2024 14:34:18 +0100 Subject: [PATCH 11/33] fix: Extract claim button to make confetti work again --- src/components/Points/ClaimButton.tsx | 25 +++++++++++++++++++++++++ src/components/Points/index.tsx | 24 +++++------------------- 2 files changed, 30 insertions(+), 19 deletions(-) create mode 100644 src/components/Points/ClaimButton.tsx diff --git a/src/components/Points/ClaimButton.tsx b/src/components/Points/ClaimButton.tsx new file mode 100644 index 00000000..6075d1e4 --- /dev/null +++ b/src/components/Points/ClaimButton.tsx @@ -0,0 +1,25 @@ +import { Box, Button } from '@mui/material' +import { useCoolMode } from '@/hooks/useCoolMode' + +const ClaimButton = ({ startClaiming }: { startClaiming: () => void }) => { + const particlesRef = useCoolMode('./images/token.svg') + + return ( + + + + ) +} + +export default ClaimButton diff --git a/src/components/Points/index.tsx b/src/components/Points/index.tsx index ad6fdbef..d05da725 100644 --- a/src/components/Points/index.tsx +++ b/src/components/Points/index.tsx @@ -13,7 +13,6 @@ import Spotlight from '@/public/images/spotlight.svg' import Star from '@/public/images/star.svg' import Star1 from '@/public/images/star1.svg' import { useCampaignsPaginated } from '@/hooks/useCampaigns' -import { useCoolMode } from '@/hooks/useCoolMode' import { createSAPClaimTxs } from '@/utils/claim' import { useSafeTokenAllocation } from '@/hooks/useSafeTokenAllocation' import { useSafeAppsSDK } from '@safe-global/safe-apps-react-sdk' @@ -21,6 +20,7 @@ import { FingerprintJSPro } from '@fingerprintjs/fingerprintjs-pro-react' import { useEffect, useState } from 'react' import { FINGERPRINT_KEY, SAP_LOCK_DATE } from '@/config/constants' import useUnsealedResult, { SealedRequest } from '@/hooks/useUnsealedResult' +import ClaimButton from '@/components/Points/ClaimButton' const Points = () => { const [sealedResult, setSealedResult] = useState() @@ -31,7 +31,6 @@ const Points = () => { const { data: globalRank } = useOwnCampaignRank(globalCampaignId) const { data: allocation } = useSafeTokenAllocation() const { sapBoosted, sapUnboosted, totalSAP } = useTaggedAllocations(eligibility?.isAllowed) - const particlesRef = useCoolMode('./images/token.svg') useEffect(() => { const fpPromise = FingerprintJSPro.load({ @@ -129,20 +128,7 @@ const Points = () => { ) : !loading ? ( - - - + ) : null} @@ -233,11 +219,11 @@ const Points = () => { - How to claim? + How are the SAFE token rewards distributed? - Press the Start claiming button on this page to redeem your tokens. Make sure you do this before{' '} - {SAP_LOCK_DATE} + Rewards are distributed based on leaderboard standing, SAFE token locking, and participation in partner + campaigns. A tiered system determines the distribution amount. From c87263dc1f349b9af91c2d804336d408a4104549 Mon Sep 17 00:00:00 2001 From: Usame Algan Date: Tue, 26 Nov 2024 11:39:33 +0100 Subject: [PATCH 12/33] fix: Show aggregate campaign rank --- src/components/Points/index.tsx | 9 ++++++++- src/config/constants.ts | 5 +++++ src/hooks/useAggregateCampaignId.ts | 7 +++++++ 3 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 src/hooks/useAggregateCampaignId.ts diff --git a/src/components/Points/index.tsx b/src/components/Points/index.tsx index d05da725..d5cdc78a 100644 --- a/src/components/Points/index.tsx +++ b/src/components/Points/index.tsx @@ -21,6 +21,7 @@ import { useEffect, useState } from 'react' import { FINGERPRINT_KEY, SAP_LOCK_DATE } from '@/config/constants' import useUnsealedResult, { SealedRequest } from '@/hooks/useUnsealedResult' import ClaimButton from '@/components/Points/ClaimButton' +import { useAggregateCampaignId } from '@/hooks/useAggregateCampaignId' const Points = () => { const [sealedResult, setSealedResult] = useState() @@ -28,10 +29,14 @@ const Points = () => { const { sdk } = useSafeAppsSDK() const { data: campaigns = [] } = useCampaignsPaginated() const globalCampaignId = useGlobalCampaignId() + const aggregateCampaignId = useAggregateCampaignId() const { data: globalRank } = useOwnCampaignRank(globalCampaignId) + const { data: aggregateRank } = useOwnCampaignRank(aggregateCampaignId) const { data: allocation } = useSafeTokenAllocation() const { sapBoosted, sapUnboosted, totalSAP } = useTaggedAllocations(eligibility?.isAllowed) + console.log(aggregateRank) + useEffect(() => { const fpPromise = FingerprintJSPro.load({ apiKey: FINGERPRINT_KEY, @@ -182,7 +187,9 @@ const Points = () => { - + {aggregateRank && ( + + )} Campaigns completed diff --git a/src/config/constants.ts b/src/config/constants.ts index 62c86709..245c04bb 100644 --- a/src/config/constants.ts +++ b/src/config/constants.ts @@ -130,3 +130,8 @@ export const MORPHO_CAMPAIGN_IDS: ChainConfig = { [Chains.SEPOLIA]: '0317a716-2818-4cc2-8571-71208996650e', [Chains.MAINNET]: 'a19a5503-b65a-42a0-bc6a-8e68144d2afa', } + +export const AGGREGATE_CAMPAIGN_IDS: ChainConfig = { + [Chains.SEPOLIA]: '2a99f13c-0aa3-4484-9479-10962005d292', + [Chains.MAINNET]: '2a99f13c-0aa3-4484-9479-10962005d292', // TBD +} diff --git a/src/hooks/useAggregateCampaignId.ts b/src/hooks/useAggregateCampaignId.ts new file mode 100644 index 00000000..e55e2fcc --- /dev/null +++ b/src/hooks/useAggregateCampaignId.ts @@ -0,0 +1,7 @@ +import { AGGREGATE_CAMPAIGN_IDS } from '@/config/constants' +import { useChainId } from './useChainId' + +export const useAggregateCampaignId = () => { + const chainId = useChainId() + return AGGREGATE_CAMPAIGN_IDS[chainId] +} From 52ad74f5970b621110c2f4253f6907f0137a9230 Mon Sep 17 00:00:00 2001 From: Usame Algan Date: Tue, 26 Nov 2024 15:08:38 +0100 Subject: [PATCH 13/33] feat: Add post-redeem success box --- src/components/Points/index.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/components/Points/index.tsx b/src/components/Points/index.tsx index d5cdc78a..8c9a3844 100644 --- a/src/components/Points/index.tsx +++ b/src/components/Points/index.tsx @@ -12,7 +12,6 @@ import { formatEther } from 'ethers/lib/utils' import Spotlight from '@/public/images/spotlight.svg' import Star from '@/public/images/star.svg' import Star1 from '@/public/images/star1.svg' -import { useCampaignsPaginated } from '@/hooks/useCampaigns' import { createSAPClaimTxs } from '@/utils/claim' import { useSafeTokenAllocation } from '@/hooks/useSafeTokenAllocation' import { useSafeAppsSDK } from '@safe-global/safe-apps-react-sdk' @@ -22,20 +21,19 @@ import { FINGERPRINT_KEY, SAP_LOCK_DATE } from '@/config/constants' import useUnsealedResult, { SealedRequest } from '@/hooks/useUnsealedResult' import ClaimButton from '@/components/Points/ClaimButton' import { useAggregateCampaignId } from '@/hooks/useAggregateCampaignId' +import { getVestingTypes } from '@/utils/vesting' const Points = () => { const [sealedResult, setSealedResult] = useState() const { data: eligibility, isLoading } = useUnsealedResult(sealedResult) const { sdk } = useSafeAppsSDK() - const { data: campaigns = [] } = useCampaignsPaginated() const globalCampaignId = useGlobalCampaignId() const aggregateCampaignId = useAggregateCampaignId() const { data: globalRank } = useOwnCampaignRank(globalCampaignId) const { data: aggregateRank } = useOwnCampaignRank(aggregateCampaignId) const { data: allocation } = useSafeTokenAllocation() const { sapBoosted, sapUnboosted, totalSAP } = useTaggedAllocations(eligibility?.isAllowed) - - console.log(aggregateRank) + const { sapUnboostedVesting } = getVestingTypes(allocation?.vestingData || []) useEffect(() => { const fpPromise = FingerprintJSPro.load({ @@ -69,6 +67,7 @@ const Points = () => { } const loading = !sealedResult || isLoading + const isSAPRedeemed = sapUnboostedVesting?.isRedeemed return ( <> @@ -132,6 +131,10 @@ const Points = () => { + ) : isSAPRedeemed ? ( + + Redeem successful! You will be able to claim your tokens starting from {SAP_LOCK_DATE}. + ) : !loading ? ( ) : null} From 713247b65d003cf69dd3a98f3e0a4dc2677981ca Mon Sep 17 00:00:00 2001 From: Usame Algan Date: Tue, 26 Nov 2024 15:10:04 +0100 Subject: [PATCH 14/33] fix: Campaign(s) plural change --- src/components/Points/index.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/components/Points/index.tsx b/src/components/Points/index.tsx index 8c9a3844..eca86505 100644 --- a/src/components/Points/index.tsx +++ b/src/components/Points/index.tsx @@ -191,11 +191,13 @@ const Points = () => { {aggregateRank && ( - + <> + + + {`Campaign${aggregateRank.totalPoints > 1 ? 's' : ''} completed`} + + )} - - Campaigns completed - From 79b479bd2f977bcd592fc204c5c63ceb908c4e05 Mon Sep 17 00:00:00 2001 From: Usame Algan Date: Wed, 27 Nov 2024 15:07:58 +0100 Subject: [PATCH 15/33] fix: Update main copy --- src/components/Points/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Points/index.tsx b/src/components/Points/index.tsx index eca86505..a79e58f5 100644 --- a/src/components/Points/index.tsx +++ b/src/components/Points/index.tsx @@ -107,8 +107,8 @@ const Points = () => { - You've made it to the finish line! The rewards program has officially ended, but the points - you've earned are just the beginning. It’s time to get SAFE tokens your way. + You've made it to the finish line! The rewards program has officially ended, and the points + earned, will be converted into SAFE tokens. {eligibility?.isVpn ? ( From f1c7b36f8d303d87ab8953c4dab3f0efc7d1edf5 Mon Sep 17 00:00:00 2001 From: Usame Algan Date: Wed, 27 Nov 2024 17:48:16 +0100 Subject: [PATCH 16/33] feat: Update locking view --- src/components/TokenLocking/index.tsx | 51 ++++++++++++++------------- 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/src/components/TokenLocking/index.tsx b/src/components/TokenLocking/index.tsx index 42148fcf..5e05e5bd 100644 --- a/src/components/TokenLocking/index.tsx +++ b/src/components/TokenLocking/index.tsx @@ -1,9 +1,5 @@ import { Box, Grid, Stack, Typography } from '@mui/material' import { useSafeTokenBalance } from '@/hooks/useSafeTokenBalance' -import { Leaderboard } from './Leaderboard' -import { CurrentStats } from './CurrentStats' -import { LockTokenWidget } from './LockTokenWidget' -import { ActivityRewardsInfo } from './ActivityRewardsInfo' import { ActionNavigation } from './BoostGraph/ActionNavigation/ActionNavigation' import PaperContainer from '../PaperContainer' import { useSummarizedLockHistory } from '@/hooks/useSummarizedLockHistory' @@ -12,43 +8,50 @@ import { useLockHistory } from '@/hooks/useLockHistory' import css from './styles.module.css' import { ExternalLink } from '../ExternalLink' import SafePassDisclaimer from '../SafePassDisclaimer' +import { useOwnLockingRank } from '@/hooks/useLeaderboard' +import { TokenAmount } from '@/components/TokenAmount' const TokenLocking = () => { - const { isLoading: safeBalanceLoading, data: safeBalance } = useSafeTokenBalance() + const { isLoading: safeBalanceLoading } = useSafeTokenBalance() const { totalLocked, totalUnlocked, totalWithdrawable } = useSummarizedLockHistory(useLockHistory()) + const ownRankResult = useOwnLockingRank() + const { data: ownRank } = ownRankResult + const isUnlockAvailable = totalLocked.gt(0) || totalUnlocked.gt(0) || totalWithdrawable.gt(0) return ( - - - {'Get your Safe{Pass} rewards'} + + + {'Safe{Pass} has concluded'} {'What is Safe{Pass}'} - - - - - - - - - + {ownRank && ( + Congratulations, you reached rank {ownRank.position}. + )} + + {isUnlockAvailable && Make sure to unlock any remaining tokens.} + + + - - - From 92300f95ecab83686b317fd20a72383373447605 Mon Sep 17 00:00:00 2001 From: Usame Algan Date: Thu, 28 Nov 2024 09:50:52 +0100 Subject: [PATCH 17/33] fix: Update main copy --- src/components/Points/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Points/index.tsx b/src/components/Points/index.tsx index a79e58f5..f9495f42 100644 --- a/src/components/Points/index.tsx +++ b/src/components/Points/index.tsx @@ -108,7 +108,7 @@ const Points = () => { You've made it to the finish line! The rewards program has officially ended, and the points - earned, will be converted into SAFE tokens. + earned, will be converted into rewards (including SAFE tokens) {eligibility?.isVpn ? ( From 963eb887778835672da1e9453a6376bac1b44664 Mon Sep 17 00:00:00 2001 From: Usame Algan Date: Thu, 28 Nov 2024 12:26:33 +0100 Subject: [PATCH 18/33] fix: Add claim txs --- src/components/Points/index.tsx | 3 +- src/config/constants.ts | 2 +- src/utils/__tests__/claim.test.ts | 70 +++++++++++++++++++++++++++++++ src/utils/airdrop.ts | 2 +- src/utils/claim.ts | 54 ++++++++++++++---------- 5 files changed, 107 insertions(+), 24 deletions(-) diff --git a/src/components/Points/index.tsx b/src/components/Points/index.tsx index f9495f42..bb19271f 100644 --- a/src/components/Points/index.tsx +++ b/src/components/Points/index.tsx @@ -26,7 +26,7 @@ import { getVestingTypes } from '@/utils/vesting' const Points = () => { const [sealedResult, setSealedResult] = useState() const { data: eligibility, isLoading } = useUnsealedResult(sealedResult) - const { sdk } = useSafeAppsSDK() + const { sdk, safe } = useSafeAppsSDK() const globalCampaignId = useGlobalCampaignId() const aggregateCampaignId = useAggregateCampaignId() const { data: globalRank } = useOwnCampaignRank(globalCampaignId) @@ -57,6 +57,7 @@ const Points = () => { vestingData: allocation?.vestingData ?? [], sapBoostedClaimable: eligibility.isAllowed ? sapBoosted.inVesting : '0', sapUnboostedClaimable: sapUnboosted.inVesting, + safeAddress: safe.safeAddress, }) try { diff --git a/src/config/constants.ts b/src/config/constants.ts index 245c04bb..f1c9caa0 100644 --- a/src/config/constants.ts +++ b/src/config/constants.ts @@ -74,7 +74,7 @@ export const VESTING_URL = `${CLAIMING_DATA_URL}/allocations` export const SEP5_EXPIRATION_DATE = '27.10.2023' export const SEP5_EXPIRATION = `${SEP5_EXPIRATION_DATE} 10:00 UTC` -export const SAP_LOCK_DATE = '01.01.2026' // TBD +export const SAP_LOCK_DATE = '06.12.2024' // TBD export const AIRDROP_TAGS = { USER: 'user', diff --git a/src/utils/__tests__/claim.test.ts b/src/utils/__tests__/claim.test.ts index 44bb9886..d539b293 100644 --- a/src/utils/__tests__/claim.test.ts +++ b/src/utils/__tests__/claim.test.ts @@ -253,4 +253,74 @@ describe('createClaimTxs', () => { expect(decodedClaimTx[1].toString().toLowerCase()).toEqual(safeAddress) // beneficiary expect(decodedClaimTx[2].toString()).toEqual(parseEther('100').toString()) // amount }) + + it('do not claim airdrop while not started', () => { + const safeAddress = hexZeroPad('0x5afe', 20) + + const vestingData: Vesting[] = [ + { + isExpired: false, + account: safeAddress, + amount: parseEther('100').toString(), + amountClaimed: '0', + chainId: 11155111, + contract: mockInvestorVestingAddress, + curve: 0, + durationWeeks: 416, + isRedeemed: true, + proof: ['0x4697528f2cd5e98bce29be252b25ed33b79d8f0245bb7a3d0f00bb32e50128bb'], + startDate: Math.floor((Date.now() + 10000000000) / 1000), // A start date thats always slightly ahead of now + tag: 'investor', + vestingId: '0xabfe3d0bfb3df17a4aa39d6967f722ff82c765601417a4957434023c97d5b111', + }, + ] + + const txs = createClaimTxs({ + vestingData, + amount: '100', + safeAddress, + investorClaimable: parseEther('100').toString(), + isMax: true, + userClaimable: '0', + sep5Claimable: '0', + isTokenPaused: false, + }) + + expect(txs).toHaveLength(0) + }) + + it('claim airdrop if it has started', () => { + const safeAddress = hexZeroPad('0x5afe', 20) + + const vestingData: Vesting[] = [ + { + isExpired: false, + account: safeAddress, + amount: parseEther('100').toString(), + amountClaimed: '0', + chainId: 11155111, + contract: mockInvestorVestingAddress, + curve: 0, + durationWeeks: 416, + isRedeemed: true, + proof: ['0x4697528f2cd5e98bce29be252b25ed33b79d8f0245bb7a3d0f00bb32e50128bb'], + startDate: Math.floor((Date.now() - 10000000000) / 1000), // A start date thats always slightly behind of now + tag: 'investor', + vestingId: '0xabfe3d0bfb3df17a4aa39d6967f722ff82c765601417a4957434023c97d5b111', + }, + ] + + const txs = createClaimTxs({ + vestingData, + amount: '100', + safeAddress, + investorClaimable: parseEther('100').toString(), + isMax: true, + userClaimable: '0', + sep5Claimable: '0', + isTokenPaused: false, + }) + + expect(txs).toHaveLength(1) + }) }) diff --git a/src/utils/airdrop.ts b/src/utils/airdrop.ts index 2742044a..0bc4fb67 100644 --- a/src/utils/airdrop.ts +++ b/src/utils/airdrop.ts @@ -1,7 +1,7 @@ import { BigNumber } from 'ethers' import { parseEther } from 'ethers/lib/utils' -const MAX_UINT128 = BigNumber.from('0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF') +export const MAX_UINT128 = BigNumber.from('0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF') /** * Splits the amount the user wants to claim into user airdrop, SEP5 airdrop and ecosystem airdrop. diff --git a/src/utils/claim.ts b/src/utils/claim.ts index 8613e6d3..fd598ae4 100644 --- a/src/utils/claim.ts +++ b/src/utils/claim.ts @@ -2,7 +2,7 @@ import { BigNumber } from 'ethers' import { getVestingTypes } from '@/utils/vesting' import { getAirdropInterface } from '@/services/contracts/Airdrop' -import { splitAirdropAmounts } from '@/utils/airdrop' +import { MAX_UINT128, splitAirdropAmounts } from '@/utils/airdrop' import type { Vesting } from '@/hooks/useSafeTokenAllocation' import { BaseTransaction } from '@/hooks/useTxSender' @@ -87,16 +87,20 @@ const createAirdropTxs = ({ txs.push(redeemTx) } + const hasStarted = Math.floor(Date.now() / 1000) >= vestingClaim.startDate + // Add claim function - const claimTx = createClaimTx({ - vestingClaim, - amount, - safeAddress, - airdropAddress, - isTokenPaused, - }) + if (hasStarted) { + const claimTx = createClaimTx({ + vestingClaim, + amount, + safeAddress, + airdropAddress, + isTokenPaused, + }) - txs.push(claimTx) + txs.push(claimTx) + } return txs } @@ -192,31 +196,39 @@ export const createSAPClaimTxs = ({ vestingData, sapBoostedClaimable, sapUnboostedClaimable, + safeAddress, }: { vestingData: Vesting[] sapBoostedClaimable: string sapUnboostedClaimable: string + safeAddress: string }): BaseTransaction[] => { const txs: BaseTransaction[] = [] const { sapBoostedVesting, sapUnboostedVesting } = getVestingTypes(vestingData) if (sapBoostedVesting && BigNumber.from(sapBoostedClaimable).gt(0)) { - const redeemTx = createRedeemTx({ - vestingClaim: sapBoostedVesting, - airdropAddress: sapBoostedVesting.contract, - }) - - txs.push(redeemTx) + txs.push( + ...createAirdropTxs({ + vestingClaim: sapBoostedVesting, + amount: MAX_UINT128.toString(), + safeAddress, + airdropAddress: sapBoostedVesting.contract, + isTokenPaused: false, + }), + ) } if (sapUnboostedVesting && BigNumber.from(sapUnboostedClaimable).gt(0)) { - const redeemTx = createRedeemTx({ - vestingClaim: sapUnboostedVesting, - airdropAddress: sapUnboostedVesting.contract, - }) - - txs.push(redeemTx) + txs.push( + ...createAirdropTxs({ + vestingClaim: sapUnboostedVesting, + amount: MAX_UINT128.toString(), + safeAddress, + airdropAddress: sapUnboostedVesting.contract, + isTokenPaused: false, + }), + ) } return txs From 4709a09f31bf58650be26883fc370fa03cfeff78 Mon Sep 17 00:00:00 2001 From: Usame Algan Date: Thu, 28 Nov 2024 12:54:22 +0100 Subject: [PATCH 19/33] feat: Display claim button once airdrop has started --- src/components/Points/ClaimButton.tsx | 4 ++-- src/components/Points/index.tsx | 13 +++++++++---- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/components/Points/ClaimButton.tsx b/src/components/Points/ClaimButton.tsx index 6075d1e4..c0c429ac 100644 --- a/src/components/Points/ClaimButton.tsx +++ b/src/components/Points/ClaimButton.tsx @@ -1,7 +1,7 @@ import { Box, Button } from '@mui/material' import { useCoolMode } from '@/hooks/useCoolMode' -const ClaimButton = ({ startClaiming }: { startClaiming: () => void }) => { +const ClaimButton = ({ startClaiming, text }: { startClaiming: () => void; text: string }) => { const particlesRef = useCoolMode('./images/token.svg') return ( @@ -16,7 +16,7 @@ const ClaimButton = ({ startClaiming }: { startClaiming: () => void }) => { }} onClick={startClaiming} > - Start claiming + {text} ) diff --git a/src/components/Points/index.tsx b/src/components/Points/index.tsx index bb19271f..e2cf2391 100644 --- a/src/components/Points/index.tsx +++ b/src/components/Points/index.tsx @@ -33,7 +33,7 @@ const Points = () => { const { data: aggregateRank } = useOwnCampaignRank(aggregateCampaignId) const { data: allocation } = useSafeTokenAllocation() const { sapBoosted, sapUnboosted, totalSAP } = useTaggedAllocations(eligibility?.isAllowed) - const { sapUnboostedVesting } = getVestingTypes(allocation?.vestingData || []) + const { sapUnboostedVesting, sapBoostedVesting } = getVestingTypes(allocation?.vestingData || []) useEffect(() => { const fpPromise = FingerprintJSPro.load({ @@ -68,7 +68,10 @@ const Points = () => { } const loading = !sealedResult || isLoading - const isSAPRedeemed = sapUnboostedVesting?.isRedeemed + const isSAPRedeemed = sapUnboostedVesting?.isRedeemed || sapBoostedVesting?.isRedeemed + const hasSAPStarted = + (sapUnboostedVesting && Math.floor(Date.now() / 1000) >= sapUnboostedVesting.startDate) || + (sapBoostedVesting && Math.floor(Date.now() / 1000) >= sapBoostedVesting.startDate) return ( <> @@ -132,12 +135,14 @@ const Points = () => { - ) : isSAPRedeemed ? ( + ) : isSAPRedeemed && !hasSAPStarted ? ( Redeem successful! You will be able to claim your tokens starting from {SAP_LOCK_DATE}. + ) : hasSAPStarted ? ( + ) : !loading ? ( - + ) : null} From e5c8f3e74cae3f8ed532f1aced6ffb4755b88822 Mon Sep 17 00:00:00 2001 From: Usame Algan Date: Thu, 28 Nov 2024 14:31:24 +0100 Subject: [PATCH 20/33] feat: Add non-eligible screen and loading spinner --- src/components/Points/index.tsx | 181 ++++++++++++++++++-------------- 1 file changed, 100 insertions(+), 81 deletions(-) diff --git a/src/components/Points/index.tsx b/src/components/Points/index.tsx index e2cf2391..a06c78fb 100644 --- a/src/components/Points/index.tsx +++ b/src/components/Points/index.tsx @@ -1,4 +1,4 @@ -import { Grid, Typography, Stack, Box, Button, SvgIcon, Alert } from '@mui/material' +import { Grid, Typography, Stack, Box, Button, SvgIcon, Alert, CircularProgress } from '@mui/material' import { ExternalLink } from '../ExternalLink' import PaperContainer from '../PaperContainer' import SafePassDisclaimer from '../SafePassDisclaimer' @@ -31,7 +31,7 @@ const Points = () => { const aggregateCampaignId = useAggregateCampaignId() const { data: globalRank } = useOwnCampaignRank(globalCampaignId) const { data: aggregateRank } = useOwnCampaignRank(aggregateCampaignId) - const { data: allocation } = useSafeTokenAllocation() + const { data: allocation, isLoading: allocationLoading } = useSafeTokenAllocation() const { sapBoosted, sapUnboosted, totalSAP } = useTaggedAllocations(eligibility?.isAllowed) const { sapUnboostedVesting, sapBoostedVesting } = getVestingTypes(allocation?.vestingData || []) @@ -67,7 +67,12 @@ const Points = () => { } } - const loading = !sealedResult || isLoading + const loading = !sealedResult || isLoading || allocationLoading + const hasSAPAllocation = + Number(sapUnboosted.inVesting) > 0 || + Number(sapUnboosted.claimable) > 0 || + Number(sapBoosted.inVesting) > 0 || + Number(sapBoosted.claimable) > 0 const isSAPRedeemed = sapUnboostedVesting?.isRedeemed || sapBoostedVesting?.isRedeemed const hasSAPStarted = (sapUnboostedVesting && Math.floor(Date.now() / 1000) >= sapUnboostedVesting.startDate) || @@ -94,7 +99,7 @@ const Points = () => { - + { color: 'static.main', }} > - - - - - Rewards Await! πŸ† - - - - You've made it to the finish line! The rewards program has officially ended, and the points - earned, will be converted into rewards (including SAFE tokens) - + {loading ? ( + + ) : hasSAPAllocation ? ( + + + + + Rewards Await! πŸ† + - {eligibility?.isVpn ? ( - - We detected that you are using a VPN. Please turn it off in order to start the claiming process. - - - - - ) : isSAPRedeemed && !hasSAPStarted ? ( - - Redeem successful! You will be able to claim your tokens starting from {SAP_LOCK_DATE}. - - ) : hasSAPStarted ? ( - - ) : !loading ? ( - - ) : null} - - + + You've made it to the finish line! The rewards program has officially ended, and the points + earned, will be converted into rewards (including SAFE tokens) + - - {!eligibility?.isVpn && !loading && ( - - - - Reward tokens - + {eligibility?.isVpn ? ( + + We detected that you are using a VPN. Please turn it off in order to start the claiming process. + + + + + ) : isSAPRedeemed && !hasSAPStarted ? ( + + Redeem successful! You will be able to claim your tokens starting from {SAP_LOCK_DATE}. + + ) : hasSAPStarted ? ( + + ) : !loading ? ( + + ) : null} + + - - - - - - - - - SAFE + + {!eligibility?.isVpn && !loading && ( + + + + Reward tokens + + + + + + + + + + SAFE + + + Available from {SAP_LOCK_DATE} - Available from {SAP_LOCK_DATE} - - )} + )} + + + ) : ( + + + + Not eligible for Rewards + + + {'Unfortunately, you are not eligible for Safe{Pass} rewards'} + - + )} From 41ce005c7f24eab5938776d631d5455a0f269d74 Mon Sep 17 00:00:00 2001 From: Usame Algan Date: Thu, 28 Nov 2024 15:45:44 +0100 Subject: [PATCH 21/33] fix: Show non-eligible if only boosted and not allowed --- src/components/Points/index.tsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/components/Points/index.tsx b/src/components/Points/index.tsx index a06c78fb..599957de 100644 --- a/src/components/Points/index.tsx +++ b/src/components/Points/index.tsx @@ -68,11 +68,12 @@ const Points = () => { } const loading = !sealedResult || isLoading || allocationLoading - const hasSAPAllocation = - Number(sapUnboosted.inVesting) > 0 || - Number(sapUnboosted.claimable) > 0 || - Number(sapBoosted.inVesting) > 0 || - Number(sapBoosted.claimable) > 0 + const hasSAPUnboostedAllocation = Number(sapUnboosted.inVesting) > 0 || Number(sapUnboosted.claimable) > 0 + const hasSAPBoostedAllocation = + (Number(sapBoosted.inVesting) > 0 || Number(sapBoosted.claimable) > 0) && eligibility?.isAllowed + + const hasSAPAllocation = hasSAPUnboostedAllocation || hasSAPBoostedAllocation + const isSAPRedeemed = sapUnboostedVesting?.isRedeemed || sapBoostedVesting?.isRedeemed const hasSAPStarted = (sapUnboostedVesting && Math.floor(Date.now() / 1000) >= sapUnboostedVesting.startDate) || From 45536abb958c0e83c9e83f576e326f249d535bc1 Mon Sep 17 00:00:00 2001 From: Usame Algan Date: Fri, 29 Nov 2024 11:38:34 +0100 Subject: [PATCH 22/33] fix: Exclude expired and not started airdrops from total voting power --- src/hooks/useSafeTokenAllocation.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/hooks/useSafeTokenAllocation.ts b/src/hooks/useSafeTokenAllocation.ts index 0cfbf480..32ad40dd 100644 --- a/src/hooks/useSafeTokenAllocation.ts +++ b/src/hooks/useSafeTokenAllocation.ts @@ -92,7 +92,10 @@ const computeVotingPower = ( lockingContractBalance: string, ): BigNumber => { const tokensInVesting = validVestingData.reduce( - (acc, data) => acc.add(data.amount).sub(data.amountClaimed), + (acc, data) => + data.isExpired || data.startDate > Math.floor(Date.now() / 1000) + ? acc + : acc.add(data.amount).sub(data.amountClaimed), BigNumber.from(0), ) From 8d1fc80a4acea433c6399b016f9e3c370969dcbd Mon Sep 17 00:00:00 2001 From: Usame Algan Date: Mon, 2 Dec 2024 11:39:46 +0100 Subject: [PATCH 23/33] Split SAP claim and redeem into two functions --- src/components/Points/index.tsx | 49 ++++++++++++++++++++----------- src/hooks/useTaggedAllocations.ts | 2 ++ src/utils/claim.ts | 34 +++++++++++++++++++++ 3 files changed, 68 insertions(+), 17 deletions(-) diff --git a/src/components/Points/index.tsx b/src/components/Points/index.tsx index 599957de..f8213489 100644 --- a/src/components/Points/index.tsx +++ b/src/components/Points/index.tsx @@ -12,7 +12,7 @@ import { formatEther } from 'ethers/lib/utils' import Spotlight from '@/public/images/spotlight.svg' import Star from '@/public/images/star.svg' import Star1 from '@/public/images/star1.svg' -import { createSAPClaimTxs } from '@/utils/claim' +import { createSAPClaimTxs, createSAPRedeemTxs } from '@/utils/claim' import { useSafeTokenAllocation } from '@/hooks/useSafeTokenAllocation' import { useSafeAppsSDK } from '@safe-global/safe-apps-react-sdk' import { FingerprintJSPro } from '@fingerprintjs/fingerprintjs-pro-react' @@ -53,10 +53,27 @@ const Points = () => { // Something went wrong with fetching the eligibility for this user so we don't let them redeem if (!eligibility) return - const txs = createSAPClaimTxs({ + const txs = createSAPRedeemTxs({ vestingData: allocation?.vestingData ?? [], sapBoostedClaimable: eligibility.isAllowed ? sapBoosted.inVesting : '0', sapUnboostedClaimable: sapUnboosted.inVesting, + }) + + try { + await sdk.txs.send({ txs }) + } catch (error) { + console.error(error) + } + } + + const claimSAP = async () => { + // Something went wrong with fetching the eligibility for this user so we don't let them redeem + if (!eligibility) return + + const txs = createSAPClaimTxs({ + vestingData: allocation?.vestingData ?? [], + sapBoostedClaimable: eligibility.isAllowed ? sapBoosted.claimable : '0', + sapUnboostedClaimable: sapUnboosted.claimable, safeAddress: safe.safeAddress, }) @@ -149,7 +166,7 @@ const Points = () => { Redeem successful! You will be able to claim your tokens starting from {SAP_LOCK_DATE}. ) : hasSAPStarted ? ( - + ) : !loading ? ( ) : null} @@ -213,20 +230,18 @@ const Points = () => { {globalRank && ( - - - - {aggregateRank && ( - <> - - - {`Campaign${aggregateRank.totalPoints > 1 ? 's' : ''} completed`} - - - )} - - - + {aggregateRank && ( + + + + + + {`Campaign${aggregateRank.totalPoints > 1 ? 's' : ''} completed`} + + + + + )} diff --git a/src/hooks/useTaggedAllocations.ts b/src/hooks/useTaggedAllocations.ts index 0c710812..4df7a6ce 100644 --- a/src/hooks/useTaggedAllocations.ts +++ b/src/hooks/useTaggedAllocations.ts @@ -65,6 +65,8 @@ export const useTaggedAllocations = ( const [sapBoostedClaimable, sapBoostedInVesting] = useAmounts(sapBoostedVesting) const [sapUnboostedClaimable, sapUnboostedInVesting] = useAmounts(sapUnboostedVesting) + console.log({ sapBoostedClaimable, sapBoostedInVesting }) + // Calculate total of claimable vs. vested amounts const totalAmountClaimable = getTotal(sep5Claimable, userClaimable, ecosystemClaimable, investorClaimable) diff --git a/src/utils/claim.ts b/src/utils/claim.ts index fd598ae4..7796c129 100644 --- a/src/utils/claim.ts +++ b/src/utils/claim.ts @@ -192,6 +192,40 @@ export const createClaimTxs = ({ return txs } +export const createSAPRedeemTxs = ({ + vestingData, + sapBoostedClaimable, + sapUnboostedClaimable, +}: { + vestingData: Vesting[] + sapBoostedClaimable: string + sapUnboostedClaimable: string +}) => { + const txs: BaseTransaction[] = [] + + const { sapBoostedVesting, sapUnboostedVesting } = getVestingTypes(vestingData) + + if (sapBoostedVesting && BigNumber.from(sapBoostedClaimable).gt(0) && !sapBoostedVesting.isRedeemed) { + const redeemTx = createRedeemTx({ + vestingClaim: sapBoostedVesting, + airdropAddress: sapBoostedVesting.contract, + }) + + txs.push(redeemTx) + } + + if (sapUnboostedVesting && BigNumber.from(sapUnboostedClaimable).gt(0) && !sapUnboostedVesting.isRedeemed) { + const redeemTx = createRedeemTx({ + vestingClaim: sapUnboostedVesting, + airdropAddress: sapUnboostedVesting.contract, + }) + + txs.push(redeemTx) + } + + return txs +} + export const createSAPClaimTxs = ({ vestingData, sapBoostedClaimable, From 41dad019d339df35afdaa4ceb6e91d0b529eec66 Mon Sep 17 00:00:00 2001 From: Usame Algan Date: Mon, 2 Dec 2024 15:35:44 +0100 Subject: [PATCH 24/33] Update start date --- src/config/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/constants.ts b/src/config/constants.ts index f1c9caa0..4fa62046 100644 --- a/src/config/constants.ts +++ b/src/config/constants.ts @@ -74,7 +74,7 @@ export const VESTING_URL = `${CLAIMING_DATA_URL}/allocations` export const SEP5_EXPIRATION_DATE = '27.10.2023' export const SEP5_EXPIRATION = `${SEP5_EXPIRATION_DATE} 10:00 UTC` -export const SAP_LOCK_DATE = '06.12.2024' // TBD +export const SAP_LOCK_DATE = '03.12.2024' // TBD export const AIRDROP_TAGS = { USER: 'user', From be0adfab8cf1af8c8af5cfd95475876690c61222 Mon Sep 17 00:00:00 2001 From: Usame Algan Date: Tue, 3 Dec 2024 10:34:46 +0100 Subject: [PATCH 25/33] Add mainnet aggregate campaign id --- src/config/constants.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/config/constants.ts b/src/config/constants.ts index 4fa62046..bb422288 100644 --- a/src/config/constants.ts +++ b/src/config/constants.ts @@ -74,7 +74,7 @@ export const VESTING_URL = `${CLAIMING_DATA_URL}/allocations` export const SEP5_EXPIRATION_DATE = '27.10.2023' export const SEP5_EXPIRATION = `${SEP5_EXPIRATION_DATE} 10:00 UTC` -export const SAP_LOCK_DATE = '03.12.2024' // TBD +export const SAP_LOCK_DATE = '03.12.2024' // TBD update to 30.11.2025 export const AIRDROP_TAGS = { USER: 'user', @@ -133,5 +133,5 @@ export const MORPHO_CAMPAIGN_IDS: ChainConfig = { export const AGGREGATE_CAMPAIGN_IDS: ChainConfig = { [Chains.SEPOLIA]: '2a99f13c-0aa3-4484-9479-10962005d292', - [Chains.MAINNET]: '2a99f13c-0aa3-4484-9479-10962005d292', // TBD + [Chains.MAINNET]: '40ab3036-dcc2-4944-acee-02017b9e2f09', } From 67af95abd5d54240453b7c9674c85286c01d17ea Mon Sep 17 00:00:00 2001 From: Usame Algan Date: Tue, 3 Dec 2024 10:46:27 +0100 Subject: [PATCH 26/33] Change vesting logic to exclude inVesting amount that hasnt started yet --- src/components/Points/index.tsx | 7 +++++-- src/hooks/useAmounts.ts | 8 ++++---- src/hooks/useTaggedAllocations.ts | 8 ++++++-- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/components/Points/index.tsx b/src/components/Points/index.tsx index f8213489..e328ddec 100644 --- a/src/components/Points/index.tsx +++ b/src/components/Points/index.tsx @@ -85,9 +85,12 @@ const Points = () => { } const loading = !sealedResult || isLoading || allocationLoading - const hasSAPUnboostedAllocation = Number(sapUnboosted.inVesting) > 0 || Number(sapUnboosted.claimable) > 0 + const hasSAPUnboostedAllocation = + Number(sapUnboosted.inVesting) > 0 || Number(sapUnboosted.claimable) > 0 || Number(sapUnboosted.allocation) > 0 const hasSAPBoostedAllocation = - (Number(sapBoosted.inVesting) > 0 || Number(sapBoosted.claimable) > 0) && eligibility?.isAllowed + Number(sapBoosted.inVesting) > 0 || + Number(sapBoosted.claimable) > 0 || + (Number(sapBoosted.allocation) > 0 && eligibility?.isAllowed) const hasSAPAllocation = hasSAPUnboostedAllocation || hasSAPBoostedAllocation diff --git a/src/hooks/useAmounts.ts b/src/hooks/useAmounts.ts index df3284cd..eac770ed 100644 --- a/src/hooks/useAmounts.ts +++ b/src/hooks/useAmounts.ts @@ -11,10 +11,10 @@ export const useAmounts = (vestingClaim: Vesting | null): [string, string] => { useEffect(() => { const refreshAmount = () => { try { - if (!vestingClaim) { - return - } - const totalAmount = vestingClaim ? vestingClaim.amount : '0' + if (!vestingClaim) return + + const totalAmount = + vestingClaim && Math.floor(Date.now() / 1000) > vestingClaim.startDate ? vestingClaim.amount : '0' let vestedAmount = vestingClaim ? calculateVestedAmount(vestingClaim) : '0' const amountClaimed = vestingClaim?.amountClaimed || '0' diff --git a/src/hooks/useTaggedAllocations.ts b/src/hooks/useTaggedAllocations.ts index 4df7a6ce..a71fc684 100644 --- a/src/hooks/useTaggedAllocations.ts +++ b/src/hooks/useTaggedAllocations.ts @@ -35,10 +35,12 @@ export const useTaggedAllocations = ( sapBoosted: { claimable: string inVesting: string + allocation: string } sapUnboosted: { claimable: string inVesting: string + allocation: string } total: { claimable: string @@ -65,8 +67,6 @@ export const useTaggedAllocations = ( const [sapBoostedClaimable, sapBoostedInVesting] = useAmounts(sapBoostedVesting) const [sapUnboostedClaimable, sapUnboostedInVesting] = useAmounts(sapUnboostedVesting) - console.log({ sapBoostedClaimable, sapBoostedInVesting }) - // Calculate total of claimable vs. vested amounts const totalAmountClaimable = getTotal(sep5Claimable, userClaimable, ecosystemClaimable, investorClaimable) @@ -85,6 +85,8 @@ export const useTaggedAllocations = ( isEligibleForBoostedSAP ? sapBoostedVesting?.amount || '0' : '0', sapUnboostedVesting?.amount || '0', ) + const totalSAPBoosted = sapBoostedVesting?.amount || '0' + const totalSAPUnboosted = sapUnboostedVesting?.amount || '0' return { sep5: { @@ -106,10 +108,12 @@ export const useTaggedAllocations = ( sapBoosted: { claimable: sapBoostedClaimable, inVesting: sapBoostedInVesting, + allocation: totalSAPBoosted, }, sapUnboosted: { claimable: sapUnboostedClaimable, inVesting: sapUnboostedInVesting, + allocation: totalSAPUnboosted, }, total: { claimable: totalAmountClaimable, From 4fb09f3ef6402850c5485eb730bab464219248b5 Mon Sep 17 00:00:00 2001 From: Usame Algan Date: Tue, 3 Dec 2024 10:58:55 +0100 Subject: [PATCH 27/33] Fix useAmounts test --- src/hooks/__tests__/useAmounts.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/__tests__/useAmounts.test.ts b/src/hooks/__tests__/useAmounts.test.ts index 21e7c849..4d0a51ac 100644 --- a/src/hooks/__tests__/useAmounts.test.ts +++ b/src/hooks/__tests__/useAmounts.test.ts @@ -66,7 +66,7 @@ describe('useAmounts', () => { it('vesting did not start yet', () => { const vestingClaim = createMockVesting(1, 1000, 1) const result = renderHook(() => useAmounts(vestingClaim)) - expect(result.result.current).toEqual(['0', parseEther('1000').toString()]) + expect(result.result.current).toEqual(['0', parseEther('0').toString()]) }) it('vesting fully vested and fully claimed', () => { From cfea31a42741ea983df9381b0ac3e9a66804106e Mon Sep 17 00:00:00 2001 From: Usame Algan Date: Tue, 3 Dec 2024 16:18:14 +0100 Subject: [PATCH 28/33] Add post claim screen --- src/components/Points/index.tsx | 161 ++++++++++++++++++-------------- 1 file changed, 91 insertions(+), 70 deletions(-) diff --git a/src/components/Points/index.tsx b/src/components/Points/index.tsx index e328ddec..0c09ef21 100644 --- a/src/components/Points/index.tsx +++ b/src/components/Points/index.tsx @@ -22,6 +22,8 @@ import useUnsealedResult, { SealedRequest } from '@/hooks/useUnsealedResult' import ClaimButton from '@/components/Points/ClaimButton' import { useAggregateCampaignId } from '@/hooks/useAggregateCampaignId' import { getVestingTypes } from '@/utils/vesting' +import { BigNumber } from 'ethers' +import { TokenAmount } from '@/components/TokenAmount' const Points = () => { const [sealedResult, setSealedResult] = useState() @@ -99,6 +101,15 @@ const Points = () => { (sapUnboostedVesting && Math.floor(Date.now() / 1000) >= sapUnboostedVesting.startDate) || (sapBoostedVesting && Math.floor(Date.now() / 1000) >= sapBoostedVesting.startDate) + const isSAPBoostedClaimed = + sapBoostedVesting && BigNumber.from(sapBoostedVesting.amount).eq(BigNumber.from(sapBoostedVesting.amountClaimed)) + + const isSAPUnboostedClaimed = + sapUnboostedVesting && + BigNumber.from(sapUnboostedVesting.amount).eq(BigNumber.from(sapUnboostedVesting.amountClaimed)) + + const isSAPClaimed = isSAPBoostedClaimed || isSAPUnboostedClaimed + return ( <> @@ -136,85 +147,95 @@ const Points = () => { - Rewards Await! πŸ† + {isSAPClaimed ? 'Congratulations, you claimed your rewards!' : 'Rewards Await! πŸ†'} +
+
+ {isSAPClaimed && }
- - You've made it to the finish line! The rewards program has officially ended, and the points - earned, will be converted into rewards (including SAFE tokens) - + {!isSAPClaimed && ( + <> + + You've made it to the finish line! The rewards program has officially ended, and the + points earned, will be converted into rewards (including SAFE tokens) + - {eligibility?.isVpn ? ( - - We detected that you are using a VPN. Please turn it off in order to start the claiming process. - - - - - ) : isSAPRedeemed && !hasSAPStarted ? ( - - Redeem successful! You will be able to claim your tokens starting from {SAP_LOCK_DATE}. - - ) : hasSAPStarted ? ( - - ) : !loading ? ( - - ) : null} + {eligibility?.isVpn ? ( + + We detected that you are using a VPN. Please turn it off in order to start the claiming + process. + + + + + ) : isSAPRedeemed && !hasSAPStarted ? ( + + Redeem successful! You will be able to claim your tokens starting from {SAP_LOCK_DATE}. + + ) : hasSAPStarted ? ( + + ) : !loading ? ( + + ) : null} + + )}
- - {!eligibility?.isVpn && !loading && ( - - - - Reward tokens - - - - - - - - - - - SAFE + {!isSAPClaimed && ( + + {!eligibility?.isVpn && !loading && ( + + + + Reward tokens + + + + + + + + + + SAFE + + + Available from {SAP_LOCK_DATE} - Available from {SAP_LOCK_DATE} - - )} - + )} +
+ )}
) : ( From 4154781d010fa4658f6cb7577f0fc7476aed7ca5 Mon Sep 17 00:00:00 2001 From: Usame Algan Date: Wed, 4 Dec 2024 15:02:18 +0100 Subject: [PATCH 29/33] Include vested SAP tokens in the voting power --- src/components/Points/index.tsx | 8 ++------ src/hooks/useAmounts.ts | 3 +-- src/hooks/useSafeTokenAllocation.ts | 5 +---- 3 files changed, 4 insertions(+), 12 deletions(-) diff --git a/src/components/Points/index.tsx b/src/components/Points/index.tsx index 0c09ef21..3ddfb34e 100644 --- a/src/components/Points/index.tsx +++ b/src/components/Points/index.tsx @@ -87,12 +87,8 @@ const Points = () => { } const loading = !sealedResult || isLoading || allocationLoading - const hasSAPUnboostedAllocation = - Number(sapUnboosted.inVesting) > 0 || Number(sapUnboosted.claimable) > 0 || Number(sapUnboosted.allocation) > 0 - const hasSAPBoostedAllocation = - Number(sapBoosted.inVesting) > 0 || - Number(sapBoosted.claimable) > 0 || - (Number(sapBoosted.allocation) > 0 && eligibility?.isAllowed) + const hasSAPUnboostedAllocation = Number(sapUnboosted.allocation) > 0 + const hasSAPBoostedAllocation = Number(sapBoosted.allocation) > 0 && eligibility?.isAllowed const hasSAPAllocation = hasSAPUnboostedAllocation || hasSAPBoostedAllocation diff --git a/src/hooks/useAmounts.ts b/src/hooks/useAmounts.ts index eac770ed..e8658547 100644 --- a/src/hooks/useAmounts.ts +++ b/src/hooks/useAmounts.ts @@ -13,8 +13,7 @@ export const useAmounts = (vestingClaim: Vesting | null): [string, string] => { try { if (!vestingClaim) return - const totalAmount = - vestingClaim && Math.floor(Date.now() / 1000) > vestingClaim.startDate ? vestingClaim.amount : '0' + const totalAmount = vestingClaim ? vestingClaim.amount : '0' let vestedAmount = vestingClaim ? calculateVestedAmount(vestingClaim) : '0' const amountClaimed = vestingClaim?.amountClaimed || '0' diff --git a/src/hooks/useSafeTokenAllocation.ts b/src/hooks/useSafeTokenAllocation.ts index 32ad40dd..0cfbf480 100644 --- a/src/hooks/useSafeTokenAllocation.ts +++ b/src/hooks/useSafeTokenAllocation.ts @@ -92,10 +92,7 @@ const computeVotingPower = ( lockingContractBalance: string, ): BigNumber => { const tokensInVesting = validVestingData.reduce( - (acc, data) => - data.isExpired || data.startDate > Math.floor(Date.now() / 1000) - ? acc - : acc.add(data.amount).sub(data.amountClaimed), + (acc, data) => acc.add(data.amount).sub(data.amountClaimed), BigNumber.from(0), ) From e93a2cd8d16d6cebe4361c12c5539858c8206452 Mon Sep 17 00:00:00 2001 From: Usame Algan Date: Wed, 4 Dec 2024 17:22:58 +0100 Subject: [PATCH 30/33] Exclude SAP allocations from total voting power --- src/hooks/useSafeTokenAllocation.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/hooks/useSafeTokenAllocation.ts b/src/hooks/useSafeTokenAllocation.ts index 0cfbf480..a053bffd 100644 --- a/src/hooks/useSafeTokenAllocation.ts +++ b/src/hooks/useSafeTokenAllocation.ts @@ -92,7 +92,8 @@ const computeVotingPower = ( lockingContractBalance: string, ): BigNumber => { const tokensInVesting = validVestingData.reduce( - (acc, data) => acc.add(data.amount).sub(data.amountClaimed), + (acc, data) => + data.tag === 'sap_boosted' || data.tag === 'sap_unboosted' ? acc : acc.add(data.amount).sub(data.amountClaimed), // Exclude the SAP allocation from total voting power BigNumber.from(0), ) From b64319993648eeea4ec4f483c67485bdb5e9d3d4 Mon Sep 17 00:00:00 2001 From: Usame Algan Date: Thu, 5 Dec 2024 10:05:35 +0100 Subject: [PATCH 31/33] fix: failing unit test --- src/hooks/__tests__/useAmounts.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/__tests__/useAmounts.test.ts b/src/hooks/__tests__/useAmounts.test.ts index 4d0a51ac..21e7c849 100644 --- a/src/hooks/__tests__/useAmounts.test.ts +++ b/src/hooks/__tests__/useAmounts.test.ts @@ -66,7 +66,7 @@ describe('useAmounts', () => { it('vesting did not start yet', () => { const vestingClaim = createMockVesting(1, 1000, 1) const result = renderHook(() => useAmounts(vestingClaim)) - expect(result.result.current).toEqual(['0', parseEther('0').toString()]) + expect(result.result.current).toEqual(['0', parseEther('1000').toString()]) }) it('vesting fully vested and fully claimed', () => { From 912d24af9380b24fd2bdd680a16ea742c3fdc0d2 Mon Sep 17 00:00:00 2001 From: Usame Algan Date: Thu, 5 Dec 2024 10:07:36 +0100 Subject: [PATCH 32/33] chore: Bump package version to 2.4.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f63719e9..11b4d145 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "safe-dao-governance-app", "homepage": "https://github.com/safe-global/safe-dao-governance-app", "license": "GPL-3.0", - "version": "2.3.0", + "version": "2.4.0", "scripts": { "build": "next build && next export", "lint": "tsc && next lint", From be77bfe7f426c2bff245000a0ed99c7a1377b670 Mon Sep 17 00:00:00 2001 From: Usame Algan Date: Thu, 5 Dec 2024 10:27:12 +0100 Subject: [PATCH 33/33] Update lock date --- src/config/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/constants.ts b/src/config/constants.ts index bb422288..f24f3c81 100644 --- a/src/config/constants.ts +++ b/src/config/constants.ts @@ -74,7 +74,7 @@ export const VESTING_URL = `${CLAIMING_DATA_URL}/allocations` export const SEP5_EXPIRATION_DATE = '27.10.2023' export const SEP5_EXPIRATION = `${SEP5_EXPIRATION_DATE} 10:00 UTC` -export const SAP_LOCK_DATE = '03.12.2024' // TBD update to 30.11.2025 +export const SAP_LOCK_DATE = '06.12.2025' export const AIRDROP_TAGS = { USER: 'user',