diff --git a/app/allocation/[category]/attestation/index.ts b/app/allocation/[category]/attestation/index.ts new file mode 100644 index 0000000..cc8feea --- /dev/null +++ b/app/allocation/[category]/attestation/index.ts @@ -0,0 +1,272 @@ +import { EAS, SchemaRegistry, SchemaEncoder } from '@ethereum-attestation-service/eas-sdk'; +import { Signer } from 'ethers'; +import { Wallet } from 'thirdweb/wallets'; +import { activeChain } from '@/app/lib/constants'; +import { axiosInstance } from '@/app/utils/axiosInstance'; +import { EASNetworks, SCHEMA_UID, convertRankingToAttestationFormat, generateRandomString, getPrevAttestationIds } from '../utils'; + +export enum AttestationState { + Initial, + Loading, + Success, + Error, +} + +type AttestFunc = { + ranking: { + id: number + name: string + ranking: { RF6Id: string, share: number }[] + } + signer: Signer + wallet: Wallet + setAttestationState: (state: AttestationState) => void + setAttestationLink: (link: string) => void +} + +export const attest = async ({ ranking, signer, wallet, setAttestationState, setAttestationLink }: AttestFunc) => { + // const localStorageTag = process.env.NEXT_PUBLIC_LOCAL_STORAGE_TAG!; + // const identityString = localStorage.getItem(localStorageTag); + + // if (!identityString) { + // console.error('Identity string is missing!'); + // router.push('/'); + // return; + // } + + // const identity = new Identity(identityString); + + if (!ranking) return; + + setAttestationState(AttestationState.Loading); + + const chainId = activeChain.id; + const easConfig = EASNetworks[chainId]; + const address = wallet?.getAccount()?.address; + + if (!easConfig) { + console.error('no eas config'); + return; + } + if (!signer || !address) { + console.error('signer', signer, 'address', address); + return; + } + + const eas = new EAS(easConfig.EASDeployment); + const schemaRegistry = new SchemaRegistry(easConfig.SchemaRegistry); + + eas.connect(signer as any); + schemaRegistry.connect(signer as any); + const schema = await schemaRegistry.getSchema({ uid: SCHEMA_UID }); + const schemaEncoder = new SchemaEncoder(schema.schema); + // let proof = ['']; + // setProgress(ProgressState.Creating); + try { + const item = await convertRankingToAttestationFormat( + ranking.ranking.map(({ RF6Id, share }) => ({ RF6Id, share })), + ranking.name, + // comment, + ); + + const schemaData = [ + { name: 'listName', type: 'string', value: item.listName }, + { + name: 'listMetadataPtr', + type: 'string', + value: item.listMetadataPtr, + }, + ]; + + // const signalData = { + // category: item.listName, + // value: item.listMetadataPtr, + // }; + + // // generate proof of vote + // const groupId = process.env.NEXT_PUBLIC_BANDADA_GROUP_ID!; + // const users = await getMembersGroup(groupId); + // if (users && identityString !== '{}') { + // const bandadaGroup = await getGroup(groupId); + // let treeDepth = 16; + // if (bandadaGroup === null) { + // console.log('The Bandada group does not exist:', groupId); + // } + // else { + // treeDepth = bandadaGroup.treeDepth; + // } + // const group = new Group(groupId, treeDepth, users); + // console.log('going to encode signalData: '); + // console.log(signalData); + // const signal = toBigInt( + // encodeBytes32String(signalData.toString()), + // ).toString(); + // const { + // proof: tempProof, + // merkleTreeRoot, + // nullifierHash, + // } = await generateProof(identity, group, groupId, signal); + // proof = tempProof; + // console.log('generated proof of vote: ', proof); + + // const { data: currentMerkleRoot, error: errorRootHistory } + // = await supabase + // .from('root_history') + // .select() + // .order('created_at', { ascending: false }) + // .limit(1); + + // if (errorRootHistory) { + // console.log(errorRootHistory); + // } + + // if (!currentMerkleRoot) { + // console.error('Wrong currentMerkleRoot'); + // } + + // if ( + // currentMerkleRoot == null + // || merkleTreeRoot !== currentMerkleRoot[0].root + // ) { + // // compare merkle tree roots + // const { + // data: dataMerkleTreeRoot, + // error: errorMerkleTreeRoot, + // } = await supabase + // .from('root_history') + // .select() + // .eq('root', merkleTreeRoot); + + // if (errorMerkleTreeRoot) { + // console.log(errorMerkleTreeRoot); + // } + + // console.log('merkleTreeRoot: ', merkleTreeRoot); + // console.log('dataMerkleTreeRoot: ', dataMerkleTreeRoot); + + // if (!dataMerkleTreeRoot) { + // console.error('Wrong dataMerkleTreeRoot'); + // } + // else if (dataMerkleTreeRoot.length === 0) { + // console.log('Merkle Root is not part of the group'); + // } + + // console.log('dataMerkleTreeRoot', dataMerkleTreeRoot); + // const merkleTreeRootDuration + // = bandadaGroup?.fingerprintDuration ?? 0; + + // if ( + // dataMerkleTreeRoot + // && Date.now() + // > Date.parse(dataMerkleTreeRoot[0].created_at) + // + merkleTreeRootDuration + // ) { + // console.log('Merkle Tree Root is expired'); + // } + // } + + // const { data: nullifier, error: errorNullifierHash } + // = await supabase + // .from('nullifier_hash') + // .select('nullifier') + // .eq('nullifier', nullifierHash); + + // if (errorNullifierHash) { + // console.log(errorNullifierHash); + // } + + // if (!nullifier) { + // console.log('Wrong nullifier'); + // } + // else if (nullifier.length > 0) { + // console.log('You are using the same nullifier twice'); + // } + + // const { error: errorNullifier } = await supabase + // .from('nullifier_hash') + // .insert([{ nullifier: nullifierHash }]); + + // if (errorNullifier) { + // console.error(errorNullifier); + // } + + // const { data: dataFeedback, error: errorFeedback } + // = await supabase + // .from('feedback') + // .insert([{ signal: schemaData }]) + // .select() + // .order('created_at', { ascending: false }); + + // if (errorFeedback) { + // console.error(errorFeedback); + // } + + // if (!dataFeedback) { + // console.error('Wrong dataFeedback'); + // } + + // // TODO everything is good so add the proof in attestation : Mahdi + // } + + const schemaDataWithProof = [ + ...schemaData, + { + name: 'proof', + type: 'string[]', + value: [generateRandomString(20)], + }, + ]; + + console.log('sdwp', schemaDataWithProof); + const encodedData = schemaEncoder.encodeData(schemaDataWithProof); + + const prevAttestations = await getPrevAttestationIds( + address, + SCHEMA_UID, + easConfig.gqlUrl, + ranking.name, + ); + + if (prevAttestations.length > 0) { + for (const id of prevAttestations) { + const revokedTransactions = await eas.revoke({ + schema: SCHEMA_UID, + data: { uid: id }, + }); + await revokedTransactions.wait(); + } + } + + const tx = await eas.attest({ + schema: SCHEMA_UID, + data: { + data: encodedData, + recipient: address, + revocable: true, + }, + }); + + const newAttestationUID = await tx.wait(); + + // posthog.capture('Attested', { + // attestedCategory: category?.data.collection?.name, + // }); + + console.log('attestaion id', newAttestationUID); + // await finishCollections(collectionId); + + const attestationLink = `${easConfig.explorer}/attestation/view/${newAttestationUID}`; + + await axiosInstance.post('/flow/report-attest', { + collectionId: ranking.id, + attestationId: attestationLink, + }); + + setAttestationState(AttestationState.Success); + setAttestationLink(attestationLink); + } + catch (e) { + console.error('error on sending tx:', e); + setAttestationState(AttestationState.Error); + } +}; diff --git a/app/allocation/[category]/page.tsx b/app/allocation/[category]/page.tsx index 7e67e6d..3455493 100644 --- a/app/allocation/[category]/page.tsx +++ b/app/allocation/[category]/page.tsx @@ -4,11 +4,6 @@ import { useCallback, useEffect, useState } from 'react'; import { useParams, useRouter } from 'next/navigation'; import debounce from 'lodash.debounce'; import { useActiveWallet } from 'thirdweb/react'; -import { - EAS, - SchemaEncoder, - SchemaRegistry, -} from '@ethereum-attestation-service/eas-sdk'; import RankingRow from './components/RankingRow'; import HeaderRF6 from '../../comparison/card/Header-RF6'; import Spinner from '@/app/components/Spinner'; @@ -33,13 +28,12 @@ import { IProjectRanking } from '@/app/comparison/utils/types'; import { ArrowLeft2Icon } from '@/public/assets/icon-components/ArrowLeft2'; import { ArrowRightIcon } from '@/public/assets/icon-components/ArrowRight'; import { modifyPercentage, RankItem } from '../utils'; -import { activeChain } from '@/app/lib/constants'; -import { convertRankingToAttestationFormat, EASNetworks, generateRandomString, getPrevAttestationIds, SCHEMA_UID, useSigner } from './utils'; -import { axiosInstance } from '@/app/utils/axiosInstance'; import Modal from '@/app/utils/Modal'; import AttestationSuccessModal from './attestation/AttestationSuccessModal'; import AttestationLoading from './attestation/AttestationLoading'; import AttestationError from './attestation/AttestationError'; +import { attest, AttestationState } from './attestation'; +import { useSigner } from './utils'; enum VotingStatus { VOTED, @@ -55,13 +49,6 @@ const votingStatusMap = { }, }; -enum AttestationState { - Initial, - Loading, - Success, - Error, -} - const RankingPage = () => { const params = useParams(); const router = useRouter(); @@ -213,257 +200,6 @@ const RankingPage = () => { } }; - const attest = async () => { - // const localStorageTag = process.env.NEXT_PUBLIC_LOCAL_STORAGE_TAG!; - // const identityString = localStorage.getItem(localStorageTag); - - // if (!identityString) { - // console.error('Identity string is missing!'); - // router.push('/'); - // return; - // } - - // const identity = new Identity(identityString); - - if (!ranking) return; - - setAttestationState(AttestationState.Loading); - - const chainId = activeChain.id; - const easConfig = EASNetworks[chainId]; - const address = wallet?.getAccount()?.address; - - if (!easConfig) { - console.error('no eas config'); - return; - } - if (!wallet) { - console.error('no wallet'); - return; - } - if (!signer || !address) { - console.error('signer', signer, 'address', address); - return; - } - - const eas = new EAS(easConfig.EASDeployment); - const schemaRegistry = new SchemaRegistry(easConfig.SchemaRegistry); - - eas.connect(signer as any); - schemaRegistry.connect(signer as any); - const schema = await schemaRegistry.getSchema({ uid: SCHEMA_UID }); - const schemaEncoder = new SchemaEncoder(schema.schema); - // let proof = ['']; - // setProgress(ProgressState.Creating); - try { - const item = await convertRankingToAttestationFormat( - ranking.ranking.map(el => ({ RF6Id: el.project.RF6Id, share: el.share })), - ranking.name, - // comment, - ); - - const schemaData = [ - { name: 'listName', type: 'string', value: item.listName }, - { - name: 'listMetadataPtr', - type: 'string', - value: item.listMetadataPtr, - }, - ]; - - // const signalData = { - // category: item.listName, - // value: item.listMetadataPtr, - // }; - - // // generate proof of vote - // const groupId = process.env.NEXT_PUBLIC_BANDADA_GROUP_ID!; - // const users = await getMembersGroup(groupId); - // if (users && identityString !== '{}') { - // const bandadaGroup = await getGroup(groupId); - // let treeDepth = 16; - // if (bandadaGroup === null) { - // console.log('The Bandada group does not exist:', groupId); - // } - // else { - // treeDepth = bandadaGroup.treeDepth; - // } - // const group = new Group(groupId, treeDepth, users); - // console.log('going to encode signalData: '); - // console.log(signalData); - // const signal = toBigInt( - // encodeBytes32String(signalData.toString()), - // ).toString(); - // const { - // proof: tempProof, - // merkleTreeRoot, - // nullifierHash, - // } = await generateProof(identity, group, groupId, signal); - // proof = tempProof; - // console.log('generated proof of vote: ', proof); - - // const { data: currentMerkleRoot, error: errorRootHistory } - // = await supabase - // .from('root_history') - // .select() - // .order('created_at', { ascending: false }) - // .limit(1); - - // if (errorRootHistory) { - // console.log(errorRootHistory); - // } - - // if (!currentMerkleRoot) { - // console.error('Wrong currentMerkleRoot'); - // } - - // if ( - // currentMerkleRoot == null - // || merkleTreeRoot !== currentMerkleRoot[0].root - // ) { - // // compare merkle tree roots - // const { - // data: dataMerkleTreeRoot, - // error: errorMerkleTreeRoot, - // } = await supabase - // .from('root_history') - // .select() - // .eq('root', merkleTreeRoot); - - // if (errorMerkleTreeRoot) { - // console.log(errorMerkleTreeRoot); - // } - - // console.log('merkleTreeRoot: ', merkleTreeRoot); - // console.log('dataMerkleTreeRoot: ', dataMerkleTreeRoot); - - // if (!dataMerkleTreeRoot) { - // console.error('Wrong dataMerkleTreeRoot'); - // } - // else if (dataMerkleTreeRoot.length === 0) { - // console.log('Merkle Root is not part of the group'); - // } - - // console.log('dataMerkleTreeRoot', dataMerkleTreeRoot); - // const merkleTreeRootDuration - // = bandadaGroup?.fingerprintDuration ?? 0; - - // if ( - // dataMerkleTreeRoot - // && Date.now() - // > Date.parse(dataMerkleTreeRoot[0].created_at) - // + merkleTreeRootDuration - // ) { - // console.log('Merkle Tree Root is expired'); - // } - // } - - // const { data: nullifier, error: errorNullifierHash } - // = await supabase - // .from('nullifier_hash') - // .select('nullifier') - // .eq('nullifier', nullifierHash); - - // if (errorNullifierHash) { - // console.log(errorNullifierHash); - // } - - // if (!nullifier) { - // console.log('Wrong nullifier'); - // } - // else if (nullifier.length > 0) { - // console.log('You are using the same nullifier twice'); - // } - - // const { error: errorNullifier } = await supabase - // .from('nullifier_hash') - // .insert([{ nullifier: nullifierHash }]); - - // if (errorNullifier) { - // console.error(errorNullifier); - // } - - // const { data: dataFeedback, error: errorFeedback } - // = await supabase - // .from('feedback') - // .insert([{ signal: schemaData }]) - // .select() - // .order('created_at', { ascending: false }); - - // if (errorFeedback) { - // console.error(errorFeedback); - // } - - // if (!dataFeedback) { - // console.error('Wrong dataFeedback'); - // } - - // // TODO everything is good so add the proof in attestation : Mahdi - // } - - const schemaDataWithProof = [ - ...schemaData, - { - name: 'proof', - type: 'string[]', - value: [generateRandomString(20)], - }, - ]; - - console.log('sdwp', schemaDataWithProof); - const encodedData = schemaEncoder.encodeData(schemaDataWithProof); - - const prevAttestations = await getPrevAttestationIds( - address, - SCHEMA_UID, - easConfig.gqlUrl, - ranking.name, - ); - - if (prevAttestations.length > 0) { - for (const id of prevAttestations) { - const revokedTransactions = await eas.revoke({ - schema: SCHEMA_UID, - data: { uid: id }, - }); - await revokedTransactions.wait(); - } - } - - const tx = await eas.attest({ - schema: SCHEMA_UID, - data: { - data: encodedData, - recipient: address, - revocable: true, - }, - }); - - const newAttestationUID = await tx.wait(); - - // posthog.capture('Attested', { - // attestedCategory: category?.data.collection?.name, - // }); - - console.log('attestaion id', newAttestationUID); - // await finishCollections(collectionId); - - const attestationLink = `${easConfig.explorer}/attestation/view/${newAttestationUID}`; - - await axiosInstance.post('/flow/report-attest', { - collectionId: ranking.id, - attestationId: attestationLink, - }); - - setAttestationState(AttestationState.Success); - setAttestationLink(attestationLink); - } - catch (e) { - console.error('error on sending tx:', e); - setAttestationState(AttestationState.Error); - } - }; - const submitVotes = async () => { console.log('Projects,', projects); if (!projects) return; @@ -477,7 +213,14 @@ const RankingPage = () => { await updateProjectRanking(); - await attest(); + if (!wallet || !ranking || !signer) { + console.error('Requirements not met for attestations'); + return; + } + + await attest({ ranking: { id: ranking.id, name: ranking.name, + ranking: projects.map(el => ({ RF6Id: el.project.RF6Id, share: el.share })) }, + setAttestationLink, setAttestationState, signer, wallet }); }; const handleAttestationModalClose = () => { diff --git a/app/allocation/page.tsx b/app/allocation/page.tsx index bdcb320..6bd5b45 100644 --- a/app/allocation/page.tsx +++ b/app/allocation/page.tsx @@ -14,7 +14,7 @@ import BudgetAllocation, { BudgetCategory, } from './components/BudgetAllocation'; import ConnectBox from './components/ConnectBox'; -import { modifyPercentage, RankItem } from './utils'; +import { modifyPercentage, RankItem, roundFractions } from './utils'; import { ArrowRightIcon } from '@/public/assets/icon-components/ArrowRight'; import { ArrowLeft2Icon } from '@/public/assets/icon-components/ArrowLeft2'; import { CustomizedSlider } from './components/Slider'; @@ -53,6 +53,11 @@ import BallotLoading from '../comparison/ballot/modals/BallotLoading'; import BallotSuccessModal from '../comparison/ballot/modals/BallotSuccessModal'; import BallotNotReady from '../comparison/ballot/modals/BallotNotReady'; import BallotErrorDelegated from '../comparison/ballot/modals/BallotErrorDelegated'; +import { attest, AttestationState } from './[category]/attestation'; +import AttestationError from './[category]/attestation/AttestationError'; +import AttestationLoading from './[category]/attestation/AttestationLoading'; +import AttestationSuccessModal from './[category]/attestation/AttestationSuccessModal'; +import { useSigner } from './[category]/utils'; const budgetCategory: BudgetCategory = { id: -1, @@ -81,6 +86,7 @@ enum BallotState { const AllocationPage = () => { const wallet = useActiveWallet(); const router = useRouter(); + const signer = useSigner(); const { address } = useAccount(); const { loggedToAgora } = useAuth(); const { isBadgeholder, category } = getJWTData(); @@ -97,6 +103,9 @@ const AllocationPage = () => { const budgetDelegateToYou = delegations?.toYou?.budget; const budgetDelegateFromYou = delegations?.fromYou?.budget; + const [attestationState, setAttestationState] = useState(AttestationState.Initial); + const [attestationLink, setAttestationLink] = useState(); + const [ballotState, setBallotState] = useState( BallotState.Initial ); @@ -124,12 +133,24 @@ const AllocationPage = () => { = useState>(); const [targetDelegate, setTargetDelegate] = useState(); - const { mutate: updateCategoriesRanking } = useUpdateCategoriesRanking({ + const { mutateAsync: updateCategoriesRanking } = useUpdateCategoriesRanking({ budget: totalValue, allocationPercentages: categoriesRanking?.map(el => el.percentage / 100) || [], }); + const handleSubmitVote = async () => { + await updateCategoriesRanking(); + + if (!wallet || !signer || !categoriesRanking) { + console.error('Requirements not met for attestations', wallet, signer, categoriesRanking); + return; + } + + await attest({ ranking: { id: -1, name: 'Budget', ranking: categoriesRanking.map(el => ({ RF6Id: el.RF6Id, share: el.percentage / 100 })) }, + setAttestationLink, setAttestationState, signer, wallet }); + }; + const handleDelegate = async (username: string, target: TargetDelegate) => { if (!categoryToDelegate) return; @@ -151,6 +172,24 @@ const AllocationPage = () => { setDelegationState(DelegationState.Success); }; + const handleAttestationModalClose = () => { + if (attestationState === AttestationState.Success) { + router.push('/allocation'); + } + else if (attestationState === AttestationState.Error) { + setAttestationState(AttestationState.Initial); + } + }; + + const handleVoteBudget = () => { + console.log('wallet?', wallet); + if (!wallet) { + setShowLoginModal(true); + return; + } + setAllocatingBudget(true); + }; + const handleLock = (id: RankItem['id']) => () => { try { if (!categoriesRanking) return; @@ -268,8 +307,9 @@ const AllocationPage = () => { if (categoryRankings) { setCategoriesRanking( categoryRankings.ranking.map(el => ({ + RF6Id: el.project.RF6Id, id: el.projectId, - percentage: Math.round(el.share * 100 * 100) / 100, + percentage: roundFractions(el.share * 100, 6), locked: false, budget: categoryRankings.budget * el.share, })) @@ -280,6 +320,22 @@ const AllocationPage = () => { return (
+ + {attestationState === AttestationState.Success && attestationLink && ( + setAttestationState(AttestationState.Initial)} + /> + )} + {attestationState === AttestationState.Loading && } + {attestationState === AttestationState.Error && } + {}} @@ -470,9 +526,7 @@ const AllocationPage = () => { setCategoryToDelegate(budgetCategory); setDelegationState(DelegationState.DelegationMethod); }} - onScore={() => { - setAllocatingBudget(true); - }} + onScore={handleVoteBudget} username={budgetDelegateFromYou?.metadata?.username} /> )} @@ -528,9 +582,7 @@ const AllocationPage = () => {