* Audit what is actually onchain — the check to run before locking anything, * and the one to point a sceptical collector at. * FROM_ID=1 TO_ID=100 npx hardhat run scripts/inspect.js --network mainnet * For every token in the range it reports the storage key, how many bytes are * under it, whether those bytes match the local AVIF exactly, whether the * metadata matches, and — for tokens already redeemed — decodes the live * tokenURI the creator serves and checks the image inside it against the same * EXTENSION / STORAGE / CREATOR default: deployments/<network>.json * FROM_ID / TO_ID token range, default 1..100 * DUMP_DIR write each decoded tokenURI image here import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { join } from "node:path"; const { ethers, net } = await connect(); const ext = await ethers.getContractAt( "ColorgasmRedeemExtension", addressFor("extension", net), const blobs = await ethers.getContractAt("ColorgasmStorage", addressFor("storage", net)); const creator = creatorAt(ethers, addressFor("creator", net)); const renderer = await ethers.getContractAt("ColorgasmRendererV3", await ext.renderer()); const preEncoded = await renderer.preEncoded(); const from = Number(process.env.FROM_ID ?? 1); const to = Number(process.env.TO_ID ?? 100); const dumpDir = process.env.DUMP_DIR; if (dumpDir) mkdirSync(dumpDir, { recursive: true }); `extension ${(await ext.storageLocked()) ? "LOCKED" : "unlocked"} ` + `renderer ${await ext.renderer()}${(await ext.rendererLocked()) ? " (LOCKED)" : ""} ` + `${preEncoded ? "pre-encoded" : "raw"}, ${(await renderer.base64Json()) ? "base64" : "utf8"} JSON ` + `redeemed ${await ext.redeemedCount()}\n`, console.log(" id key stored sealed image metadata redeemed tokenURI"); for (let id = from; id <= to; id++) { const key = await ext.artworkKeyOf(id); if (key === ethers.ZeroHash) { console.log(` ${pad(id)} —`); const [size, sealed, onchainMeta, redeemedId] = await Promise.all([ if (!sealed) problems.push(`#${id} "${keyName(key)}" is not sealed — its bytes can still change`); // compare the stored bytes against the local file const imgPath = avifPath(id); let imageState = "no file"; if (existsSync(imgPath)) { const image = readFileSync(imgPath); const want = Buffer.concat( (preEncoded ? base64Chunks(image) : rawChunks(image)).map((c) => Buffer.from(c.slice(2), "hex"), const stored = await readBlobBytes(blobs, ethers.provider, key); imageState = stored.equals(want) ? "exact" : `MISMATCH ${stored.length}/${want.length}`; if (!stored.equals(want)) problems.push(`#${id} stored bytes differ from ${imgPath}`); const mPath = metaPath(id); let metaState = "no file"; const localMeta = "0x" + readFileSync(mPath).toString("hex"); metaState = onchainMeta === localMeta ? "exact" : "MISMATCH"; if (onchainMeta !== localMeta) problems.push(`#${id} metadata differs from ${mPath}`); // the live tokenURI, for tokens that have been redeemed const { json, imageBytes } = decodeTokenURI(await creator.tokenURI(redeemedId)); const local = existsSync(imgPath) ? readFileSync(imgPath) : null; ? `${json.name} (${imageBytes.length} B)` : imageBytes.equals(local) : `${json.name} IMAGE MISMATCH`; if (local !== null && !imageBytes.equals(local)) { problems.push(`#${id} rendered tokenURI image differs from ${imgPath}`); if (dumpDir) writeFileSync(join(dumpDir, `${id}.avif`), imageBytes); uriState = `ERROR ${e.message}`; problems.push(`#${id} tokenURI: ${e.message}`); ` ${pad(id)} ${keyName(key).padEnd(15)} ${String(size).padStart(7)} ` + `${(sealed ? "yes" : "NO").padEnd(6)} ${imageState.padEnd(9)} ${metaState.padEnd(9)} ` + `${String(redeemedId === 0n ? "no" : `#${redeemedId}`).padEnd(9)} ${uriState}`, const d = readDeployment(net); `\n${pointed} token(s) pointed at artwork, ${bytes} bytes stored` + (d.storage ? `\nstorage ${d.storage}` : "") + (dumpDir ? `\ndecoded images written to ${dumpDir}/` : ""), if (problems.length > 0) { console.log(`\n${problems.length} problem(s):`); for (const p of problems) console.log(` ✗ ${p}`); } else if (pointed > 0) { console.log("everything onchain matches the local files"); const pad = (n) => String(n).padStart(3);