a manifold extension to port fully onchain an existing collection minted with offchain content
clgsms/contracts/ColorgasmRendererV3.sol
Solidity · 247 lines · 10 KBRAWDOWNLOAD
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Ownable} from "solady/src/auth/Ownable.sol";
import {Base64} from "solady/src/utils/Base64.sol";
import {IColorgasmArtworkSource, IColorgasmBlobStorage, IColorgasmRenderer} from "./interfaces/IColorgasm.sol";
/// @title COLORGASM onchain renderer, v3 — single-pass assembly
///
/// @notice Builds the whole token document in ONE buffer: the output is sized
/// exactly, allocated once, and every byte is written into its final
/// place and never moved again. v1 and v2 both built the image, then a
/// base64 of it, then the JSON, then sometimes a base64 of that — four
/// full-size buffers, and EVM memory is billed with the square of how
/// far it stretches.
///
/// Two storage shapes, chosen per deployment:
///
/// PRE-ENCODED (`preEncoded = true`, the cheapest to render) — the
/// bytes in storage are already the base64 of the image, so rendering
/// is a straight `extcodecopy` of each chunk into the output. No
/// encoding onchain at all. Costs 33% more to store, since base64 is
/// 4 bytes per 3.
///
/// RAW (`preEncoded = false`) — the bytes in storage are the image
/// itself and the renderer base64s them. Because ColorgasmStorage caps
/// chunks at a multiple of 3, each chunk encodes independently and the
/// results concatenate exactly, so this still needs only one scratch
/// chunk of working memory rather than a copy of the whole image.
/// Cheapest to store; slightly more expensive to render.
///
/// Both produce byte-identical documents. Measure both for your own
/// sizes with `npm run ceiling` before committing to either.
///
/// @dev Reads the artwork pointer (a storage key) and the metadata fragment
/// from the extension, then the chunk pointers from the blob storage.
/// Nothing here is token-aware beyond that, so it is a plain
/// `setRenderer()` swap.
contract ColorgasmRendererV3 is Ownable, IColorgasmRenderer {
error NothingStored();
error ChunkNotAligned(uint256 index);
/// @notice The redeem extension: artwork pointers and metadata fragments.
IColorgasmArtworkSource public immutable source;
/// @notice Where the artwork bytes live.
IColorgasmBlobStorage public immutable blobs;
/// @notice MIME type used for the image data URI.
string public imageMime;
/// @notice True when stored bytes are already base64 of the image.
bool public immutable preEncoded;
/// @notice When true the JSON document is base64-encoded (maximum
/// compatibility); when false it is served as UTF-8, which is
/// roughly half the gas and what makes large tokens renderable.
bool public base64Json;
constructor(address source_, address blobs_, bool preEncoded_) {
_initializeOwner(msg.sender);
source = IColorgasmArtworkSource(source_);
blobs = IColorgasmBlobStorage(blobs_);
preEncoded = preEncoded_;
imageMime = "image/avif";
base64Json = true;
}
function setImageMime(string calldata imageMime_) external onlyOwner {
imageMime = imageMime_;
}
/// @notice Switch the JSON document between base64 and plain UTF-8.
function setBase64Json(bool base64Json_) external onlyOwner {
base64Json = base64Json_;
}
/// @inheritdoc IColorgasmRenderer
function tokenURI(address, uint256, uint256 originalTokenId)
external
view
override
returns (string memory)
{
if (base64Json) {
bytes memory json = _assemble(originalTokenId, "");
return string(abi.encodePacked("data:application/json;base64,", Base64.encode(json)));
}
// The cheap path: the prefix is reserved at the front of the same
// buffer, so the finished document is never copied again.
return string(_assemble(originalTokenId, "data:application/json;utf8,"));
}
/// @notice Size in bytes of the document `tokenURI` will return, without
/// building it. Useful to check a token against an RPC's response
/// limits before anyone tries to load it.
function renderedSize(uint256 originalTokenId) external view returns (uint256) {
(, uint256 imageChars) = _imageLayout(originalTokenId);
bytes memory meta = source.metadataOf(originalTokenId);
uint256 metaLen = base64Json ? meta.length : _escapeForUri(meta).length;
uint256 json = _headLength(metaLen) + imageChars + 2;
return base64Json ? 28 + 4 * ((json + 2) / 3) : 26 + json;
}
// -------------------------------------------------------------- internal --
/// @dev `{` + fragment + `,` + `"image":"data:<mime>;base64,`
function _headLength(uint256 metaLen) internal view returns (uint256) {
// "{" + meta + ","(only when meta is present)
// + '"image":"data:' (14) + mime + ';base64,' (8)
return 1 + metaLen + (metaLen == 0 ? 0 : 1) + 14 + bytes(imageMime).length + 8;
}
/// @dev Chunk pointers plus how many base64 characters they will produce.
function _imageLayout(uint256 originalTokenId)
internal
view
returns (address[] memory pointers, uint256 imageChars)
{
pointers = blobs.chunksOf(source.artworkKeyOf(originalTokenId));
uint256 n = pointers.length;
if (n == 0) revert NothingStored();
uint256 dataLen;
for (uint256 i; i < n; ++i) {
uint256 size = pointers[i].code.length - 1;
// Every chunk but the last must be a multiple of 3, otherwise
// encoding chunk by chunk would insert padding mid-image.
if (!preEncoded && i + 1 < n && size % 3 != 0) revert ChunkNotAligned(i);
dataLen += size;
}
imageChars = preEncoded ? dataLen : 4 * ((dataLen + 2) / 3);
}
/// @dev Build `<prefix>{<meta>,"image":"data:<mime>;base64,<image>"}` into a
/// single exactly-sized buffer.
function _assemble(uint256 originalTokenId, bytes memory prefix)
internal
view
returns (bytes memory out)
{
(address[] memory pointers, uint256 imageChars) = _imageLayout(originalTokenId);
bytes memory meta = source.metadataOf(originalTokenId);
if (!base64Json) meta = _escapeForUri(meta);
bytes memory head = meta.length == 0
? abi.encodePacked("{", '"image":"data:', imageMime, ";base64,")
: abi.encodePacked("{", meta, ",", '"image":"data:', imageMime, ";base64,");
uint256 total = prefix.length + head.length + imageChars + 2;
assembly {
out := mload(0x40)
mstore(out, total)
// Zero the final partial word so no stale memory can leak into the
// string, then reserve the whole thing.
let end := add(add(out, 0x20), total)
mstore(end, 0)
mstore(0x40, add(out, and(add(add(total, 0x20), 0x3f), not(0x1f))))
}
uint256 dst;
assembly {
dst := add(out, 0x20)
}
dst = _copy(dst, prefix);
dst = _copy(dst, head);
uint256 n = pointers.length;
for (uint256 i; i < n; ++i) {
address pointer = pointers[i];
if (preEncoded) {
// Straight copy: the stored bytes are already base64.
assembly {
let size := sub(extcodesize(pointer), 1)
extcodecopy(pointer, dst, 1, size)
dst := add(dst, size)
}
} else {
// Encode this chunk on its own and copy the result in. The
// scratch and the encoder's output are both released again by
// restoring the free-memory pointer, so working memory stays at
// one chunk however many there are.
uint256 mark;
assembly {
mark := mload(0x40)
}
bytes memory raw;
assembly {
let size := sub(extcodesize(pointer), 1)
raw := mload(0x40)
mstore(raw, size)
extcodecopy(pointer, add(raw, 0x20), 1, size)
mstore(0x40, add(raw, and(add(add(size, 0x20), 0x3f), not(0x1f))))
}
dst = _copy(dst, bytes(Base64.encode(raw)));
assembly {
mstore(0x40, mark)
}
}
}
_copy(dst, bytes('"}'));
}
/// @dev Copy `data` to `dst`, returning the position just past it.
function _copy(uint256 dst, bytes memory data) internal pure returns (uint256) {
assembly {
let len := mload(data)
let src := add(data, 0x20)
for { let o := 0 } lt(o, len) { o := add(o, 0x20) } {
mstore(add(dst, o), mload(add(src, o)))
}
dst := add(dst, len)
}
return dst;
}
/// @dev Percent-encode what would break a raw data URI. Every COLORGASM
/// name contains a `#`, which starts the fragment and would truncate
/// the document at the name; `%` starts an escape. Applied only to the
/// metadata fragment — base64 output is URI-safe throughout.
function _escapeForUri(bytes memory input) internal pure returns (bytes memory) {
uint256 extra;
for (uint256 i; i < input.length; ++i) {
uint8 c = uint8(input[i]);
if (c == 0x25 || c == 0x23 || c < 0x21 || c > 0x7e) extra += 2;
}
if (extra == 0) return input;
bytes memory out = new bytes(input.length + extra);
bytes16 hexDigits = "0123456789ABCDEF";
uint256 j;
for (uint256 i; i < input.length; ++i) {
uint8 c = uint8(input[i]);
if (c == 0x25 || c == 0x23 || c < 0x21 || c > 0x7e) {
out[j++] = "%";
out[j++] = hexDigits[c >> 4];
out[j++] = hexDigits[c & 0x0f];
} else {
out[j++] = input[i];
}
}
return out;
}
}