diff --git a/app/models/ethscription_transaction.rb b/app/models/ethscription_transaction.rb
index fe63d88..5ccca59 100644
--- a/app/models/ethscription_transaction.rb
+++ b/app/models/ethscription_transaction.rb
@@ -187,8 +187,8 @@ def build_create_calldata
)
# Hash the content for protocol uniqueness
- content_uri_hash_hex = Digest::SHA256.hexdigest(content_uri)
- content_uri_hash = [content_uri_hash_hex].pack('H*')
+ content_uri_sha_hex = Digest::SHA256.hexdigest(content_uri)
+ content_uri_sha = [content_uri_sha_hex].pack('H*')
# Convert hex strings to binary for ABI encoding
tx_hash_bin = hex_to_bin(eth_transaction.transaction_hash)
@@ -204,7 +204,7 @@ def build_create_calldata
# Encode parameters
params = [
tx_hash_bin, # bytes32 ethscriptionId (L1 tx hash)
- content_uri_hash, # bytes32 contentUriHash
+ content_uri_sha, # bytes32 contentUriHash
owner_bin, # address
raw_content, # bytes content
mimetype.b, # string
diff --git a/contracts/script/L2Genesis.s.sol b/contracts/script/L2Genesis.s.sol
index 5162c07..1850d52 100644
--- a/contracts/script/L2Genesis.s.sol
+++ b/contracts/script/L2Genesis.s.sol
@@ -6,6 +6,7 @@ import {L2GenesisConfig} from "./L2GenesisConfig.sol";
import {Predeploys} from "../src/libraries/Predeploys.sol";
import {Constants} from "../src/libraries/Constants.sol";
import {Ethscriptions} from "../src/Ethscriptions.sol";
+import {MetaStoreLib} from "../src/libraries/MetaStoreLib.sol";
import "forge-std/console.sol";
/// @title GenesisEthscriptions
@@ -32,28 +33,37 @@ contract GenesisEthscriptions is Ethscriptions {
require(ethscriptions[params.ethscriptionId].creator == address(0), "Ethscription already exists");
// Check protocol uniqueness using content URI hash
- if (firstEthscriptionByContentUri[params.contentUriHash] != bytes32(0)) {
+ if (firstEthscriptionByContentUri[params.contentUriSha] != bytes32(0)) {
if (!params.esip6) revert DuplicateContentUri();
}
- // Store content and get content SHA (reusing parent's helper)
- bytes32 contentSha = _storeContent(params.content);
+ // Store content and get content hash (reusing parent's helper)
+ bytes32 contentHash = _storeContent(params.content);
// Mark content URI as used by storing this ethscription's tx hash
- firstEthscriptionByContentUri[params.contentUriHash] = params.ethscriptionId;
+ firstEthscriptionByContentUri[params.contentUriSha] = params.ethscriptionId;
+
+ // Store metadata (mimetype, protocol, operation)
+ // Genesis ethscriptions have no protocol parameters
+ bytes32 metaRef = MetaStoreLib.store(
+ params.mimetype,
+ params.protocolParams.protocolName,
+ params.protocolParams.operation,
+ metadataStorage
+ );
// Set all values including genesis-specific ones
ethscriptions[params.ethscriptionId] = EthscriptionStorage({
// Fixed-size fields
- contentUriHash: params.contentUriHash,
- contentSha: contentSha,
+ contentUriSha: params.contentUriSha,
+ contentHash: contentHash,
l1BlockHash: l1BlockHash,
// Packed slot 3
creator: creator,
createdAt: uint48(createdAt),
l1BlockNumber: uint48(l1BlockNumber),
- // Dynamic field
- mimetype: params.mimetype,
+ // Metadata reference
+ metaRef: metaRef,
// Packed slot N
initialOwner: params.initialOwner,
ethscriptionNumber: uint48(totalSupply()),
@@ -333,7 +343,7 @@ contract L2Genesis is Script {
// The JSON already has all the properly processed data
Ethscriptions.CreateEthscriptionParams memory params;
params.ethscriptionId = vm.parseJsonBytes32(json, string.concat(basePath, ".transaction_hash"));
- params.contentUriHash = vm.parseJsonBytes32(json, string.concat(basePath, ".content_uri_hash"));
+ params.contentUriSha = vm.parseJsonBytes32(json, string.concat(basePath, ".content_uri_hash"));
params.initialOwner = initialOwner;
params.content = vm.parseJsonBytes(json, string.concat(basePath, ".content"));
params.mimetype = vm.parseJsonString(json, string.concat(basePath, ".mimetype"));
diff --git a/contracts/script/TestTokenUri.s.sol b/contracts/script/TestTokenUri.s.sol
index 5fc7ab9..4b61e5e 100644
--- a/contracts/script/TestTokenUri.s.sol
+++ b/contracts/script/TestTokenUri.s.sol
@@ -18,7 +18,7 @@ contract TestTokenUri is Script {
vm.prank(address(0x1111));
eth.createEthscription(Ethscriptions.CreateEthscriptionParams({
ethscriptionId: keccak256("text1"),
- contentUriHash: keccak256("data:text/plain,Hello World!"),
+ contentUriSha: keccak256("data:text/plain,Hello World!"),
initialOwner: address(0x1111),
content: bytes("Hello World!"),
mimetype: "text/plain",
@@ -30,7 +30,7 @@ contract TestTokenUri is Script {
vm.prank(address(0x2222));
eth.createEthscription(Ethscriptions.CreateEthscriptionParams({
ethscriptionId: keccak256("json1"),
- contentUriHash: keccak256('data:application/json,{"p":"erc-20","op":"mint","tick":"test","amt":"1000"}'),
+ contentUriSha: keccak256('data:application/json,{"p":"erc-20","op":"mint","tick":"test","amt":"1000"}'),
initialOwner: address(0x2222),
content: bytes('{"p":"erc-20","op":"mint","tick":"test","amt":"1000"}'),
mimetype: "application/json",
@@ -42,7 +42,7 @@ contract TestTokenUri is Script {
vm.prank(address(0x3333));
eth.createEthscription(Ethscriptions.CreateEthscriptionParams({
ethscriptionId: keccak256("html1"),
- contentUriHash: keccak256('data:text/html,
Ethscriptions Rule!
'),
+ contentUriSha: keccak256('data:text/html,Ethscriptions Rule!
'),
initialOwner: address(0x3333),
content: bytes('Ethscriptions Rule!
'),
mimetype: "text/html",
@@ -55,7 +55,7 @@ contract TestTokenUri is Script {
vm.prank(address(0x4444));
eth.createEthscription(Ethscriptions.CreateEthscriptionParams({
ethscriptionId: keccak256("image1"),
- contentUriHash: keccak256("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAm0lEQVR42mNgGITgPxTTxvBleTo0swBsOK0s+N8aJkczC1AMR7KAKpb8v72xAY5hFsD4lFoCN+j56ZUoliBbSoklGIZjwxRbQAjT1YK7d+82kGUBeuQii5FrAYYrL81NwCpGFQtoEUT/6RoHWAyknQV0S6ZI5RE6Jt8CZIOOHTuGgR9Fq5FkCf19QM3wx5rZKHEtsRZQt5qkhgUAR6cGaUehOD4AAAAASUVORK5CYII="),
+ contentUriSha: keccak256("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAm0lEQVR42mNgGITgPxTTxvBleTo0swBsOK0s+N8aJkczC1AMR7KAKpb8v72xAY5hFsD4lFoCN+j56ZUoliBbSoklGIZjwxRbQAjT1YK7d+82kGUBeuQii5FrAYYrL81NwCpGFQtoEUT/6RoHWAyknQV0S6ZI5RE6Jt8CZIOOHTuGgR9Fq5FkCf19QM3wx5rZKHEtsRZQt5qkhgUAR6cGaUehOD4AAAAASUVORK5CYII="),
initialOwner: address(0x4444),
content: redPixelPng,
mimetype: "image/png",
@@ -67,7 +67,7 @@ contract TestTokenUri is Script {
vm.prank(address(0x5555));
eth.createEthscription(Ethscriptions.CreateEthscriptionParams({
ethscriptionId: keccak256("css1"),
- contentUriHash: keccak256("data:text/css,body { background: #000; color: #0f0; font-family: 'Courier New'; }"),
+ contentUriSha: keccak256("data:text/css,body { background: #000; color: #0f0; font-family: 'Courier New'; }"),
initialOwner: address(0x5555),
content: bytes("body { background: #000; color: #0f0; font-family: 'Courier New'; }"),
mimetype: "text/css",
diff --git a/contracts/script/process_genesis_json.rb b/contracts/script/process_genesis_json.rb
index f666749..30ce413 100644
--- a/contracts/script/process_genesis_json.rb
+++ b/contracts/script/process_genesis_json.rb
@@ -13,8 +13,8 @@
content_uri = ethscription['content_uri']
# Calculate content URI hash (as hex string with 0x prefix for JSON)
- content_uri_hash = '0x' + Digest::SHA256.hexdigest(content_uri)
- ethscription['content_uri_hash'] = content_uri_hash
+ content_uri_sha = '0x' + Digest::SHA256.hexdigest(content_uri)
+ ethscription['content_uri_sha'] = content_uri_sha
# Parse the data URI
if content_uri.start_with?('data:')
@@ -56,4 +56,4 @@
File.write(json_path, JSON.pretty_generate(data))
puts "Processed #{data['ethscriptions'].length} ethscriptions"
-puts "Added content_uri_hash and content fields"
\ No newline at end of file
+puts "Added content_uri_sha and content fields"
\ No newline at end of file
diff --git a/contracts/src/Ethscriptions.sol b/contracts/src/Ethscriptions.sol
index 124a4c1..079a4dd 100644
--- a/contracts/src/Ethscriptions.sol
+++ b/contracts/src/Ethscriptions.sol
@@ -3,8 +3,8 @@ pragma solidity 0.8.24;
import "./ERC721EthscriptionsSequentialEnumerableUpgradeable.sol";
import {LibString} from "solady/utils/LibString.sol";
-import "./libraries/SSTORE2Unlimited.sol";
-import "./libraries/BytePackLib.sol";
+import "./libraries/DedupedBlobStore.sol";
+import "./libraries/MetaStoreLib.sol";
import "./libraries/EthscriptionsRendererLib.sol";
import "./EthscriptionsProver.sol";
import "./libraries/Predeploys.sol";
@@ -17,7 +17,6 @@ import "./libraries/Constants.sol";
/// @dev Uses ethscription number as token ID and name, while transaction hash remains the primary identifier for function calls
contract Ethscriptions is ERC721EthscriptionsSequentialEnumerableUpgradeable {
using LibString for *;
- using EthscriptionsRendererLib for EthscriptionStorage;
// =============================================================
// STRUCTS
@@ -26,15 +25,15 @@ contract Ethscriptions is ERC721EthscriptionsSequentialEnumerableUpgradeable {
/// @notice Internal storage struct for ethscriptions (optimized for storage)
struct EthscriptionStorage {
// Full slots
- bytes32 contentUriHash;
- bytes32 contentSha;
+ bytes32 contentUriSha; // sha256 of content URI (for protocol uniqueness check)
+ bytes32 contentHash; // keccak256 of content (for deduplication)
bytes32 l1BlockHash;
// Packed slot (32 bytes)
address creator;
uint48 createdAt;
uint48 l1BlockNumber;
- // Dynamic
- string mimetype;
+ // Metadata reference (replaces dynamic mimetype string)
+ bytes32 metaRef; // Reference to deduplicated metadata (mimetype, protocol, operation)
// Packed slot (27 bytes used, 5 free)
address initialOwner;
uint48 ethscriptionNumber;
@@ -52,7 +51,7 @@ contract Ethscriptions is ERC721EthscriptionsSequentialEnumerableUpgradeable {
struct CreateEthscriptionParams {
bytes32 ethscriptionId;
- bytes32 contentUriHash; // SHA256 of raw content URI (for protocol uniqueness)
+ bytes32 contentUriSha; // sha256 of content URI (for protocol uniqueness)
address initialOwner;
bytes content; // Raw decoded bytes (not Base64)
string mimetype;
@@ -78,8 +77,8 @@ contract Ethscriptions is ERC721EthscriptionsSequentialEnumerableUpgradeable {
uint256 ethscriptionNumber; // Token ID
// Core metadata
- bytes32 contentUriHash;
- bytes32 contentSha;
+ bytes32 contentUriSha; // sha256 of content URI (protocol)
+ bytes32 contentHash; // keccak256 of content
string mimetype;
bytes content; // Full content bytes (empty when includeContent=false)
@@ -97,6 +96,8 @@ contract Ethscriptions is ERC721EthscriptionsSequentialEnumerableUpgradeable {
// Protocol
bool esip6;
+ string protocolName; // Protocol identifier (empty if none)
+ string operation; // Operation name (empty if none)
}
// =============================================================
@@ -124,8 +125,11 @@ contract Ethscriptions is ERC721EthscriptionsSequentialEnumerableUpgradeable {
/// @dev Ethscription ID (L1 tx hash) => Ethscription data
mapping(bytes32 => EthscriptionStorage) internal ethscriptions;
- /// @dev Content SHA => packed content (for <32 bytes) or SSTORE2 pointer (for >=32 bytes)
- mapping(bytes32 => bytes32) public contentStorageBySha;
+ /// @dev Content hash (keccak256) => packed content (for <32 bytes) or SSTORE2 pointer (for >=32 bytes)
+ mapping(bytes32 => bytes32) public contentStorage;
+
+ /// @dev Metadata blob hash (keccak256) => packed metadata or SSTORE2 pointer (for deduplicated metadata storage)
+ mapping(bytes32 => bytes32) public metadataStorage;
/// @dev Content URI hash => first ethscription tx hash that used it (for protocol uniqueness check)
/// @dev bytes32(0) means unused, non-zero means the content URI has been used
@@ -137,9 +141,6 @@ contract Ethscriptions is ERC721EthscriptionsSequentialEnumerableUpgradeable {
/// @dev Protocol registry - maps protocol names to handler addresses
mapping(string => address) public protocolHandlers;
- /// @dev Track which protocol an ethscription uses
- mapping(bytes32 => string) public protocolOf;
-
/// @dev Array of genesis ethscription transaction hashes that need events emitted
/// @notice This array is populated during genesis and cleared (by popping) when events are emitted
bytes32[] internal pendingGenesisEvents;
@@ -169,8 +170,8 @@ contract Ethscriptions is ERC721EthscriptionsSequentialEnumerableUpgradeable {
bytes32 indexed ethscriptionId,
address indexed creator,
address indexed initialOwner,
- bytes32 contentUriHash,
- bytes32 contentSha,
+ bytes32 contentUriSha,
+ bytes32 contentHash,
uint256 ethscriptionNumber
);
@@ -231,12 +232,14 @@ contract Ethscriptions is ERC721EthscriptionsSequentialEnumerableUpgradeable {
/// @param protocol The protocol identifier (e.g., "erc-20-fixed-denomination", "erc-721-ethscriptions-collection")
/// @param handler The address of the handler contract
/// @dev Only callable by the depositor address (used during genesis setup)
+ /// @dev Protocol names should already be normalized (lowercase) by the caller
function registerProtocol(string calldata protocol, address handler) external {
if (msg.sender != Predeploys.DEPOSITOR_ACCOUNT) revert OnlyDepositor();
if (handler == address(0)) revert InvalidHandler();
if (protocolHandlers[protocol] != address(0)) revert ProtocolAlreadyRegistered();
protocolHandlers[protocol] = handler;
+
emit ProtocolRegistered(protocol, handler);
}
@@ -255,25 +258,33 @@ contract Ethscriptions is ERC721EthscriptionsSequentialEnumerableUpgradeable {
if (creator == address(0)) revert InvalidCreator();
if (_ethscriptionExists(params.ethscriptionId)) revert EthscriptionAlreadyExists();
- bool contentUriAlreadySeen = firstEthscriptionByContentUri[params.contentUriHash] != bytes32(0);
+ bool contentUriAlreadySeen = firstEthscriptionByContentUri[params.contentUriSha] != bytes32(0);
if (contentUriAlreadySeen) {
if (!params.esip6) revert DuplicateContentUri();
} else {
- firstEthscriptionByContentUri[params.contentUriHash] = params.ethscriptionId;
+ firstEthscriptionByContentUri[params.contentUriSha] = params.ethscriptionId;
}
- // Store content and get content SHA (of raw bytes)
- bytes32 contentSha = _storeContent(params.content);
+ // Store content and get content hash (keccak256 of raw bytes)
+ bytes32 contentHash = _storeContent(params.content);
+
+ // Store metadata (mimetype, protocol, operation)
+ bytes32 metaRef = MetaStoreLib.store(
+ params.mimetype,
+ params.protocolParams.protocolName,
+ params.protocolParams.operation,
+ metadataStorage
+ );
ethscriptions[params.ethscriptionId] = EthscriptionStorage({
- contentUriHash: params.contentUriHash,
- contentSha: contentSha,
+ contentUriSha: params.contentUriSha,
+ contentHash: contentHash,
l1BlockHash: l1Block.hash(),
creator: creator,
createdAt: uint48(block.timestamp),
l1BlockNumber: uint48(l1Block.number()),
- mimetype: params.mimetype,
+ metaRef: metaRef,
initialOwner: params.initialOwner,
ethscriptionNumber: uint48(totalSupply()),
esip6: params.esip6,
@@ -299,8 +310,8 @@ contract Ethscriptions is ERC721EthscriptionsSequentialEnumerableUpgradeable {
params.ethscriptionId,
creator,
params.initialOwner,
- params.contentUriHash,
- contentSha,
+ params.contentUriSha,
+ contentHash,
tokenId
);
@@ -398,8 +409,12 @@ contract Ethscriptions is ERC721EthscriptionsSequentialEnumerableUpgradeable {
// Get content
bytes memory content = _getEthscriptionContent(id);
- // Build complete token URI using the library - it handles everything internally
- return EthscriptionsRendererLib.buildTokenURI(ethscription, id, content);
+ // Decode metadata from reference
+ (string memory mimetype, string memory protocolName, string memory operation) =
+ MetaStoreLib.decode(ethscription.metaRef);
+
+ // Build complete token URI using the library
+ return EthscriptionsRendererLib.buildTokenURI(ethscription, id, mimetype, protocolName, operation, content);
}
/// @notice Get the media URI for an ethscription (image or animation_url)
@@ -409,7 +424,11 @@ contract Ethscriptions is ERC721EthscriptionsSequentialEnumerableUpgradeable {
function getMediaUri(bytes32 ethscriptionId) external view returns (string memory mediaType, string memory mediaUri) {
EthscriptionStorage storage ethscription = _getEthscriptionOrRevert(ethscriptionId);
bytes memory content = _getEthscriptionContent(ethscriptionId);
- return ethscription.getMediaUri(content);
+
+ // Decode mimetype from metadata reference
+ string memory mimetype = MetaStoreLib.getMimetype(ethscription.metaRef);
+
+ return EthscriptionsRendererLib.getMediaUri(mimetype, content);
}
// -------------------- Data Retrieval --------------------
@@ -421,15 +440,19 @@ contract Ethscriptions is ERC721EthscriptionsSequentialEnumerableUpgradeable {
function _buildEthscription(bytes32 ethscriptionId, bool includeContent) internal view returns (Ethscription memory) {
EthscriptionStorage storage ethscription = _getEthscriptionOrRevert(ethscriptionId);
+ // Decode metadata from reference
+ (string memory mimetype, string memory protocolName, string memory operation) =
+ MetaStoreLib.decode(ethscription.metaRef);
+
return Ethscription({
// Identity
ethscriptionId: ethscriptionId,
ethscriptionNumber: uint256(ethscription.ethscriptionNumber),
// Core metadata
- contentUriHash: ethscription.contentUriHash,
- contentSha: ethscription.contentSha,
- mimetype: ethscription.mimetype,
+ contentUriSha: ethscription.contentUriSha,
+ contentHash: ethscription.contentHash,
+ mimetype: mimetype,
content: includeContent ? _getEthscriptionContent(ethscriptionId) : bytes(""),
// Ownership
@@ -445,7 +468,9 @@ contract Ethscriptions is ERC721EthscriptionsSequentialEnumerableUpgradeable {
createdAt: uint256(ethscription.createdAt),
// Protocol
- esip6: ethscription.esip6
+ esip6: ethscription.esip6,
+ protocolName: protocolName,
+ operation: operation
});
}
@@ -595,17 +620,8 @@ contract Ethscriptions is ERC721EthscriptionsSequentialEnumerableUpgradeable {
/// @dev Kept as internal for tokenURI and other internal uses
function _getEthscriptionContent(bytes32 ethscriptionId) internal view returns (bytes memory) {
EthscriptionStorage storage ethscription = _getEthscriptionOrRevert(ethscriptionId);
- bytes32 ref = contentStorageBySha[ethscription.contentSha];
-
- // Check if it's inline content using BytePackLib
- if (BytePackLib.isPacked(ref)) {
- return BytePackLib.unpack(ref);
- }
-
- // It's a pointer to SSTORE2 contract
- address pointer = address(uint160(uint256(ref)));
-
- return SSTORE2Unlimited.read(pointer);
+ // Use shared retrieval logic
+ return DedupedBlobStore.readByHash(ethscription.contentHash, contentStorage);
}
// ---------------- Ownership & Existence Checks ----------------
@@ -646,6 +662,39 @@ contract Ethscriptions is ERC721EthscriptionsSequentialEnumerableUpgradeable {
return id;
}
+ // -------------------- Metadata Helpers --------------------
+
+ /// @notice Get the MIME type of an ethscription
+ /// @param ethscriptionId The ethscription ID to query
+ /// @return mimetype The MIME type string
+ function getMimetype(bytes32 ethscriptionId) external view returns (string memory) {
+ EthscriptionStorage storage ethscription = _getEthscriptionOrRevert(ethscriptionId);
+ return MetaStoreLib.getMimetype(ethscription.metaRef);
+ }
+
+ /// @notice Get the protocol information for an ethscription
+ /// @param ethscriptionId The ethscription ID to query
+ /// @return protocolName The protocol identifier (empty if none)
+ /// @return operation The operation name (empty if none)
+ function getProtocol(bytes32 ethscriptionId) external view returns (string memory protocolName, string memory operation) {
+ EthscriptionStorage storage ethscription = _getEthscriptionOrRevert(ethscriptionId);
+ return MetaStoreLib.getProtocol(ethscription.metaRef);
+ }
+
+ /// @notice Get complete metadata for an ethscription
+ /// @param ethscriptionId The ethscription ID to query
+ /// @return mimetype The MIME type
+ /// @return protocolName The protocol identifier (empty if none)
+ /// @return operation The operation name (empty if none)
+ function getMetadata(bytes32 ethscriptionId) external view returns (
+ string memory mimetype,
+ string memory protocolName,
+ string memory operation
+ ) {
+ EthscriptionStorage storage ethscription = _getEthscriptionOrRevert(ethscriptionId);
+ return MetaStoreLib.decode(ethscription.metaRef);
+ }
+
// =============================================================
// INTERNAL FUNCTIONS
// =============================================================
@@ -689,31 +738,13 @@ contract Ethscriptions is ERC721EthscriptionsSequentialEnumerableUpgradeable {
return ethscriptions[ethscriptionId].creator != address(0);
}
- /// @notice Internal helper to store content and return its SHA
+ /// @notice Internal helper to store content and return its hash
/// @param content The raw content bytes to store
- /// @return contentSha The SHA256 hash of the content
- function _storeContent(bytes calldata content) internal returns (bytes32 contentSha) {
- // Compute SHA256 hash of content first
- contentSha = sha256(content);
-
- // Check if content already exists
- bytes32 existing = contentStorageBySha[contentSha];
- if (existing != bytes32(0)) {
- // Content already stored, just return the SHA
- return contentSha;
- }
-
- // Store based on size
- if (content.length <= 31) {
- // Pack small content directly into bytes32 (0-31 bytes)
- contentStorageBySha[contentSha] = BytePackLib.packCalldata(content);
- } else {
- // Deploy via SSTORE2 for larger content (32+ bytes)
- address pointer = SSTORE2Unlimited.write(content);
- contentStorageBySha[contentSha] = bytes32(uint256(uint160(pointer)));
- }
-
- return contentSha;
+ /// @return contentHash The keccak256 hash of the content
+ function _storeContent(bytes calldata content) internal returns (bytes32 contentHash) {
+ // Use shared deduplication logic with keccak256
+ (contentHash,) = DedupedBlobStore.storeCalldata(content, contentStorage);
+ return contentHash;
}
function _queueForProving(bytes32 ethscriptionId) internal {
@@ -734,9 +765,6 @@ contract Ethscriptions is ERC721EthscriptionsSequentialEnumerableUpgradeable {
return;
}
- // Track which protocol this ethscription uses
- protocolOf[ethscriptionId] = protocolParams.protocolName;
-
address handler = protocolHandlers[protocolParams.protocolName];
// Skip if no handler is registered
@@ -770,14 +798,17 @@ contract Ethscriptions is ERC721EthscriptionsSequentialEnumerableUpgradeable {
address from,
address to
) internal {
- string memory protocol = protocolOf[ethscriptionId];
+ // Get protocol from metadata
+ EthscriptionStorage storage etsc = ethscriptions[ethscriptionId];
+ (string memory protocolName,) = MetaStoreLib.getProtocol(etsc.metaRef);
// Skip if no protocol assigned
- if (bytes(protocol).length == 0) {
+ if (bytes(protocolName).length == 0) {
return;
}
- address handler = protocolHandlers[protocol];
+ // Protocol names are stored normalized (lowercase)
+ address handler = protocolHandlers[protocolName];
// Skip if no handler is registered
if (handler == address(0)) {
@@ -787,9 +818,9 @@ contract Ethscriptions is ERC721EthscriptionsSequentialEnumerableUpgradeable {
// Use try/catch for cleaner error handling
try IProtocolHandler(handler).onTransfer(ethscriptionId, from, to) {
// onTransfer doesn't return data, so pass empty bytes
- emit ProtocolHandlerSuccess(ethscriptionId, protocol, "");
+ emit ProtocolHandlerSuccess(ethscriptionId, protocolName, "");
} catch (bytes memory revertData) {
- emit ProtocolHandlerFailed(ethscriptionId, protocol, revertData);
+ emit ProtocolHandlerFailed(ethscriptionId, protocolName, revertData);
}
}
@@ -844,8 +875,8 @@ contract Ethscriptions is ERC721EthscriptionsSequentialEnumerableUpgradeable {
ethscriptionId,
ethscription.creator,
ethscription.initialOwner,
- ethscription.contentUriHash,
- ethscription.contentSha,
+ ethscription.contentUriSha,
+ ethscription.contentHash,
ethscription.ethscriptionNumber
);
}
diff --git a/contracts/src/EthscriptionsProver.sol b/contracts/src/EthscriptionsProver.sol
index dc787b6..231a324 100644
--- a/contracts/src/EthscriptionsProver.sol
+++ b/contracts/src/EthscriptionsProver.sol
@@ -28,8 +28,8 @@ contract EthscriptionsProver {
/// @notice Struct for ethscription data proof
struct EthscriptionDataProof {
bytes32 ethscriptionId;
- bytes32 contentSha;
- bytes32 contentUriHash;
+ bytes32 contentHash;
+ bytes32 contentUriSha;
bytes32 l1BlockHash;
address creator;
address currentOwner;
@@ -144,8 +144,8 @@ contract EthscriptionsProver {
// Create proof struct with all ethscription data
EthscriptionDataProof memory proof = EthscriptionDataProof({
ethscriptionId: ethscriptionId,
- contentSha: ethscription.contentSha,
- contentUriHash: ethscription.contentUriHash,
+ contentHash: ethscription.contentHash,
+ contentUriSha: ethscription.contentUriSha,
l1BlockHash: proofInfo.l1BlockHash,
creator: ethscription.creator,
currentOwner: currentOwner,
diff --git a/contracts/src/libraries/BytePackLib.sol b/contracts/src/libraries/BytePackLib.sol
index 49fd887..1c6f4aa 100644
--- a/contracts/src/libraries/BytePackLib.sol
+++ b/contracts/src/libraries/BytePackLib.sol
@@ -25,6 +25,23 @@ library BytePackLib {
}
}
+ /// @notice Pack bytes memory up to 31 bytes into a bytes32
+ /// @dev Memory version for when data is in memory
+ /// @param data The data to pack (must be <= 31 bytes)
+ /// @return packed The packed bytes32 value
+ function pack(bytes memory data) internal pure returns (bytes32 packed) {
+ uint256 len = data.length;
+ if (len >= 32) revert ContentTooLarge(len);
+
+ assembly {
+ // Pack: tag byte (len+1) | first 31 bytes of data
+ packed := or(
+ shl(248, add(len, 1)), // Tag in first byte
+ shr(8, mload(add(data, 0x20))) // Data in remaining 31 bytes (skip length prefix)
+ )
+ }
+ }
+
/// @notice Unpack a bytes32 value into bytes
/// @dev Extracts the data based on the tag byte (length + 1)
/// @param packed The packed bytes32 value
diff --git a/contracts/src/libraries/DedupedBlobStore.sol b/contracts/src/libraries/DedupedBlobStore.sol
new file mode 100644
index 0000000..904224a
--- /dev/null
+++ b/contracts/src/libraries/DedupedBlobStore.sol
@@ -0,0 +1,107 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.24;
+
+import "./BytePackLib.sol";
+import "./SSTORE2Unlimited.sol";
+
+/// @title DedupedBlobStore
+/// @notice Shared library for deduplicated blob storage using inline packing or SSTORE2
+/// @dev Used by both content storage and metadata storage to eliminate code duplication
+library DedupedBlobStore {
+
+ /// @notice Store calldata blob with deduplication using keccak256
+ /// @dev Uses keccak256 for dedup key, stores either packed (≤31 bytes) or SSTORE2 pointer
+ /// @param data The calldata to store
+ /// @param store The storage mapping (hash => ref)
+ /// @return hash The keccak256 hash of the data (dedup key)
+ /// @return ref The storage reference (packed or SSTORE2 pointer)
+ function storeCalldata(
+ bytes calldata data,
+ mapping(bytes32 => bytes32) storage store
+ ) internal returns (bytes32 hash, bytes32 ref) {
+ hash = keccak256(data);
+
+ // Check if already stored
+ bytes32 existing = store[hash];
+ if (existing != bytes32(0)) {
+ return (hash, existing);
+ }
+
+ // Store based on size - use calldata packing for efficiency
+ ref = data.length <= 31 ? BytePackLib.packCalldata(data) : _deploySST0RE2Calldata(data);
+
+ // Store the mapping: hash -> reference
+ store[hash] = ref;
+ return (hash, ref);
+ }
+
+ /// @notice Store memory blob with deduplication using keccak256
+ /// @dev Uses keccak256 for dedup key, stores either packed (≤31 bytes) or SSTORE2 pointer
+ /// @param data The memory data to store
+ /// @param store The storage mapping (hash => ref)
+ /// @return hash The keccak256 hash of the data (dedup key)
+ /// @return ref The storage reference (packed or SSTORE2 pointer)
+ function storeMemory(
+ bytes memory data,
+ mapping(bytes32 => bytes32) storage store
+ ) internal returns (bytes32 hash, bytes32 ref) {
+ hash = keccak256(data);
+
+ // Check if already stored
+ bytes32 existing = store[hash];
+ if (existing != bytes32(0)) {
+ return (hash, existing);
+ }
+
+ // Store based on size - use memory packing
+ ref = data.length <= 31 ? BytePackLib.pack(data) : _deploySST0RE2Memory(data);
+
+ // Store the mapping: hash -> reference
+ store[hash] = ref;
+ return (hash, ref);
+ }
+
+ /// @notice Deploy SSTORE2 contract and return reference
+ /// @param data The data to deploy (calldata or memory)
+ /// @return ref The SSTORE2 pointer as bytes32
+ function _deploySST0RE2Calldata(bytes calldata data) private returns (bytes32 ref) {
+ address pointer = SSTORE2Unlimited.write(data);
+ return bytes32(uint256(uint160(pointer)));
+ }
+
+ /// @notice Deploy SSTORE2 contract and return reference
+ /// @param data The data to deploy (calldata or memory)
+ /// @return ref The SSTORE2 pointer as bytes32
+ function _deploySST0RE2Memory(bytes memory data) private returns (bytes32 ref) {
+ address pointer = SSTORE2Unlimited.write(data);
+ return bytes32(uint256(uint160(pointer)));
+ }
+
+ /// @notice Read blob from storage reference
+ /// @dev Automatically detects packed vs SSTORE2 and retrieves accordingly
+ /// @param ref The storage reference (packed or SSTORE2 pointer)
+ /// @return data The retrieved blob
+ function read(bytes32 ref) internal view returns (bytes memory) {
+ // Check if it's inline packed content
+ if (BytePackLib.isPacked(ref)) {
+ return BytePackLib.unpack(ref);
+ }
+
+ // It's a pointer to SSTORE2 contract
+ address pointer = address(uint160(uint256(ref)));
+ return SSTORE2Unlimited.read(pointer);
+ }
+
+ /// @notice Read blob from storage mapping by hash
+ /// @dev Looks up reference in mapping, then reads
+ /// @param hash The hash key
+ /// @param store The storage mapping
+ /// @return data The retrieved blob
+ function readByHash(
+ bytes32 hash,
+ mapping(bytes32 => bytes32) storage store
+ ) internal view returns (bytes memory) {
+ bytes32 ref = store[hash];
+ return read(ref);
+ }
+}
diff --git a/contracts/src/libraries/EthscriptionsRendererLib.sol b/contracts/src/libraries/EthscriptionsRendererLib.sol
index 81f11b4..2714c47 100644
--- a/contracts/src/libraries/EthscriptionsRendererLib.sol
+++ b/contracts/src/libraries/EthscriptionsRendererLib.sol
@@ -14,8 +14,17 @@ library EthscriptionsRendererLib {
/// @notice Build attributes JSON array from ethscription data
/// @param etsc Storage pointer to the ethscription
/// @param ethscriptionId The ethscription ID (L1 tx hash)
+ /// @param mimetype The MIME type string (decoded from metadata)
+ /// @param protocolName The protocol name (empty if none)
+ /// @param operation The operation name (empty if none)
/// @return JSON string of attributes array
- function buildAttributes(Ethscriptions.EthscriptionStorage storage etsc, bytes32 ethscriptionId)
+ function buildAttributes(
+ Ethscriptions.EthscriptionStorage storage etsc,
+ bytes32 ethscriptionId,
+ string memory mimetype,
+ string memory protocolName,
+ string memory operation
+ )
internal
view
returns (string memory)
@@ -33,12 +42,32 @@ library EthscriptionsRendererLib {
);
string memory part2 = string.concat(
- '"},{"trait_type":"Content SHA","value":"',
- uint256(etsc.contentSha).toHexString(),
+ '"},{"trait_type":"Content Hash","value":"',
+ uint256(etsc.contentHash).toHexString(),
'"},{"trait_type":"MIME Type","value":"',
- etsc.mimetype.escapeJSON(),
+ mimetype.escapeJSON(),
'"},{"trait_type":"ESIP-6","value":"',
- etsc.esip6 ? "true" : "false",
+ etsc.esip6 ? "true" : "false"
+ );
+
+ // Add protocol info if present
+ string memory protocolAttrs = "";
+ if (bytes(protocolName).length > 0) {
+ protocolAttrs = string.concat(
+ '"},{"trait_type":"Protocol","value":"',
+ protocolName.escapeJSON()
+ );
+ if (bytes(operation).length > 0) {
+ protocolAttrs = string.concat(
+ protocolAttrs,
+ '"},{"trait_type":"Operation","value":"',
+ operation.escapeJSON()
+ );
+ }
+ }
+
+ string memory part3 = string.concat(
+ protocolAttrs,
'"},{"trait_type":"L1 Block Number","display_type":"number","value":',
uint256(etsc.l1BlockNumber).toString(),
'},{"trait_type":"L2 Block Number","display_type":"number","value":',
@@ -48,35 +77,35 @@ library EthscriptionsRendererLib {
'}]'
);
- return string.concat(part1, part2);
+ return string.concat(part1, part2, part3);
}
/// @notice Generate the media URI for an ethscription
- /// @param etsc Storage pointer to the ethscription
+ /// @param mimetype The MIME type string
/// @param content The content bytes
/// @return mediaType Either "image" or "animation_url"
/// @return mediaUri The data URI for the media
- function getMediaUri(Ethscriptions.EthscriptionStorage storage etsc, bytes memory content)
+ function getMediaUri(string memory mimetype, bytes memory content)
internal
- view
+ pure
returns (string memory mediaType, string memory mediaUri)
{
- if (etsc.mimetype.startsWith("image/")) {
+ if (mimetype.startsWith("image/")) {
// Image content: wrap in SVG for pixel-perfect rendering
- string memory imageDataUri = constructDataURI(etsc.mimetype, content);
+ string memory imageDataUri = constructDataURI(mimetype, content);
string memory svg = wrapImageInSVG(imageDataUri);
mediaUri = constructDataURI("image/svg+xml", bytes(svg));
return ("image", mediaUri);
} else {
// Non-image content: use animation_url
- if (etsc.mimetype.startsWith("video/") ||
- etsc.mimetype.startsWith("audio/") ||
- etsc.mimetype.eq("text/html")) {
+ if (mimetype.startsWith("video/") ||
+ mimetype.startsWith("audio/") ||
+ mimetype.eq("text/html")) {
// Video, audio, and HTML pass through directly as data URIs
- mediaUri = constructDataURI(etsc.mimetype, content);
+ mediaUri = constructDataURI(mimetype, content);
} else {
// Everything else (text/plain, application/json, etc.) uses the HTML viewer
- mediaUri = createTextViewerDataURI(etsc.mimetype, content);
+ mediaUri = createTextViewerDataURI(mimetype, content);
}
return ("animation_url", mediaUri);
}
@@ -85,18 +114,24 @@ library EthscriptionsRendererLib {
/// @notice Build complete token URI JSON
/// @param etsc Storage pointer to the ethscription
/// @param ethscriptionId The ethscription ID (L1 tx hash)
+ /// @param mimetype The MIME type string (decoded from metadata)
+ /// @param protocolName The protocol name (empty if none)
+ /// @param operation The operation name (empty if none)
/// @param content The content bytes
/// @return The complete base64-encoded data URI
function buildTokenURI(
Ethscriptions.EthscriptionStorage storage etsc,
bytes32 ethscriptionId,
+ string memory mimetype,
+ string memory protocolName,
+ string memory operation,
bytes memory content
) internal view returns (string memory) {
// Get media URI
- (string memory mediaType, string memory mediaUri) = getMediaUri(etsc, content);
+ (string memory mediaType, string memory mediaUri) = getMediaUri(mimetype, content);
// Build attributes
- string memory attributes = buildAttributes(etsc, ethscriptionId);
+ string memory attributes = buildAttributes(etsc, ethscriptionId, mimetype, protocolName, operation);
// Build JSON
string memory json = string.concat(
@@ -109,7 +144,7 @@ library EthscriptionsRendererLib {
'","',
mediaType,
'":"',
- mediaUri.escapeJSON(),
+ mediaUri,
'","attributes":',
attributes,
'}'
@@ -132,7 +167,7 @@ library EthscriptionsRendererLib {
{
return string.concat(
"data:",
- mimetype,
+ mimetype.escapeJSON(),
";base64,",
Base64.encode(content)
);
diff --git a/contracts/src/libraries/MetaStoreLib.sol b/contracts/src/libraries/MetaStoreLib.sol
new file mode 100644
index 0000000..55f5883
--- /dev/null
+++ b/contracts/src/libraries/MetaStoreLib.sol
@@ -0,0 +1,231 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.24;
+
+import {LibBytes} from "solady/utils/LibBytes.sol";
+import "./DedupedBlobStore.sol";
+
+/// @title MetaStoreLib
+/// @notice Library for deduplicated storage of ethscription metadata (mimetype, protocol, operation)
+/// @dev Encodes metadata as: mimetype\x00protocol\x00operation, stores once per unique combination
+library MetaStoreLib {
+ using LibBytes for bytes;
+
+ /// @dev Null byte (0x00) used to separate metadata components
+ /// @dev Safe to use as Ruby indexer strips all null bytes from input strings
+ bytes1 constant SEPARATOR = 0x00;
+
+ /// @dev Sentinel value for "text/plain" with no protocol (most common case)
+ bytes32 constant EMPTY_REF = bytes32(0);
+
+ // Custom errors
+ error InvalidSeparatorInInput();
+ error InvalidMetadataRef();
+ error MetadataNotStored();
+ error InvalidFormat();
+
+ /// @notice Store metadata components (encode + deduplicate)
+ /// @dev High-level API for callers - combines encode() and intern()
+ /// @param mimetype MIME type string (preserve case for standards compliance)
+ /// @param protocolName Protocol identifier (should already be normalized by Ruby)
+ /// @param operation Operation to perform (should already be normalized by Ruby)
+ /// @param metaStore Storage mapping for metadata blobs
+ /// @return metaRef The metadata reference (bytes32(0), packed, or SSTORE2 pointer)
+ function store(
+ string memory mimetype,
+ string memory protocolName,
+ string memory operation,
+ mapping(bytes32 => bytes32) storage metaStore
+ ) internal returns (bytes32 metaRef) {
+ bytes memory blob = encode(mimetype, protocolName, operation);
+ return intern(blob, metaStore);
+ }
+
+ /// @notice Encode metadata components into a blob
+ /// @dev Lower-level API - most callers should use store() instead
+ /// @param mimetype MIME type string (not normalized - preserve case for standards compliance)
+ /// @param protocolName Protocol identifier (should already be normalized by Ruby)
+ /// @param operation Operation name (should already be normalized by Ruby)
+ /// @return blob The encoded metadata blob (empty if all components empty/default)
+ function encode(
+ string memory mimetype,
+ string memory protocolName,
+ string memory operation
+ ) internal pure returns (bytes memory blob) {
+ // Validate inputs don't contain separator
+ if (_containsByte(bytes(mimetype), SEPARATOR)) revert InvalidSeparatorInInput();
+ if (_containsByte(bytes(protocolName), SEPARATOR)) revert InvalidSeparatorInInput();
+ if (_containsByte(bytes(operation), SEPARATOR)) revert InvalidSeparatorInInput();
+
+ // Note: normalization (lowercase, trim) is handled by Ruby indexer before submission
+
+ // Normalize "text/plain" to empty string (convention: empty = text/plain)
+ if (keccak256(bytes(mimetype)) == keccak256(bytes("text/plain"))) {
+ mimetype = "";
+ }
+
+ // Special case: empty mimetype + no protocol → empty blob (most common case!)
+ if (bytes(mimetype).length == 0 && bytes(protocolName).length == 0 && bytes(operation).length == 0) {
+ return bytes(""); // Will map to EMPTY_REF (bytes32(0))
+ }
+
+ // Always encode in same format: mimetype\x1Fprotocol\x1Foperation
+ // Any component can be empty string
+ return abi.encodePacked(mimetype, SEPARATOR, protocolName, SEPARATOR, operation);
+ }
+
+ /// @notice Decode a metadata reference into components
+ /// @param metaRef The metadata reference (bytes32(0), packed, or SSTORE2 pointer)
+ /// @return mimetype The MIME type
+ /// @return protocolName The protocol identifier (normalized)
+ /// @return operation The operation name (normalized)
+ function decode(bytes32 metaRef) internal view returns (
+ string memory mimetype,
+ string memory protocolName,
+ string memory operation
+ ) {
+ bytes[] memory parts = _getParts(metaRef);
+ return _partsToStrings(parts);
+ }
+
+ /// @notice Get only the mimetype from a metadata reference (gas-optimized)
+ /// @param metaRef The metadata reference
+ /// @return mimetype The MIME type
+ function getMimetype(bytes32 metaRef) internal view returns (string memory mimetype) {
+ bytes[] memory parts = _getParts(metaRef);
+
+ // First part is always mimetype (empty = text/plain)
+ string memory mime = string(parts[0]);
+ return bytes(mime).length == 0 ? "text/plain" : mime;
+ }
+
+ /// @notice Get protocol information from a metadata reference
+ /// @param metaRef The metadata reference
+ /// @return protocolName The protocol identifier (normalized, empty if none)
+ /// @return operation The operation name (normalized, empty if none)
+ function getProtocol(bytes32 metaRef) internal view returns (
+ string memory protocolName,
+ string memory operation
+ ) {
+ bytes[] memory parts = _getParts(metaRef);
+
+ // parts[0] = mimetype, parts[1] = protocol, parts[2] = operation
+ protocolName = string(parts[1]);
+ operation = string(parts[2]);
+ return (protocolName, operation);
+ }
+
+
+ /// @notice Intern a metadata blob (deduplicate and store)
+ /// @dev Lower-level API - most callers should use store() instead
+ /// @param blob The encoded metadata blob
+ /// @param metaStore Storage mapping for metadata blobs
+ /// @return metaRef The metadata reference (bytes32(0), packed, or SSTORE2 pointer)
+ function intern(
+ bytes memory blob,
+ mapping(bytes32 => bytes32) storage metaStore
+ ) internal returns (bytes32 metaRef) {
+ // Special case: empty blob = EMPTY_REF sentinel
+ if (blob.length == 0) {
+ return EMPTY_REF;
+ }
+
+ // Use shared deduplication logic with keccak256
+ (, metaRef) = DedupedBlobStore.storeMemory(blob, metaStore);
+ return metaRef;
+ }
+
+
+ // =============================================================
+ // INTERNAL HELPERS
+ // =============================================================
+
+ /// @notice Retrieve a blob from storage
+ /// @param metaRef The metadata reference
+ /// @return blob The retrieved blob
+ function _retrieve(bytes32 metaRef) private view returns (bytes memory blob) {
+ if (metaRef == EMPTY_REF) {
+ return bytes("");
+ }
+
+ // Use shared read logic
+ return DedupedBlobStore.read(metaRef);
+ }
+
+ /// @notice Get parts array from metadata reference (single point for blob.length check)
+ /// @param metaRef The metadata reference
+ /// @return parts Array of 3 byte parts [mimetype, protocol, operation]
+ function _getParts(bytes32 metaRef) private view returns (bytes[] memory parts) {
+ bytes memory blob = _retrieve(metaRef);
+
+ // Single check for empty blob (text/plain + no protocol case)
+ if (blob.length == 0) {
+ parts = new bytes[](3);
+ parts[0] = bytes(""); // Empty = text/plain
+ parts[1] = bytes(""); // No protocol
+ parts[2] = bytes(""); // No operation
+ return parts;
+ }
+
+ // Split keeping empty parts - always get 3 parts
+ return _splitKeepEmpty(blob, SEPARATOR);
+ }
+
+ /// @notice Convert parts array to strings with text/plain default
+ /// @param parts Array of 3 byte parts [mimetype, protocol, operation]
+ /// @return mimetype The MIME type
+ /// @return protocolName The protocol identifier
+ /// @return operation The operation name
+ function _partsToStrings(bytes[] memory parts) private pure returns (
+ string memory mimetype,
+ string memory protocolName,
+ string memory operation
+ ) {
+ // Extract mimetype (empty = text/plain)
+ mimetype = string(parts[0]);
+ if (bytes(mimetype).length == 0) {
+ mimetype = "text/plain";
+ }
+
+ // Extract protocol and operation (may be empty)
+ protocolName = string(parts[1]);
+ operation = string(parts[2]);
+
+ return (mimetype, protocolName, operation);
+ }
+
+ /// @notice Split a blob by single-byte delimiter, keeping empty parts
+ /// @dev Enforces exactly 2 separators (3 parts): [mimetype, protocol, operation]
+ /// @param subject The blob to split
+ /// @param delim The single-byte delimiter
+ /// @return out Array with exactly 3 parts (some may be empty)
+ function _splitKeepEmpty(bytes memory subject, bytes1 delim)
+ private
+ pure
+ returns (bytes[] memory out)
+ {
+ // Find first separator
+ uint256 a = subject.indexOfByte(delim, 0);
+ if (a == LibBytes.NOT_FOUND) revert InvalidFormat();
+
+ // Find second separator
+ uint256 b = subject.indexOfByte(delim, a + 1);
+ if (b == LibBytes.NOT_FOUND) revert InvalidFormat();
+
+ // Ensure no third separator (enforce format)
+ if (subject.indexOfByte(delim, b + 1) != LibBytes.NOT_FOUND) revert InvalidFormat();
+
+ out = new bytes[](3);
+ out[0] = subject.slice(0, a); // mimetype (may be empty)
+ out[1] = subject.slice(a + 1, b); // protocol (may be empty)
+ out[2] = subject.slice(b + 1, subject.length); // operation (may be empty)
+ }
+
+ /// @notice Check if bytes contains a specific byte
+ /// @dev Custom helper since LibBytes.contains requires bytes memory, not bytes1
+ /// @param data The data to search
+ /// @param target The byte to find
+ /// @return True if found
+ function _containsByte(bytes memory data, bytes1 target) private pure returns (bool) {
+ return data.indexOfByte(target) != LibBytes.NOT_FOUND;
+ }
+}
diff --git a/contracts/test/AddressPrediction.t.sol b/contracts/test/AddressPrediction.t.sol
index 39b7a7c..c4029c9 100644
--- a/contracts/test/AddressPrediction.t.sol
+++ b/contracts/test/AddressPrediction.t.sol
@@ -46,7 +46,7 @@ contract AddressPredictionTest is TestSetup {
// First, create the ethscription that will represent this collection
Ethscriptions.CreateEthscriptionParams memory ethscriptionParams = Ethscriptions.CreateEthscriptionParams({
ethscriptionId: collectionId,
- contentUriHash: keccak256("collection-content"),
+ contentUriSha: keccak256("collection-content"),
initialOwner: creator,
content: bytes("collection-content"),
mimetype: "text/plain",
diff --git a/contracts/test/CollectionsManager.t.sol b/contracts/test/CollectionsManager.t.sol
index f120955..676a16e 100644
--- a/contracts/test/CollectionsManager.t.sol
+++ b/contracts/test/CollectionsManager.t.sol
@@ -44,7 +44,7 @@ contract ERC721EthscriptionsCollectionManagerTest is TestSetup {
Ethscriptions.CreateEthscriptionParams memory params = Ethscriptions.CreateEthscriptionParams({
ethscriptionId: COLLECTION_TX_HASH,
- contentUriHash: sha256(bytes(collectionContent)),
+ contentUriSha: sha256(bytes(collectionContent)),
initialOwner: alice,
content: bytes(collectionContent),
mimetype: "application/json",
@@ -128,7 +128,7 @@ contract ERC721EthscriptionsCollectionManagerTest is TestSetup {
Ethscriptions.CreateEthscriptionParams memory ethscriptionParams =
Ethscriptions.CreateEthscriptionParams({
ethscriptionId: collectionAndItemId,
- contentUriHash: sha256(bytes("collection and item content")),
+ contentUriSha: sha256(bytes("collection and item content")),
initialOwner: alice,
content: bytes("collection and item content"),
mimetype: "text/plain",
@@ -208,7 +208,7 @@ contract ERC721EthscriptionsCollectionManagerTest is TestSetup {
// Create the ethscription with protocol set to add itself to the collection
Ethscriptions.CreateEthscriptionParams memory itemParams = Ethscriptions.CreateEthscriptionParams({
ethscriptionId: ITEM1_TX_HASH,
- contentUriHash: sha256(bytes(itemContent)),
+ contentUriSha: sha256(bytes(itemContent)),
initialOwner: alice,
content: bytes(itemContent),
mimetype: "application/json",
@@ -303,7 +303,7 @@ contract ERC721EthscriptionsCollectionManagerTest is TestSetup {
Ethscriptions.CreateEthscriptionParams memory removeParams = Ethscriptions.CreateEthscriptionParams({
ethscriptionId: bytes32(uint256(0xFEED)),
- contentUriHash: sha256(bytes(removeContent)),
+ contentUriSha: sha256(bytes(removeContent)),
initialOwner: alice,
content: bytes(removeContent),
mimetype: "application/json",
@@ -354,7 +354,7 @@ contract ERC721EthscriptionsCollectionManagerTest is TestSetup {
Ethscriptions.CreateEthscriptionParams memory removeParams = Ethscriptions.CreateEthscriptionParams({
ethscriptionId: bytes32(uint256(0xBAD)),
- contentUriHash: sha256(bytes("data:,remove")),
+ contentUriSha: sha256(bytes("data:,remove")),
initialOwner: bob,
content: bytes("remove"),
mimetype: "text/plain",
@@ -414,7 +414,7 @@ contract ERC721EthscriptionsCollectionManagerTest is TestSetup {
// Create ethscription that adds itself to the collection
Ethscriptions.CreateEthscriptionParams memory itemParams = Ethscriptions.CreateEthscriptionParams({
ethscriptionId: itemHashes[i],
- contentUriHash: sha256(abi.encodePacked("item", i)),
+ contentUriSha: sha256(abi.encodePacked("item", i)),
initialOwner: owners[i],
content: abi.encodePacked("item", i),
mimetype: "text/plain",
@@ -488,7 +488,7 @@ contract ERC721EthscriptionsCollectionManagerTest is TestSetup {
// Create the ethscription with image content
Ethscriptions.CreateEthscriptionParams memory itemParams = Ethscriptions.CreateEthscriptionParams({
ethscriptionId: ITEM1_TX_HASH,
- contentUriHash: sha256(bytes(imageContent)),
+ contentUriSha: sha256(bytes(imageContent)),
initialOwner: alice,
content: bytes(imageContent),
mimetype: "image/png",
@@ -557,7 +557,7 @@ contract ERC721EthscriptionsCollectionManagerTest is TestSetup {
Ethscriptions.CreateEthscriptionParams memory editParams = Ethscriptions.CreateEthscriptionParams({
ethscriptionId: bytes32(uint256(0xED171)),
- contentUriHash: sha256(bytes("edit")),
+ contentUriSha: sha256(bytes("edit")),
initialOwner: alice,
content: bytes("edit"),
mimetype: "text/plain",
@@ -612,7 +612,7 @@ contract ERC721EthscriptionsCollectionManagerTest is TestSetup {
Ethscriptions.CreateEthscriptionParams memory itemParams = Ethscriptions.CreateEthscriptionParams({
ethscriptionId: ITEM1_TX_HASH,
- contentUriHash: sha256(bytes("item content")),
+ contentUriSha: sha256(bytes("item content")),
initialOwner: alice,
content: bytes("item content"),
mimetype: "text/plain",
@@ -641,7 +641,7 @@ contract ERC721EthscriptionsCollectionManagerTest is TestSetup {
Ethscriptions.CreateEthscriptionParams memory editParams = Ethscriptions.CreateEthscriptionParams({
ethscriptionId: bytes32(uint256(0xED172)),
- contentUriHash: sha256(bytes("partial-edit")),
+ contentUriSha: sha256(bytes("partial-edit")),
initialOwner: alice,
content: bytes("partial-edit"),
mimetype: "text/plain",
@@ -684,7 +684,7 @@ contract ERC721EthscriptionsCollectionManagerTest is TestSetup {
Ethscriptions.CreateEthscriptionParams memory editParams = Ethscriptions.CreateEthscriptionParams({
ethscriptionId: bytes32(uint256(0xBADED17)),
- contentUriHash: sha256(bytes("bad-edit")),
+ contentUriSha: sha256(bytes("bad-edit")),
initialOwner: bob,
content: bytes("bad-edit"),
mimetype: "text/plain",
@@ -722,7 +722,7 @@ contract ERC721EthscriptionsCollectionManagerTest is TestSetup {
Ethscriptions.CreateEthscriptionParams memory editParams = Ethscriptions.CreateEthscriptionParams({
ethscriptionId: bytes32(uint256(0x901743)),
- contentUriHash: sha256(bytes("no-item")),
+ contentUriSha: sha256(bytes("no-item")),
initialOwner: alice,
content: bytes("no-item"),
mimetype: "text/plain",
@@ -771,7 +771,7 @@ contract ERC721EthscriptionsCollectionManagerTest is TestSetup {
Ethscriptions.CreateEthscriptionParams memory syncParams = Ethscriptions.CreateEthscriptionParams({
ethscriptionId: bytes32(uint256(0x5914C)),
- contentUriHash: sha256(bytes("sync")),
+ contentUriSha: sha256(bytes("sync")),
initialOwner: charlie,
content: bytes("sync"),
mimetype: "text/plain",
@@ -824,7 +824,7 @@ contract ERC721EthscriptionsCollectionManagerTest is TestSetup {
Ethscriptions.CreateEthscriptionParams memory syncParams = Ethscriptions.CreateEthscriptionParams({
ethscriptionId: bytes32(uint256(0x5914CE)),
- contentUriHash: sha256(bytes("sync-nonexistent")),
+ contentUriSha: sha256(bytes("sync-nonexistent")),
initialOwner: alice,
content: bytes("sync-nonexistent"),
mimetype: "text/plain",
@@ -850,7 +850,7 @@ contract ERC721EthscriptionsCollectionManagerTest is TestSetup {
Ethscriptions.CreateEthscriptionParams memory syncParams = Ethscriptions.CreateEthscriptionParams({
ethscriptionId: bytes32(uint256(0x5914CF)),
- contentUriHash: sha256(bytes("sync-fake")),
+ contentUriSha: sha256(bytes("sync-fake")),
initialOwner: alice,
content: bytes("sync-fake"),
mimetype: "text/plain",
@@ -878,7 +878,7 @@ contract ERC721EthscriptionsCollectionManagerTest is TestSetup {
Ethscriptions.CreateEthscriptionParams memory lockParams = Ethscriptions.CreateEthscriptionParams({
ethscriptionId: bytes32(uint256(0x10CCC)),
- contentUriHash: sha256(bytes("lock")),
+ contentUriSha: sha256(bytes("lock")),
initialOwner: alice,
content: bytes("lock"),
mimetype: "text/plain",
@@ -907,7 +907,7 @@ contract ERC721EthscriptionsCollectionManagerTest is TestSetup {
Ethscriptions.CreateEthscriptionParams memory editParams = Ethscriptions.CreateEthscriptionParams({
ethscriptionId: bytes32(uint256(0x10C3ED)),
- contentUriHash: sha256(bytes("locked-edit")),
+ contentUriSha: sha256(bytes("locked-edit")),
initialOwner: alice,
content: bytes("locked-edit"),
mimetype: "text/plain",
diff --git a/contracts/test/CollectionsProtocol.t.sol b/contracts/test/CollectionsProtocol.t.sol
index a7acb70..7ef7d3b 100644
--- a/contracts/test/CollectionsProtocol.t.sol
+++ b/contracts/test/CollectionsProtocol.t.sol
@@ -36,7 +36,7 @@ contract CollectionsProtocolTest is TestSetup {
// First, create the ethscription that will represent this collection
Ethscriptions.CreateEthscriptionParams memory ethscriptionParams = Ethscriptions.CreateEthscriptionParams({
ethscriptionId: txHash,
- contentUriHash: keccak256("test-collection-content"),
+ contentUriSha: keccak256("test-collection-content"),
initialOwner: alice,
content: bytes("test-collection-content"),
mimetype: "text/plain",
@@ -102,7 +102,7 @@ contract CollectionsProtocolTest is TestSetup {
Ethscriptions.CreateEthscriptionParams memory params = Ethscriptions.CreateEthscriptionParams({
ethscriptionId: txHash,
- contentUriHash: keccak256(bytes(json)),
+ contentUriSha: keccak256(bytes(json)),
initialOwner: alice,
content: bytes(json),
mimetype: "application/json",
@@ -162,7 +162,7 @@ contract CollectionsProtocolTest is TestSetup {
// First, create the ethscription that will represent this collection
Ethscriptions.CreateEthscriptionParams memory ethscriptionParams = Ethscriptions.CreateEthscriptionParams({
ethscriptionId: txHash,
- contentUriHash: keccak256("call-test-content"),
+ contentUriSha: keccak256("call-test-content"),
initialOwner: alice,
content: bytes("call-test-content"),
mimetype: "text/plain",
diff --git a/contracts/test/ERC721Enumerable.t.sol b/contracts/test/ERC721Enumerable.t.sol
index 8ba6647..b9a1370 100644
--- a/contracts/test/ERC721Enumerable.t.sol
+++ b/contracts/test/ERC721Enumerable.t.sol
@@ -19,7 +19,7 @@ contract ERC721EnumerableTest is TestSetup {
return ethscriptions.createEthscription(
Ethscriptions.CreateEthscriptionParams({
ethscriptionId: txHash,
- contentUriHash: keccak256(bytes(content)),
+ contentUriSha: keccak256(bytes(content)),
initialOwner: owner,
content: bytes(content),
mimetype: "text/plain",
diff --git a/contracts/test/EthscriptionsFailureHandling.t.sol b/contracts/test/EthscriptionsFailureHandling.t.sol
index 248296f..16ff186 100644
--- a/contracts/test/EthscriptionsFailureHandling.t.sol
+++ b/contracts/test/EthscriptionsFailureHandling.t.sol
@@ -100,7 +100,7 @@ contract EthscriptionsFailureHandlingTest is TestSetup {
Ethscriptions.CreateEthscriptionParams memory params = Ethscriptions.CreateEthscriptionParams({
ethscriptionId: txHash,
- contentUriHash: sha256(bytes(dataUri)),
+ contentUriSha: sha256(bytes(dataUri)),
initialOwner: address(this),
content: bytes("Hello World with failing token manager"),
mimetype: "text/plain",
@@ -182,7 +182,7 @@ contract EthscriptionsFailureHandlingTest is TestSetup {
Ethscriptions.CreateEthscriptionParams memory params = Ethscriptions.CreateEthscriptionParams({
ethscriptionId: txHash,
- contentUriHash: sha256(bytes(dataUri)),
+ contentUriSha: sha256(bytes(dataUri)),
initialOwner: address(this),
content: bytes("{\"p\":\"test\",\"op\":\"deploy\",\"tick\":\"FAIL\",\"max\":\"1000\",\"lim\":\"10\"}"),
mimetype: "application/json",
diff --git a/contracts/test/EthscriptionsMultiTransfer.t.sol b/contracts/test/EthscriptionsMultiTransfer.t.sol
index 32845fb..b3e51c2 100644
--- a/contracts/test/EthscriptionsMultiTransfer.t.sol
+++ b/contracts/test/EthscriptionsMultiTransfer.t.sol
@@ -231,7 +231,7 @@ contract EthscriptionsMultiTransferTest is TestSetup {
"Ethscription Number",
"Creator",
"Initial Owner",
- "Content SHA",
+ "Content Hash",
"MIME Type",
"ESIP-6",
"L1 Block Number",
diff --git a/contracts/test/EthscriptionsNullOwnership.t.sol b/contracts/test/EthscriptionsNullOwnership.t.sol
index b584728..5b50635 100644
--- a/contracts/test/EthscriptionsNullOwnership.t.sol
+++ b/contracts/test/EthscriptionsNullOwnership.t.sol
@@ -22,15 +22,15 @@ contract EthscriptionsNullOwnershipTest is TestSetup {
);
// Expect only one EthscriptionCreated event (no EthscriptionTransferred since from == address(0))
- bytes32 contentUriHash = sha256(bytes("data:text/plain,Null owned")); // Full data URI hash
- bytes32 contentSha = sha256(bytes("Null owned")); // Raw content, not data URI
+ bytes32 contentUriSha = sha256(bytes("data:text/plain,Null owned")); // Full data URI hash
+ bytes32 contentHash = keccak256(bytes("Null owned")); // Raw content hash (keccak256, not sha256)
vm.expectEmit(true, true, true, true);
emit Ethscriptions.EthscriptionCreated(
txHash,
alice, // creator
address(0), // initialOwner
- contentUriHash,
- contentSha,
+ contentUriSha,
+ contentHash,
11 // ethscription number (after 10 genesis)
);
diff --git a/contracts/test/EthscriptionsProver.t.sol b/contracts/test/EthscriptionsProver.t.sol
index 67b42c7..720fa2c 100644
--- a/contracts/test/EthscriptionsProver.t.sol
+++ b/contracts/test/EthscriptionsProver.t.sol
@@ -72,8 +72,8 @@ contract EthscriptionsProverTest is TestSetup {
assertEq(decodedProof.previousOwner, alice);
// assertEq(decodedProof.ethscriptionNumber, 0);
assertEq(decodedProof.esip6, false);
- assertTrue(decodedProof.contentSha != bytes32(0));
- assertTrue(decodedProof.contentUriHash != bytes32(0));
+ assertTrue(decodedProof.contentHash != bytes32(0));
+ assertTrue(decodedProof.contentUriSha != bytes32(0));
// l1BlockHash can be zero in test environment
assertEq(decodedProof.l1BlockHash, bytes32(0));
}
@@ -101,7 +101,7 @@ contract EthscriptionsProverTest is TestSetup {
ethscriptions.createEthscription(
Ethscriptions.CreateEthscriptionParams({
ethscriptionId: txHash1,
- contentUriHash: keccak256("data:,test1"),
+ contentUriSha: keccak256("data:,test1"),
initialOwner: alice,
content: bytes("test1"),
mimetype: "text/plain",
@@ -115,7 +115,7 @@ contract EthscriptionsProverTest is TestSetup {
ethscriptions.createEthscription(
Ethscriptions.CreateEthscriptionParams({
ethscriptionId: txHash2,
- contentUriHash: keccak256("data:,test2"),
+ contentUriSha: keccak256("data:,test2"),
initialOwner: bob,
content: bytes("test2"),
mimetype: "text/plain",
@@ -135,7 +135,7 @@ contract EthscriptionsProverTest is TestSetup {
ethscriptions.createEthscription(
Ethscriptions.CreateEthscriptionParams({
ethscriptionId: txHash3,
- contentUriHash: keccak256("data:,test3"),
+ contentUriSha: keccak256("data:,test3"),
initialOwner: charlie,
content: bytes("test3"),
mimetype: "text/plain",
diff --git a/contracts/test/EthscriptionsToken.t.sol b/contracts/test/EthscriptionsToken.t.sol
index 098f690..23c230f 100644
--- a/contracts/test/EthscriptionsToken.t.sol
+++ b/contracts/test/EthscriptionsToken.t.sol
@@ -35,7 +35,7 @@ contract EthscriptionsTokenTest is TestSetup {
bytes memory data
) internal pure returns (Ethscriptions.CreateEthscriptionParams memory) {
bytes memory contentUriBytes = bytes(contentUri);
- bytes32 contentUriHash = sha256(contentUriBytes); // Use SHA-256 to match production
+ bytes32 contentUriSha = sha256(contentUriBytes); // Use SHA-256 to match production
// Extract content after "data:,"
bytes memory content;
@@ -48,7 +48,7 @@ contract EthscriptionsTokenTest is TestSetup {
return Ethscriptions.CreateEthscriptionParams({
ethscriptionId: transactionHash,
- contentUriHash: contentUriHash,
+ contentUriSha: contentUriSha,
initialOwner: initialOwner,
content: content,
mimetype: "text/plain",
diff --git a/contracts/test/EthscriptionsTokenParams.t.sol b/contracts/test/EthscriptionsTokenParams.t.sol
index 55cc51c..0076e8d 100644
--- a/contracts/test/EthscriptionsTokenParams.t.sol
+++ b/contracts/test/EthscriptionsTokenParams.t.sol
@@ -11,7 +11,7 @@ contract EthscriptionsTokenParamsTest is TestSetup {
// Create a token deploy ethscription
string memory tokenJson = '{"p":"erc-20","op":"deploy","tick":"eths","max":"21000000","lim":"1000"}';
string memory dataUri = string.concat("data:,", tokenJson);
- bytes32 contentUriHash = sha256(bytes(dataUri));
+ bytes32 contentUriSha = sha256(bytes(dataUri));
ERC20FixedDenominationManager.DeployOperation memory deployOp = ERC20FixedDenominationManager.DeployOperation({
tick: "eths",
@@ -21,7 +21,7 @@ contract EthscriptionsTokenParamsTest is TestSetup {
Ethscriptions.CreateEthscriptionParams memory params = Ethscriptions.CreateEthscriptionParams({
ethscriptionId: bytes32(uint256(1)),
- contentUriHash: contentUriHash,
+ contentUriSha: contentUriSha,
initialOwner: address(this),
content: bytes(tokenJson),
mimetype: "text/plain",
@@ -49,7 +49,7 @@ contract EthscriptionsTokenParamsTest is TestSetup {
// Create a token mint ethscription
string memory tokenJson = '{"p":"erc-20","op":"mint","tick":"eths","id":"1","amt":"1000"}';
string memory dataUri = string.concat("data:,", tokenJson);
- bytes32 contentUriHash = sha256(bytes(dataUri));
+ bytes32 contentUriSha = sha256(bytes(dataUri));
ERC20FixedDenominationManager.MintOperation memory mintOp = ERC20FixedDenominationManager.MintOperation({
tick: "eths",
@@ -59,7 +59,7 @@ contract EthscriptionsTokenParamsTest is TestSetup {
Ethscriptions.CreateEthscriptionParams memory params = Ethscriptions.CreateEthscriptionParams({
ethscriptionId: bytes32(uint256(2)),
- contentUriHash: contentUriHash,
+ contentUriSha: contentUriSha,
initialOwner: address(this),
content: bytes(tokenJson),
mimetype: "text/plain",
@@ -82,11 +82,11 @@ contract EthscriptionsTokenParamsTest is TestSetup {
// Create a regular non-token ethscription
string memory content = "Hello, World!";
string memory dataUri = string.concat("data:,", content);
- bytes32 contentUriHash = sha256(bytes(dataUri));
+ bytes32 contentUriSha = sha256(bytes(dataUri));
Ethscriptions.CreateEthscriptionParams memory params = Ethscriptions.CreateEthscriptionParams({
ethscriptionId: bytes32(uint256(3)),
- contentUriHash: contentUriHash,
+ contentUriSha: contentUriSha,
initialOwner: address(this),
content: bytes(content),
mimetype: "text/plain",
@@ -119,7 +119,7 @@ contract EthscriptionsTokenParamsTest is TestSetup {
Ethscriptions.CreateEthscriptionParams memory deployParams = Ethscriptions.CreateEthscriptionParams({
ethscriptionId: keccak256("deploy_tx"),
- contentUriHash: sha256(bytes(deployUri)),
+ contentUriSha: sha256(bytes(deployUri)),
initialOwner: address(this),
content: bytes(deployJson),
mimetype: "application/json",
@@ -145,7 +145,7 @@ contract EthscriptionsTokenParamsTest is TestSetup {
Ethscriptions.CreateEthscriptionParams memory mintParams = Ethscriptions.CreateEthscriptionParams({
ethscriptionId: keccak256("mint_tx"),
- contentUriHash: sha256(bytes(mintUri)),
+ contentUriSha: sha256(bytes(mintUri)),
initialOwner: address(this),
content: bytes(mintJson),
mimetype: "application/json",
diff --git a/contracts/test/EthscriptionsWithContent.t.sol b/contracts/test/EthscriptionsWithContent.t.sol
index f3d5bd8..a17f3ad 100644
--- a/contracts/test/EthscriptionsWithContent.t.sol
+++ b/contracts/test/EthscriptionsWithContent.t.sol
@@ -16,7 +16,7 @@ contract EthscriptionsWithContentTest is TestSetup {
vm.prank(creator);
Ethscriptions.CreateEthscriptionParams memory params = Ethscriptions.CreateEthscriptionParams({
ethscriptionId: txHash,
- contentUriHash: keccak256(bytes("data:text/plain,Hello, World!")),
+ contentUriSha: keccak256(bytes("data:text/plain,Hello, World!")),
initialOwner: initialOwner,
content: bytes(testContent),
mimetype: "text/plain",
@@ -68,7 +68,7 @@ contract EthscriptionsWithContentTest is TestSetup {
vm.prank(creator);
Ethscriptions.CreateEthscriptionParams memory params = Ethscriptions.CreateEthscriptionParams({
ethscriptionId: txHash,
- contentUriHash: keccak256(bytes("data:text/plain,Test by token ID")),
+ contentUriSha: keccak256(bytes("data:text/plain,Test by token ID")),
initialOwner: initialOwner,
content: bytes(testContent),
mimetype: "text/plain",
@@ -126,7 +126,7 @@ contract EthscriptionsWithContentTest is TestSetup {
vm.prank(creator);
Ethscriptions.CreateEthscriptionParams memory params = Ethscriptions.CreateEthscriptionParams({
ethscriptionId: txHash,
- contentUriHash: keccak256(bytes("data:application/octet-stream,")),
+ contentUriSha: keccak256(bytes("data:application/octet-stream,")),
initialOwner: initialOwner,
content: largeContent,
mimetype: "application/octet-stream",
@@ -172,7 +172,7 @@ contract EthscriptionsWithContentTest is TestSetup {
vm.prank(creator);
Ethscriptions.CreateEthscriptionParams memory params = Ethscriptions.CreateEthscriptionParams({
ethscriptionId: txHash,
- contentUriHash: keccak256(bytes("data:text/plain,HelloWorld")),
+ contentUriSha: keccak256(bytes("data:text/plain,HelloWorld")),
initialOwner: initialOwner,
content: smallContent,
mimetype: "text/plain",
diff --git a/contracts/test/EthscriptionsWithTestFunctions.sol b/contracts/test/EthscriptionsWithTestFunctions.sol
index ab54ad9..75d42b7 100644
--- a/contracts/test/EthscriptionsWithTestFunctions.sol
+++ b/contracts/test/EthscriptionsWithTestFunctions.sol
@@ -15,21 +15,21 @@ contract EthscriptionsWithTestFunctions is Ethscriptions {
/// @dev Test-only function to check if content exists
function hasContent(bytes32 ethscriptionId) external view returns (bool) {
EthscriptionStorage storage ethscription = _getEthscriptionOrRevert(ethscriptionId);
- return contentStorageBySha[ethscription.contentSha] != bytes32(0);
+ return contentStorage[ethscription.contentHash] != bytes32(0);
}
/// @notice Get the content storage value for an ethscription
/// @dev Test-only function to inspect storage (either packed bytes or SSTORE2 address)
function getContentStorage(bytes32 ethscriptionId) external view returns (bytes32) {
EthscriptionStorage storage ethscription = _getEthscriptionOrRevert(ethscriptionId);
- return contentStorageBySha[ethscription.contentSha];
+ return contentStorage[ethscription.contentHash];
}
/// @notice Get the content pointer for an ethscription (only for SSTORE2 stored content)
/// @dev Test-only function to inspect SSTORE2 address
function getContentPointer(bytes32 ethscriptionId) external view returns (address) {
EthscriptionStorage storage ethscription = _getEthscriptionOrRevert(ethscriptionId);
- bytes32 stored = contentStorageBySha[ethscription.contentSha];
+ bytes32 stored = contentStorage[ethscription.contentHash];
// Check if it's inline content using BytePackLib
if (BytePackLib.isPacked(stored)) {
diff --git a/contracts/test/MetaStore.t.sol b/contracts/test/MetaStore.t.sol
new file mode 100644
index 0000000..6538e1d
--- /dev/null
+++ b/contracts/test/MetaStore.t.sol
@@ -0,0 +1,348 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.24;
+
+import "forge-std/Test.sol";
+import "../src/libraries/MetaStoreLib.sol";
+
+contract MetaStoreTest is Test {
+ // Test mapping for metadata storage
+ mapping(bytes32 => bytes32) public metaStore;
+
+ function setUp() public {}
+
+ // =============================================================
+ // ENCODING TESTS
+ // =============================================================
+
+ function test_EncodeEmptyMimeNoProtocol() public {
+ bytes memory blob = MetaStoreLib.encode("", "", "");
+ assertEq(blob.length, 0, "Empty mime + no protocol should be empty blob");
+ }
+
+ function test_EncodeTextPlainNoProtocol() public {
+ bytes memory blob = MetaStoreLib.encode("text/plain", "", "");
+ assertEq(blob.length, 0, "text/plain + no protocol should be empty blob");
+ }
+
+ function test_EncodeMimeOnly() public {
+ bytes memory blob = MetaStoreLib.encode("image/png", "", "");
+ string memory expected = string(abi.encodePacked(
+ "image/png",
+ bytes1(0x00),
+ "", // empty protocol
+ bytes1(0x00),
+ "" // empty operation
+ ));
+ assertEq(string(blob), expected, "Should contain mimetype with empty protocol/operation");
+ }
+
+ function test_EncodeMimeAndProtocol() public {
+ bytes memory blob = MetaStoreLib.encode("application/json", "tokens", "");
+ string memory expected = string(abi.encodePacked(
+ "application/json",
+ bytes1(0x00),
+ "tokens",
+ bytes1(0x00),
+ "" // empty operation
+ ));
+ assertEq(string(blob), expected, "Should contain mime + protocol with empty operation");
+ }
+
+ function test_EncodeFullMetadata() public {
+ bytes memory blob = MetaStoreLib.encode("application/json", "tokens", "mint");
+ string memory expected = string(abi.encodePacked(
+ "application/json",
+ bytes1(0x00),
+ "tokens",
+ bytes1(0x00),
+ "mint"
+ ));
+ assertEq(string(blob), expected, "Should contain all three components");
+ }
+
+ function test_EncodeTextPlainWithProtocol() public {
+ // text/plain is stored as empty string (convention)
+ bytes memory blob = MetaStoreLib.encode("text/plain", "tokens", "mint");
+ string memory expected = string(abi.encodePacked(
+ // Empty mimetype (text/plain is normalized to empty)
+ bytes1(0x00),
+ "tokens",
+ bytes1(0x00),
+ "mint"
+ ));
+ assertEq(string(blob), expected, "text/plain should be stored as empty string");
+ }
+
+ function test_EncodeDoesNotNormalizeMimetype() public {
+ bytes memory blob = MetaStoreLib.encode("Image/PNG", "", "");
+ string memory expected = string(abi.encodePacked(
+ "Image/PNG",
+ bytes1(0x00),
+ "",
+ bytes1(0x00),
+ ""
+ ));
+ assertEq(string(blob), expected, "Should preserve mimetype case");
+ }
+
+ function test_EncodeRejectsInvalidSeparator() public pure {
+ // Test that we reject separator in mimetype
+ bytes memory blob = MetaStoreLib.encode("text/plain", "", "");
+ // If we get here without reverting, the input was valid (as expected)
+ assertTrue(blob.length == 0, "Should encode valid input");
+
+ // Note: Testing for revert with separator character requires vm.expectRevert
+ // which doesn't work well with nested pure function calls
+ }
+
+ // =============================================================
+ // INTERNING TESTS
+ // =============================================================
+
+ function test_InternEmptyBlob() public {
+ bytes memory blob = bytes("");
+ bytes32 ref = MetaStoreLib.intern(blob, metaStore);
+ assertEq(ref, bytes32(0), "Empty blob should return zero sentinel");
+ }
+
+ function test_InternSmallBlob() public {
+ bytes memory blob = abi.encodePacked("image/png");
+ bytes32 ref = MetaStoreLib.intern(blob, metaStore);
+
+ // Should be packed (first byte > 0 and <= 32)
+ assertTrue(uint8(uint256(ref >> 248)) > 0 && uint8(uint256(ref >> 248)) <= 32, "Should be packed");
+ }
+
+ function test_InternLargeBlob() public {
+ // Create a 50-byte blob
+ bytes memory blob = new bytes(50);
+ for (uint i = 0; i < 50; i++) {
+ blob[i] = bytes1(uint8(97 + (i % 26))); // a-z
+ }
+
+ bytes32 ref = MetaStoreLib.intern(blob, metaStore);
+
+ // Should be an SSTORE2 pointer (not packed)
+ assertFalse(uint8(uint256(ref >> 248)) > 0 && uint8(uint256(ref >> 248)) <= 32, "Should not be packed");
+ }
+
+ function test_InternDeduplicates() public {
+ bytes memory blob = abi.encodePacked("image/png");
+
+ bytes32 ref1 = MetaStoreLib.intern(blob, metaStore);
+ bytes32 ref2 = MetaStoreLib.intern(blob, metaStore);
+
+ assertEq(ref1, ref2, "Should return same reference for identical blobs");
+ }
+
+ // =============================================================
+ // DECODING TESTS
+ // =============================================================
+
+ function test_DecodeEmptySentinel() public view {
+ (string memory mime, string memory protocol, string memory op) =
+ MetaStoreLib.decode(bytes32(0));
+
+ assertEq(mime, "text/plain", "Should default to text/plain");
+ assertEq(protocol, "", "Should have no protocol");
+ assertEq(op, "", "Should have no operation");
+ }
+
+ function test_DecodeMimeOnly() public {
+ // Properly formatted blob with 2 separators
+ bytes memory blob = abi.encodePacked("image/png", bytes1(0x00), bytes1(0x00));
+ bytes32 ref = MetaStoreLib.intern(blob, metaStore);
+
+ (string memory mime, string memory protocol, string memory op) =
+ MetaStoreLib.decode(ref);
+
+ assertEq(mime, "image/png", "Should decode mimetype");
+ assertEq(protocol, "", "Should have no protocol");
+ assertEq(op, "", "Should have no operation");
+ }
+
+ function test_DecodeMimeAndProtocol() public {
+ bytes memory blob = abi.encodePacked(
+ "application/json",
+ bytes1(0x00),
+ "tokens",
+ bytes1(0x00) // empty operation
+ );
+ bytes32 ref = MetaStoreLib.intern(blob, metaStore);
+
+ (string memory mime, string memory protocol, string memory op) =
+ MetaStoreLib.decode(ref);
+
+ assertEq(mime, "application/json", "Should decode mimetype");
+ assertEq(protocol, "tokens", "Should decode protocol");
+ assertEq(op, "", "Should have no operation");
+ }
+
+ function test_DecodeFullMetadata() public {
+ bytes memory blob = abi.encodePacked(
+ "application/json",
+ bytes1(0x00),
+ "tokens",
+ bytes1(0x00),
+ "mint"
+ );
+ bytes32 ref = MetaStoreLib.intern(blob, metaStore);
+
+ (string memory mime, string memory protocol, string memory op) =
+ MetaStoreLib.decode(ref);
+
+ assertEq(mime, "application/json", "Should decode mimetype");
+ assertEq(protocol, "tokens", "Should decode protocol");
+ assertEq(op, "mint", "Should decode operation");
+ }
+
+ function test_DecodeEmptyMimeDefaultsToTextPlain() public {
+ // Manually create blob with empty mimetype (0x00tokens0x00mint)
+ bytes memory blob = abi.encodePacked(
+ bytes1(0x00),
+ "tokens",
+ bytes1(0x00),
+ "mint"
+ );
+ bytes32 ref = MetaStoreLib.intern(blob, metaStore);
+
+ (string memory mime, string memory protocol, string memory op) =
+ MetaStoreLib.decode(ref);
+
+ assertEq(mime, "text/plain", "Empty mime should default to text/plain");
+ assertEq(protocol, "tokens", "Should decode protocol");
+ assertEq(op, "mint", "Should decode operation");
+ }
+
+ function test_RoundTripTextPlainWithProtocol() public {
+ // Encoding "text/plain" should store as empty and decode back to "text/plain"
+ bytes memory blob = MetaStoreLib.encode("text/plain", "tokens", "mint");
+ bytes32 ref = MetaStoreLib.intern(blob, metaStore);
+
+ (string memory mime, string memory protocol, string memory op) =
+ MetaStoreLib.decode(ref);
+
+ assertEq(mime, "text/plain", "Should decode to text/plain");
+ assertEq(protocol, "tokens", "Should decode protocol");
+ assertEq(op, "mint", "Should decode operation");
+ }
+
+ // =============================================================
+ // GET MIMETYPE TESTS
+ // =============================================================
+
+ function test_GetMimetypeFromEmpty() public view {
+ string memory mime = MetaStoreLib.getMimetype(bytes32(0));
+ assertEq(mime, "text/plain", "Should return text/plain for empty ref");
+ }
+
+ function test_GetMimetypeFromMimeOnly() public {
+ bytes memory blob = abi.encodePacked("image/png", bytes1(0x00), bytes1(0x00));
+ bytes32 ref = MetaStoreLib.intern(blob, metaStore);
+
+ string memory mime = MetaStoreLib.getMimetype(ref);
+ assertEq(mime, "image/png", "Should extract mimetype");
+ }
+
+ function test_GetMimetypeFromFull() public {
+ bytes memory blob = abi.encodePacked(
+ "application/json",
+ bytes1(0x00),
+ "tokens",
+ bytes1(0x00),
+ "mint"
+ );
+ bytes32 ref = MetaStoreLib.intern(blob, metaStore);
+
+ string memory mime = MetaStoreLib.getMimetype(ref);
+ assertEq(mime, "application/json", "Should extract mimetype before separator");
+ }
+
+ // =============================================================
+ // GET PROTOCOL TESTS
+ // =============================================================
+
+ function test_GetProtocolFromEmpty() public view {
+ (string memory protocol, string memory op) = MetaStoreLib.getProtocol(bytes32(0));
+ assertEq(protocol, "", "Should have no protocol");
+ assertEq(op, "", "Should have no operation");
+ }
+
+ function test_GetProtocolFromMimeOnly() public {
+ bytes memory blob = abi.encodePacked("image/png", bytes1(0x00), bytes1(0x00));
+ bytes32 ref = MetaStoreLib.intern(blob, metaStore);
+
+ (string memory protocol, string memory op) = MetaStoreLib.getProtocol(ref);
+ assertEq(protocol, "", "Should have no protocol");
+ assertEq(op, "", "Should have no operation");
+ }
+
+ function test_GetProtocolFromMimeAndProtocol() public {
+ bytes memory blob = abi.encodePacked(
+ "application/json",
+ bytes1(0x00),
+ "tokens",
+ bytes1(0x00) // empty operation
+ );
+ bytes32 ref = MetaStoreLib.intern(blob, metaStore);
+
+ (string memory protocol, string memory op) = MetaStoreLib.getProtocol(ref);
+ assertEq(protocol, "tokens", "Should extract protocol");
+ assertEq(op, "", "Should have no operation");
+ }
+
+ function test_GetProtocolFromFull() public {
+ bytes memory blob = abi.encodePacked(
+ "application/json",
+ bytes1(0x00),
+ "tokens",
+ bytes1(0x00),
+ "mint"
+ );
+ bytes32 ref = MetaStoreLib.intern(blob, metaStore);
+
+ (string memory protocol, string memory op) = MetaStoreLib.getProtocol(ref);
+ assertEq(protocol, "tokens", "Should extract protocol");
+ assertEq(op, "mint", "Should extract operation");
+ }
+
+
+ // =============================================================
+ // ROUND-TRIP TESTS
+ // =============================================================
+
+ function test_RoundTripMimeOnly() public {
+ bytes memory original = MetaStoreLib.encode("image/svg+xml", "", "");
+ bytes32 ref = MetaStoreLib.intern(original, metaStore);
+ (string memory mime, string memory protocol, string memory op) =
+ MetaStoreLib.decode(ref);
+
+ assertEq(mime, "image/svg+xml", "Should preserve mimetype");
+ assertEq(protocol, "", "Should have no protocol");
+ assertEq(op, "", "Should have no operation");
+ }
+
+ function test_RoundTripFull() public {
+ bytes memory original = MetaStoreLib.encode("application/json", "erc-721-collection", "create");
+ bytes32 ref = MetaStoreLib.intern(original, metaStore);
+ (string memory mime, string memory protocol, string memory op) =
+ MetaStoreLib.decode(ref);
+
+ assertEq(mime, "application/json", "Should preserve mimetype");
+ assertEq(protocol, "erc-721-collection", "Should preserve normalized protocol");
+ assertEq(op, "create", "Should preserve normalized operation");
+ }
+
+ function test_RoundTripLongBlob() public {
+ // Create a blob that will require SSTORE2
+ string memory longMime = "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
+ bytes memory original = MetaStoreLib.encode(longMime, "very-long-protocol-name", "very-long-operation-name");
+ bytes32 ref = MetaStoreLib.intern(original, metaStore);
+ (string memory mime, string memory protocol, string memory op) =
+ MetaStoreLib.decode(ref);
+
+ assertEq(mime, longMime, "Should preserve long mimetype");
+ assertEq(protocol, "very-long-protocol-name", "Should preserve long protocol");
+ assertEq(op, "very-long-operation-name", "Should preserve long operation");
+ }
+}
diff --git a/contracts/test/Multicall3.t.sol b/contracts/test/Multicall3.t.sol
index afd2686..7de469e 100644
--- a/contracts/test/Multicall3.t.sol
+++ b/contracts/test/Multicall3.t.sol
@@ -51,7 +51,7 @@ contract Multicall3Test is TestSetup {
vm.prank(creator);
Ethscriptions.CreateEthscriptionParams memory params = Ethscriptions.CreateEthscriptionParams({
ethscriptionId: txHash,
- contentUriHash: keccak256(abi.encodePacked("data:text/plain,Test", i)),
+ contentUriSha: keccak256(abi.encodePacked("data:text/plain,Test", i)),
initialOwner: initialOwner,
content: bytes(string(abi.encodePacked("Test content ", Strings.toString(i)))),
mimetype: "text/plain",
@@ -113,7 +113,7 @@ contract Multicall3Test is TestSetup {
vm.prank(creator);
Ethscriptions.CreateEthscriptionParams memory params = Ethscriptions.CreateEthscriptionParams({
ethscriptionId: txHash,
- contentUriHash: keccak256("data:text/plain,Mixed test"),
+ contentUriSha: keccak256("data:text/plain,Mixed test"),
initialOwner: initialOwner,
content: bytes("Mixed test content"),
mimetype: "text/plain",
diff --git a/contracts/test/PaginationGas.t.sol b/contracts/test/PaginationGas.t.sol
index 3f3f8b7..fa9f2c4 100644
--- a/contracts/test/PaginationGas.t.sol
+++ b/contracts/test/PaginationGas.t.sol
@@ -41,7 +41,7 @@ contract PaginationGasTest is TestSetup {
vm.prank(creator);
Ethscriptions.CreateEthscriptionParams memory params = Ethscriptions.CreateEthscriptionParams({
ethscriptionId: txHash,
- contentUriHash: keccak256(abi.encodePacked("uri", i)),
+ contentUriSha: keccak256(abi.encodePacked("uri", i)),
initialOwner: owner,
content: content,
mimetype: "application/octet-stream",
diff --git a/contracts/test/PaginationGas1000.t.sol b/contracts/test/PaginationGas1000.t.sol
index 40b15e1..bd6b14c 100644
--- a/contracts/test/PaginationGas1000.t.sol
+++ b/contracts/test/PaginationGas1000.t.sol
@@ -27,7 +27,7 @@ contract PaginationGas1000Test is TestSetup {
vm.prank(creator);
Ethscriptions.CreateEthscriptionParams memory params = Ethscriptions.CreateEthscriptionParams({
ethscriptionId: txHash,
- contentUriHash: keccak256(abi.encodePacked("uri", i)),
+ contentUriSha: keccak256(abi.encodePacked("uri", i)),
initialOwner: owner,
content: content,
mimetype: "text/plain",
diff --git a/contracts/test/PaginationGas1000Simple.t.sol b/contracts/test/PaginationGas1000Simple.t.sol
index 915f65a..f265512 100644
--- a/contracts/test/PaginationGas1000Simple.t.sol
+++ b/contracts/test/PaginationGas1000Simple.t.sol
@@ -34,7 +34,7 @@ contract PaginationGas1000SimpleTest is TestSetup {
vm.prank(creator);
Ethscriptions.CreateEthscriptionParams memory params = Ethscriptions.CreateEthscriptionParams({
ethscriptionId: txHash,
- contentUriHash: keccak256(abi.encodePacked("uri", startId + i)),
+ contentUriSha: keccak256(abi.encodePacked("uri", startId + i)),
initialOwner: owner,
content: content,
mimetype: "text/plain",
diff --git a/contracts/test/ProtocolRegistration.t.sol b/contracts/test/ProtocolRegistration.t.sol
index a2b0880..ac6b91a 100644
--- a/contracts/test/ProtocolRegistration.t.sol
+++ b/contracts/test/ProtocolRegistration.t.sol
@@ -113,7 +113,7 @@ contract ProtocolRegistrationTest is TestSetup {
Ethscriptions.CreateEthscriptionParams memory params = Ethscriptions.CreateEthscriptionParams({
ethscriptionId: txHash,
- contentUriHash: sha256(bytes('data:,{"p":"mock-protocol","op":"test"}')),
+ contentUriSha: sha256(bytes('data:,{"p":"mock-protocol","op":"test"}')),
initialOwner: alice,
content: bytes('{"p":"mock-protocol","op":"test"}'),
mimetype: "application/json",
@@ -157,7 +157,7 @@ contract ProtocolRegistrationTest is TestSetup {
// Create ethscription with unregistered protocol
Ethscriptions.CreateEthscriptionParams memory params = Ethscriptions.CreateEthscriptionParams({
ethscriptionId: txHash,
- contentUriHash: sha256(bytes('data:,{"p":"unregistered","op":"test"}')),
+ contentUriSha: sha256(bytes('data:,{"p":"unregistered","op":"test"}')),
initialOwner: alice,
content: bytes('{"p":"unregistered","op":"test"}'),
mimetype: "application/json",
diff --git a/contracts/test/TestImageSVGWrapper.t.sol b/contracts/test/TestImageSVGWrapper.t.sol
index 00b2e10..e3a351d 100644
--- a/contracts/test/TestImageSVGWrapper.t.sol
+++ b/contracts/test/TestImageSVGWrapper.t.sol
@@ -18,7 +18,7 @@ contract TestImageSVGWrapper is TestSetup {
vm.prank(alice);
ethscriptions.createEthscription(Ethscriptions.CreateEthscriptionParams({
ethscriptionId: txHash,
- contentUriHash: sha256(bytes(pngDataUri)),
+ contentUriSha: sha256(bytes(pngDataUri)),
initialOwner: alice,
content: pngContent,
mimetype: "image/png",
diff --git a/contracts/test/TestSetup.sol b/contracts/test/TestSetup.sol
index b9add35..b5dcd38 100644
--- a/contracts/test/TestSetup.sol
+++ b/contracts/test/TestSetup.sol
@@ -48,7 +48,7 @@ abstract contract TestSetup is Test {
) internal pure returns (Ethscriptions.CreateEthscriptionParams memory) {
// Parse the data URI to extract needed info
bytes memory contentUriBytes = bytes(dataUri);
- bytes32 contentUriHash = sha256(contentUriBytes); // Use SHA-256 to match production
+ bytes32 contentUriSha = sha256(contentUriBytes); // Use SHA-256 to match production
// Simple parsing for tests
bytes memory content;
@@ -116,7 +116,7 @@ abstract contract TestSetup is Test {
return Ethscriptions.CreateEthscriptionParams({
ethscriptionId: transactionHash,
- contentUriHash: contentUriHash,
+ contentUriSha: contentUriSha,
initialOwner: initialOwner,
content: content,
mimetype: mimetype,
diff --git a/lib/block_validator.rb b/lib/block_validator.rb
index 7678d98..69eb05e 100644
--- a/lib/block_validator.rb
+++ b/lib/block_validator.rb
@@ -423,31 +423,31 @@ def verify_ethscription_storage(creation, l1_block_num, block_tag)
end
end
- # Verify content_uri_hash - this is the hash of the original content URI
- stored_uri_hash = stored[:content_uri_hash]&.downcase&.delete_prefix('0x')
+ # Verify content_uri_sha - this is the hash of the original content URI
+ stored_uri_hash = stored[:content_uri_sha]&.downcase&.delete_prefix('0x')
if creation[:content_uri]
# Hash the content_uri from the API to compare
expected_uri_hash = Digest::SHA256.hexdigest(creation[:content_uri]).downcase
if stored_uri_hash != expected_uri_hash
- @errors << "Storage content_uri_hash mismatch for #{tx_hash}: stored=#{stored_uri_hash}, expected=#{expected_uri_hash}"
+ @errors << "Storage content_uri_sha mismatch for #{tx_hash}: stored=#{stored_uri_hash}, expected=#{expected_uri_hash}"
end
end
- # Verify content_sha - always present in API, must match exactly (with instrumentation)
- stored_sha = stored[:content_sha]&.downcase&.delete_prefix('0x')
- expected_sha = creation[:content_sha].downcase.delete_prefix('0x')
- content_sha_match = stored_sha == expected_sha
- content_sha_check = record_comparison(
- "storage_content_sha_check",
+ # Verify content_hash - always present in API, must match exactly (with instrumentation)
+ stored_sha = stored[:content_hash]&.downcase&.delete_prefix('0x')
+ expected_sha = creation[:content_hash].downcase.delete_prefix('0x')
+ content_hash_match = stored_sha == expected_sha
+ content_hash_check = record_comparison(
+ "storage_content_hash_check",
tx_hash,
expected_sha,
stored_sha,
- content_sha_match,
+ content_hash_match,
{ l1_block: l1_block_num }
)
- if !content_sha_match
- @errors << "Storage content_sha mismatch for #{tx_hash}: stored=#{stored[:content_sha]}, expected=#{creation[:content_sha]}"
+ if !content_hash_match
+ @errors << "Storage content_hash mismatch for #{tx_hash}: stored=#{stored[:content_hash]}, expected=#{creation[:content_hash]}"
end
# Verify mimetype - normalize to binary for comparison (with instrumentation)
diff --git a/lib/event_decoder.rb b/lib/event_decoder.rb
index 87af82e..8b5cc4d 100644
--- a/lib/event_decoder.rb
+++ b/lib/event_decoder.rb
@@ -91,16 +91,16 @@ def decode_creation(log)
return nil if data_bytes.length < 96 # Need at least 3 * 32 bytes
- content_uri_hash = '0x' + data_bytes[0, 32].unpack1('H*')
- content_sha = '0x' + data_bytes[32, 32].unpack1('H*')
+ content_uri_sha = '0x' + data_bytes[0, 32].unpack1('H*')
+ content_hash = '0x' + data_bytes[32, 32].unpack1('H*')
ethscription_number = data_bytes[64, 32].unpack1('H*').to_i(16)
{
tx_hash: tx_hash,
creator: creator,
initial_owner: initial_owner,
- content_uri_hash: content_uri_hash,
- content_sha: content_sha,
+ content_uri_sha: content_uri_sha,
+ content_hash: content_hash,
ethscription_number: ethscription_number
}
rescue => e
diff --git a/lib/storage_reader.rb b/lib/storage_reader.rb
index b4147eb..cbe6c73 100644
--- a/lib/storage_reader.rb
+++ b/lib/storage_reader.rb
@@ -120,10 +120,10 @@ def get_ethscription_with_content(tx_hash, block_tag: 'latest')
raise StandardError, "RPC call failed for ethscription #{tx_hash}" if result.nil?
# Decode the single Ethscription struct with all fields including content
- # New struct order: ethscriptionId, ethscriptionNumber, contentUriHash, contentSha, mimetype, content,
- # currentOwner, creator, initialOwner, previousOwner, l1BlockHash,
- # l1BlockNumber, l2BlockNumber, createdAt, esip6
- types = ['(bytes32,uint256,bytes32,bytes32,string,bytes,address,address,address,address,bytes32,uint256,uint256,uint256,bool)']
+ # Struct order: ethscriptionId, ethscriptionNumber, contentUriSha, contentHash, mimetype, content,
+ # currentOwner, creator, initialOwner, previousOwner, l1BlockHash,
+ # l1BlockNumber, l2BlockNumber, createdAt, esip6, protocolName, operation
+ types = ['(bytes32,uint256,bytes32,bytes32,string,bytes,address,address,address,address,bytes32,uint256,uint256,uint256,bool,string,string)']
decoded = Eth::Abi.decode(types, result)
# The struct is returned as an array
@@ -135,8 +135,8 @@ def get_ethscription_with_content(tx_hash, block_tag: 'latest')
ethscription_number: ethscription_data[1],
# Content fields
- content_uri_hash: '0x' + ethscription_data[2].unpack1('H*'),
- content_sha: '0x' + ethscription_data[3].unpack1('H*'),
+ content_uri_sha: '0x' + ethscription_data[2].unpack1('H*'),
+ content_hash: '0x' + ethscription_data[3].unpack1('H*'),
mimetype: ethscription_data[4],
content: ethscription_data[5], # content is now at index 5
@@ -153,7 +153,9 @@ def get_ethscription_with_content(tx_hash, block_tag: 'latest')
created_at: ethscription_data[13],
# Protocol
- esip6: ethscription_data[14]
+ esip6: ethscription_data[14],
+ protocol_name: ethscription_data[15],
+ operation: ethscription_data[16]
}
rescue => e
Rails.logger.error "Failed to get ethscription with content #{tx_hash}: #{e.message}"
@@ -180,10 +182,10 @@ def get_ethscription(tx_hash, block_tag: 'latest')
raise StandardError, "RPC call failed for ethscription #{tx_hash}" if result.nil?
# Decode the Ethscription struct without content
- # Struct order: ethscriptionId, ethscriptionNumber, contentUriHash, contentSha, mimetype, content (empty),
+ # Struct order: ethscriptionId, ethscriptionNumber, contentUriSha, contentHash, mimetype, content (empty),
# currentOwner, creator, initialOwner, previousOwner, l1BlockHash,
- # l1BlockNumber, l2BlockNumber, createdAt, esip6
- types = ['(bytes32,uint256,bytes32,bytes32,string,bytes,address,address,address,address,bytes32,uint256,uint256,uint256,bool)']
+ # l1BlockNumber, l2BlockNumber, createdAt, esip6, protocolName, operation
+ types = ['(bytes32,uint256,bytes32,bytes32,string,bytes,address,address,address,address,bytes32,uint256,uint256,uint256,bool,string,string)']
decoded = Eth::Abi.decode(types, result)
# The struct is returned as an array
@@ -195,8 +197,8 @@ def get_ethscription(tx_hash, block_tag: 'latest')
ethscription_number: ethscription_data[1],
# Content fields (no actual content bytes)
- content_uri_hash: '0x' + ethscription_data[2].unpack1('H*'),
- content_sha: '0x' + ethscription_data[3].unpack1('H*'),
+ content_uri_sha: '0x' + ethscription_data[2].unpack1('H*'),
+ content_hash: '0x' + ethscription_data[3].unpack1('H*'),
mimetype: ethscription_data[4],
# Skip content at index 5 (it's empty for WithoutContent)
@@ -213,7 +215,9 @@ def get_ethscription(tx_hash, block_tag: 'latest')
created_at: ethscription_data[13],
# Protocol
- esip6: ethscription_data[14]
+ esip6: ethscription_data[14],
+ protocol_name: ethscription_data[15],
+ operation: ethscription_data[16]
}
rescue EthRpcClient::ExecutionRevertedError => e
# Contract reverted - ethscription doesn't exist
diff --git a/spec/integration/ethscriptions_creation_spec.rb b/spec/integration/ethscriptions_creation_spec.rb
index 08030a6..6bb9ea4 100644
--- a/spec/integration/ethscriptions_creation_spec.rb
+++ b/spec/integration/ethscriptions_creation_spec.rb
@@ -400,7 +400,7 @@
first_stored = get_ethscription_content(first_result[:ethscription_ids].first)
second_stored = get_ethscription_content(second_result[:ethscription_ids].first)
- expect(first_stored[:content_uri_hash]).to eq(second_stored[:content_uri_hash])
+ expect(first_stored[:content_uri_sha]).to eq(second_stored[:content_uri_sha])
expect(first_stored[:esip6]).to be_truthy
expect(second_stored[:esip6]).to be_truthy
@@ -533,7 +533,7 @@
# Verify content URI hash
expected_hash = Digest::SHA256.hexdigest(content_uri)
- expect(stored[:content_uri_hash]).to eq("0x#{expected_hash}")
+ expect(stored[:content_uri_sha]).to eq("0x#{expected_hash}")
# Verify block references are set
expect(stored[:l1_block_number]).to be > 0
@@ -554,7 +554,7 @@
# Verify content URI hash matches
expected_hash = Digest::SHA256.hexdigest(content_uri)
- expect(stored[:content_uri_hash]).to eq("0x#{expected_hash}")
+ expect(stored[:content_uri_sha]).to eq("0x#{expected_hash}")
end
end
end