Skip to content

Conversation

@fcanela
Copy link
Contributor

@fcanela fcanela commented Oct 8, 2025

Replaces the generic notification for a new Reality question with the following two:

  • An alert notification for failed validations (hashes do not match, for example)
  • A warning notification for questions that can't be validated (due to misconfiguration, missing fields, etc)

The validation logic resides in src/utils/reality-question-validation/index.ts, so we may want to review that file with special attention.

Summary by CodeRabbit

  • New Features

    • Automatic validation for proposal questions with three outcomes: valid, incomplete data (warning), and alert.
    • New notification types with clearer details and links.
    • New email, Slack, and Telegram templates for valid, incomplete, and alert cases, including updated subjects and richer content.
  • Configuration

    • docker-compose now exposes the SMTP web interface at 127.0.0.1:1080 for local testing.
  • Chores

    • Minor Dockerfile environment variable syntax update (no functional impact).

@fcanela fcanela self-assigned this Oct 8, 2025
@coderabbitai
Copy link

coderabbitai bot commented Oct 8, 2025

Walkthrough

Implements validation-driven proposal notification flow. Adds Snapshot service (module address/proposal fetch), Reality service event shape change, new validation utilities, updated notification types, processing logic branching (valid/incomplete/alert), new templates for channels, DB helper for space deletion, expanded tests, and minor Docker/compose tweaks.

Changes

Cohort / File(s) Summary
Docker & Compose
Dockerfile, docker-compose.yml
Adjust ENV assignment syntax; expose smtp web UI on 127.0.0.1:1080.
Notification types & links
src/notify.ts, src/utils/links.ts, src/utils/links.test.ts, src/utils/test-mocks.ts
Replace PROPOSAL_QUESTION_CREATED with VALID/INCOMPLETE_DATA/ALERT; add InvalidProposalNotification; update unions and link utility signatures; update tests/mocks accordingly.
Processing flow & validation
src/processing.ts, src/processing.integration-test.ts, src/utils/reality-question-validation/index.ts, src/utils/reality-question-validation/errors.ts, src/utils/reality-question-validation/eip-712-transaction-hash.ts, src/utils/reality-question-validation/index.test.ts, src/utils/reality-question-validation/eip-712-transaction-hash.test.ts
Add Snapshot proposal fetch and Reality question validation; build notification based on ValidationResult; branch into VALID/INCOMPLETE/ALERT; introduce validation utilities, errors, EIP-712 tx hash; integrate tests and cleanup helpers.
Snapshot service
src/services/snapshot/index.ts, src/services/snapshot/index.integration-test.ts
New GraphQL-backed service exposing getRealityModuleAddress and getProposal; add integration tests asserting address lookup and proposal shape.
Reality service updates
src/services/reality/index.ts, src/services/reality/index.integration-test.ts
Import module address from Snapshot; remove local getter; change LogNewQuestion.question from string[] to object {proposalId, safeHash}; update parsing and tests.
DB helpers & tests
src/services/db/spaces.ts, src/services/db/proposal.integration-test.ts
Add removeSpaceByEns(ens); align tests to ValidProposalNotification type.
Errors
src/utils/errors.ts
Replace InvalidLogNewQuestionArgsEventError with MissingSnapshotProposalError(proposalId, questionId, txHash); add Hex import.
Templates (email/slack/telegram/test)
templates/email/*proposal-*.ejs, templates/slack/*proposal-*.ejs, templates/telegram/*proposal-*.ejs, templates/test/*proposal-*.ejs
Add new templates for valid/incomplete-data/alert across channels; subjects/plain/HTML; test fixtures updated to new notification shapes, include validationMessage where applicable.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Reality as Reality Module
  participant Proc as Processing
  participant Snap as Snapshot Service
  participant Val as Validation Utils
  participant DB as DB
  participant Notify as Notifier

  Reality->>Proc: LogNewQuestion(event)
  Proc->>Snap: getProposal(proposalId)
  alt Proposal found
    Proc->>Val: validateRealityQuestion(event, proposal)
    alt isValid = true
      Note right of Proc: Build ValidProposalNotification
      Proc->>Notify: PROPOSAL_QUESTION_VALID
    else isValid = false (INCOMPLETE_DATA)
      Note right of Proc: Build InvalidProposalNotification (warning)
      Proc->>Notify: PROPOSAL_QUESTION_INCOMPLETE_DATA
    else isValid = false (ALERT)
      Note right of Proc: Build InvalidProposalNotification (alert)
      Proc->>Notify: PROPOSAL_QUESTION_ALERT
    end
    Proc->>DB: insert proposal
  else Proposal missing
    Proc-->>Proc: throw MissingSnapshotProposalError
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Suggested labels

Type: Feature🗿

Suggested reviewers

  • divyangchauhan
  • salgozino
  • wadader
  • jaybuidl

Poem

A nibble of hashes, a hop through the chain,
I sniff the proposals—valid or arcane.
When safes hum true, my whiskers applaud;
If data’s astray, I thump and nod.
Alerts? I drum a rapid beat—
Then burrow the logs, neat, complete. 🐇✨

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title accurately summarizes the core feature of the changeset by indicating that new proposal Reality questions will now be automatically verified, aligning with the validation logic and notification updates introduced across the PR without including extraneous details.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/question-validation

Comment @coderabbitai help to get the list of available commands and usage tips.


ARG TARGETPLATFORM
ENV WAITDEPS_VERSION 0.0.1
ENV WAITDEPS_VERSION=0.0.1
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixes a warning on Docker side, the previous syntax seems deprecated

insertProposalFn: insertProposal,
afterEach(async () => {
await removeProposalByQuestionId(proposal.questionId);
await removeSpaceByEns(space.ens);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Promise.all can't be use here due to the database relationship between proposal and space

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The changes on this file are mainly to assert that the right notification is built depending on the proposal validation result. There is some code duplication which I left as it because it seems more clean and easy to follow than creating functions for 3 times reuse.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/utils/links.ts (1)

67-71: txHash should be a Hash, not Address

Transaction hashes are 32-byte hashes. Adjust type to prevent misuse.

-import type { Address } from "viem";
+import type { Address, Hash } from "viem";
...
-export const getBlockExplorerLinkForTx = (
-  txHash: Address,
+export const getBlockExplorerLinkForTx = (
+  txHash: Hash,
   template: string = env.BLOCK_EXPLORER_TX_LINK_TEMPLATE,
 ): string => {
src/services/reality/index.ts (1)

225-240: Verify safe parsing of question string.

The parsing logic splits rawQuestion on the "␟" character and accesses questionParts[0] and questionParts[1] without bounds checking. If the question format is malformed or doesn't contain the separator, this will result in undefined values being cast to Hex and Hash types.

Additionally, line 239 unconditionally prefixes questionParts[1] with "0x", which could result in double-prefixing if the value already contains the prefix.

Consider adding validation:

 const rawQuestion = decoded.args.question;
 const questionParts = rawQuestion.split("␟");
+
+if (questionParts.length < 2) {
+  throw new Error(`Invalid question format in LogNewQuestion event: ${rawQuestion}`);
+}

 let { opening_ts: openingTs, timeout } = decoded.args;
 if (openingTs === 0) {
   const block = await publicClient.getBlock({
     blockNumber: log.blockNumber,
   });
   openingTs = Number(block.timestamp);
 }

 return {
   questionId: decoded.args.question_id.toLowerCase() as Hash,
   question: {
     proposalId: questionParts[0] as Hex,
-    safeHash: `0x${questionParts[1]}` as Hash,
+    safeHash: (questionParts[1].startsWith('0x') ? questionParts[1] : `0x${questionParts[1]}`) as Hash,
   },
🧹 Nitpick comments (23)
Dockerfile (1)

4-4: ENV change is fine; consider hardening the download step

Using the equals form is equivalent here. To improve supply‑chain safety, consider verifying the tarball with a checksum/signature instead of blind piping to tar.

src/notify.ts (2)

74-85: Dedup race: make insert idempotent to avoid duplicate sends

If two processes handle the same txHash concurrently, both may send before insertUsedTransport records the transport. Ensure insertUsedTransport is idempotent (unique constraint + “on conflict do nothing”) and ignored on duplicate to prevent Promise.all from rejecting post‑send.

Confirm insertUsedTransport ignores unique violations and returns success semantics. Optionally catch/ignore duplicate errors around the insert.


35-40: Include structured error details in InvalidProposalNotification

  • Replace validationMessage: string with validationSummary: string, errorCode: string, and severity: ValidationErrorSeverity (sourced from the caught ValidationResultFailed).
  • Optionally add errors?: ValidationResultFailed[] if you plan to accumulate multiple validation issues in the future.
templates/test/proposal-alert.ejs (1)

3-17: Harden template rendering for optional dates and bigint fields

Guard nullable dates and explicitly stringify bigints to avoid runtime errors in EJS.

Apply:

-space.startBlock: <%- notification.space.startBlock %>
-space.lastProcessedBlock: <%- notification.space.lastProcessedBlock %>
+space.startBlock: <%- String(notification.space.startBlock) %>
+space.lastProcessedBlock: <%- notification.space.lastProcessedBlock == null ? "" : String(notification.space.lastProcessedBlock) %>
-event.blockNumber: <%- notification.event.blockNumber %>
+event.blockNumber: <%- String(notification.event.blockNumber) %>
-event.startedAt: <%- notification.event.startedAt.toISOString() %>
-event.finishedAt: <%- notification.event.finishedAt.toISOString() %>
+event.startedAt: <%- notification.event.startedAt?.toISOString?.() || "" %>
+event.finishedAt: <%- notification.event.finishedAt?.toISOString?.() || "" %>
-event.timeout: <%- notification.event.timeout %>
+event.timeout: <%- String(notification.event.timeout) %>
-event.happenedAt: <%- notification.event.happenedAt.toISOString() %>
+event.happenedAt: <%- notification.event.happenedAt?.toISOString?.() || "" %>
-validationMessage: <%- notification.validationMessage %>
+validationMessage: <%- notification.validationMessage ?? "" %>
src/services/reality-question-validation/eip-712-transaction-hash.test.ts (1)

6-21: Add one negative/variant case for robustness

Great golden test. Add a second case (e.g., operation 0 vs 1, or different chainId) to ensure domain/message changes alter the hash as expected. Optionally assert type conformance by typing “to” as Address for symmetry.

src/services/reality-question-validation/eip-712-transaction-hash.ts (2)

13-16: Consider adding name and version to the Domain type.

EIP-712 domain separators typically include name and version fields in addition to chainId and verifyingContract. While not strictly required by the EIP-712 spec, omitting these fields may cause compatibility issues with tools that expect a complete domain separator, and reduces the uniqueness guarantees of the domain.

Apply this diff to add the optional fields:

 type Domain = {
+  name?: string;
+  version?: string;
   chainId: number;
   verifyingContract: Address;
 };

3-11: Consider exporting the types constant for reuse.

If other parts of the codebase need to reference the Transaction type schema (e.g., for validation, testing, or documentation), exporting the types constant would improve maintainability.

Apply this diff if the types need to be reused elsewhere:

-const types = {
+export const types = {
   Transaction: [
     { name: "to", type: "address" },
     { name: "value", type: "uint256" },
     { name: "data", type: "bytes" },
     { name: "operation", type: "uint8" },
     { name: "nonce", type: "uint256" },
   ],
 } as const;
templates/email/proposal-valid-plain.ejs (1)

13-13: Clarify the hash description.

"keccak of the concatenation" is missing "hash" after "keccak" for clarity. The sentence should read "keccak hash of the concatenation" to be more precise.

Apply this diff:

-- The proposal Safes hashes are the keccak of the concatenation of the individual hashes
+- The proposal Safes hashes are the keccak hash of the concatenation of the individual hashes
templates/telegram/proposal-valid.ejs (1)

11-11: Clarify the hash description.

"keccak of the concatenation" is missing "hash" after "keccak" for clarity.

Apply this diff:

-- The proposal Safes hashes are the keccak of the concatenation of the individual hashes
+- The proposal Safes hashes are the keccak hash of the concatenation of the individual hashes
src/utils/notification-template.test.ts (1)

65-113: Remove unnecessary async and stray property in invalid cases

  • describe callback doesn’t need to be async; Mocha won’t await it.
  • Second case object has an extra top-level type (outside notification) — dead field.

Apply:

-describe("should interpolate an invalid Proposal notification into a test template", async () => {
+describe("should interpolate an invalid Proposal notification into a test template", () => {
       {
         title: "when the notification is an alert due to validation failure",
-        type: EventType.PROPOSAL_QUESTION_ALERT,
         notification: {
           ...randomizeProposalNotification(),
           type: EventType.PROPOSAL_QUESTION_ALERT,
           validationMessage: "This is the alert message reason",
         } as InvalidProposalNotification,
       },
src/utils/links.ts (1)

5-12: Fix JSDoc typo

“propoasal” → “proposal”.

- * Given a notification, generates a link to the Snapshot propoasal page.
+ * Given a notification, generates a link to the Snapshot proposal page.
src/services/snapshot/index.ts (1)

26-32: Prefer GraphQL variables over string interpolation

Use variables for ids to avoid quoting/injection pitfalls and to simplify escaping.

Example shape:

  • query($id: String!) { space(id: $id) { plugins } }
  • query($id: String!) { proposal(id: $id) { ... } }

Also applies to: 79-89

src/services/reality-question-validation/errors.ts (1)

8-15: Consider setting error name explicitly.

The BaseProposalValidationError class extends Error but doesn't set this.name. While JavaScript automatically sets the name property to the class name in modern environments, explicitly setting it ensures consistency across all runtime environments and makes the error more debuggable.

Apply this diff:

 export class BaseProposalValidationError extends Error {
   public severity: ValidationErrorSeverity;

   constructor(severity: ValidationErrorSeverity, message: string) {
-    super(message); // Pass message to the Error constructor
+    super(message);
+    this.name = this.constructor.name;
     this.severity = severity;
+    Error.captureStackTrace(this, this.constructor);
   }
 }
src/services/reality-question-validation/index.ts (5)

31-33: Use strict equality for type safety.

Line 31 uses loose equality (!=) instead of strict equality (!==). Since both proposal.id and event.question.proposalId are typed as Hex (which is a branded string type), using strict equality is more type-safe and prevents unexpected type coercion.

Apply this diff:

-  if (proposal.id != event.question.proposalId) {
+  if (proposal.id !== event.question.proposalId) {
     throw new ProposalIdMismatchError(proposal.id, event.question.proposalId);
   }

61-61: Use strict equality for hash comparison.

Line 61 uses loose equality (!=) for hash comparison. Use strict equality (!==) for type safety.

Apply this diff:

-      if (hash != calculatedHash) throw new TxHashMismatchError(hash, calculatedHash);
+      if (hash !== calculatedHash) throw new TxHashMismatchError(hash, calculatedHash);

66-66: Use strict equality for hash comparison.

Line 66 uses loose equality (!=) for hash comparison. Use strict equality (!==) for type safety.

Apply this diff:

-    if (calculatedSafeHash != hash) throw new SafeHashCalculationError(hash, calculatedSafeHash);
+    if (calculatedSafeHash !== hash) throw new SafeHashCalculationError(hash, calculatedSafeHash);

71-71: Use strict equality for hash comparison.

Line 71 uses loose equality (!=) for hash comparison. Use strict equality (!==) for type safety.

Apply this diff:

-  if (proposalNetworkSafe.hash != event.question.safeHash) throw new SafeHashMismatchError();
+  if (proposalNetworkSafe.hash !== event.question.safeHash) throw new SafeHashMismatchError();

47-63: Potential issue: Destructuring hash from tx shadows the outer hash from safe.

On line 49, the code destructures hash from tx, which shadows the hash variable from the outer scope (line 40). This could lead to confusion and potential bugs if the wrong hash is referenced later. While the current code works correctly, this pattern reduces readability.

Consider renaming the inner hash to make the code clearer:

     const txHashes = txs.map((tx) => {
       const {
-        hash,
+        hash: txHash,
         mainTransaction: { to, data, nonce, value, operation },
       } = tx;

       const calculatedHash = calculateTxHash(domain, {
         to: to as Address,
         data: data as Hex,
         nonce: BigInt(nonce),
         value: BigInt(value),
         operation: Number(operation),
       });

-      if (hash !== calculatedHash) throw new TxHashMismatchError(hash, calculatedHash);
+      if (txHash !== calculatedHash) throw new TxHashMismatchError(txHash, calculatedHash);
       return calculatedHash;
     });
templates/slack/proposal-alert.ejs (1)

1-1: Sanitize unescaped fields to avoid Slack formatting/injection issues

space.ens and validationMessage are rendered unescaped. If any part is user/externally sourced, escape or sanitize for Slack Markdown (e.g., escape * _ \ < > &`). If you intend rich formatting, document that these strings are trusted/internal.

Also applies to: 5-6

templates/email/proposal-alert-plain.html.ejs (1)

5-5: Escape HTML and make links clickable

  • Escape validationMessage and space.ens to prevent HTML injection.
  • Use anchor tags for reliable clickability in email clients.
- > <%- notification.event.validationMessage %>
+ > <%= notification.event.validationMessage %>
-A new Reality question has been created for space <%- notification.space.ens %>.
+A new Reality question has been created for space <%= notification.space.ens %>.
-- Snapshot proposal: <%- getSnapshotProposalLink(notification) %>
-- Reality question: <%- getRealityQuestionLink(notification) %>
-- Transaction: <%- getBlockExplorerLinkForTx(notification.event.txHash) %>
+- Snapshot proposal: <a href="<%= getSnapshotProposalLink(notification) %>">Snapshot proposal</a>
+- Reality question: <a href="<%= getRealityQuestionLink(notification) %>">Reality question</a>
+- Transaction: <a href="<%= getBlockExplorerLinkForTx(notification.event.txHash) %>">Transaction</a>

Also applies to: 11-11, 15-17

templates/telegram/proposal-incomplete-data.ejs (2)

6-6: Escape/sanitize for Telegram Markdown

validationMessage is unescaped and may contain characters that break Telegram Markdown (e.g., _ * [ ] ( ) ~ \ > # + - = | { } . !`). Ensure it’s escaped or sanitized for the chosen parse mode (MarkdownV2/HTML), or render via a helper that returns a safe string.


18-25: Grammar fix

“has PASSED” → “have PASSED”.

-The following automatic checks has PASSED:
+The following automatic checks have PASSED:
templates/email/proposal-valid-html.ejs (1)

7-7: Escape space name to prevent HTML injection

Use escaped interpolation for space.ens.

-            <h1 ...>A new Reality question has been created for a Snapshot proposal in the space <%- notification.space.ens %>.</h1>
+            <h1 ...>A new Reality question has been created for a Snapshot proposal in the space <%= notification.space.ens %>.</h1>
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7762390 and 1f78586.

📒 Files selected for processing (36)
  • Dockerfile (1 hunks)
  • docker-compose.yml (1 hunks)
  • src/notify.ts (2 hunks)
  • src/processing.integration-test.ts (3 hunks)
  • src/processing.ts (3 hunks)
  • src/services/db/proposal.integration-test.ts (3 hunks)
  • src/services/db/spaces.ts (1 hunks)
  • src/services/reality-question-validation/eip-712-transaction-hash.test.ts (1 hunks)
  • src/services/reality-question-validation/eip-712-transaction-hash.ts (1 hunks)
  • src/services/reality-question-validation/errors.ts (1 hunks)
  • src/services/reality-question-validation/index.test.ts (1 hunks)
  • src/services/reality-question-validation/index.ts (1 hunks)
  • src/services/reality/index.integration-test.ts (1 hunks)
  • src/services/reality/index.ts (5 hunks)
  • src/services/snapshot/index.integration-test.ts (1 hunks)
  • src/services/snapshot/index.ts (1 hunks)
  • src/utils/errors.ts (2 hunks)
  • src/utils/links.test.ts (3 hunks)
  • src/utils/links.ts (4 hunks)
  • src/utils/notification-template.test.ts (4 hunks)
  • src/utils/test-mocks.ts (2 hunks)
  • templates/email/proposal-alert-plain.html.ejs (1 hunks)
  • templates/email/proposal-alert-subject.ejs (1 hunks)
  • templates/email/proposal-incomplete-data-html.ejs (1 hunks)
  • templates/email/proposal-incomplete-data-plain.ejs (1 hunks)
  • templates/email/proposal-incomplete-data-subject.ejs (1 hunks)
  • templates/email/proposal-valid-html.ejs (1 hunks)
  • templates/email/proposal-valid-plain.ejs (1 hunks)
  • templates/slack/proposal-alert.ejs (1 hunks)
  • templates/slack/proposal-incomplete-data.ejs (1 hunks)
  • templates/telegram/proposal-alert.ejs (1 hunks)
  • templates/telegram/proposal-incomplete-data.ejs (1 hunks)
  • templates/telegram/proposal-valid.ejs (1 hunks)
  • templates/test/proposal-alert.ejs (1 hunks)
  • templates/test/proposal-incomplete-data.ejs (1 hunks)
  • templates/test/proposal-valid.ejs (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (14)
src/services/db/spaces.ts (1)
src/services/db/connection.ts (1)
  • getConnection (40-46)
src/services/reality-question-validation/eip-712-transaction-hash.test.ts (1)
src/services/reality-question-validation/eip-712-transaction-hash.ts (1)
  • calculateTxHash (36-43)
src/utils/notification-template.test.ts (3)
src/utils/tests-setup.ts (1)
  • expect (9-9)
src/notify.ts (2)
  • TransportName (9-9)
  • InvalidProposalNotification (35-40)
src/utils/test-mocks.ts (1)
  • randomizeProposalNotification (74-81)
src/utils/links.ts (1)
src/notify.ts (2)
  • ValidProposalNotification (29-33)
  • AnswerNotification (42-46)
src/utils/links.test.ts (1)
src/notify.ts (1)
  • ValidProposalNotification (29-33)
src/utils/test-mocks.ts (1)
src/notify.ts (1)
  • ValidProposalNotification (29-33)
src/notify.ts (3)
src/services/reality/index.ts (2)
  • ProposalQuestionCreated (118-125)
  • LogNewAnswer (255-264)
src/services/db/proposals.ts (1)
  • Proposal (10-10)
src/types.ts (1)
  • Space (3-7)
src/services/reality-question-validation/index.ts (4)
src/services/reality/index.ts (1)
  • LogNewQuestion (174-187)
src/services/snapshot/index.ts (1)
  • SnapshotProposal (76-76)
src/services/reality-question-validation/errors.ts (7)
  • ProposalIdMismatchError (17-24)
  • SafeSnapPluginConfigurationError (25-32)
  • TxHashMismatchError (34-41)
  • SafeHashCalculationError (43-50)
  • SafeNotFoundForProposalNetworkError (52-59)
  • SafeHashMismatchError (61-68)
  • BaseProposalValidationError (8-15)
src/services/reality-question-validation/eip-712-transaction-hash.ts (1)
  • calculateTxHash (36-43)
src/processing.ts (5)
src/services/snapshot/index.ts (1)
  • getProposal (78-92)
src/services/reality-question-validation/index.ts (2)
  • validateRealityQuestion (98-112)
  • ValidationResult (85-85)
src/notify.ts (4)
  • notify (58-65)
  • Notification (47-47)
  • ValidProposalNotification (29-33)
  • InvalidProposalNotification (35-40)
src/services/reality/index.ts (3)
  • ProposalQuestionCreated (118-125)
  • LogNewQuestion (174-187)
  • getLogNewQuestion (204-253)
src/utils/errors.ts (1)
  • MissingSnapshotProposalError (12-19)
src/services/reality-question-validation/index.test.ts (4)
src/services/reality/index.ts (1)
  • LogNewQuestion (174-187)
src/services/snapshot/index.ts (1)
  • SnapshotProposal (76-76)
src/services/reality-question-validation/index.ts (1)
  • assertValidRealityQuestion (30-72)
src/services/reality-question-validation/errors.ts (6)
  • TxHashMismatchError (34-41)
  • SafeHashCalculationError (43-50)
  • SafeHashMismatchError (61-68)
  • SafeSnapPluginConfigurationError (25-32)
  • ProposalIdMismatchError (17-24)
  • SafeNotFoundForProposalNetworkError (52-59)
src/services/snapshot/index.integration-test.ts (2)
src/services/snapshot/index.ts (2)
  • getRealityModuleAddress (25-36)
  • getProposal (78-92)
src/utils/tests-setup.ts (2)
  • expect (9-9)
  • ONEINCH_MODULE_ADDRESS (40-40)
src/services/db/proposal.integration-test.ts (1)
src/notify.ts (1)
  • ValidProposalNotification (29-33)
src/services/snapshot/index.ts (2)
src/utils/fetch-graphql.ts (1)
  • graphQLFetch (2-17)
src/utils/env.ts (1)
  • env (193-193)
src/processing.integration-test.ts (7)
src/utils/test-mocks.ts (1)
  • randomizeSpace (21-27)
src/utils/tests-setup.ts (2)
  • ONEINCH_ORACLE_ADDRESS (41-41)
  • expect (9-9)
src/services/db/spaces.ts (2)
  • insertSpaces (30-33)
  • removeSpaceByEns (59-62)
src/services/db/proposals.ts (3)
  • removeProposalByQuestionId (57-61)
  • insertProposal (24-28)
  • findProposalByQuestionId (39-47)
src/services/reality/index.ts (1)
  • getLogNewQuestion (204-253)
src/services/reality-question-validation/index.ts (2)
  • validateRealityQuestion (98-112)
  • ValidationResult (85-85)
src/services/snapshot/index.ts (1)
  • getProposal (78-92)
🔇 Additional comments (27)
docker-compose.yml (1)

80-81: Good hardening: bind MailDev UI to loopback only

Limiting the MailDev web UI to 127.0.0.1 reduces exposure while containers still reach smtp:1080 internally. LGTM.

src/notify.ts (1)

19-22: Enum change verified – no stale references remain
No occurrences of PROPOSAL_QUESTION_CREATED or ProposalNotification alias detected; new enum values are correctly used throughout.

templates/email/proposal-incomplete-data-plain.ejs (1)

1-23: LGTM!

The plain text email template is well-structured and clear. The warning block effectively highlights incomplete validation, and the use of unescaped interpolation (<%- %>) is appropriate for plain text emails.

templates/email/proposal-incomplete-data-subject.ejs (1)

1-1: LGTM!

The subject line template is concise and informative. The ENS and question ID fields are typically well-formed (ENS domains and hex hashes), so unescaped interpolation is acceptable here.

templates/telegram/proposal-alert.ejs (1)

1-14: LGTM!

The alert template effectively highlights validation failures with clear visual indicators (🚨 emojis). The structure is consistent with other Telegram templates in the codebase.

templates/email/proposal-alert-subject.ejs (1)

1-1: LGTM!

The alert subject line clearly communicates the severity with the 🚨 emoji and provides essential context.

src/services/db/spaces.ts (1)

59-62: LGTM!

The implementation is clean and follows the existing patterns in the codebase. The function correctly uses Drizzle ORM to delete a space by ENS.

src/services/db/proposal.integration-test.ts (1)

3-3: LGTM!

The type updates from ProposalNotification to ValidProposalNotification align with the PR's introduction of validation-specific notification types. The changes are minimal and maintain test integrity.

Also applies to: 12-12

src/utils/test-mocks.ts (1)

7-7: LGTM!

The updates to use ValidProposalNotification and the new event type PROPOSAL_QUESTION_VALID are consistent with the PR's validation-driven notification architecture. The changes maintain the existing test data generation patterns.

Also applies to: 74-81

templates/test/proposal-valid.ejs (1)

1-16: LGTM!

The test template comprehensively captures all notification fields in a structured, parseable format. The use of toISOString() for timestamps ensures consistent date formatting across test outputs.

src/services/reality-question-validation/eip-712-transaction-hash.ts (1)

36-43: LGTM: Hash calculation implementation is correct.

The function correctly delegates to viem's hashTypedData with the appropriate domain, types, primaryType, and message. The implementation follows EIP-712 standards for typed data hashing.

src/utils/links.test.ts (1)

1-41: LGTM: Test updates align with notification type changes.

The test correctly updates the import and type assertion to use ValidProposalNotification instead of ProposalNotification, consistent with the broader API changes in the PR.

templates/slack/proposal-incomplete-data.ejs (1)

1-16: LGTM: Slack template is well-structured.

The template correctly formats the incomplete data notification with appropriate warning indicators, includes all relevant links and details, and follows Slack markdown conventions.

templates/email/proposal-incomplete-data-html.ejs (1)

1-47: LGTM: HTML email template is well-structured.

The template correctly uses shared layout components, follows the established email template structure, and properly displays all required notification details with appropriate styling.

src/services/snapshot/index.integration-test.ts (1)

1-48: LGTM: Integration tests are comprehensive.

The test suite thoroughly validates both getRealityModuleAddress and getProposal functions with appropriate positive and negative test cases. The assertions cover multiple levels of nested data, ensuring the GraphQL query and response parsing work correctly.

templates/test/proposal-incomplete-data.ejs (1)

17-17: validationMessage path is correct validationMessage is a top-level field on InvalidProposalNotification (see src/notify.ts:39) and tests reference it accordingly—no change needed.

Likely an incorrect or invalid review comment.

src/utils/notification-template.test.ts (1)

15-23: Template path assertions updated correctly

Paths now target proposal-valid variants; assertions look right.

src/services/reality-question-validation/index.test.ts (1)

71-126: Good, comprehensive validation coverage

Happy path + clear alert/warn cases (tx hash, safe hash, plugin config, proposal id/network). Reads well and is deterministic.

src/processing.integration-test.ts (4)

145-162: LGTM!

The new beforeEach/afterEach pattern properly sets up and tears down test data for each test, ensuring test isolation and preventing data leakage between tests.


164-193: LGTM!

The test properly validates the happy path where a valid proposal passes validation and results in a PROPOSAL_QUESTION_VALID notification. The test verifies both the notification structure and that the proposal is persisted to the database.


195-235: LGTM!

The test properly validates the incomplete data path where validation fails due to missing data, resulting in a PROPOSAL_QUESTION_INCOMPLETE_DATA notification with the validation message.


237-277: LGTM!

The test properly validates the security alert path where validation fails due to a security concern, resulting in a PROPOSAL_QUESTION_ALERT notification with the validation message.

src/services/reality-question-validation/index.ts (2)

98-112: LGTM!

The validateRealityQuestion wrapper properly catches BaseProposalValidationError instances and maps them to a structured ValidationResult, while re-throwing unexpected errors. This provides a clean API for callers.


39-67: Review comment incorrect: verifyingContract correctly uses the Safe address (realityAddress), field conversions will error on invalid inputs, and keccak256(concat(txHashes)) matches the SafeSnap spec.

Likely an incorrect or invalid review comment.

src/processing.ts (2)

219-249: LGTM!

The buildProposalNotification function cleanly separates the notification construction logic and properly maps validation results to the appropriate notification types (VALID, INCOMPLETE_DATA, or ALERT).


282-285: LGTM!

The processing flow properly fetches the snapshot proposal and validates the reality question before notification. The error handling for missing snapshot proposals is appropriate.

src/services/reality/index.integration-test.ts (1)

102-105: LGTM: updated event shape assertion

Asserting question as an object with proposalId and safeHash matches the new event schema.

Comment on lines +287 to +292
const logNewQuestionFields = {
snapshotId: question.proposalId,
startedAt,
finishedAt,
timeout,
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify correct field assignment in logNewQuestionFields.

Line 288 assigns snapshotId: question.proposalId, where question is destructured from newQuestionEvent on line 280. This means snapshotId is set to the proposalId from the Reality question event.

However, this should be validated against the actual event.proposalId from the ProposalQuestionCreated event to ensure consistency. While the validation logic in assertValidRealityQuestion checks that proposal.id matches event.question.proposalId, it doesn't verify that this matches event.proposalId.

Run the following script to verify that proposalId is consistent across events:

If event.proposalId (from ProposalQuestionCreated) and question.proposalId (from LogNewQuestion) can differ, consider adding a validation check to ensure they match.


🏁 Script executed:

#!/bin/bash
# Description: Verify that proposalId from ProposalQuestionCreated matches proposalId in LogNewQuestion

# Search for ProposalQuestionCreated event structure
ast-grep --pattern $'type ProposalQuestionCreated = {
  $$$
  proposalId: $_;
  $$$
}'

# Search for LogNewQuestion question field structure
ast-grep --pattern $'question: {
  proposalId: $_;
  $$$
}'

Length of output: 414


🏁 Script executed:

#!/bin/bash
sed -n '260,330p' src/processing.ts

Length of output: 2151


🏁 Script executed:

#!/bin/bash
rg -n "configurableProcessProposals" -n .

Length of output: 421


🏁 Script executed:

#!/bin/bash
sed -n '160,240p' src/processing.ts
rg -n "type ConfigurableProcessProposalsDeps" -n src/processing.ts

Length of output: 2351


🏁 Script executed:

#!/bin/bash
# Show imports and early context in processing.ts
sed -n '1,80p' src/processing.ts

# Locate validateRealityQuestion implementation or declaration
rg -n "validateRealityQuestion" -n src

# Locate LogNewQuestion type definition
rg -n "type LogNewQuestion" -n src

Length of output: 4100


🏁 Script executed:

#!/bin/bash
# Show LogNewQuestion type definition with context
sed -n '160,220p' src/services/reality/index.ts

Length of output: 1513


🏁 Script executed:

#!/bin/bash
# Inspect getProposalQuestionsCreated implementation and its event argument mapping
rg -n "export const getProposalQuestionsCreated" -n src/services/reality/index.ts -C10

Length of output: 915


Validate matching proposal IDs before using them. Add an explicit check in configurableProcessProposals so that

if (newQuestionEvent.question.proposalId !== event.proposalId) {
  throw new Error(`Mismatched proposalId: logNewQuestion=${newQuestionEvent.question.proposalId} vs createdEvent=${event.proposalId}`);
}

before calling getSnapshotProposalFn and inserting into the database.

🤖 Prompt for AI Agents
In src/processing.ts around lines 287 to 292, add an explicit validation that
the proposalId on the newQuestionEvent matches the proposalId on the created
event before calling getSnapshotProposalFn or inserting into the DB; if they
differ, throw an Error with a clear message such as "Mismatched proposalId:
logNewQuestion=<value> vs createdEvent=<value>" to fail fast and avoid inserting
inconsistent data.

Comment on lines +51 to +52
/**
* Removes an space by its ens
Copy link

@coderabbitai coderabbitai bot Oct 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix typo in documentation.

The doc comment contains a grammatical error: "an space" should be "a space".

Apply this diff:

 /**
- * Removes an space by its ens
+ * Removes a space by its ENS
  *
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* Removes an space by its ens
/**
* Removes a space by its ENS
*
🤖 Prompt for AI Agents
In src/services/db/spaces.ts around lines 51 to 52, the doc comment has a typo
"an space" — update the comment to read "a space" so it correctly states
"Removes a space by its ens"; ensure spacing and punctuation remain consistent
with surrounding JSDoc style.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noted. I will fix it.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fcanela, thank you for acknowledging this! I appreciate your attention to the documentation quality. 🙏

    (\(\
    ( -.-)
    o_(")(")
    Happy to help!

Comment on lines +5 to +13
type SnapshotSpaceResponse = {
space: {
plugins: {
safeSnap: {
address: Address;
};
};
};
};
Copy link

@coderabbitai coderabbitai bot Oct 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Harden null-safety when reading plugins.safeSnap.address

Avoid runtime exceptions when any layer is missing.

-type SnapshotSpaceResponse = {
-  space: {
-    plugins: {
-      safeSnap: {
-        address: Address;
-      };
-    };
-  };
-};
+type SnapshotSpaceResponse = {
+  space: {
+    plugins?: {
+      safeSnap?: {
+        address?: Address;
+      };
+    } | null;
+  } | null;
+};
...
-  const { space } = await graphQLFetch<SnapshotSpaceResponse>(env.SNAPSHOT_GRAPHQL_URL, query);
-
-  return space ? space.plugins.safeSnap.address : null;
+  const { space } = await graphQLFetch<SnapshotSpaceResponse>(env.SNAPSHOT_GRAPHQL_URL, query);
+  return space?.plugins?.safeSnap?.address ?? null;

Also applies to: 33-36

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good suggestion. I will implement it.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fcanela Thank you for confirming! Looking forward to seeing the null-safety improvements implemented. 👍

Comment on lines +12 to +19
export class MissingSnapshotProposalError extends Error {
constructor(proposalId: Hex, questionId: Hex, txHash: Hash) {
const message = `Unable to resolve proposal ${proposalId} related to Reality question ${questionId} present in the LogNewQuestion event in tx ${txHash}`;
super(message);
this.name = this.constructor.name;
Error.captureStackTrace(this, InvalidLogNewQuestionArgsEventError);
Error.captureStackTrace(this, MissingLogNewQuestionEventError);
}
}
}
Copy link

@coderabbitai coderabbitai bot Oct 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Wrong class passed to captureStackTrace

Use the current class to get a clean stack.

-    Error.captureStackTrace(this, MissingLogNewQuestionEventError);
+    Error.captureStackTrace(this, MissingSnapshotProposalError);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export class MissingSnapshotProposalError extends Error {
constructor(proposalId: Hex, questionId: Hex, txHash: Hash) {
const message = `Unable to resolve proposal ${proposalId} related to Reality question ${questionId} present in the LogNewQuestion event in tx ${txHash}`;
super(message);
this.name = this.constructor.name;
Error.captureStackTrace(this, InvalidLogNewQuestionArgsEventError);
Error.captureStackTrace(this, MissingLogNewQuestionEventError);
}
}
}
export class MissingSnapshotProposalError extends Error {
constructor(proposalId: Hex, questionId: Hex, txHash: Hash) {
const message = `Unable to resolve proposal ${proposalId} related to Reality question ${questionId} present in the LogNewQuestion event in tx ${txHash}`;
super(message);
this.name = this.constructor.name;
Error.captureStackTrace(this, MissingSnapshotProposalError);
}
}
🤖 Prompt for AI Agents
In src/utils/errors.ts around lines 12 to 19, the Error.captureStackTrace call
passes the wrong class (MissingLogNewQuestionEventError); replace it to use the
current class so the stack is captured correctly—call
Error.captureStackTrace(this, MissingSnapshotProposalError) or
Error.captureStackTrace(this, this.constructor) after super(...) so the stack
trace is associated with MissingSnapshotProposalError.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy paste error :) I will fix it!

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fcanela Thank you for confirming! No problem—easy fix. Looking forward to seeing it corrected in the next commit. 👍

Comment on lines +12 to +14
The following automatic checks has PASSED:
- The proposal Safes hashes are the keccak of the concatenation of the individual hashes
- The hash included on the Reality.eth question is the Proposal safe hash
Copy link

@coderabbitai coderabbitai bot Oct 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix grammar: "checks has" → "checks have"

The subject "checks" is plural and requires the plural verb form "have".

Apply this diff:

-The following automatic checks has PASSED:
+The following automatic checks have PASSED:
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
The following automatic checks has PASSED:
- The proposal Safes hashes are the keccak of the concatenation of the individual hashes
- The hash included on the Reality.eth question is the Proposal safe hash
The following automatic checks have PASSED:
- The proposal Safes hashes are the keccak of the concatenation of the individual hashes
- The hash included on the Reality.eth question is the Proposal safe hash
🤖 Prompt for AI Agents
In templates/email/proposal-valid-plain.ejs around lines 12 to 14, the sentence
"The following automatic checks has PASSED:" uses the singular verb "has" with
plural subject "checks"; change it to "The following automatic checks have
PASSED:" so the verb agrees with the plural subject (replace "has" with "have").

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noted. I will fix this!

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fcanela, thank you for confirming! I appreciate you addressing the grammar issue. 👍

Comment on lines +10 to +12
The following automatic checks has PASSED:
- The proposal Safes hashes are the keccak of the concatenation of the individual hashes
- The hash included on the Reality.eth question is the Proposal safe hash
Copy link

@coderabbitai coderabbitai bot Oct 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix grammar: "checks has" → "checks have"

The subject "checks" is plural and requires the plural verb form "have".

Apply this diff:

-The following automatic checks has PASSED:
+The following automatic checks have PASSED:
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
The following automatic checks has PASSED:
- The proposal Safes hashes are the keccak of the concatenation of the individual hashes
- The hash included on the Reality.eth question is the Proposal safe hash
The following automatic checks have PASSED:
- The proposal Safes hashes are the keccak of the concatenation of the individual hashes
- The hash included on the Reality.eth question is the Proposal safe hash
🤖 Prompt for AI Agents
templates/telegram/proposal-valid.ejs lines 10-12: fix subject-verb agreement in
the message by changing "The following automatic checks has PASSED:" to "The
following automatic checks have PASSED:" (update the template text accordingly
so "checks" uses the plural verb "have").

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noted. I will fix this!

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fcanela, thank you for confirming! I appreciate you addressing the grammar issue. 🙏

}
const safes = proposal.plugins?.safeSnap?.safes;
if (!safes || safes.length < 1) {
throw new SafeSnapPluginConfigurationError();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I scraped all the SafeSnap proposals for inspection to see what could go wrong. I noticed some of them had a plugins.safeSnap field but set to null.

operation: Number(operation),
});

if (hash != calculatedHash) throw new TxHashMismatchError(hash, calculatedHash);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This kind of error shouldn't happen, but as we are using Snapshot GraphQL instead of an on-chain contract, I validated the integrity of the result.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
src/processing.ts (1)

282-304: Guard against mismatched proposal IDs before persisting.

Line 288 currently trusts event.proposalId without confirming it matches question.proposalId. If those ever diverge (upstream bug, tampering, partial sync), we would emit a notification containing one proposalId while validating/persisting another, and we would store a DB row whose proposalId no longer corresponds to the validated Snapshot proposal. That is a data-integrity break and undermines the whole validation flow flagged for special review. Please fail fast here (ideally with a dedicated validation error/severity) before calling getSnapshotProposalFn/insertProposalFn.

+      if (event.proposalId !== question.proposalId) {
+        throw new Error(
+          `Mismatch between ProposalQuestionCreated.proposalId (${event.proposalId}) and LogNewQuestion.proposalId (${question.proposalId})`,
+        );
+      }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1f78586 and 673ae1f.

📒 Files selected for processing (7)
  • src/processing.integration-test.ts (3 hunks)
  • src/processing.ts (3 hunks)
  • src/utils/reality-question-validation/eip-712-transaction-hash.test.ts (1 hunks)
  • src/utils/reality-question-validation/eip-712-transaction-hash.ts (1 hunks)
  • src/utils/reality-question-validation/errors.ts (1 hunks)
  • src/utils/reality-question-validation/index.test.ts (1 hunks)
  • src/utils/reality-question-validation/index.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
src/utils/reality-question-validation/eip-712-transaction-hash.test.ts (1)
src/utils/reality-question-validation/eip-712-transaction-hash.ts (1)
  • calculateTxHash (36-43)
src/utils/reality-question-validation/index.test.ts (4)
src/services/reality/index.ts (1)
  • LogNewQuestion (174-187)
src/services/snapshot/index.ts (1)
  • SnapshotProposal (76-76)
src/utils/reality-question-validation/index.ts (1)
  • assertValidRealityQuestion (30-72)
src/utils/reality-question-validation/errors.ts (6)
  • TxHashMismatchError (40-47)
  • SafeHashCalculationError (49-56)
  • SafeHashMismatchError (67-74)
  • SafeSnapPluginConfigurationError (31-38)
  • ProposalIdMismatchError (23-30)
  • SafeNotFoundForProposalNetworkError (58-65)
src/processing.integration-test.ts (10)
src/services/db/schema.ts (2)
  • space (3-7)
  • proposal (24-44)
src/types.ts (1)
  • Space (3-7)
src/utils/test-mocks.ts (1)
  • randomizeSpace (21-27)
src/utils/tests-setup.ts (2)
  • ONEINCH_ORACLE_ADDRESS (41-41)
  • expect (9-9)
src/services/db/spaces.ts (2)
  • insertSpaces (30-33)
  • removeSpaceByEns (59-62)
src/services/db/proposals.ts (3)
  • removeProposalByQuestionId (57-61)
  • insertProposal (24-28)
  • findProposalByQuestionId (39-47)
src/services/reality/index.ts (1)
  • getLogNewQuestion (204-253)
src/utils/reality-question-validation/index.ts (2)
  • validateRealityQuestion (98-112)
  • ValidationResult (85-85)
src/services/snapshot/index.ts (1)
  • getProposal (78-92)
src/notify.ts (1)
  • Notification (47-47)
src/utils/reality-question-validation/index.ts (4)
src/services/reality/index.ts (1)
  • LogNewQuestion (174-187)
src/services/snapshot/index.ts (1)
  • SnapshotProposal (76-76)
src/utils/reality-question-validation/errors.ts (7)
  • ProposalIdMismatchError (23-30)
  • SafeSnapPluginConfigurationError (31-38)
  • TxHashMismatchError (40-47)
  • SafeHashCalculationError (49-56)
  • SafeNotFoundForProposalNetworkError (58-65)
  • SafeHashMismatchError (67-74)
  • BaseProposalValidationError (14-21)
src/utils/reality-question-validation/eip-712-transaction-hash.ts (1)
  • calculateTxHash (36-43)
src/processing.ts (8)
src/services/snapshot/index.ts (1)
  • getProposal (78-92)
src/utils/reality-question-validation/index.ts (2)
  • validateRealityQuestion (98-112)
  • ValidationResult (85-85)
src/notify.ts (4)
  • notify (58-65)
  • Notification (47-47)
  • ValidProposalNotification (29-33)
  • InvalidProposalNotification (35-40)
src/services/db/proposals.ts (1)
  • insertProposal (24-28)
src/services/db/schema.ts (3)
  • space (3-7)
  • notification (11-22)
  • proposal (24-44)
src/types.ts (1)
  • Space (3-7)
src/services/reality/index.ts (3)
  • ProposalQuestionCreated (118-125)
  • LogNewQuestion (174-187)
  • getLogNewQuestion (204-253)
src/utils/errors.ts (1)
  • MissingSnapshotProposalError (12-19)

if (calculatedSafeHash != hash) throw new SafeHashCalculationError(hash, calculatedSafeHash);
});

const proposalNetworkSafe = safes.find((safe) => safe.network === proposal.network);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have a question here.

After scrapping all the proposals and investigating some edge cases, I noticed that there are proposals that have multiple safes. When that happens, the safes are in different networks.

My understanding is that in those cases with multiple safes, the Reality question contains the hash of the safe in the proposal network and that is the one to compare against.

I lack knowledge here to understand how that multi-safe setup works and if there is any extra validation required. A quick answer indicating if I should pursue the topic further will suffice.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These templates are placeholder. Feel free to indicate if you want to change anything. I tried to make the message alerting, which the alert as quickly noticeable as possible.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These files are just for testing, it validates that the values are reaching the templates

@fcanela fcanela requested a review from jaybuidl October 8, 2025 10:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants