-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Add weekly claims model * ci: version bump to 0.93.1-rc-weekly-claims.0 * Add onchain claims * ci: version bump to 0.93.2-rc-weekly-claims.0 * Relocate protocol eas and merkle proofs to core * ci: version bump to 0.94.1-rc-weekly-claims.0 * Fix encoder * Fix userRefUID typing * ci: version bump to 0.94.1-rc-weekly-claims.1 * Bump version * ci: version bump to 0.94.1-rc-weekly-claims.3 * Fix type for merkle tree proofs return type * ci: version bump to 0.94.1-rc-weekly-claims.4 * Cleanup schema definitions * ci: version bump to 0.94.1-rc-weekly-claims.5 * Add encoding for name schema args * ci: version bump to 0.94.1-rc-weekly-claims.6 * Fix encoder for name schema * ci: version bump to 0.94.1-rc-weekly-claims.7 * Add onchain scout profile * ci: version bump to 0.94.1-rc-weekly-claims.8 * Rename the schema * Fix build * Fix schema name * ci: version bump to 0.94.1-rc-weekly-claims.9 * ci: version bump to 0.94.1-rc-weekly-claims.10 * Resync prisma with main branch * ci: version bump to 0.95.1-rc-weekly-claims.0 --------- Co-authored-by: Automated Version Bump <[email protected]>
- Loading branch information
Showing
11 changed files
with
423 additions
and
58 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import { getSchemaUID, SchemaEncoder } from '@ethereum-attestation-service/eas-sdk'; | ||
|
||
export const NULL_EAS_REF_UID = '0x0000000000000000000000000000000000000000000000000000000000000000'; | ||
|
||
export const NULL_EVM_ADDRESS = '0x0000000000000000000000000000000000000000'; | ||
|
||
// This allows us to encode the schemaId and name of a name schema attestation | ||
// Obtained from https://github.com/ethereum-attestation-service/eas-contracts/blob/558250dae4cb434859b1ac3b6d32833c6448be21/deploy/scripts/000004-name-initial-schemas.ts#L10C1-L11C1 | ||
export const NAME_SCHEMA_DEFINITION = 'bytes32 schemaId,string name'; | ||
|
||
export const NAME_SCHEMA_UID = getSchemaUID(NAME_SCHEMA_DEFINITION, NULL_EVM_ADDRESS, true) as `0x${string}`; | ||
|
||
export type NameSchemaAttestation = { | ||
schemaId: `0x${string}`; | ||
name: string; | ||
}; | ||
|
||
export function encodeNameSchemaAttestation({ name, schemaId }: NameSchemaAttestation): `0x${string}` { | ||
const encoder = new SchemaEncoder(NAME_SCHEMA_DEFINITION); | ||
|
||
return encoder.encodeData([ | ||
{ name: 'schemaId', type: 'bytes32', value: schemaId }, | ||
{ name: 'name', type: 'string', value: name } | ||
]) as `0x${string}`; | ||
} |
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,62 @@ | ||
import { SchemaEncoder } from '@ethereum-attestation-service/eas-sdk'; | ||
import type { EASSchema } from 'protocol'; | ||
|
||
const contributionReceiptEASSchema = | ||
'bytes32 userRefUID,string description,string url,string metadataUrl,uint256 value,string type'; | ||
|
||
const contributionReceiptSchemaName = 'Contribution Receipt'; | ||
|
||
export const contributionSchemaDefinition: EASSchema = { | ||
schema: contributionReceiptEASSchema, | ||
name: contributionReceiptSchemaName | ||
}; | ||
|
||
export type ContributionReceiptAttestation = { | ||
userRefUID: `0x${string}`; | ||
description: string; | ||
url: string; | ||
metadataUrl: string; | ||
value: number; | ||
type: string; | ||
}; | ||
|
||
const encoder = new SchemaEncoder(contributionReceiptEASSchema); | ||
|
||
export function encodeContributionReceiptAttestation(attestation: ContributionReceiptAttestation): `0x${string}` { | ||
const encodedData = encoder.encodeData([ | ||
{ name: 'userRefUID', type: 'bytes32', value: attestation.userRefUID }, | ||
{ name: 'description', type: 'string', value: attestation.description }, | ||
{ | ||
name: 'url', | ||
type: 'string', | ||
value: attestation.url | ||
}, | ||
{ | ||
name: 'metadataUrl', | ||
type: 'string', | ||
value: attestation.metadataUrl | ||
}, | ||
{ name: 'value', type: 'uint256', value: attestation.value }, | ||
{ name: 'type', type: 'string', value: attestation.type } | ||
]); | ||
|
||
return encodedData as `0x${string}`; | ||
} | ||
|
||
export function decodeContributionReceiptAttestation(rawData: string): ContributionReceiptAttestation { | ||
const parsed = encoder.decodeData(rawData); | ||
const values = parsed.reduce((acc, item) => { | ||
const key = item.name as keyof ContributionReceiptAttestation; | ||
|
||
if (key === 'value') { | ||
acc[key] = parseInt(item.value.value as string); | ||
} else if (key === 'userRefUID') { | ||
acc[key] = item.value.value as `0x${string}`; | ||
} else { | ||
acc[key] = item.value.value as string; | ||
} | ||
return acc; | ||
}, {} as ContributionReceiptAttestation); | ||
|
||
return values as ContributionReceiptAttestation; | ||
} |
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,10 @@ | ||
import { contributionSchemaDefinition } from './contributionReceiptSchema'; | ||
import { scoutGameUserProfileSchemaDefinition } from './scoutGameUserProfileSchema'; | ||
import type { EASSchema } from './types'; | ||
|
||
export * from './constants'; | ||
export * from './contributionReceiptSchema'; | ||
export * from './scoutGameUserProfileSchema'; | ||
export * from './types'; | ||
|
||
export const allSchemas: EASSchema[] = [contributionSchemaDefinition, scoutGameUserProfileSchemaDefinition]; |
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,40 @@ | ||
import { SchemaEncoder } from '@ethereum-attestation-service/eas-sdk'; | ||
import type { EASSchema } from 'protocol'; | ||
|
||
const scoutGameUserProfileEASSchema = 'string id,string metadataUrl'; | ||
|
||
const scoutGameUserProfileSchemaName = 'Scout Game User Profile'; | ||
|
||
export const scoutGameUserProfileSchemaDefinition: EASSchema = { | ||
schema: scoutGameUserProfileEASSchema, | ||
name: scoutGameUserProfileSchemaName | ||
}; | ||
|
||
export type ScoutGameUserProfileAttestation = { | ||
id: string; | ||
metadataUrl: string; | ||
}; | ||
|
||
const encoder = new SchemaEncoder(scoutGameUserProfileEASSchema); | ||
|
||
export function encodeScoutGameUserProfileAttestation(attestation: ScoutGameUserProfileAttestation): `0x${string}` { | ||
const encodedData = encoder.encodeData([ | ||
{ name: 'id', type: 'string', value: attestation.id }, | ||
{ name: 'metadataUrl', type: 'string', value: attestation.metadataUrl } | ||
]); | ||
|
||
return encodedData as `0x${string}`; | ||
} | ||
|
||
export function decodeScoutGameUserProfileAttestation(rawData: string): ScoutGameUserProfileAttestation { | ||
const parsed = encoder.decodeData(rawData); | ||
const values = parsed.reduce((acc, item) => { | ||
const key = item.name as keyof ScoutGameUserProfileAttestation; | ||
|
||
acc[key] = item.value.value as string; | ||
|
||
return acc; | ||
}, {} as ScoutGameUserProfileAttestation); | ||
|
||
return values as ScoutGameUserProfileAttestation; | ||
} |
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,4 @@ | ||
export type EASSchema = { | ||
schema: string; | ||
name: string; | ||
}; |
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,70 @@ | ||
import { generateMerkleTree, getMerkleProofs, verifyMerkleClaim, type ProvableClaim } from '../merkleTree'; | ||
|
||
const claimsInput: ProvableClaim[] = [ | ||
{ | ||
// Key here so we can copy to other tests: 57b7b9b29419b66ac8156f844a7b0eb18d94f729699b3f15a3d8817d3f5980a3 | ||
address: '0x3F2A655d4e39E6c4470703e1063e9a843586886A', | ||
amount: 100 | ||
}, | ||
{ | ||
// Key here so we can copy to other tests: aa03d22263ff3e4df4105a20d08f62873f5e100974862fdc1f99083ba11e6adc | ||
address: '0x2Fe1B8C9C8722f0D3e5B9a9D4115559bB8f04931', | ||
amount: 200 | ||
}, | ||
{ | ||
// Key here so we can copy to other tests: c674865dde0163f480f818a78fc4d316c64d60b05666600734df8e8f37147f64 | ||
address: '0x03F8B139fF6dbbb7475bAA5A71c16fcDD9495cc4', | ||
amount: 300 | ||
}, | ||
{ | ||
address: '0x36446eF671954753801f9d73C415a80C0e550b32', | ||
amount: 400 | ||
}, | ||
{ | ||
address: '0xD02953857250D32EC72064d9E2320B43296E52C0', | ||
amount: 500 | ||
} | ||
]; | ||
|
||
describe('verifyMerkleClaim', () => { | ||
it('should return true if the claim is valid', () => { | ||
const { tree, rootHash } = generateMerkleTree(claimsInput); | ||
const claim = claimsInput[0]; | ||
const proofs = getMerkleProofs(tree, claim); | ||
|
||
expect(verifyMerkleClaim(tree, claim, proofs)).toBe(true); | ||
}); | ||
|
||
it('should return false if the claim is invalid', () => { | ||
const { tree } = generateMerkleTree(claimsInput); | ||
const claim: ProvableClaim = { | ||
address: '0x36446eF671954753801f9d73C415a80C0e550b32', | ||
amount: 200 | ||
}; | ||
const proof = getMerkleProofs(tree, claim); | ||
expect(verifyMerkleClaim(tree, claim, proof)).toBe(false); | ||
}); | ||
|
||
it('should sort inputs so that it is not reliant on ordering of the claims', () => { | ||
function shuffleArray<T>(array: T[]): T[] { | ||
const newArray = [...array]; // Create a copy of the array to avoid mutating the original | ||
for (let i = newArray.length - 1; i > 0; i--) { | ||
const j = Math.floor(Math.random() * (i + 1)); | ||
[newArray[i], newArray[j]] = [newArray[j], newArray[i]]; // Swap elements | ||
} | ||
return newArray; | ||
} | ||
|
||
const shuffledOne = shuffleArray(claimsInput); | ||
|
||
const shuffledTwo = shuffleArray(claimsInput); | ||
|
||
// Make sure sorting worked | ||
expect(JSON.stringify(shuffledOne)).not.toEqual(JSON.stringify(shuffledTwo)); | ||
|
||
const { rootHash: rootHashOne } = generateMerkleTree(shuffledOne); | ||
const { rootHash: rootHashTwo } = generateMerkleTree(shuffledTwo); | ||
|
||
expect(rootHashOne).toEqual(rootHashTwo); | ||
}); | ||
}); |
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,37 @@ | ||
import { MerkleTree } from 'merkletreejs'; | ||
import type { Address } from 'viem'; | ||
import { keccak256, encodePacked } from 'viem/utils'; | ||
|
||
export type ProvableClaim = { | ||
address: Address; | ||
amount: number; | ||
}; | ||
|
||
function hashLeaf(claim: ProvableClaim): string { | ||
// Mimic Solidity's keccak256(abi.encodePacked(address, amount)) | ||
const packedData = encodePacked(['address', 'uint256'], [claim.address, BigInt(claim.amount)]); | ||
return keccak256(packedData); | ||
} | ||
|
||
export function generateMerkleTree(claims: ProvableClaim[]): { tree: MerkleTree; rootHash: string } { | ||
const inputs = claims.map(hashLeaf); | ||
|
||
const tree = new MerkleTree(inputs, keccak256, { | ||
sort: true | ||
// concatenator: ([left, right]) => Buffer.from(hashLeaf(Buffer.concat([left, right]).toString())) | ||
}); | ||
|
||
return { | ||
tree, | ||
rootHash: tree.getRoot().toString('hex') | ||
}; | ||
} | ||
|
||
export function getMerkleProofs(tree: MerkleTree, claim: ProvableClaim): `0x${string}`[] { | ||
return tree.getHexProof(hashLeaf(claim)) as `0x${string}`[]; | ||
} | ||
|
||
export function verifyMerkleClaim(tree: MerkleTree, claim: ProvableClaim, proof: string[]): boolean { | ||
const root = tree.getRoot().toString('hex'); | ||
return tree.verify(proof, hashLeaf(claim), root); | ||
} |
68 changes: 68 additions & 0 deletions
68
src/prisma/migrations/20241107150504_onchain_claims/migration.sql
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,68 @@ | ||
-- AlterTable | ||
ALTER TABLE "BuilderEvent" ADD COLUMN "weeklyClaimId" UUID; | ||
|
||
-- AlterTable | ||
ALTER TABLE "GemsReceipt" ADD COLUMN "onchainAttestationRevoked" BOOLEAN DEFAULT false, | ||
ADD COLUMN "onchainAttestationUid" TEXT, | ||
ADD COLUMN "onchainChainId" INTEGER; | ||
|
||
-- AlterTable | ||
ALTER TABLE "Scout" ADD COLUMN "onchainProfileAttestationChainId" INTEGER, | ||
ADD COLUMN "onchainProfileAttestationUid" TEXT; | ||
|
||
-- AlterTable | ||
ALTER TABLE "ScoutGameActivity" ADD COLUMN "tokensReceiptId" UUID; | ||
|
||
-- CreateTable | ||
CREATE TABLE "WeeklyClaims" ( | ||
"id" UUID NOT NULL, | ||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||
"season" TEXT NOT NULL, | ||
"week" TEXT NOT NULL, | ||
"merkleTreeRoot" TEXT NOT NULL, | ||
"totalClaimable" INTEGER NOT NULL, | ||
"claims" JSONB NOT NULL, | ||
"proofsMap" JSONB NOT NULL, | ||
|
||
CONSTRAINT "WeeklyClaims_pkey" PRIMARY KEY ("id") | ||
); | ||
|
||
-- CreateTable | ||
CREATE TABLE "TokensReceipt" ( | ||
"id" UUID NOT NULL, | ||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||
"value" INTEGER NOT NULL, | ||
"claimedAt" TIMESTAMP(3), | ||
"eventId" UUID NOT NULL, | ||
"recipientId" UUID, | ||
"senderId" UUID, | ||
|
||
CONSTRAINT "TokensReceipt_pkey" PRIMARY KEY ("id") | ||
); | ||
|
||
-- CreateIndex | ||
CREATE UNIQUE INDEX "WeeklyClaims_week_key" ON "WeeklyClaims"("week"); | ||
|
||
-- CreateIndex | ||
CREATE INDEX "TokensReceipt_recipientId_idx" ON "TokensReceipt"("recipientId"); | ||
|
||
-- CreateIndex | ||
CREATE INDEX "TokensReceipt_senderId_idx" ON "TokensReceipt"("senderId"); | ||
|
||
-- CreateIndex | ||
CREATE INDEX "TokensReceipt_eventId_idx" ON "TokensReceipt"("eventId"); | ||
|
||
-- AddForeignKey | ||
ALTER TABLE "BuilderEvent" ADD CONSTRAINT "BuilderEvent_weeklyClaimId_fkey" FOREIGN KEY ("weeklyClaimId") REFERENCES "WeeklyClaims"("id") ON DELETE SET NULL ON UPDATE CASCADE; | ||
|
||
-- AddForeignKey | ||
ALTER TABLE "ScoutGameActivity" ADD CONSTRAINT "ScoutGameActivity_tokensReceiptId_fkey" FOREIGN KEY ("tokensReceiptId") REFERENCES "TokensReceipt"("id") ON DELETE SET NULL ON UPDATE CASCADE; | ||
|
||
-- AddForeignKey | ||
ALTER TABLE "TokensReceipt" ADD CONSTRAINT "TokensReceipt_eventId_fkey" FOREIGN KEY ("eventId") REFERENCES "BuilderEvent"("id") ON DELETE CASCADE ON UPDATE CASCADE; | ||
|
||
-- AddForeignKey | ||
ALTER TABLE "TokensReceipt" ADD CONSTRAINT "TokensReceipt_recipientId_fkey" FOREIGN KEY ("recipientId") REFERENCES "Scout"("id") ON DELETE CASCADE ON UPDATE CASCADE; | ||
|
||
-- AddForeignKey | ||
ALTER TABLE "TokensReceipt" ADD CONSTRAINT "TokensReceipt_senderId_fkey" FOREIGN KEY ("senderId") REFERENCES "Scout"("id") ON DELETE CASCADE ON UPDATE CASCADE; |
Oops, something went wrong.