a manifold extension to port fully onchain an existing collection minted with offchain content
clgsms/test/fork.test.js
JavaScript · 157 lines · 6 KBRAWDOWNLOAD
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");
});
},
);