a manifold extension to port fully onchain an existing collection minted with offchain content
clgsms/scripts/inspect.js
JavaScript · 149 lines · 5 KBRAWDOWNLOAD
/**
* 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
* file.
*
* Env:
* 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";
import {
addressFor,
avifPath,
base64Chunks,
connect,
creatorAt,
decodeTokenURI,
keyName,
main,
metaPath,
rawChunks,
readBlobBytes,
readDeployment,
} from "./_lib.js";
main(async () => {
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 });
console.log(
`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");
let pointed = 0;
let bytes = 0n;
const problems = [];
for (let id = from; id <= to; id++) {
const key = await ext.artworkKeyOf(id);
if (key === ethers.ZeroHash) {
console.log(` ${pad(id)} —`);
continue;
}
pointed++;
const [size, sealed, onchainMeta, redeemedId] = await Promise.all([
blobs.sizeOf(key),
blobs.isSealed(key),
ext.metadataOf(id),
ext.redeemedTokenOf(id),
]);
bytes += size;
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";
if (existsSync(mPath)) {
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
let uriState = "—";
if (redeemedId !== 0n) {
try {
const { json, imageBytes } = decodeTokenURI(await creator.tokenURI(redeemedId));
const local = existsSync(imgPath) ? readFileSync(imgPath) : null;
uriState =
local === null
? `${json.name} (${imageBytes.length} B)`
: imageBytes.equals(local)
? `${json.name} ✓`
: `${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);
} catch (e) {
uriState = `ERROR ${e.message}`;
problems.push(`#${id} tokenURI: ${e.message}`);
}
}
console.log(
` ${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);
console.log(
`\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}`);
process.exitCode = 1;
} else if (pointed > 0) {
console.log("everything onchain matches the local files");
}
});
const pad = (n) => String(n).padStart(3);