-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Adds generation of attestation proofs (#104)
* Adds generation of attesattion proofs * Adds dynamic proofs from attestations * Fixes linter issues * Fixes review comments * Removes typo from .env.example
- Loading branch information
1 parent
d64b634
commit 91113ef
Showing
9 changed files
with
517 additions
and
349 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
"use client"; | ||
import { LoadingCircle } from "@/components/navigation/loading"; | ||
import { Button } from "@/components/ui/button"; | ||
import { | ||
Dialog, | ||
DialogClose, | ||
DialogContent, | ||
DialogDescription, | ||
DialogFooter, | ||
DialogHeader, | ||
DialogTitle, | ||
DialogTrigger, | ||
} from "@/components/ui/dialog"; | ||
import { useEthersSigner } from "@/lib/ethersUtils"; | ||
import axios from "axios"; | ||
import { useEffect, useState } from "react"; | ||
import { useAccount } from "wagmi"; | ||
import { createAttestation, createProofs } from "./utils/attestation-utils"; | ||
import { useSession } from "next-auth/react"; | ||
|
||
type GenerateAttestationModalProps = { | ||
teamId: string; | ||
}; | ||
|
||
export function GenerateAttestationModal({ | ||
...props | ||
}: GenerateAttestationModalProps) { | ||
const account = useAccount(); | ||
const signer = useEthersSigner(); | ||
const session = useSession(); | ||
const [userAddress, setUserAddress] = useState<string | undefined>(undefined); | ||
const [attestationPrivateData, setAttestationPrivateData] = useState<any>(); | ||
const [attestationUuid, setAttestationUuid] = useState<string | undefined>( | ||
undefined, | ||
); | ||
const [userSalt, setUserSalt] = useState<string | undefined>(undefined); | ||
const [userLogin, setUserLogin] = useState<string | undefined>(undefined); | ||
|
||
useEffect(() => { | ||
if (account) { | ||
setUserAddress(account.address); | ||
} | ||
}, [account]); | ||
|
||
useEffect(() => { | ||
if (session.data?.user?.name) { | ||
setUserLogin(session.data.githubLogin); | ||
} | ||
}, [session]); | ||
|
||
useEffect(() => { | ||
if ( | ||
signer && | ||
userAddress && | ||
attestationPrivateData && | ||
userLogin && | ||
userSalt | ||
) { | ||
createAttestation({ | ||
address: "0xB5E5559C6b85e8e867405bFFf3D15f59693eBE2f", | ||
privateData: attestationPrivateData, | ||
signer: signer, | ||
salt: userSalt, | ||
}).then((attestationUuid) => { | ||
setAttestationUuid(attestationUuid.attestationUuid); | ||
const proof = createProofs( | ||
attestationPrivateData, | ||
[userLogin], | ||
userSalt, | ||
); | ||
console.log(JSON.stringify(proof)); | ||
}); | ||
} | ||
}, [signer, userAddress, attestationPrivateData, userSalt]); | ||
|
||
const handleFetchAttestationPrivateData = async () => { | ||
const { data } = await axios.get( | ||
"/api/attestations?team_id=" + props.teamId, | ||
); | ||
if (data.success) { | ||
setAttestationPrivateData(data.privateAttestationData); | ||
setUserSalt(data.userSalt); | ||
} | ||
}; | ||
|
||
return ( | ||
<Dialog> | ||
<DialogTrigger asChild> | ||
<Button | ||
className="mr-2" | ||
disabled={!account.isConnected} | ||
onClick={() => { | ||
handleFetchAttestationPrivateData(); | ||
}} | ||
> | ||
{account.isConnected ? ( | ||
<>Create Attestation</> | ||
) : account.isConnecting || account.isReconnecting ? ( | ||
<>Connecting...</> | ||
) : ( | ||
<>Connect wallet to create attestation</> | ||
)} | ||
</Button> | ||
</DialogTrigger> | ||
<DialogContent className="sm:max-w-md"> | ||
<DialogHeader> | ||
<DialogTitle>Loading contribution graph</DialogTitle> | ||
<DialogDescription> | ||
Please wait while we are generating your contribution graph | ||
</DialogDescription> | ||
</DialogHeader> | ||
<div className="flex flex-col items-center space-x-2 pt-6 pb-6"> | ||
<LoadingCircle></LoadingCircle> | ||
</div> | ||
<DialogFooter className="sm:justify-start"> | ||
<DialogClose asChild onClick={() => {}}></DialogClose> | ||
</DialogFooter> | ||
</DialogContent> | ||
</Dialog> | ||
); | ||
} |
134 changes: 134 additions & 0 deletions
134
src/app/(dashboard)/attestation/utils/attestation-utils.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
import { EAS, SchemaEncoder } from "@ethereum-attestation-service/eas-sdk"; | ||
import { JsonRpcSigner } from "ethers"; | ||
import { StandardMerkleTree } from "@openzeppelin/merkle-tree"; | ||
import { | ||
EncodedMerkleValue, | ||
MerkleValueWithSalt, | ||
decodeMerkleValues, | ||
encodeMerkleValues, | ||
encodeValuesToMerkleTree, | ||
} from "./merkle-utils"; | ||
|
||
export type CreateAttestationBodyDto = { | ||
address: string; | ||
privateData: AttestationPrivateDataDto; | ||
signer: JsonRpcSigner; | ||
salt: string; | ||
}; | ||
|
||
export type AttestationPrivateDataDto = { | ||
[key: string]: string; | ||
}; | ||
|
||
export type ContributorDataDto = { | ||
githubUsername: string; | ||
rank: string; | ||
score: string; | ||
}; | ||
|
||
export type WeightsConfigDto = { | ||
prReview: string; | ||
pr: string; | ||
}; | ||
|
||
export type AttestationUuidDto = { | ||
attestationUuid: string; | ||
}; | ||
|
||
export async function createAttestation({ | ||
address, | ||
privateData, | ||
signer, | ||
salt, | ||
}: CreateAttestationBodyDto): Promise<AttestationUuidDto> { | ||
// Sepolia private data schema | ||
// @TODO Implement a mapping to reference the correct private data schema for each network | ||
const schemaUID = | ||
"0x20351f973fdec1478924c89dfa533d8f872defa108d9c3c6512267d7e7e5dbc2"; | ||
|
||
const merkleTree = createMerkleTree(privateData, salt); | ||
const merkleRoot = merkleTree.root; | ||
|
||
const eas = await initializeEAS(signer); | ||
const schemaEncoder = new SchemaEncoder("bytes32 privateData"); | ||
const encodedData = schemaEncoder.encodeData([ | ||
{ name: "privateData", value: merkleRoot, type: "bytes32" }, | ||
]); | ||
|
||
const tx = await eas.attest({ | ||
schema: schemaUID, | ||
data: { | ||
recipient: address, | ||
expirationTime: undefined, | ||
revocable: false, | ||
data: encodedData, | ||
}, | ||
}); | ||
const attestationUuid = await tx.wait(); | ||
|
||
return { attestationUuid: attestationUuid }; | ||
} | ||
|
||
export function createMerkleTree( | ||
privateData: AttestationPrivateDataDto, | ||
salt: string, | ||
): StandardMerkleTree<EncodedMerkleValue> { | ||
const merkleTreeValuesWithSalt: MerkleValueWithSalt[] = []; | ||
|
||
let privateDataKey: keyof AttestationPrivateDataDto; | ||
for (privateDataKey in privateData) { | ||
merkleTreeValuesWithSalt.push({ | ||
type: "string", | ||
name: privateDataKey, | ||
value: privateData[privateDataKey], | ||
salt: salt, | ||
} as MerkleValueWithSalt); | ||
} | ||
return encodeValuesToMerkleTree(merkleTreeValuesWithSalt); | ||
} | ||
|
||
export function createProofs( | ||
privateData: AttestationPrivateDataDto, | ||
fields: string[], | ||
salt: string, | ||
) { | ||
const merkleTree = createMerkleTree(privateData, salt); | ||
let privateDataKey: keyof AttestationPrivateDataDto; | ||
const valuesWithSalt: MerkleValueWithSalt[] = []; | ||
for (privateDataKey in privateData) { | ||
if (fields.includes(privateDataKey)) { | ||
valuesWithSalt.push( | ||
valueWithSalt(privateDataKey, privateData[privateDataKey], salt), | ||
); | ||
} | ||
} | ||
const merkleValues = encodeMerkleValues(valuesWithSalt); | ||
const multiproof = merkleTree.getMultiProof(merkleValues); | ||
const proofs = { | ||
...multiproof, | ||
leaves: decodeMerkleValues(multiproof.leaves), | ||
}; | ||
|
||
return { proofs: proofs }; | ||
} | ||
|
||
function valueWithSalt( | ||
field: string, | ||
value: string, | ||
salt: string, | ||
): MerkleValueWithSalt { | ||
return { | ||
type: "string", | ||
name: field, | ||
value: value, | ||
salt, | ||
}; | ||
} | ||
|
||
export async function initializeEAS(signer: JsonRpcSigner): Promise<EAS> { | ||
// Sepolia v0.26 schema contract | ||
const easContractAddress = "0xC2679fBD37d54388Ce493F1DB75320D236e1815e"; | ||
const eas = new EAS(easContractAddress); | ||
eas.connect(signer); | ||
return eas; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
import { AbiCoder } from "ethers"; | ||
import { StandardMerkleTree } from "@openzeppelin/merkle-tree"; | ||
|
||
export type FullMerkleDataTree = { | ||
root: string; | ||
values: MerkleValueWithSalt[]; | ||
}; | ||
|
||
export type FullMerkleProofTreeWithRelatedUid = FullMerkleDataTree & { | ||
relatedUid: string; | ||
}; | ||
|
||
export interface MerkleMultiProof { | ||
leaves: Leaf[]; | ||
proof: string[]; | ||
proofFlags: boolean[]; | ||
} | ||
|
||
export interface Leaf { | ||
type: string; | ||
name: string; | ||
value: any; | ||
salt: string; | ||
} | ||
|
||
export type PartialMerkleProof = { | ||
/** Hash of the partial proof */ | ||
id: string; | ||
root: string; | ||
proof: MerkleMultiProof; | ||
relatedUid: string; | ||
}; | ||
|
||
export interface MerkleValue { | ||
type: string; | ||
name: string; | ||
value: any; | ||
} | ||
|
||
export type MerkleValueWithSalt = MerkleValue & { salt: string }; | ||
|
||
export type EncodedMerkleValue = [string, string, string, string]; | ||
|
||
export const merkleValueAbiEncoding: EncodedMerkleValue = [ | ||
"string", | ||
"string", | ||
"bytes", | ||
"bytes32", | ||
]; | ||
|
||
export function encodeValuesToMerkleTree( | ||
valuesWithSalt: MerkleValueWithSalt[], | ||
) { | ||
const encodedValues = encodeMerkleValues(valuesWithSalt); | ||
|
||
return StandardMerkleTree.of(encodedValues, merkleValueAbiEncoding); | ||
} | ||
|
||
export function encodeMerkleValues( | ||
inValues: MerkleValueWithSalt[], | ||
): EncodedMerkleValue[] { | ||
const abiCoder = new AbiCoder(); | ||
return inValues.map((v) => [ | ||
v.type, | ||
v.name, | ||
abiCoder.encode([v.type], [v.value]), | ||
v.salt, | ||
]); | ||
} | ||
|
||
export function verifyFullMerkleTree(tree: FullMerkleDataTree): string { | ||
const encodedValues = encodeMerkleValues(tree.values); | ||
const merkleTree = StandardMerkleTree.of( | ||
encodedValues, | ||
merkleValueAbiEncoding, | ||
); | ||
return merkleTree.root; | ||
} | ||
|
||
export function decodeMerkleValues( | ||
inValues: EncodedMerkleValue[], | ||
): MerkleValueWithSalt[] { | ||
const abiCoder = new AbiCoder(); | ||
return inValues.map((v) => ({ | ||
type: v[0], | ||
name: v[1], | ||
value: abiCoder.decode([v[0]], v[2])[0], | ||
salt: v[3], | ||
})); | ||
} | ||
|
||
export function verifyMultiProof( | ||
root: string, | ||
proof: MerkleMultiProof, | ||
): boolean { | ||
const multiproof = { | ||
...proof, | ||
leaves: encodeMerkleValues(proof.leaves), | ||
}; | ||
|
||
return StandardMerkleTree.verifyMultiProof( | ||
root, | ||
merkleValueAbiEncoding, | ||
multiproof, | ||
); | ||
} |
Oops, something went wrong.