From da17eb1213c3f614c55bd9c69a8d033878513419 Mon Sep 17 00:00:00 2001 From: pazerny kormoran <38940023+pazernykormoran@users.noreply.github.com> Date: Thu, 9 May 2024 14:14:14 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=8C=91=20Integrate=20with=20Gitcoin=20bac?= =?UTF-8?q?kend=20(#55)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: p-sad --- packages/frontend/.env.example | 1 + .../frontend/src/backend/getPassportScore.ts | 68 +++++++++++++++++ .../src/blockchain/hooks/useAuctionState.ts | 1 - .../userActious/UserActionSection.tsx | 2 - .../components/userActious/bid/BidFlow.tsx | 8 +- .../userActious/bid/InitialBidFlow.tsx | 25 ++++++ .../userActious/bid/PlaceBid/PlaceBidFlow.tsx | 15 +++- ...nPassword.tsx => CheckGitcoinPassport.tsx} | 10 ++- .../gitcoin/CheckingGitcoinScore.tsx | 44 ++++++++++- .../userActious/gitcoin/GitcoinFlow.tsx | 76 +++++++++++++++++++ .../userActious/gitcoin/GitcointFlow.tsx | 30 -------- .../gitcoin/InsufficientUserScore.tsx | 4 +- .../gitcoin/SufficientUserScore.tsx | 4 +- .../userActious/gitcoin/UserGitcoinScore.tsx | 14 ++-- .../src/pages/api/scorer/[userAddress].ts | 2 +- packages/frontend/src/types/api/scorer.ts | 4 +- .../src/types/passport/GticoinCredentials.ts | 6 ++ 17 files changed, 254 insertions(+), 60 deletions(-) create mode 100644 packages/frontend/src/backend/getPassportScore.ts create mode 100644 packages/frontend/src/components/userActious/bid/InitialBidFlow.tsx rename packages/frontend/src/components/userActious/gitcoin/{CheckGitcoinPassword.tsx => CheckGitcoinPassport.tsx} (78%) create mode 100644 packages/frontend/src/components/userActious/gitcoin/GitcoinFlow.tsx delete mode 100644 packages/frontend/src/components/userActious/gitcoin/GitcointFlow.tsx create mode 100644 packages/frontend/src/types/passport/GticoinCredentials.ts diff --git a/packages/frontend/.env.example b/packages/frontend/.env.example index 0bf114e82..d9a242c29 100644 --- a/packages/frontend/.env.example +++ b/packages/frontend/.env.example @@ -1,4 +1,5 @@ NEXT_PUBLIC_INFURA_KEY= +NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID= NEXT_PUBLIC_VOUCHER_REDEEM_DEADLINE="2024-04-25T17:44:44.773Z" RATE_LIMIT_GLOBAL=100 RATE_LIMIT_NONCES=5 diff --git a/packages/frontend/src/backend/getPassportScore.ts b/packages/frontend/src/backend/getPassportScore.ts new file mode 100644 index 000000000..0e9c7cedd --- /dev/null +++ b/packages/frontend/src/backend/getPassportScore.ts @@ -0,0 +1,68 @@ +import { isApiErrorResponse } from '@/types/api/error' +import { + GetPassportScorerNonceResponseSchema, + GetResponseSchema, + SubmitAddressForScoringRequest, + SubmitAddressForScoringResponseSchema, +} from '@/types/api/scorer' +import { useMutation } from '@tanstack/react-query' +import { Hex } from 'viem' +import { useAccount, useChainId, useSignMessage } from 'wagmi' + +export const useSendForScoring = () => { + const { address } = useAccount() + const chainId = useChainId() + const { signMessageAsync } = useSignMessage() + + return useMutation({ + mutationFn: async () => { + if (!address) { + throw new Error('No address') + } + + try { + return await getGitcoinScore(address, chainId) + } catch (error) { + if (!(error instanceof ErrorWithStatus) || error.status != 404) { + throw error + } + } + + const nonceData = await getGitcoinNonce() + const signature = await signMessageAsync({ message: nonceData.message }) + await sendForScoring({ userAddress: address as Hex, signature, nonce: nonceData.nonce }) + }, + }) +} + +class ErrorWithStatus extends Error { + constructor(message: string, public status: number) { + super(message) + } +} + +export const getGitcoinScore = async (userAddress: Hex, chainId: number) => { + const result = await fetch(`/api/scorer/${userAddress}?chainId=${chainId}`) + const data = GetResponseSchema.parse(await result.json()) + if (isApiErrorResponse(data)) throw new ErrorWithStatus(data.error, result.status) + return data +} + +const getGitcoinNonce = async () => { + const response = await fetch(`/api/scorer/nonce`) + const data = GetPassportScorerNonceResponseSchema.parse(await response.json()) + if (isApiErrorResponse(data)) throw new Error(data.error) + return data +} + +const sendForScoring = async (requestData: SubmitAddressForScoringRequest) => { + const response = await fetch(`/api/scorer`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(requestData), + }) + const data = SubmitAddressForScoringResponseSchema.parse(await response.json()) + if (isApiErrorResponse(data)) throw new Error(data.error) +} diff --git a/packages/frontend/src/blockchain/hooks/useAuctionState.ts b/packages/frontend/src/blockchain/hooks/useAuctionState.ts index 265079165..e7ede536c 100644 --- a/packages/frontend/src/blockchain/hooks/useAuctionState.ts +++ b/packages/frontend/src/blockchain/hooks/useAuctionState.ts @@ -12,7 +12,6 @@ export type AuctionState = | 'AwaitingResults' | 'ClaimingFlow' | 'ClaimingClosed' - | 'GitcoinFlow' export function useAuctionState(): AuctionState | undefined { const { address, chainId: userChainId } = useAccount() diff --git a/packages/frontend/src/components/userActious/UserActionSection.tsx b/packages/frontend/src/components/userActious/UserActionSection.tsx index 3ac0bb361..5bacf7e17 100644 --- a/packages/frontend/src/components/userActious/UserActionSection.tsx +++ b/packages/frontend/src/components/userActious/UserActionSection.tsx @@ -3,7 +3,6 @@ import { Colors } from '@/styles/colors' import { ReactElement } from 'react' import styled from 'styled-components' import { ConnectWalletWarning } from './ConnectWalletWarning' -import { GitcoinFlow } from './gitcoin/GitcointFlow' import { BidFlow } from './bid/BidFlow' import { AwaitingResults } from '@/components/userActious/claim/AwaitingResults' import { ClaimingClosed } from '@/components/userActious/claim/ClaimingClosed' @@ -19,7 +18,6 @@ const UserActions: Record ReactElement> = { AwaitingResults: AwaitingResults, ClaimingFlow: ClaimingFlow, ClaimingClosed: ClaimingClosed, - GitcoinFlow: GitcoinFlow, } export const UserActionSection = () => { diff --git a/packages/frontend/src/components/userActious/bid/BidFlow.tsx b/packages/frontend/src/components/userActious/bid/BidFlow.tsx index b2ae1b6b7..a13841cd3 100644 --- a/packages/frontend/src/components/userActious/bid/BidFlow.tsx +++ b/packages/frontend/src/components/userActious/bid/BidFlow.tsx @@ -1,7 +1,7 @@ import { useUserBid } from '@/blockchain/hooks/useUserBid' import { useEffect, useState } from 'react' -import { PlaceBidFlow } from './PlaceBid/PlaceBidFlow' import { BumpBidFlow } from './BumpBid/BumpBidFlow' +import { InitialBidFlow } from './InitialBidFlow' export const BidFlow = () => { const userBid = useUserBid() @@ -15,9 +15,5 @@ export const BidFlow = () => { } }, [isTransactionViewLock, userBidExists]) - return isInitialBid ? : -} - -export interface FlowProps { - setTransactionViewLock: (value: boolean) => void + return isInitialBid ? : } diff --git a/packages/frontend/src/components/userActious/bid/InitialBidFlow.tsx b/packages/frontend/src/components/userActious/bid/InitialBidFlow.tsx new file mode 100644 index 000000000..881713a09 --- /dev/null +++ b/packages/frontend/src/components/userActious/bid/InitialBidFlow.tsx @@ -0,0 +1,25 @@ +import { GitcoinCredentials } from '@/types/passport/GticoinCredentials' +import { useState } from 'react' +import { GitcoinFlow } from '../gitcoin/GitcoinFlow' +import { PlaceBidFlow } from './PlaceBid/PlaceBidFlow' + +interface Props { + setTransactionViewLock: (value: boolean) => void +} + +export const InitialBidFlow = ({ setTransactionViewLock }: Props) => { + const [gitcoinCredentials, setGitcoinCredentials] = useState() + const [gitcoinSettled, setGitcoinSettled] = useState(false) + + if (!gitcoinCredentials || !gitcoinSettled) { + return ( + setGitcoinSettled(true)} + /> + ) + } + + return +} diff --git a/packages/frontend/src/components/userActious/bid/PlaceBid/PlaceBidFlow.tsx b/packages/frontend/src/components/userActious/bid/PlaceBid/PlaceBidFlow.tsx index e2789eab0..30ce99ba4 100644 --- a/packages/frontend/src/components/userActious/bid/PlaceBid/PlaceBidFlow.tsx +++ b/packages/frontend/src/components/userActious/bid/PlaceBid/PlaceBidFlow.tsx @@ -1,5 +1,4 @@ import { useAccount } from 'wagmi' -import { FlowProps } from '../BidFlow' import { TxFlowSteps } from '@/components/auction/TxFlowSteps' import { useEffect, useMemo, useState } from 'react' import { useMinimumBid } from '@/blockchain/hooks/useMinimumBid' @@ -7,14 +6,24 @@ import { PlaceBidForm } from './PlaceBidForm' import { formatEther, parseEther } from 'viem' import { AuctionTransaction } from '@/components/auction/AuctionTransaction' import { usePlaceBid } from './usePlaceBid' +import { GitcoinCredentials } from '@/types/passport/GticoinCredentials' -export const PlaceBidFlow = ({ setTransactionViewLock }: FlowProps) => { +interface FlowProps { + setTransactionViewLock: (value: boolean) => void + gitcoinCredentials: GitcoinCredentials +} + +export const PlaceBidFlow = ({ setTransactionViewLock, gitcoinCredentials }: FlowProps) => { const { address } = useAccount() const [view, setView] = useState(TxFlowSteps.Placing) const minimumBid = useMinimumBid() const [bid, setBid] = useState('0') const parsedBid = useMemo(() => parseEther(bid || '0'), [bid]) - const bidAction = usePlaceBid({ value: parsedBid, score: BigInt(20), proof: '0x' }) + const bidAction = usePlaceBid({ + value: parsedBid, + score: BigInt(gitcoinCredentials.score), + proof: gitcoinCredentials.proof, + }) useEffect(() => setTransactionViewLock(bidAction.status !== 'idle'), [bidAction.status, setTransactionViewLock]) useEffect(() => setView(TxFlowSteps.Placing), [address]) diff --git a/packages/frontend/src/components/userActious/gitcoin/CheckGitcoinPassword.tsx b/packages/frontend/src/components/userActious/gitcoin/CheckGitcoinPassport.tsx similarity index 78% rename from packages/frontend/src/components/userActious/gitcoin/CheckGitcoinPassword.tsx rename to packages/frontend/src/components/userActious/gitcoin/CheckGitcoinPassport.tsx index 2fd240eff..5f90c97c0 100644 --- a/packages/frontend/src/components/userActious/gitcoin/CheckGitcoinPassword.tsx +++ b/packages/frontend/src/components/userActious/gitcoin/CheckGitcoinPassport.tsx @@ -3,14 +3,20 @@ import { FormHeading, FormRow, FormWrapper } from '../../form' import { Button } from '../../buttons' import { SeparatorWithText } from '@/components/common/Separator' -export const CheckGitcoinPassword = () => { +interface Props { + onCheckScoreClick: () => void +} + +export const CheckGitcoinPassport = ({ onCheckScoreClick }: Props) => { return ( Check Gitcoin Passport To place a bid we need to check your score. By verifying your score we checking if you are a human. - + diff --git a/packages/frontend/src/components/userActious/gitcoin/CheckingGitcoinScore.tsx b/packages/frontend/src/components/userActious/gitcoin/CheckingGitcoinScore.tsx index 2446ed226..47b25753e 100644 --- a/packages/frontend/src/components/userActious/gitcoin/CheckingGitcoinScore.tsx +++ b/packages/frontend/src/components/userActious/gitcoin/CheckingGitcoinScore.tsx @@ -3,6 +3,12 @@ import { FormHeading, FormRow, FormWrapper } from '../../form' import { Stepper } from '@/components/stepper/Stepper' import { ClockIcon } from '@/components/icons' import { Button } from '@/components/buttons' +import { useEffect } from 'react' +import { useQuery } from '@tanstack/react-query' +import { useAccount, useChainId } from 'wagmi' +import { Hex } from 'viem' +import { getGitcoinScore } from '@/backend/getPassportScore' +import { GetScoreResponseSuccess } from '@/types/api/scorer' const gitcoinScoreSteps = [ { @@ -31,7 +37,37 @@ const gitcoinScoreSteps = [ }, ] -export const CheckGitcoinScore = () => { +interface CheckGitcoinScoreProps { + setGitcoinCredentials: (credentials: GetScoreResponseSuccess) => void + gitcoinRequestSettled: boolean + gitcoinRequestError: boolean + onSignAgainClick: () => void +} + +export const CheckGitcoinScore = ({ + setGitcoinCredentials, + gitcoinRequestSettled, + gitcoinRequestError, + onSignAgainClick, +}: CheckGitcoinScoreProps) => { + const { address } = useAccount() + const chainId = useChainId() + const queryEnabled = !!(gitcoinRequestSettled && address) + const { data } = useQuery({ + queryKey: ['gitcoinScore', address, chainId], + queryFn: () => getGitcoinScore(address as Hex, chainId), + enabled: queryEnabled, + retry: true, + retryDelay: 5000, + }) + const step = gitcoinRequestSettled ? 2 : 1 + + useEffect(() => { + if (data && data.status == 'done') { + setGitcoinCredentials(data) + } + }, [data, setGitcoinCredentials]) + return ( @@ -41,8 +77,10 @@ export const CheckGitcoinScore = () => { It will take about 1 minute. Please stay on this page. - - + + ) } diff --git a/packages/frontend/src/components/userActious/gitcoin/GitcoinFlow.tsx b/packages/frontend/src/components/userActious/gitcoin/GitcoinFlow.tsx new file mode 100644 index 000000000..de4f4b3da --- /dev/null +++ b/packages/frontend/src/components/userActious/gitcoin/GitcoinFlow.tsx @@ -0,0 +1,76 @@ +import { useState } from 'react' +import { CheckGitcoinPassport } from './CheckGitcoinPassport' +import { CheckGitcoinScore } from './CheckingGitcoinScore' +import { UserGitcoinScore } from '@/components/userActious/gitcoin/UserGitcoinScore' +import { MissingGitcoinPassport } from './MissingGitcoinPassport' +import { useSendForScoring } from '@/backend/getPassportScore' +import { GitcoinCredentials } from '@/types/passport/GticoinCredentials' +import { GetScoreResponseSuccess } from '@/types/api/scorer' + +enum GitcoinState { + INITIAL_PAGE, + CHECKING_SCORE, + MISSING_PASSPORT, + YOUR_SCORE, +} + +const ScoreDecimals = 8 +const ScoreMultiplier = 10 ** ScoreDecimals + +interface Props { + setGitcoinCredentials: (credentials: GitcoinCredentials) => void + gitcoinCredentials: GitcoinCredentials | undefined + gitcoinSettled: () => void +} + +export const GitcoinFlow = ({ gitcoinCredentials, setGitcoinCredentials, gitcoinSettled }: Props) => { + const [gitcoinState, setGitcoinState] = useState(GitcoinState.INITIAL_PAGE) + const { mutateAsync, isSuccess, isError } = useSendForScoring() + + const setCredentials = (credentials: GetScoreResponseSuccess) => { + setGitcoinCredentials({ + score: BigInt(credentials?.score), + proof: credentials.signature, + }) + setGitcoinState(GitcoinState.YOUR_SCORE) + } + + const sendForScoring = async () => { + const data = await mutateAsync() + if (data?.status === 'done') { + setCredentials(data) + } + } + + const onCheckScoreClick = () => { + setGitcoinState(GitcoinState.CHECKING_SCORE) + sendForScoring() + } + + switch (gitcoinState) { + case GitcoinState.INITIAL_PAGE: + return + case GitcoinState.CHECKING_SCORE: + return ( + + ) + case GitcoinState.YOUR_SCORE: + return ( + + ) + case GitcoinState.MISSING_PASSPORT: + return setGitcoinState(GitcoinState.INITIAL_PAGE)} /> + + default: + return + } +} diff --git a/packages/frontend/src/components/userActious/gitcoin/GitcointFlow.tsx b/packages/frontend/src/components/userActious/gitcoin/GitcointFlow.tsx deleted file mode 100644 index ce0a34882..000000000 --- a/packages/frontend/src/components/userActious/gitcoin/GitcointFlow.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { useState } from 'react' -import { CheckGitcoinPassword } from './CheckGitcoinPassword' -import { CheckGitcoinScore } from './CheckingGitcoinScore' -import { UserGitcoinScore } from '@/components/userActious/gitcoin/UserGitcoinScore' -import { MissingGitcoinPassport } from './MissingGitcoinPassport' - -enum GitcoinState { - INITIAL_PAGE, - CHECKING_SCORE, - MISSING_PASSPORT, - YOUR_SCORE, -} - -export const GitcoinFlow = () => { - const [gitcoinState, setGitcoinState] = useState(GitcoinState.MISSING_PASSPORT) - - switch (gitcoinState) { - case GitcoinState.INITIAL_PAGE: - return - case GitcoinState.CHECKING_SCORE: - return - case GitcoinState.YOUR_SCORE: - return - case GitcoinState.MISSING_PASSPORT: - return setGitcoinState(GitcoinState.INITIAL_PAGE)} /> - - default: - return - } -} diff --git a/packages/frontend/src/components/userActious/gitcoin/InsufficientUserScore.tsx b/packages/frontend/src/components/userActious/gitcoin/InsufficientUserScore.tsx index 2527e6421..226e7f46a 100644 --- a/packages/frontend/src/components/userActious/gitcoin/InsufficientUserScore.tsx +++ b/packages/frontend/src/components/userActious/gitcoin/InsufficientUserScore.tsx @@ -7,7 +7,7 @@ import { urls } from '@/constants/urls' import { UserScoreProps } from '@/components/userActious/gitcoin/UserGitcoinScore' import { environment } from '@/config/environment' -export const InsufficientUserScore = ({ userScore }: UserScoreProps) => { +export const InsufficientUserScore = ({ userScore, getBackToScoring }: UserScoreProps) => { return ( @@ -32,7 +32,7 @@ export const InsufficientUserScore = ({ userScore }: UserScoreProps) => {

- + ) } diff --git a/packages/frontend/src/components/userActious/gitcoin/SufficientUserScore.tsx b/packages/frontend/src/components/userActious/gitcoin/SufficientUserScore.tsx index dca23e72a..50caa8934 100644 --- a/packages/frontend/src/components/userActious/gitcoin/SufficientUserScore.tsx +++ b/packages/frontend/src/components/userActious/gitcoin/SufficientUserScore.tsx @@ -5,7 +5,7 @@ import { environment } from '@/config/environment' import styled from 'styled-components' import { Colors } from '@/styles/colors' -export const SufficientUserScore = ({ userScore }: UserScoreProps) => { +export const SufficientUserScore = ({ userScore, gitcoinSettled }: UserScoreProps) => { return ( @@ -20,7 +20,7 @@ export const SufficientUserScore = ({ userScore }: UserScoreProps) => {

You can place your bid now!

- +
) } diff --git a/packages/frontend/src/components/userActious/gitcoin/UserGitcoinScore.tsx b/packages/frontend/src/components/userActious/gitcoin/UserGitcoinScore.tsx index 580fab0fd..2151c3588 100644 --- a/packages/frontend/src/components/userActious/gitcoin/UserGitcoinScore.tsx +++ b/packages/frontend/src/components/userActious/gitcoin/UserGitcoinScore.tsx @@ -2,17 +2,17 @@ import { SufficientUserScore } from '@/components/userActious/gitcoin/Sufficient import { InsufficientUserScore } from '@/components/userActious/gitcoin/InsufficientUserScore' import { environment } from '@/config/environment' -const userScore = 17 - export interface UserScoreProps { - userScore: number + userScore?: number + gitcoinSettled?: () => void + getBackToScoring?: () => void } -export const UserGitcoinScore = () => { - const isSufficientScore = userScore >= environment.gitcoinRequiredScore +export const UserGitcoinScore = ({ userScore, gitcoinSettled, getBackToScoring }: UserScoreProps) => { + const isSufficientScore = userScore && userScore >= environment.gitcoinRequiredScore return isSufficientScore ? ( - + ) : ( - + ) } diff --git a/packages/frontend/src/pages/api/scorer/[userAddress].ts b/packages/frontend/src/pages/api/scorer/[userAddress].ts index ae6af2965..25693f9fe 100644 --- a/packages/frontend/src/pages/api/scorer/[userAddress].ts +++ b/packages/frontend/src/pages/api/scorer/[userAddress].ts @@ -61,7 +61,7 @@ async function getScore(req: NextApiRequest, res: NextApiResponse + +export const GetResponseSchema = z.union([ ErrorResponseSchema, GetScoreResponseProcessingSchema, GetScoreResponseSuccessSchema, diff --git a/packages/frontend/src/types/passport/GticoinCredentials.ts b/packages/frontend/src/types/passport/GticoinCredentials.ts new file mode 100644 index 000000000..35e152867 --- /dev/null +++ b/packages/frontend/src/types/passport/GticoinCredentials.ts @@ -0,0 +1,6 @@ +import { Hex } from 'viem' + +export type GitcoinCredentials = { + score: bigint + proof: Hex +}