diff --git a/.env.example b/.env.example index 3d12a59..12bbd04 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,10 @@ # FRONTEND NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=bab3792638df5214933091cdfc569452 NEXT_PUBLIC_WHITELABEL_ENV="OPTIMISM" +NEXT_PUBLIC_ROUND_NAME="OP Citizen Grants Council" + +# BACKEND +SYNAPS_SESSION_ID="" # RAILWAY RAILWAY_ENVIRONMENT_NAME="LOCAL_DEV" diff --git a/README.md b/README.md index 9bae1ae..14e6382 100644 --- a/README.md +++ b/README.md @@ -60,3 +60,8 @@ The OP Claim Tool is a web application for Optimism grant recipients to claim an - `pnpm format`: Run Biome formatter For more details, refer to the project documentation. + +## Testing +- For testing use sepolia +- [Mint PLBR token](https://eth-sepolia.blockscout.com/token/0xdF0A43D15B036c065f6895734B878fD31269Bfa3?tab=read_write_contract) +- Create a hedgey token grant diff --git a/config/contracts/addresses.ts b/config/contracts/addresses.ts index 9a44a06..97d35d8 100644 --- a/config/contracts/addresses.ts +++ b/config/contracts/addresses.ts @@ -1,4 +1,5 @@ import { + base, mainnet, optimism, sepolia, @@ -10,6 +11,7 @@ export const hedgeyContractAddresses = { [optimism.id]: '0x8A2725a6f04816A5274dDD9FEaDd3bd0C253C1A6', [mainnet.id]: '0x8A2725a6f04816A5274dDD9FEaDd3bd0C253C1A6', [sepolia.id]: '0x8A2725a6f04816A5274dDD9FEaDd3bd0C253C1A6', + [base.id]: '0x8A2725a6f04816A5274dDD9FEaDd3bd0C253C1A6', // TODO: Add correct contract addresses for zkSync once they have been deployed [zksync.id]: '0x83FD45623D1627258D5e336e8BaeE3796F47a1C5', [zksyncSepoliaTestnet.id]: '0xUnknown', diff --git a/config/features.ts b/config/features.ts index b734bfd..3c8ce90 100644 --- a/config/features.ts +++ b/config/features.ts @@ -1,5 +1,6 @@ import colors from 'tailwindcss/colors'; import { + base, mainnet, optimism, optimismSepolia, @@ -8,7 +9,7 @@ import { zksyncSepoliaTestnet, } from 'wagmi/chains'; -type WHITELABEL_ENV = 'OPTIMISM' | 'ZK_SYNC'; +type WHITELABEL_ENV = 'OPTIMISM' | 'ZK_SYNC' | 'BASE'; const _WHITELABEL_ENV = process.env.NEXT_PUBLIC_WHITELABEL_ENV; @@ -16,15 +17,18 @@ if (!_WHITELABEL_ENV) { throw new Error('NEXT_PUBLIC_WHITELABEL_ENV is not set'); } -if (!(_WHITELABEL_ENV === 'OPTIMISM' || _WHITELABEL_ENV === 'ZK_SYNC')) { +if (!['OPTIMISM', 'ZK_SYNC', 'BASE'].includes(_WHITELABEL_ENV)) { throw new Error('NEXT_PUBLIC_WHITELABEL_ENV is not set to a valid value'); } export const WHITELABEL_ENV = _WHITELABEL_ENV; +export const ROUND_NAME = + process.env.NEXT_PUBLIC_ROUND_NAME || `${WHITELABEL_ENV} Round`; + interface Features { APP_NAME: string; - BG_IMAGE: { + BG_IMAGE?: { src: string; }; DELEGATION_REQUIRED: boolean; @@ -54,6 +58,12 @@ const featureMatrix: Record = { DELEGATES_URL: 'https://vote.zknation.io/dao/delegates', CONFIRMATION_CHECKMARK_BG_COLOR: 'black', }, + BASE: { + APP_NAME: 'Base Claim Tool', + DELEGATION_REQUIRED: false, + DELEGATION_ENABLED: false, + CONFIRMATION_CHECKMARK_BG_COLOR: '#0052FF', + }, }; export const FEATURES = featureMatrix[_WHITELABEL_ENV]; @@ -76,6 +86,11 @@ export const getChainConfig = () => { appName: 'ZKsync Claim Tool', chains: [mainnet, zksync, zksyncSepoliaTestnet], }; + case 'BASE': + return { + appName: 'Base Claim Tool', + chains: [base, sepolia], + }; } }; @@ -99,5 +114,11 @@ export const getWhitelabelThemeColors = (): WhitelabelThemeColors => { primaryAction: colors.blue[500], primaryActionButtonBg: colors.blue[900], }; + case 'BASE': + return { + bgClaimcardHeader: colors.blue[100], + primaryAction: colors.blue[400], + primaryActionButtonBg: colors.blue[700], + }; } }; diff --git a/package.json b/package.json index 6c1ea5e..2fbdabf 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "@rainbow-me/rainbowkit": "^2.1.7", "@rainbow-me/rainbowkit-siwe-next-auth": "^0.5.0", "@remixicon/react": "^4.3.0", + "@synaps-io/verify-sdk": "^4.0.48", "@tanstack/react-query": "^5.59.0", "@tanstack/react-query-devtools": "5.62.11", "@types/lodash": "^4.17.14", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 37ec3a2..0a50502 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -59,6 +59,9 @@ importers: '@remixicon/react': specifier: ^4.3.0 version: 4.3.0(react@18.0.0) + '@synaps-io/verify-sdk': + specifier: ^4.0.48 + version: 4.0.48 '@tanstack/react-query': specifier: ^5.59.0 version: 5.59.0(react@18.0.0) @@ -1928,6 +1931,9 @@ packages: '@swc/helpers@0.5.5': resolution: {integrity: sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==} + '@synaps-io/verify-sdk@4.0.48': + resolution: {integrity: sha512-Oa3vVDq8Um2lJ2L1nG/vCPdOzTF6ccgbizrtaLPf/37VjbiLr4CGb1fVqMhzsMFs0niK4qL97NZni7DR/8nDmg==} + '@tanstack/query-core@5.59.0': resolution: {integrity: sha512-WGD8uIhX6/deH/tkZqPNcRyAhDUqs729bWKoByYHSogcshXfFbppOdTER5+qY7mFvu8KEFJwT0nxr8RfPTVh0Q==} @@ -7175,6 +7181,8 @@ snapshots: '@swc/counter': 0.1.3 tslib: 2.8.1 + '@synaps-io/verify-sdk@4.0.48': {} + '@tanstack/query-core@5.59.0': {} '@tanstack/query-devtools@5.62.9': {} diff --git a/public/base-logo.svg b/public/base-logo.svg new file mode 100644 index 0000000..67c4322 --- /dev/null +++ b/public/base-logo.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/app/api/grants/route.ts b/src/app/api/grants/route.ts index 320a1b5..1f6d140 100644 --- a/src/app/api/grants/route.ts +++ b/src/app/api/grants/route.ts @@ -68,7 +68,6 @@ export async function GET(req: NextRequest) { ); const sheetNames = await sheetNamesResponse.json(); const title: string = sheetNames?.sheets[0]?.properties?.title; - if (!title) { return Response.json({ success: false, diff --git a/src/app/claim-history/page.tsx b/src/app/claim-history/page.tsx index 496d5e8..bd5465f 100644 --- a/src/app/claim-history/page.tsx +++ b/src/app/claim-history/page.tsx @@ -77,7 +77,8 @@ const ClaimHistory = () => {

Remaining:

- {Math.round(grant.grantAmount - grant.claimed)}{' '} + {Math.round(grant.grantAmount - grant.claimed) || + 0}{' '} {grant.campaign.token?.ticker}

diff --git a/src/app/grants/[grantId]/page.tsx b/src/app/grants/[grantId]/page.tsx index 69ab3cd..3a9aa16 100644 --- a/src/app/grants/[grantId]/page.tsx +++ b/src/app/grants/[grantId]/page.tsx @@ -33,11 +33,13 @@ const GrantPage = () => { Back to all grants -

You're claiming

+

You're claiming for

- {grant?.title} + + {grant?.title} + with - + {grant?.grantAmount} {grant?.campaign.token?.ticker} diff --git a/src/app/grants/page.tsx b/src/app/grants/page.tsx index 75ad1a2..e856475 100644 --- a/src/app/grants/page.tsx +++ b/src/app/grants/page.tsx @@ -4,7 +4,6 @@ import WalletConnectButton from '@/components/auth/ConnectButton'; import GrantsList from '@/components/common/GrantList'; import { GrantCardSkeleton } from '@/components/common/skeletons/GrantCardSkeleton'; import { Button } from '@/components/ui/button'; -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; import { Select, @@ -16,7 +15,8 @@ import { import { FilterOption, useGrants } from '@/context/GrantsContext'; import { Search } from 'lucide-react'; import { useMemo, useState } from 'react'; -import { useAccount, useConnectorClient } from 'wagmi'; +import { useAccount } from 'wagmi'; +import { ROUND_NAME } from '../../../config/features'; const Grants = () => { const { displayedGrants, loadMore, grants, isLoading, isFetched } = @@ -24,7 +24,6 @@ const Grants = () => { const [searchTerm, setSearchTerm] = useState(''); const [filter, setFilter] = useState(FilterOption.Highest); - const { isFetched: isFetchedConnector } = useConnectorClient(); const { isConnected } = useAccount(); const filteredAndSortedGrants = useMemo(() => { @@ -60,18 +59,6 @@ const Grants = () => { return filtered; }, [displayedGrants, searchTerm, filter]); - if (isFetchedConnector && !isConnected) { - // Display a card to connect wallet, with connect button - return ( - - - Connect your wallet to view grants - - - - ); - } - if (!isFetched && !isLoading) { return null; } @@ -81,17 +68,24 @@ const Grants = () => {

Grants

- Explore all grants from the OP Citizen Grants Council and who they've - delegated to. For grantees, this claiming tool offers a self-serve - interface to claim and delegate your grant. + Explore all grants from the {ROUND_NAME} and who they've delegated to. + For grantees, this claiming tool offers a self-serve interface to + claim and delegate your grant.

+ + {!isConnected && ( + + )}
setSearchTerm(e.target.value)} /> @@ -101,7 +95,7 @@ const Grants = () => { value={filter} onValueChange={(value) => setFilter(value as FilterOption)} > - + @@ -112,15 +106,16 @@ const Grants = () => {
-
-

+

+

{isLoading ? 'Loading' : grants.length} Projects

-

+

Claimed /{' '} Grant amount

+ {isLoading ? (
diff --git a/src/app/layout.tsx b/src/app/layout.tsx index bb85cc8..6eaf0ca 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -30,13 +30,15 @@ export default function RootLayout({ }: { children: React.ReactNode }) { return ( - + {children} -
- -
+ {FEATURES.BG_IMAGE && ( +
+ +
+ )} diff --git a/src/components/auth/ConnectButton.tsx b/src/components/auth/ConnectButton.tsx index 743aafc..7eaa3ca 100644 --- a/src/components/auth/ConnectButton.tsx +++ b/src/components/auth/ConnectButton.tsx @@ -1,6 +1,7 @@ 'use client'; import { useDisconnect } from '@/hooks/useAuth'; +import { cn } from '@/lib/utils'; import { ConnectButton } from '@rainbow-me/rainbowkit'; import { RiFileHistoryLine, RiLogoutBoxRLine } from '@remixicon/react'; import { useRouter } from 'next/navigation'; @@ -11,8 +12,13 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from '../ui/dropdown-menu'; - -const WalletConnectButton = () => { +const WalletConnectButton = ({ + text, + classNames, +}: { + text?: string; + classNames?: string; +}) => { const { disconnect } = useDisconnect(); return ( @@ -49,11 +55,14 @@ const WalletConnectButton = () => { return ( ); } diff --git a/src/components/common/ClaimCard.tsx b/src/components/common/ClaimCard.tsx index 7ad227d..0539851 100644 --- a/src/components/common/ClaimCard.tsx +++ b/src/components/common/ClaimCard.tsx @@ -8,7 +8,7 @@ import { import { zodResolver } from '@hookform/resolvers/zod'; import { RiArrowRightUpLine } from '@remixicon/react'; import Link from 'next/link'; -import { useMemo, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { useForm } from 'react-hook-form'; import { ContractFunctionExecutionError, @@ -28,6 +28,7 @@ import { import { Input } from '../ui/input'; import SuccessCheckmark from './images/SuccessCheckmark'; +import { Synaps } from '@synaps-io/verify-sdk'; import { Loader2 } from 'lucide-react'; import { useChainId } from 'wagmi'; import { FEATURES } from '../../../config/features'; @@ -158,94 +159,116 @@ export default function ClaimCard({ grant }: { grant: Grant }) { return 'Claim'; }, [isPending, isCorrectChain, grant.chainId, isDelegationRequired]); + Synaps.init({ + sessionId: process.env.SYNAPS_SESSION_ID || '', // TODO: Add session id to env + onFinish: () => { + alert('Verification finished'); + }, + mode: 'modal', + }); + + const handleOpen = () => { + console.log('open'); + Synaps.show(); + }; + return ( - - {step === 'form' && ( -
- - - {DELEGATION_REQUIRED ? ( - <> -
- ( - - - Enter the delegate's address - - - - - - - )} + <> +
+ +
+ + {step === 'form' && ( + + + + {DELEGATION_REQUIRED ? ( + <> +
+ ( + + + Enter the delegate's address + + + + + + + )} + /> +
+ {DELEGATES_URL && ( +

+ You can visit{' '} + + this page + {' '} + to find the delegate who should represent for you, or + delegate the token to yourself. +

+ )} + + ) : ( +

+ Excellent. You are now ready to claim your rewards. +

+ )} +
+ + + + + + )} + {step === 'confirmation' && ( + +

All done!

+ +
+ {txHash && ( + +
- {DELEGATES_URL && ( -

- You can visit{' '} - - this page - {' '} - to find the delegate who should represent for you, or - delegate the token to yourself. -

- )} - - ) : ( -
-

Claim the token.

-

Ready to claim your rewards?

-
+ + )} -
- - - - - - )} - {step === 'confirmation' && ( - -

All done!

- -
- {txHash && ( - - - - )} -
-
- )} -
+
+
+ )} +
+ ); } diff --git a/src/components/common/GrantCard.tsx b/src/components/common/GrantCard.tsx index d7d5597..915db7a 100644 --- a/src/components/common/GrantCard.tsx +++ b/src/components/common/GrantCard.tsx @@ -24,7 +24,7 @@ const GrantCard = ({ grant }: { grant: Grant }) => { return ( <> - + {grant.currentUserCanClaim && isConnected && (

@@ -58,99 +58,107 @@ const GrantCard = ({ grant }: { grant: Grant }) => { )}

)} - +
{grant.campaign.ended && ( - Cancelled + Cancelled + )} + {grant.proof?.claimed && ( + Claimed )} - {grant.proof?.claimed && Claimed}
-
- -
-

{grant.title}

-

- {grant.description} -

-
-

- Date of award:{' '} - - {grant.date.toLocaleDateString()} - +

+
+ +
+

{grant.title}

+

+ {grant.description}

- {grant.tokenReleasedInDays && ( - <> - -

- Released in:{' '} - - {grant.tokenReleasedInDays}{' '} - {grant.tokenReleasedInDays > 1 ? 'days' : 'day'} - -

- - )} - {grant.delegateTo && ( - <> - -
-

Delegate to:

- - {truncate(grant.delegateTo, 11)}{' '} -
- - )} - {grant.latestClaimHash && ( - <> - - - - )}
+ + {grant.campaign.token && ( +
+ + + {grant.grantAmount} {grant.campaign.token.ticker} + +
+ )}
- {grant.campaign.token && ( -
- - - {grant.grantAmount} {grant.campaign.token.ticker} - +
+
+
+

+ Date of award:{' '} + + {grant.date.toLocaleDateString()} + +

+ {grant.tokenReleasedInDays && ( + <> + +

+ Released in:{' '} + + {grant.tokenReleasedInDays}{' '} + {grant.tokenReleasedInDays > 1 ? 'days' : 'day'} + +

+ + )} + {grant.delegateTo && ( + <> + +
+

Delegate to:

+ + {truncate(grant.delegateTo, 11)}{' '} +
+ + )} + {grant.latestClaimHash && ( + <> + + + + )}
- )} +
diff --git a/src/components/common/ProjectCard.tsx b/src/components/common/ProjectCard.tsx index a193a4d..b268a0f 100644 --- a/src/components/common/ProjectCard.tsx +++ b/src/components/common/ProjectCard.tsx @@ -3,12 +3,12 @@ import type { Grant } from '@/context/GrantsContext'; const ProjectCard = ({ grant }: { grant: Grant }) => { return ( - + -

Your project

+

Your project

Project logo diff --git a/src/components/common/images/BgImage.tsx b/src/components/common/images/BgImage.tsx index 0275d4e..d6426a7 100644 --- a/src/components/common/images/BgImage.tsx +++ b/src/components/common/images/BgImage.tsx @@ -4,6 +4,7 @@ import { WHITELABEL_ENV } from '../../../../config/features'; const getClassName = () => { switch (WHITELABEL_ENV) { case 'ZK_SYNC': + case 'BASE': return 'opacity-10'; default: return ''; diff --git a/src/components/common/images/Logo.tsx b/src/components/common/images/Logo.tsx index 78559c5..99de1e2 100644 --- a/src/components/common/images/Logo.tsx +++ b/src/components/common/images/Logo.tsx @@ -1,5 +1,6 @@ import Image from 'next/image'; import { WHITELABEL_ENV } from '../../../../config/features'; +import BaseLogo from '../../../../public/base-logo.svg'; import OpLogo from '../../../../public/op-logo.svg'; import ZkSyncLogo from '../../../../public/zksync_logo_dark.svg'; const Logo = () => { @@ -8,6 +9,8 @@ const Logo = () => { return OP Logo; case 'ZK_SYNC': return ZKSync Logo; + case 'BASE': + return Base Logo; default: throw new Error('Invalid WHITE_LABEL_ENV'); } diff --git a/src/components/common/images/ProjectImage.tsx b/src/components/common/images/ProjectImage.tsx index 636810b..ba1ec31 100644 --- a/src/components/common/images/ProjectImage.tsx +++ b/src/components/common/images/ProjectImage.tsx @@ -2,8 +2,8 @@ import { Hexagon } from 'lucide-react'; export const ProjectImage = ({ src: image }: { src?: string }) => { if (image) { - return Project logo; + return Project logo; } - return ; + return ; }; diff --git a/src/components/common/skeletons/GrantCardSkeleton.tsx b/src/components/common/skeletons/GrantCardSkeleton.tsx index affdac4..8fc4465 100644 --- a/src/components/common/skeletons/GrantCardSkeleton.tsx +++ b/src/components/common/skeletons/GrantCardSkeleton.tsx @@ -6,7 +6,7 @@ export const GrantCardSkeleton = () => {
- +
diff --git a/src/hooks/useGetGrants.ts b/src/hooks/useGetGrants.ts index efe3e48..cecd298 100644 --- a/src/hooks/useGetGrants.ts +++ b/src/hooks/useGetGrants.ts @@ -315,6 +315,9 @@ export const useGetGrants = () => { // Fetch hedgey campaigns from their graphql api const hedgeyCampaigns = await fetchCampaigns(campaignIds); + console.log('----> grants', grants); + console.log('----> hedgeyCampaigns', hedgeyCampaigns); + const [proofs, claimHistory] = await Promise.all([ // Fetch proofs from their rest api getProofs( @@ -332,6 +335,7 @@ export const useGetGrants = () => { }) .filter((x) => !!x), ), + // Fetch claim history from their graphql api getClaimHistory( address, @@ -341,6 +345,8 @@ export const useGetGrants = () => { })), ), ]); + console.log('----> proofs', proofs); + console.log('----> claimHistory', claimHistory); // Combine the data const mappedGrants = grants.data @@ -381,6 +387,8 @@ export const useGetGrants = () => { campaign.claimLockup, )?.daysUntilNextRelease; + console.log('----> grant', grant); + return { id: grant.uuid, title: grant.title, diff --git a/src/lib/getPublicClientForChain.ts b/src/lib/getPublicClientForChain.ts index e5b3d43..250decd 100644 --- a/src/lib/getPublicClientForChain.ts +++ b/src/lib/getPublicClientForChain.ts @@ -1,5 +1,6 @@ import { http, createPublicClient, isAddress } from 'viem'; import { + base, mainnet, optimism, optimismSepolia, @@ -19,6 +20,8 @@ export const getChainForChainId = (chainId: number) => { return optimismSepolia; case zksync.id: return zksync; + case base.id: + return base; default: throw new Error(`Unsupported chain: ${chainId}`); } @@ -36,6 +39,8 @@ const getRpcUrlForChain = (chainId: number) => { return 'https://sepolia.optimism.io'; case zksync.id: return 'https://mainnet.era.zksync.io'; + case base.id: + return 'https://base-rpc.publicnode.com'; default: throw new Error(`Unsupported chain: ${chainId}`); } @@ -53,6 +58,8 @@ export const getChainIdByNetworkName = (networkName: string | null) => { return optimismSepolia.id; case 'zksync-era': return zksync.id; + case 'base': + return base.id; default: throw new Error(`Unsupported network name: ${networkName}`); }