From cb139ead7007303d18c0151638ca79203a0c5fed Mon Sep 17 00:00:00 2001 From: Anton <14254374+0xmad@users.noreply.github.com> Date: Fri, 13 Dec 2024 15:10:39 -0600 Subject: [PATCH] feat(subgraph): add chain hashes and ipfs messages to subgraph - [x] Add chain hash event handler - [x] Add ipfs event handler and vote processing --- apps/subgraph/schemas/schema.v1.graphql | 12 ++- apps/subgraph/src/maci.ts | 2 +- apps/subgraph/src/poll.ts | 82 ++++++++++++++++++- .../subgraph/templates/subgraph.template.yaml | 6 ++ apps/subgraph/tests/ipfs/batch-0.json | 14 ++++ apps/subgraph/tests/ipfs/batch-1.json | 1 + apps/subgraph/tests/poll/poll.test.ts | 65 +++++++++++++-- apps/subgraph/tests/poll/utils.ts | 22 ++++- 8 files changed, 191 insertions(+), 13 deletions(-) create mode 100644 apps/subgraph/tests/ipfs/batch-0.json create mode 100644 apps/subgraph/tests/ipfs/batch-1.json diff --git a/apps/subgraph/schemas/schema.v1.graphql b/apps/subgraph/schemas/schema.v1.graphql index 743b4898a6..621bacc739 100644 --- a/apps/subgraph/schemas/schema.v1.graphql +++ b/apps/subgraph/schemas/schema.v1.graphql @@ -32,7 +32,7 @@ type Poll @entity { pollId: BigInt! # uint256 duration: BigInt! # uint256 treeDepth: BigInt! # uint8 - maxVoteOption: BigInt! + maxVoteOptions: BigInt! messageProcessor: Bytes! # address tally: Bytes! # address createdAt: BigInt! @@ -46,12 +46,22 @@ type Poll @entity { owner: Bytes! maci: MACI! votes: [Vote!]! @derivedFrom(field: "poll") + chainHashes: [ChainHash!]! @derivedFrom(field: "poll") } type Vote @entity(immutable: true) { id: Bytes! data: [BigInt!]! # uint256[10] timestamp: BigInt! + cid: String + + "relations" + poll: Poll! +} + +type ChainHash @entity(immutable: true) { + id: ID! # chain hash + timestamp: BigInt! "relations" poll: Poll! diff --git a/apps/subgraph/src/maci.ts b/apps/subgraph/src/maci.ts index ca2473c783..45d1aacf6b 100644 --- a/apps/subgraph/src/maci.ts +++ b/apps/subgraph/src/maci.ts @@ -25,7 +25,7 @@ export function handleDeployPoll(event: DeployPollEvent): void { poll.pollId = event.params._pollId; poll.messageProcessor = contracts.messageProcessor; poll.tally = contracts.tally; - poll.maxVoteOption = maxVoteOptions; + poll.maxVoteOptions = maxVoteOptions; poll.treeDepth = GraphBN.fromI32(treeDepths.value0); poll.duration = durations.value1; poll.mode = GraphBN.fromI32(event.params._mode); diff --git a/apps/subgraph/src/poll.ts b/apps/subgraph/src/poll.ts index d57a1dd010..520bff7980 100644 --- a/apps/subgraph/src/poll.ts +++ b/apps/subgraph/src/poll.ts @@ -1,7 +1,14 @@ /* eslint-disable no-underscore-dangle */ -import { Poll, Vote, MACI } from "../generated/schema"; -import { MergeState as MergeStateEvent, PublishMessage as PublishMessageEvent } from "../generated/templates/Poll/Poll"; +import { Bytes, ipfs, log, Value, BigInt as GraphBN, JSONValue } from "@graphprotocol/graph-ts"; + +import { Poll, Vote, MACI, ChainHash } from "../generated/schema"; +import { + MergeState as MergeStateEvent, + PublishMessage as PublishMessageEvent, + ChainHashUpdated as ChainHashUpdatedEvent, + IpfsHashAdded as IpfsHashAddedEvent, +} from "../generated/templates/Poll/Poll"; import { ONE_BIG_INT } from "./utils/constants"; @@ -39,3 +46,74 @@ export function handlePublishMessage(event: PublishMessageEvent): void { poll.save(); } } + +export function handleChainHashUpdate(event: ChainHashUpdatedEvent): void { + const chainHash = new ChainHash(event.params._chainHash.toString()); + chainHash.poll = event.address; + chainHash.timestamp = event.block.timestamp; + chainHash.save(); + + const poll = Poll.load(event.address); + + if (poll) { + poll.updatedAt = event.block.timestamp; + poll.save(); + } +} + +export function handleIpfsHashAdded(event: IpfsHashAddedEvent): void { + const CID_VERSION = "0x1220"; + const cid = Bytes.fromHexString(CID_VERSION).concat(event.params._ipfsHash).toBase58(); + const timestamp = event.block.timestamp.toString(); + const voteId = event.transaction.hash.concatI32(event.logIndex.toI32()).toHexString(); + + ipfs.mapJSON(cid, "processIpfsVotes", Value.fromStringArray([cid, voteId, timestamp, event.address.toHexString()])); +} + +export function processIpfsVotes(data: JSONValue, userData: Value): void { + const params = userData.toArray(); + const cid = params[0].toString(); + const voteId = params[1].toString(); + const timestamp = params[2].toString(); + const pollAddress = Bytes.fromHexString(params[3].toString()); + + const jsonData = data.toObject(); + const jsonMessages = jsonData.get("messages"); + + if (!jsonMessages) { + log.warning("Message batch file {} has invalid format", [cid]); + return; + } + + const messages = jsonMessages.toArray(); + + for (let index = 0; index < messages.length; index += 1) { + const vote = new Vote(Bytes.fromHexString(voteId).concatI32(index)); + + vote.data = castToBigIntArray(messages[index].toArray()); + vote.poll = pollAddress; + vote.cid = cid; + vote.timestamp = GraphBN.fromString(timestamp); + vote.save(); + } + + const poll = Poll.load(pollAddress); + + if (poll) { + poll.numMessages = poll.numMessages.plus(GraphBN.fromI32(messages.length)); + poll.updatedAt = GraphBN.fromString(timestamp); + poll.save(); + } +} + +function castToBigIntArray(array: JSONValue[]): GraphBN[] { + const result: GraphBN[] = []; + + // eslint-disable-next-line @typescript-eslint/prefer-for-of + for (let index = 0; index < array.length; index += 1) { + const value = array[index]; + result.push(GraphBN.fromString(value.toString())); + } + + return result; +} diff --git a/apps/subgraph/templates/subgraph.template.yaml b/apps/subgraph/templates/subgraph.template.yaml index 76b6ac291c..a5a5b0e768 100644 --- a/apps/subgraph/templates/subgraph.template.yaml +++ b/apps/subgraph/templates/subgraph.template.yaml @@ -5,6 +5,8 @@ indexerHints: prune: auto schema: file: ./schema.graphql +features: + - ipfsOnEthereumContracts dataSources: - kind: ethereum name: MACI @@ -58,4 +60,8 @@ templates: handler: handleMergeState - event: PublishMessage((uint256[10]),(uint256,uint256)) handler: handlePublishMessage + - event: ChainHashUpdated(indexed uint256) + handler: handleChainHashUpdate + - event: IpfsHashAdded(indexed bytes32) + handler: handleIpfsHashAdded file: ./src/poll.ts diff --git a/apps/subgraph/tests/ipfs/batch-0.json b/apps/subgraph/tests/ipfs/batch-0.json new file mode 100644 index 0000000000..edce397c4a --- /dev/null +++ b/apps/subgraph/tests/ipfs/batch-0.json @@ -0,0 +1,14 @@ +[ + { + "messages": [ + ["0", "0", "0", "0", "0", "0", "0", "0", "0", "0"], + ["0", "0", "0", "0", "0", "0", "0", "0", "0", "0"], + ["0", "0", "0", "0", "0", "0", "0", "0", "0", "0"] + ], + "encPubKeys": [ + ["0", "0"], + ["0", "0"], + ["0", "0"] + ] + } +] diff --git a/apps/subgraph/tests/ipfs/batch-1.json b/apps/subgraph/tests/ipfs/batch-1.json new file mode 100644 index 0000000000..93d51406d6 --- /dev/null +++ b/apps/subgraph/tests/ipfs/batch-1.json @@ -0,0 +1 @@ +[{}] diff --git a/apps/subgraph/tests/poll/poll.test.ts b/apps/subgraph/tests/poll/poll.test.ts index bc4e224d80..de1a5cbbe3 100644 --- a/apps/subgraph/tests/poll/poll.test.ts +++ b/apps/subgraph/tests/poll/poll.test.ts @@ -1,22 +1,38 @@ /* eslint-disable no-underscore-dangle, @typescript-eslint/no-unnecessary-type-assertion */ -import { BigInt } from "@graphprotocol/graph-ts"; -import { test, describe, afterEach, clearStore, assert, beforeEach } from "matchstick-as"; +import { BigInt, Bytes } from "@graphprotocol/graph-ts"; +import { test, describe, afterEach, clearStore, assert, beforeEach, mockIpfsFile, beforeAll } from "matchstick-as"; -import { MACI, Poll } from "../../generated/schema"; +import { ChainHash, MACI, Poll } from "../../generated/schema"; import { handleDeployPoll } from "../../src/maci"; -import { handleMergeState, handlePublishMessage } from "../../src/poll"; +import { + handleMergeState, + handlePublishMessage, + handleChainHashUpdate, + handleIpfsHashAdded, + processIpfsVotes, +} from "../../src/poll"; import { DEFAULT_POLL_ADDRESS, mockMaciContract, mockPollContract } from "../common"; import { createDeployPollEvent } from "../maci/utils"; -import { createMergeStateEvent, createPublishMessageEvent } from "./utils"; +import { + createChainHashUpdatedEvent, + createIpfsHashAddedEvent, + createMergeStateEvent, + createPublishMessageEvent, +} from "./utils"; -export { handleMergeState, handlePublishMessage }; +export { handleMergeState, handlePublishMessage, handleChainHashUpdate, handleIpfsHashAdded, processIpfsVotes }; describe("Poll", () => { - beforeEach(() => { + beforeAll(() => { + mockIpfsFile("TspRr", "tests/ipfs/batch-0.json"); + mockIpfsFile("Tsn1k", "tests/ipfs/batch-1.json"); + mockMaciContract(); mockPollContract(); + }); + beforeEach(() => { // mock the deploy poll event with non qv mode set const event = createDeployPollEvent(BigInt.fromI32(1), BigInt.fromI32(1), BigInt.fromI32(1), BigInt.fromI32(1)); @@ -69,4 +85,39 @@ describe("Poll", () => { assert.entityCount("Vote", 1); assert.fieldEquals("Poll", poll.id.toHex(), "numMessages", "1"); }); + + test("should handle chain hash update properly", () => { + const event = createChainHashUpdatedEvent(DEFAULT_POLL_ADDRESS, BigInt.fromI32(123443221)); + + handleChainHashUpdate(event); + + const chainHash = ChainHash.load(event.params._chainHash.toString())!; + + assert.entityCount("ChainHash", 1); + assert.fieldEquals("ChainHash", chainHash.id, "id", event.params._chainHash.toString()); + }); + + test("should handle ipfs message processing properly", () => { + const expectedTotalMessages = 3; + + const event = createIpfsHashAddedEvent(DEFAULT_POLL_ADDRESS, Bytes.fromHexString("0xdead")); + + handleIpfsHashAdded(event); + + const poll = Poll.load(event.address)!; + + assert.fieldEquals("Poll", poll.id.toHex(), "numMessages", expectedTotalMessages.toString()); + assert.entityCount("Vote", expectedTotalMessages); + }); + + test("should not add votes if there is no ipfs file", () => { + const event = createIpfsHashAddedEvent(DEFAULT_POLL_ADDRESS, Bytes.fromHexString("0xbeef")); + + handleIpfsHashAdded(event); + + const poll = Poll.load(event.address)!; + + assert.fieldEquals("Poll", poll.id.toHex(), "numMessages", "0"); + assert.entityCount("Vote", 0); + }); }); diff --git a/apps/subgraph/tests/poll/utils.ts b/apps/subgraph/tests/poll/utils.ts index ce3ec3b136..ac45cf5af8 100644 --- a/apps/subgraph/tests/poll/utils.ts +++ b/apps/subgraph/tests/poll/utils.ts @@ -1,8 +1,8 @@ -import { Address, BigInt as GraphBN, ethereum } from "@graphprotocol/graph-ts"; +import { Address, Bytes, BigInt as GraphBN, ethereum } from "@graphprotocol/graph-ts"; // eslint-disable-next-line import/no-extraneous-dependencies import { newMockEvent } from "matchstick-as"; -import { MergeState, PublishMessage } from "../../generated/templates/Poll/Poll"; +import { MergeState, PublishMessage, ChainHashUpdated, IpfsHashAdded } from "../../generated/templates/Poll/Poll"; export function createMergeStateEvent(address: Address, stateRoot: GraphBN, numSignups: GraphBN): MergeState { const event = changetype(newMockEvent()); @@ -40,3 +40,21 @@ export function createPublishMessageEvent( return event; } + +export function createChainHashUpdatedEvent(address: Address, chainHash: GraphBN): ChainHashUpdated { + const event = changetype(newMockEvent()); + + event.parameters.push(new ethereum.EventParam("_chainHash", ethereum.Value.fromUnsignedBigInt(chainHash))); + event.address = address; + + return event; +} + +export function createIpfsHashAddedEvent(address: Address, ipfsHash: Bytes): IpfsHashAdded { + const event = changetype(newMockEvent()); + + event.parameters.push(new ethereum.EventParam("_ipfsHash", ethereum.Value.fromBytes(ipfsHash))); + event.address = address; + + return event; +}