Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pop-subgraph/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -1717,7 +1717,7 @@ If IPFS content is unavailable (404), this entity won't be created and the propo
reference will point to a non-existent entity (queries will return null).
"""
type ProposalMetadata @entity(immutable: true) {
id: String! # IPFS CID (Qm...)
id: String! # Proposal entity ID (contractAddress-proposalId)
description: String! # Full proposal description text
optionNames: [String!]! # Names for each voting option
createdAt: BigInt # Timestamp from IPFS metadata
Expand Down
16 changes: 8 additions & 8 deletions pop-subgraph/src/direct-democracy-voting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,10 @@ function createProposalMetadataDataSource(descriptionHash: Bytes, proposalEntity
// Convert bytes32 to IPFS CIDv0
let ipfsCid = bytes32ToCid(descriptionHash);

// Skip if ProposalMetadata already exists - prevents duplicate file data sources
let existingMetadata = ProposalMetadata.load(ipfsCid);
// Use proposalEntityId as the metadata entity ID (not CID) so each proposal
// gets its own immutable entity — avoids INSERT conflicts when the same CID
// is reused across proposals in different blocks (offchain causality regions)
let existingMetadata = ProposalMetadata.load(proposalEntityId);
if (existingMetadata != null) {
return;
}
Expand Down Expand Up @@ -340,10 +342,9 @@ export function handleNewProposal(event: NewProposal): void {
proposal.createdAtBlock = event.block.number;
proposal.transactionHash = event.transaction.hash;

// Set metadata link - entity will be created by IPFS handler if content exists
// Link metadata by proposalId (not CID) — each proposal gets its own metadata entity
if (!event.params.descriptionHash.equals(ZERO_HASH)) {
let metadataCid = bytes32ToCid(event.params.descriptionHash);
proposal.metadata = metadataCid;
proposal.metadata = proposalId;
}

proposal.save();
Expand Down Expand Up @@ -376,10 +377,9 @@ export function handleNewHatProposal(event: NewHatProposal): void {
proposal.createdAtBlock = event.block.number;
proposal.transactionHash = event.transaction.hash;

// Set metadata link - entity will be created by IPFS handler if content exists
// Link metadata by proposalId (not CID)
if (!event.params.descriptionHash.equals(ZERO_HASH)) {
let metadataCid = bytes32ToCid(event.params.descriptionHash);
proposal.metadata = metadataCid;
proposal.metadata = proposalId;
}

proposal.save();
Expand Down
18 changes: 8 additions & 10 deletions pop-subgraph/src/hybrid-voting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,10 @@ function createProposalMetadataDataSource(descriptionHash: Bytes, proposalEntity
// Convert bytes32 to IPFS CIDv0
let ipfsCid = bytes32ToCid(descriptionHash);

// Skip if ProposalMetadata already exists - prevents duplicate file data sources
let existingMetadata = ProposalMetadata.load(ipfsCid);
// Use proposalEntityId as the metadata entity ID (not CID) so each proposal
// gets its own immutable entity — avoids INSERT conflicts when the same CID
// is reused across proposals in different blocks (offchain causality regions)
let existingMetadata = ProposalMetadata.load(proposalEntityId);
if (existingMetadata != null) {
return;
}
Expand Down Expand Up @@ -304,12 +306,9 @@ export function handleNewProposal(event: NewProposal): void {
proposal.createdAtBlock = event.block.number;
proposal.transactionHash = event.transaction.hash;

// Set metadata link - entity will be created by IPFS handler if content exists
// Note: We don't create a stub here because file data sources run in a separate causality
// region, and both would try to Insert in the same block causing conflicts.
// Link metadata by proposalId (not CID) — each proposal gets its own metadata entity
if (!event.params.descriptionHash.equals(ZERO_HASH)) {
let metadataCid = bytes32ToCid(event.params.descriptionHash);
proposal.metadata = metadataCid;
proposal.metadata = proposalId;
}

proposal.save();
Expand Down Expand Up @@ -361,10 +360,9 @@ export function handleNewHatProposal(event: NewHatProposal): void {
proposal.createdAtBlock = event.block.number;
proposal.transactionHash = event.transaction.hash;

// Set metadata link - entity will be created by IPFS handler if content exists
// Link metadata by proposalId (not CID)
if (!event.params.descriptionHash.equals(ZERO_HASH)) {
let metadataCid = bytes32ToCid(event.params.descriptionHash);
proposal.metadata = metadataCid;
proposal.metadata = proposalId;
}

proposal.save();
Expand Down
17 changes: 9 additions & 8 deletions pop-subgraph/src/proposal-metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,28 +12,29 @@ import { ProposalMetadata } from "../generated/schema";
* }
*
* ProposalMetadata is immutable — the entity store only allows INSERT, not UPDATE.
* This handler follows the single-path pattern: one existence check at the top,
* one entity creation, one save. Multiple code paths with separate load/new/save
* cycles cause INSERT conflicts when the same CID is triggered more than once
* in the same block.
* Uses proposalEntityId (from DataSourceContext) as the entity ID instead of CID,
* so each proposal gets its own metadata entity. This avoids INSERT conflicts when
* two proposals share the same CID but land in different blocks (which run in
* separate offchain causality regions in specVersion >= 1.0.0).
*/
export function handleProposalMetadata(content: Bytes): void {
let ipfsCid = dataSource.stringParam();
let context = dataSource.context();
let proposalEntityId = context.getString("proposalEntityId");

// Immutable — skip if already exists
let existing = ProposalMetadata.load(ipfsCid);
let existing = ProposalMetadata.load(proposalEntityId);
if (existing != null) {
return;
}

let metadata = new ProposalMetadata(ipfsCid);
let metadata = new ProposalMetadata(proposalEntityId);
metadata.description = "";
metadata.optionNames = [];

// Try to parse the JSON content
let jsonResult = json.try_fromBytes(content);
if (jsonResult.isError) {
log.warning("[ProposalMetadata] Failed to parse JSON for CID: {}", [ipfsCid]);
log.warning("[ProposalMetadata] Failed to parse JSON for proposal: {}", [proposalEntityId]);
metadata.save();
return;
}
Expand Down
Loading