diff --git a/.gitignore b/.gitignore index ccc356f..ead3d97 100644 --- a/.gitignore +++ b/.gitignore @@ -40,6 +40,9 @@ deployments/*.json !deployments/allowlists/ !deployments/allowlists/*.json +# Snapshot signed payloads (local only) +space-message*.json + # Logs *.log npm-debug.log* diff --git a/README.md b/README.md index 4e1834c..700f6c7 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,11 @@ KEYSTORE_PATH=keystores/deployer-0x
.json ## Deployment +### Deployment outputs + +- **Source of truth**: `config/chains.json` +- **Local backups**: `deployments/*.json` (gitignored) + ### Full Chain Deployment Deploys all modules + aggregator: @@ -97,6 +102,33 @@ pnpm exec hardhat verify --network
Get the aggregator address from `config/chains.json` → `chains..deployed.aggregator`. +## Debugging Snapshot Settings + +To confirm what Snapshot Hub currently has stored for a space: + +```bash +pnpm exec tsx scripts/check-snapshot-space.ts +SPACE=quickvote.eth pnpm exec tsx scripts/check-snapshot-space.ts +``` + +## Publishing Snapshot Settings (Safe) + +When using Safe multi-sig, Snapshot space updates are signed as a Safe “message”, but the signed payload still needs to be broadcast to Snapshot Hub. + +1) Save the typed-data JSON you signed (the object with `domain`, `types`, `primaryType`, `message`) to a file, e.g. `space-message.json`. + +2) Publish it using the Safe prepared signature: + +```bash +pnpm exec tsx scripts/publish-snapshot-settings.ts --file ./space-message.json --sig 0x +``` + +3) Verify it’s live: + +```bash +pnpm exec tsx scripts/check-snapshot-space.ts +``` + ## Testing ```bash diff --git a/scripts/check-snapshot-space.ts b/scripts/check-snapshot-space.ts new file mode 100644 index 0000000..161a4ab --- /dev/null +++ b/scripts/check-snapshot-space.ts @@ -0,0 +1,77 @@ +/** + * Check Snapshot space settings (strategies) directly from Snapshot Hub. + * + * Usage: + * pnpm exec tsx scripts/check-snapshot-space.ts + * SPACE=quickvote.eth pnpm exec tsx scripts/check-snapshot-space.ts + * + * Notes: + * - Snapshot settings updates are off-chain. A Safe "message" being signed does NOT + * guarantee the settings were published to Snapshot Hub. + */ + +type Strategy = { name: string; network: string; params: Record }; + +const SPACE = process.env.SPACE || "quickvote.eth"; +const HUB_URL = process.env.SNAPSHOT_HUB_URL || "https://hub.snapshot.org/graphql"; + +const query = ` + query Space($id: String!) { + space(id: $id) { + id + name + network + strategies { + name + network + params + } + } + } +`; + +function shortAddr(v: unknown): string { + if (typeof v !== "string") return String(v); + if (!v.startsWith("0x") || v.length < 10) return v; + return `${v.slice(0, 6)}…${v.slice(-4)}`; +} + +async function main() { + const res = await fetch(HUB_URL, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ query, variables: { id: SPACE } }), + }); + + if (!res.ok) { + const text = await res.text().catch(() => ""); + throw new Error(`Snapshot Hub request failed (${res.status}): ${text.slice(0, 300)}`); + } + + const json = (await res.json()) as any; + if (json.errors?.length) { + throw new Error(`Snapshot Hub GraphQL error: ${JSON.stringify(json.errors, null, 2)}`); + } + + const space = json.data?.space as + | { id: string; name: string; network: string; strategies: Strategy[] } + | null; + if (!space) throw new Error(`Space not found: ${SPACE}`); + + console.log(`Space: ${space.name} (${space.id})`); + console.log(`Default network: ${space.network}`); + console.log(`Strategies (${space.strategies.length}):`); + for (const [i, s] of space.strategies.entries()) { + const addr = (s.params as any)?.address; + console.log(` ${i + 1}. ${s.name} (network=${s.network}) address=${addr ? shortAddr(addr) : "n/a"}`); + } +} + +main().catch((e) => { + console.error(e); + process.exit(1); +}); + + + + diff --git a/scripts/publish-snapshot-settings.ts b/scripts/publish-snapshot-settings.ts new file mode 100644 index 0000000..e30dfa7 --- /dev/null +++ b/scripts/publish-snapshot-settings.ts @@ -0,0 +1,128 @@ +/** + * Publish a signed Snapshot Space settings message to Snapshot Hub. + * + * This is the missing step when using Safe "Messages": collecting signatures in Safe does NOT + * automatically broadcast the signed payload to Snapshot Hub. + * + * Usage: + * pnpm exec tsx scripts/publish-snapshot-settings.ts --file ./space-message.json --sig 0x... + * + * Optional: + * --hub https://hub.snapshot.org + * --space quickvote.eth + * + * The JSON file must contain the EIP-712 typed data: + * { domain, primaryType, types, message } + * + * Notes: + * - Do NOT re-serialize `message.settings` yourself. It must match exactly what was signed. + * - For Safe, `--sig` should be the Safe "prepared signature" (concatenated signatures). + */ + +type TypedData = { + domain: { name: string; version: string }; + primaryType: string; + types: Record>; + message: { from: string; space: string; timestamp: string | number; settings: string }; +}; + +import { getAddress } from "viem"; + +function getArg(flag: string): string | undefined { + const i = process.argv.indexOf(flag); + if (i === -1) return undefined; + return process.argv[i + 1]; +} + +async function postJson(url: string, body: unknown) { + const res = await fetch(url, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify(body), + }); + + const text = await res.text().catch(() => ""); + let json: any = undefined; + try { + json = text ? JSON.parse(text) : undefined; + } catch { + // ignore + } + + if (!res.ok) { + throw new Error(`Hub request failed (${res.status}): ${text.slice(0, 500)}`); + } + + return json ?? text; +} + +async function main() { + const file = getArg("--file") || process.env.SNAPSHOT_TYPED_DATA_FILE; + const sig = getArg("--sig") || process.env.SNAPSHOT_SIG; + const hub = (getArg("--hub") || process.env.SNAPSHOT_HUB || "https://hub.snapshot.org").replace(/\/+$/, ""); + const overrideSpace = getArg("--space") || process.env.SNAPSHOT_SPACE; + + if (!file) { + throw new Error("Missing --file (or SNAPSHOT_TYPED_DATA_FILE)."); + } + if (!sig || !sig.startsWith("0x")) { + throw new Error("Missing --sig (or SNAPSHOT_SIG). Expected 0x-prefixed hex string."); + } + + const fs = await import("node:fs"); + const raw = fs.readFileSync(file, "utf8"); + const typed = JSON.parse(raw) as TypedData; + + // Normalize without changing semantics: + // - timestamp must be a number for some encoders; keep value identical. + const timestamp = + typeof typed.message.timestamp === "string" ? Number(typed.message.timestamp) : typed.message.timestamp; + if (!Number.isFinite(timestamp)) throw new Error("Invalid message.timestamp"); + + const message = { + ...typed.message, + timestamp, + space: overrideSpace ?? typed.message.space, + }; + + // Snapshot Hub expects EIP-55 checksummed addresses in the envelope. + // Also normalize message.from to checksum to avoid mismatches. + const from = getAddress(message.from); + message.from = from; + + const data = { + domain: typed.domain, + types: typed.types, + primaryType: typed.primaryType, + message, + }; + + // Snapshot Hub expects: + // { address, sig, data } + // Where `address` is the signer (Safe address for EIP-1271 verification). + const payload = { + address: from, + sig, + data, + }; + + console.log("Publishing to Snapshot Hub…"); + console.log(" hub:", hub); + console.log(" space:", message.space); + console.log(" from:", message.from); + console.log(" api:", `${hub}/api/msg`); + + const result = await postJson(`${hub}/api/msg`, payload); + console.log("\n✅ Hub response:"); + console.log(typeof result === "string" ? result : JSON.stringify(result, null, 2)); + + console.log("\nNext: verify with:"); + console.log(" pnpm exec tsx scripts/check-snapshot-space.ts"); +} + +main().catch((e) => { + console.error(e); + process.exit(1); +}); + +