import assert from "node:assert/strict"; import { describe, it, before } from "node:test"; import { network } from "hardhat"; import { CHUNK, CLGSM, creatorAt, decodeTokenURI, deployStack, fakeImage, fragment, sameBytes, upload, } from "./helpers.js"; /** * The same flow as test/local.test.js, but against the REAL CLGSM creator * contract on a mainnet fork: real proxy, real admin, real holders, real * originals still pointing at Arweave. * * npm run test:fork # token 1's current holder * FORK_TOKEN_ID=57 npm run test:fork * * Needs ALCHEMY_KEY (or INFURA_KEY, or MAINNET_RPC_URL) in .env. Pin * FORK_BLOCK once you have a run you like — the fork cache then makes repeat * runs fast and the holder addresses stop moving under you. */ const NO_RPC = !( process.env.ALCHEMY_KEY || process.env.INFURA_KEY || process.env.MAINNET_RPC_URL ); describe( "COLORGASM redeem — mainnet fork", { skip: NO_RPC && "no mainnet RPC — set ALCHEMY_KEY, INFURA_KEY or MAINNET_RPC_URL in .env" }, () => { const TOKEN_ID = Number(process.env.FORK_TOKEN_ID ?? 1); let ethers, networkHelpers; let artist, creator, blobs, ext, renderer; let holder, admin; before(async () => { ({ ethers, networkHelpers } = await network.create({ network: "forked" })); [artist] = await ethers.getSigners(); creator = creatorAt(ethers, CLGSM); // The two mainnet identities this migration depends on. const adminAddress = await creator.owner(); const holderAddress = await creator.ownerOf(TOKEN_ID); for (const a of [adminAddress, holderAddress]) { await networkHelpers.impersonateAccount(a); await networkHelpers.setBalance(a, 10n ** 18n); } admin = await ethers.getSigner(adminAddress); holder = await ethers.getSigner(holderAddress); ({ blobs, ext, renderer } = await deployStack(ethers, CLGSM)); await ( await creator.connect(admin).registerExtension(await ext.getAddress(), "") ).wait(); }); it("is talking to the real COLORGASM contract", async () => { assert.equal(await creator.symbol(), "CLGSM"); assert.match(await creator.tokenURI(TOKEN_ID), /^https:\/\/arweave\.net\//); // the edition is 100 pieces and nothing beyond it exists assert.notEqual(await creator.ownerOf(100), ethers.ZeroAddress); await assert.rejects(creator.ownerOf(101)); }); it("registers as a tokenURI extension on the creator", async () => { assert.equal( await creator.getExtensions().then((xs) => xs.includes(ext.target)), true, ); }); it("blocks redeem until the artwork is stored", async () => { assert.equal(await ext.isRedeemable(TOKEN_ID), false); await assert.rejects( ext.connect(holder).burnRedeem(TOKEN_ID), (e) => e.message.includes("NotRedeemable"), "redeem must be impossible before PHASE 1", ); }); it("uploads the artwork and points the token at it", async () => { // Sized like a real encode: multi-chunk, odd tail. const image = fakeImage(CHUNK * 2 + 4_321, TOKEN_ID); const meta = fragment(TOKEN_ID); const { key, chunks, gas } = await upload(blobs, TOKEN_ID, image); console.log( ` upload(${TOKEN_ID}): ${image.length} B image in ${chunks} chunk(s), ` + `${gas} gas (${(Number(gas) / image.length).toFixed(0)} gas/byte of image)`, ); const receipt = await ( await ext.setArtwork(TOKEN_ID, key, ethers.toUtf8Bytes(meta)) ).wait(); console.log(` setArtwork(${TOKEN_ID}): ${receipt.gasUsed} gas`); assert.equal(await ext.artworkKeyOf(TOKEN_ID), key); assert.equal(ethers.toUtf8String(await ext.metadataOf(TOKEN_ID)), meta); assert.equal(await ext.isRedeemable(TOKEN_ID), true); }); it("burns the original and mints a fully onchain replacement", async () => { const balanceBefore = await creator.balanceOf(holder.address); await (await creator.connect(holder).approve(await ext.getAddress(), TOKEN_ID)).wait(); const newTokenId = await ext.connect(holder).burnRedeem.staticCall(TOKEN_ID); const receipt = await (await ext.connect(holder).burnRedeem(TOKEN_ID)).wait(); console.log(` burnRedeem(${TOKEN_ID}) -> #${newTokenId}: ${receipt.gasUsed} gas`); await assert.rejects(creator.ownerOf(TOKEN_ID), "the original must be burned"); assert.equal(await creator.ownerOf(newTokenId), holder.address); assert.equal(await ext.originalOf(newTokenId), BigInt(TOKEN_ID)); assert.equal(await ext.redeemedTokenOf(TOKEN_ID), newTokenId); // one burned, one minted: the holder's balance is unchanged assert.equal(await creator.balanceOf(holder.address), balanceBefore); assert.equal(await creator.tokenExtension(newTokenId), await ext.getAddress()); // The creator core must delegate tokenURI to us, and the whole token must // come back out of chain state. const { json, mime, imageBytes } = decodeTokenURI(await creator.tokenURI(newTokenId)); assert.equal(mime, "image/avif"); assert.equal(imageBytes.length, CHUNK * 2 + 4_321); sameBytes(imageBytes, fakeImage(CHUNK * 2 + 4_321, TOKEN_ID)); console.log(` tokenURI: ${(await creator.tokenURI(newTokenId)).length} characters`); assert.equal(json.name, `COLORGASM #${String(TOKEN_ID).padStart(3, "0")}`); assert.equal(json.created_by, "FelixFelixFelix"); }); it("leaves every other original untouched on Arweave", async () => { const other = TOKEN_ID === 1 ? 2 : 1; assert.match(await creator.tokenURI(other), /^https:\/\/arweave\.net\//); assert.equal(await ext.isRedeemable(other), false); }); it("keeps royalties on the redeemed token", async () => { const newTokenId = await ext.redeemedTokenOf(TOKEN_ID); const royalties = await creator.royaltyInfo(newTokenId, 10_000n).catch(() => null); if (royalties === null) { console.log(" royaltyInfo not available on this creator — skipped"); return; } const [recipient, amount] = royalties; console.log(` royaltyInfo(#${newTokenId}, 10000) -> ${amount} to ${recipient}`); assert.equal(typeof amount, "bigint"); }); }, );