Skip to content

Commit

Permalink
🌑 Integrate with Gitcoin backend (#55)
Browse files Browse the repository at this point in the history
Co-authored-by: p-sad <[email protected]>
  • Loading branch information
pazernykormoran and p-sad authored May 9, 2024
1 parent 7d26659 commit da17eb1
Show file tree
Hide file tree
Showing 17 changed files with 254 additions and 60 deletions.
1 change: 1 addition & 0 deletions packages/frontend/.env.example
Original file line number Diff line number Diff line change
@@ -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
Expand Down
68 changes: 68 additions & 0 deletions packages/frontend/src/backend/getPassportScore.ts
Original file line number Diff line number Diff line change
@@ -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)
}
1 change: 0 additions & 1 deletion packages/frontend/src/blockchain/hooks/useAuctionState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ export type AuctionState =
| 'AwaitingResults'
| 'ClaimingFlow'
| 'ClaimingClosed'
| 'GitcoinFlow'

export function useAuctionState(): AuctionState | undefined {
const { address, chainId: userChainId } = useAccount()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -19,7 +18,6 @@ const UserActions: Record<AuctionState, () => ReactElement> = {
AwaitingResults: AwaitingResults,
ClaimingFlow: ClaimingFlow,
ClaimingClosed: ClaimingClosed,
GitcoinFlow: GitcoinFlow,
}

export const UserActionSection = () => {
Expand Down
8 changes: 2 additions & 6 deletions packages/frontend/src/components/userActious/bid/BidFlow.tsx
Original file line number Diff line number Diff line change
@@ -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()
Expand All @@ -15,9 +15,5 @@ export const BidFlow = () => {
}
}, [isTransactionViewLock, userBidExists])

return isInitialBid ? <PlaceBidFlow setTransactionViewLock={setTransactionViewLock} /> : <BumpBidFlow />
}

export interface FlowProps {
setTransactionViewLock: (value: boolean) => void
return isInitialBid ? <InitialBidFlow setTransactionViewLock={setTransactionViewLock} /> : <BumpBidFlow />
}
Original file line number Diff line number Diff line change
@@ -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<GitcoinCredentials | undefined>()
const [gitcoinSettled, setGitcoinSettled] = useState<boolean>(false)

if (!gitcoinCredentials || !gitcoinSettled) {
return (
<GitcoinFlow
setGitcoinCredentials={setGitcoinCredentials}
gitcoinCredentials={gitcoinCredentials}
gitcoinSettled={() => setGitcoinSettled(true)}
/>
)
}

return <PlaceBidFlow setTransactionViewLock={setTransactionViewLock} gitcoinCredentials={gitcoinCredentials} />
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,29 @@
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'
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>(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])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<Wrapper>
<FormHeading>Check Gitcoin Passport</FormHeading>
<FormRow>
<span>To place a bid we need to check your score. By verifying your score we checking if you are a human.</span>
</FormRow>
<Button wide>Check Score</Button>
<Button wide onClick={onCheckScoreClick}>
Check Score
</Button>
<SeparatorWithText text="Or" />
<FormRow>
<span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
{
Expand Down Expand Up @@ -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 (
<Wrapper>
<Row>
Expand All @@ -41,8 +77,10 @@ export const CheckGitcoinScore = () => {
<FormRow>
<span>It will take about 1 minute. Please stay on this page.</span>
</FormRow>
<Stepper steps={gitcoinScoreSteps} currentStep={1} isFailed={false} />
<Button>Sign Again</Button>
<Stepper steps={gitcoinScoreSteps} currentStep={step} isFailed={gitcoinRequestError} />
<Button isLoading={queryEnabled} disabled={!gitcoinRequestError} onClick={onSignAgainClick}>
Retry
</Button>
</Wrapper>
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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>(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 <CheckGitcoinPassport onCheckScoreClick={onCheckScoreClick} />
case GitcoinState.CHECKING_SCORE:
return (
<CheckGitcoinScore
setGitcoinCredentials={setCredentials}
gitcoinRequestSettled={isSuccess}
gitcoinRequestError={isError}
onSignAgainClick={sendForScoring}
/>
)
case GitcoinState.YOUR_SCORE:
return (
<UserGitcoinScore
userScore={Number(gitcoinCredentials?.score) / ScoreMultiplier}
gitcoinSettled={gitcoinSettled}
getBackToScoring={onCheckScoreClick}
/>
)
case GitcoinState.MISSING_PASSPORT:
return <MissingGitcoinPassport afterCreateClick={() => setGitcoinState(GitcoinState.INITIAL_PAGE)} />

default:
return <CheckGitcoinPassport onCheckScoreClick={onCheckScoreClick} />
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<FormWrapper>
<FormHeading>
Expand All @@ -32,7 +32,7 @@ export const InsufficientUserScore = ({ userScore }: UserScoreProps) => {
</p>
</div>
</FormRow>
<Button>Recalculate Score</Button>
<Button onClick={getBackToScoring}>Recalculate Score</Button>
</FormWrapper>
)
}
Expand Down
Loading

0 comments on commit da17eb1

Please sign in to comment.