Skip to content

Commit cc0073a

Browse files
committed
Refactor contracts
1 parent a303cc0 commit cc0073a

27 files changed

+745
-561
lines changed

contracts/script/L2Genesis.s.sol

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,15 @@ contract GenesisEthscriptions is Ethscriptions {
3232
require(ethscriptions[params.transactionHash].creator == address(0), "Ethscription already exists");
3333

3434
// Check protocol uniqueness using content URI hash
35-
if (contentUriExists[params.contentUriHash]) {
35+
if (firstEthscriptionByContentUri[params.contentUriHash] != bytes32(0)) {
3636
if (!params.esip6) revert DuplicateContentUri();
3737
}
3838

3939
// Store content and get content SHA (reusing parent's helper)
4040
bytes32 contentSha = _storeContent(params.content);
4141

42-
// Mark content URI as used
43-
contentUriExists[params.contentUriHash] = true;
42+
// Mark content URI as used by storing this ethscription's tx hash
43+
firstEthscriptionByContentUri[params.contentUriHash] = params.transactionHash;
4444

4545
// Set all values including genesis-specific ones
4646
ethscriptions[params.transactionHash] = Ethscription({
@@ -341,7 +341,7 @@ contract L2Genesis is Script {
341341
params.mimeSubtype = vm.parseJsonString(json, string.concat(basePath, ".mime_subtype"));
342342
params.esip6 = vm.parseJsonBool(json, string.concat(basePath, ".esip6"));
343343
params.protocolParams = Ethscriptions.ProtocolParams({
344-
protocol: "",
344+
protocolName: "",
345345
operation: "",
346346
data: ""
347347
});

contracts/src/Ethscriptions.sol

Lines changed: 305 additions & 486 deletions
Large diffs are not rendered by default.

contracts/src/EthscriptionsProver.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ contract EthscriptionsProver {
106106
function _createAndSendProof(bytes32 ethscriptionTxHash, QueuedProof memory proofInfo) internal {
107107
// Get ethscription data including previous owner
108108
Ethscriptions.Ethscription memory etsc = ethscriptions.getEthscription(ethscriptionTxHash);
109-
address currentOwner = ethscriptions.currentOwner(ethscriptionTxHash);
109+
address currentOwner = ethscriptions.ownerOf(ethscriptionTxHash);
110110

111111
// Create proof struct with all ethscription data
112112
EthscriptionDataProof memory proof = EthscriptionDataProof({

contracts/src/L2/L1Block.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ contract L1Block {
9090
}
9191

9292
function _flushProofsIfLive() internal {
93-
if (block.timestamp >= 1760630077) {
93+
if (block.timestamp >= Constants.historicalBackfillApproxDoneAt) {
9494
// Each proof includes its own block number and timestamp from when it was queued
9595
IEthscriptionsProver(Predeploys.ETHSCRIPTIONS_PROVER).flushAllProofs();
9696
}

contracts/src/libraries/Constants.sol

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,6 @@ library Constants {
2121
/// @notice Storage slot for Initializable contract's initialized flag
2222
/// @dev This is the keccak256 of "eip1967.proxy.initialized" - 1
2323
bytes32 internal constant INITIALIZABLE_STORAGE = 0xf0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00;
24+
25+
uint256 internal constant historicalBackfillApproxDoneAt = 1760630077;
2426
}
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity 0.8.24;
3+
4+
import {Base64} from "solady/utils/Base64.sol";
5+
import {LibString} from "solady/utils/LibString.sol";
6+
import {Ethscriptions} from "../Ethscriptions.sol";
7+
8+
/// @title EthscriptionsRendererLib
9+
/// @notice Library for rendering Ethscription metadata and media URIs
10+
/// @dev Contains all token URI generation, media handling, and metadata formatting logic
11+
library EthscriptionsRendererLib {
12+
using LibString for *;
13+
14+
/// @notice Build attributes JSON array from ethscription data
15+
/// @param etsc Storage pointer to the ethscription
16+
/// @param txHash The transaction hash of the ethscription
17+
/// @return JSON string of attributes array
18+
function buildAttributes(Ethscriptions.Ethscription storage etsc, bytes32 txHash)
19+
internal
20+
view
21+
returns (string memory)
22+
{
23+
// Build in chunks to avoid stack too deep
24+
string memory part1 = string.concat(
25+
'[{"trait_type":"Transaction Hash","value":"',
26+
uint256(txHash).toHexString(),
27+
'"},{"trait_type":"Ethscription Number","display_type":"number","value":',
28+
etsc.ethscriptionNumber.toString(),
29+
'},{"trait_type":"Creator","value":"',
30+
etsc.creator.toHexString(),
31+
'"},{"trait_type":"Initial Owner","value":"',
32+
etsc.initialOwner.toHexString()
33+
);
34+
35+
string memory part2 = string.concat(
36+
'"},{"trait_type":"Content SHA","value":"',
37+
uint256(etsc.content.contentSha).toHexString(),
38+
'"},{"trait_type":"MIME Type","value":"',
39+
etsc.content.mimetype.escapeJSON(),
40+
'"},{"trait_type":"Media Type","value":"',
41+
etsc.content.mediaType.escapeJSON(),
42+
'"},{"trait_type":"MIME Subtype","value":"',
43+
etsc.content.mimeSubtype.escapeJSON()
44+
);
45+
46+
string memory part3 = string.concat(
47+
'"},{"trait_type":"ESIP-6","value":"',
48+
etsc.content.esip6 ? "true" : "false",
49+
'"},{"trait_type":"L1 Block Number","display_type":"number","value":',
50+
uint256(etsc.l1BlockNumber).toString(),
51+
'},{"trait_type":"L2 Block Number","display_type":"number","value":',
52+
uint256(etsc.l2BlockNumber).toString(),
53+
'},{"trait_type":"Created At","display_type":"date","value":',
54+
etsc.createdAt.toString(),
55+
'}]'
56+
);
57+
58+
return string.concat(part1, part2, part3);
59+
}
60+
61+
/// @notice Generate the media URI for an ethscription
62+
/// @param etsc Storage pointer to the ethscription
63+
/// @param content The content bytes
64+
/// @return mediaType Either "image" or "animation_url"
65+
/// @return mediaUri The data URI for the media
66+
function getMediaUri(Ethscriptions.Ethscription storage etsc, bytes memory content)
67+
internal
68+
view
69+
returns (string memory mediaType, string memory mediaUri)
70+
{
71+
if (etsc.content.mimetype.startsWith("image/")) {
72+
// Image content: wrap in SVG for pixel-perfect rendering
73+
string memory imageDataUri = constructDataURI(etsc.content.mimetype, content);
74+
string memory svg = wrapImageInSVG(imageDataUri);
75+
mediaUri = constructDataURI("image/svg+xml", bytes(svg));
76+
return ("image", mediaUri);
77+
} else {
78+
// Non-image content: use animation_url
79+
if (etsc.content.mimetype.startsWith("video/") ||
80+
etsc.content.mimetype.startsWith("audio/") ||
81+
etsc.content.mimetype.eq("text/html")) {
82+
// Video, audio, and HTML pass through directly as data URIs
83+
mediaUri = constructDataURI(etsc.content.mimetype, content);
84+
} else {
85+
// Everything else (text/plain, application/json, etc.) uses the HTML viewer
86+
mediaUri = createTextViewerDataURI(etsc.content.mimetype, content);
87+
}
88+
return ("animation_url", mediaUri);
89+
}
90+
}
91+
92+
/// @notice Build complete token URI JSON
93+
/// @param etsc Storage pointer to the ethscription
94+
/// @param txHash The transaction hash of the ethscription
95+
/// @param content The content bytes
96+
/// @return The complete base64-encoded data URI
97+
function buildTokenURI(
98+
Ethscriptions.Ethscription storage etsc,
99+
bytes32 txHash,
100+
bytes memory content
101+
) internal view returns (string memory) {
102+
// Get media URI
103+
(string memory mediaType, string memory mediaUri) = getMediaUri(etsc, content);
104+
105+
// Build attributes
106+
string memory attributes = buildAttributes(etsc, txHash);
107+
108+
// Build JSON
109+
string memory json = string.concat(
110+
'{"name":"Ethscription #',
111+
etsc.ethscriptionNumber.toString(),
112+
'","description":"Ethscription #',
113+
etsc.ethscriptionNumber.toString(),
114+
' created by ',
115+
etsc.creator.toHexString(),
116+
'","',
117+
mediaType,
118+
'":"',
119+
mediaUri.escapeJSON(),
120+
'","attributes":',
121+
attributes,
122+
'}'
123+
);
124+
125+
return string.concat(
126+
"data:application/json;base64,",
127+
Base64.encode(bytes(json))
128+
);
129+
}
130+
131+
/// @notice Construct a base64-encoded data URI
132+
/// @param mimetype The MIME type
133+
/// @param content The content bytes
134+
/// @return The complete data URI
135+
function constructDataURI(string memory mimetype, bytes memory content)
136+
internal
137+
pure
138+
returns (string memory)
139+
{
140+
return string.concat(
141+
"data:",
142+
mimetype,
143+
";base64,",
144+
Base64.encode(content)
145+
);
146+
}
147+
148+
/// @notice Wrap an image in SVG for pixel-perfect rendering
149+
/// @param imageDataUri The image data URI to wrap
150+
/// @return The SVG markup
151+
function wrapImageInSVG(string memory imageDataUri)
152+
internal
153+
pure
154+
returns (string memory)
155+
{
156+
// SVG wrapper that enforces pixelated/nearest-neighbor scaling for pixel art
157+
return string.concat(
158+
'<svg width="1200" height="1200" viewBox="0 0 1200 1200" version="1.2" xmlns="http://www.w3.org/2000/svg" style="background-image:url(',
159+
imageDataUri,
160+
');background-repeat:no-repeat;background-size:contain;background-position:center;image-rendering:-webkit-optimize-contrast;image-rendering:-moz-crisp-edges;image-rendering:pixelated;"></svg>'
161+
);
162+
}
163+
164+
/// @notice Create an HTML viewer data URI for text content
165+
/// @param mimetype The MIME type of the content
166+
/// @param content The content bytes
167+
/// @return The HTML viewer data URI
168+
function createTextViewerDataURI(string memory mimetype, bytes memory content)
169+
internal
170+
pure
171+
returns (string memory)
172+
{
173+
// Base64 encode the content for embedding in HTML
174+
string memory encodedContent = Base64.encode(content);
175+
176+
// Generate HTML with embedded content
177+
string memory html = generateTextViewerHTML(encodedContent, mimetype);
178+
179+
// Return as base64-encoded HTML data URI
180+
return constructDataURI("text/html", bytes(html));
181+
}
182+
183+
/// @notice Generate minimal HTML viewer for text content
184+
/// @param encodedPayload Base64-encoded content
185+
/// @param mimetype The MIME type
186+
/// @return The complete HTML string
187+
function generateTextViewerHTML(string memory encodedPayload, string memory mimetype)
188+
internal
189+
pure
190+
returns (string memory)
191+
{
192+
// Ultra-minimal HTML with inline styles optimized for iframe display
193+
return string.concat(
194+
'<!DOCTYPE html><html><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/>',
195+
'<style>*{box-sizing:border-box;margin:0;padding:0;border:0}body{padding:6dvw;background:#0b0b0c;color:#f5f5f5;font-family:monospace;display:flex;justify-content:center;align-items:center;min-height:100dvh;overflow:hidden}',
196+
'pre{white-space:pre-wrap;word-break:break-word;overflow-wrap:anywhere;line-height:1.4;font-size:14px}</style></head>',
197+
'<body><pre id="o"></pre><script>',
198+
'const p="', encodedPayload, '";',
199+
'const m="', mimetype.escapeJSON(), '";',
200+
'function d(b){try{return decodeURIComponent(atob(b).split("").map(c=>"%"+("00"+c.charCodeAt(0).toString(16)).slice(-2)).join(""))}catch{return null}}',
201+
'const r=d(p);let t="";',
202+
'if(r!==null){t=r;try{const j=JSON.parse(r);t=JSON.stringify(j,null,2)}catch{}}',
203+
'else{t="data:"+m+";base64,"+p}',
204+
'document.getElementById("o").textContent=t||"(empty)";',
205+
'</script></body></html>'
206+
);
207+
}
208+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity 0.8.24;
3+
4+
import {SSTORE2} from "solady/utils/SSTORE2.sol";
5+
6+
/// @title SSTORE2ChunkedStorageLib
7+
/// @notice Generic library for storing and reading large content using chunked SSTORE2
8+
/// @dev Handles content larger than SSTORE2's single-contract limit by splitting into chunks
9+
library SSTORE2ChunkedStorageLib {
10+
/// @dev Maximum chunk size for SSTORE2 (24KB - 1 byte for STOP opcode)
11+
uint256 internal constant CHUNK_SIZE = 24575;
12+
13+
/// @notice Store content in chunked SSTORE2 contracts
14+
/// @param content The content to store
15+
/// @return pointers Array of SSTORE2 contract addresses containing the chunks
16+
function store(bytes calldata content)
17+
internal
18+
returns (address[] memory pointers)
19+
{
20+
uint256 contentLength = content.length;
21+
22+
if (contentLength == 0) {
23+
// Return empty array for empty content
24+
return pointers;
25+
}
26+
27+
// Calculate number of chunks needed
28+
uint256 numChunks = (contentLength + CHUNK_SIZE - 1) / CHUNK_SIZE;
29+
pointers = new address[](numChunks);
30+
31+
// Split content into chunks and store each via SSTORE2
32+
for (uint256 i = 0; i < numChunks; i++) {
33+
uint256 start = i * CHUNK_SIZE;
34+
uint256 end = start + CHUNK_SIZE;
35+
if (end > contentLength) {
36+
end = contentLength;
37+
}
38+
39+
// Use calldata slicing for efficiency
40+
bytes calldata chunk = content[start:end];
41+
42+
// Store chunk and save pointer
43+
pointers[i] = SSTORE2.write(chunk);
44+
}
45+
46+
return pointers;
47+
}
48+
49+
/// @notice Read content from storage array of SSTORE2 pointers
50+
/// @param pointers Storage array of SSTORE2 contract addresses
51+
/// @return content The concatenated content from all chunks
52+
function read(address[] storage pointers)
53+
internal
54+
view
55+
returns (bytes memory content)
56+
{
57+
uint256 length = pointers.length;
58+
59+
if (length == 0) {
60+
return "";
61+
}
62+
63+
if (length == 1) {
64+
return SSTORE2.read(pointers[0]);
65+
}
66+
67+
// Multiple chunks - use assembly for efficient concatenation
68+
assembly {
69+
// Calculate total size needed
70+
let totalSize := 0
71+
let pointersSlot := pointers.slot
72+
let pointersLength := sload(pointersSlot)
73+
let dataOffset := 0x01 // SSTORE2 data starts after STOP opcode
74+
75+
for { let i := 0 } lt(i, pointersLength) { i := add(i, 1) } {
76+
// Storage array elements are at keccak256(slot) + index
77+
mstore(0, pointersSlot)
78+
let elementSlot := add(keccak256(0, 0x20), i)
79+
let pointer := sload(elementSlot)
80+
let codeSize := extcodesize(pointer)
81+
totalSize := add(totalSize, sub(codeSize, dataOffset))
82+
}
83+
84+
// Allocate result buffer
85+
content := mload(0x40)
86+
let contentPtr := add(content, 0x20)
87+
88+
// Copy data from each pointer
89+
let currentOffset := 0
90+
for { let i := 0 } lt(i, pointersLength) { i := add(i, 1) } {
91+
mstore(0, pointersSlot)
92+
let elementSlot := add(keccak256(0, 0x20), i)
93+
let pointer := sload(elementSlot)
94+
let codeSize := extcodesize(pointer)
95+
let chunkSize := sub(codeSize, dataOffset)
96+
extcodecopy(pointer, add(contentPtr, currentOffset), dataOffset, chunkSize)
97+
currentOffset := add(currentOffset, chunkSize)
98+
}
99+
100+
// Update length and free memory pointer with proper alignment
101+
mstore(content, totalSize)
102+
mstore(0x40, and(add(add(contentPtr, totalSize), 0x1f), not(0x1f)))
103+
}
104+
}
105+
}

0 commit comments

Comments
 (0)