Skip to content

Commit 772146b

Browse files
committed
Add URI resolution for Ethscriptions in ERC721 Collection
- Introduced `_resolveEthscriptionURI` function to handle `esc://ethscriptions/{id}/data` references, resolving them to their corresponding media URIs. - Updated `contractURI` method to utilize resolved URIs for logo and banner images, enhancing metadata representation. - Added comprehensive tests for URI resolution, including validation for regular HTTP URIs, data URIs, and handling of malformed or non-existent ethscription references.
1 parent f7f0997 commit 772146b

File tree

2 files changed

+245
-2
lines changed

2 files changed

+245
-2
lines changed

contracts/src/ERC721EthscriptionsCollection.sol

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import "./Ethscriptions.sol";
66
import "./libraries/Predeploys.sol";
77
import {LibString} from "solady/utils/LibString.sol";
88
import {Base64} from "solady/utils/Base64.sol";
9+
import {JSONParserLib} from "solady/utils/JSONParserLib.sol";
910
import "./ERC721EthscriptionsCollectionManager.sol";
1011
import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
1112
import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
@@ -147,12 +148,16 @@ contract ERC721EthscriptionsCollection is ERC721EthscriptionsEnumerableUpgradeab
147148
ERC721EthscriptionsCollectionManager.CollectionMetadata memory metadata =
148149
manager.getCollectionByAddress(address(this));
149150

151+
// Resolve URIs (handles esc://ethscriptions/{id}/data references)
152+
string memory image = _resolveEthscriptionURI(metadata.logoImageUri);
153+
string memory bannerImage = _resolveEthscriptionURI(metadata.bannerImageUri);
154+
150155
// Build JSON with OpenSea fields
151156
string memory json = string.concat(
152157
'{"name":"', metadata.name.escapeJSON(),
153158
'","description":"', metadata.description.escapeJSON(),
154-
'","image":"', metadata.logoImageUri.escapeJSON(),
155-
'","banner_image":"', metadata.bannerImageUri.escapeJSON(),
159+
'","image":"', image.escapeJSON(),
160+
'","banner_image":"', bannerImage.escapeJSON(),
156161
'","external_link":"', metadata.websiteLink.escapeJSON(),
157162
'"}'
158163
);
@@ -183,4 +188,54 @@ contract ERC721EthscriptionsCollection is ERC721EthscriptionsEnumerableUpgradeab
183188
{
184189
revert TransferNotAllowed();
185190
}
191+
192+
// -------------------- URI Resolution Helpers --------------------
193+
194+
/// @notice Resolve URI, handling esc://ethscriptions/{id}/data format
195+
/// @dev Returns empty string if esc:// reference not found (doesn't revert)
196+
/// @param uri The URI to resolve (can be regular URI, data URI, or esc:// reference)
197+
/// @return The resolved URI (or empty string if esc:// reference not found)
198+
function _resolveEthscriptionURI(string memory uri) private view returns (string memory) {
199+
// Check if it's an ethscription reference
200+
if (!uri.startsWith("esc://ethscriptions/")) {
201+
return uri; // Regular URI or data URI, pass through
202+
}
203+
204+
// Format: esc://ethscriptions/0x{64 hex chars}/data
205+
// Split by "/" to extract parts: ["esc:", "", "ethscriptions", "0x{id}", "data"]
206+
string[] memory parts = uri.split("/");
207+
208+
if (parts.length != 5 || !parts[4].eq("data")) {
209+
return ""; // Invalid format
210+
}
211+
212+
// The ID should be at index 3 (after esc: / / ethscriptions /)
213+
string memory hexId = parts[3];
214+
215+
// Validate hex ID format before parsing
216+
if (bytes(hexId).length != 66) {
217+
return ""; // Must be 0x + 64 hex chars
218+
}
219+
220+
// Parse hex string to bytes32 using JSONParserLib (reverts on invalid)
221+
bytes32 ethscriptionId;
222+
try this._parseHexToBytes32(hexId) returns (bytes32 parsed) {
223+
ethscriptionId = parsed;
224+
} catch {
225+
return ""; // Invalid hex format
226+
}
227+
228+
// Try to get the ethscription's media URI
229+
try ethscriptions.getMediaUri(ethscriptionId) returns (string memory, string memory mediaUri) {
230+
return mediaUri; // Return the data URI from the referenced ethscription
231+
} catch {
232+
return ""; // Ethscription doesn't exist, return empty (don't revert)
233+
}
234+
}
235+
236+
/// @notice Parse hex string to bytes32 (external for try/catch)
237+
/// @dev Must be external to allow try/catch usage
238+
function _parseHexToBytes32(string calldata hexStr) external pure returns (bytes32) {
239+
return bytes32(JSONParserLib.parseUintFromHex(hexStr));
240+
}
186241
}
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.24;
3+
4+
import "./TestSetup.sol";
5+
import {LibString} from "solady/utils/LibString.sol";
6+
7+
contract CollectionURIResolutionTest is TestSetup {
8+
using LibString for *;
9+
bytes32 constant COLLECTION_TX_HASH = keccak256("collection_uri_test");
10+
bytes32 constant IMAGE_ETSC_TX_HASH = keccak256("image_ethscription");
11+
12+
address alice = makeAddr("alice");
13+
14+
function setUp() public override {
15+
super.setUp();
16+
}
17+
18+
function test_RegularHTTPURIPassesThrough() public {
19+
// Create collection with regular HTTP URI
20+
string memory regularUri = "https://example.com/logo.png";
21+
22+
bytes32 collectionId = _createCollectionWithLogo(regularUri);
23+
24+
// Get collection metadata
25+
ERC721EthscriptionsCollectionManager.CollectionMetadata memory metadata =
26+
collectionsHandler.getCollection(collectionId);
27+
28+
assertEq(metadata.logoImageUri, regularUri, "Should preserve regular URI");
29+
30+
// contractURI should also pass it through
31+
address collectionAddr = metadata.collectionContract;
32+
ERC721EthscriptionsCollection collection = ERC721EthscriptionsCollection(collectionAddr);
33+
string memory contractUri = collection.contractURI();
34+
35+
assertTrue(bytes(contractUri).length > 0, "Should have contractURI");
36+
assertTrue(contractUri.contains(regularUri), "Should contain original URI");
37+
}
38+
39+
function test_DataURIPassesThrough() public {
40+
// Create collection with data URI
41+
string memory dataUri = "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAiIGhlaWdodD0iMTAiPjwvc3ZnPg==";
42+
43+
bytes32 collectionId = _createCollectionWithLogo(dataUri);
44+
45+
ERC721EthscriptionsCollectionManager.CollectionMetadata memory metadata =
46+
collectionsHandler.getCollection(collectionId);
47+
48+
assertEq(metadata.logoImageUri, dataUri, "Should preserve data URI");
49+
}
50+
51+
function test_EthscriptionReferenceResolvesToMediaURI() public {
52+
// First create an ethscription with image content
53+
string memory imageContent = "data:image/png;base64,iVBORw0KGgo=";
54+
55+
Ethscriptions.CreateEthscriptionParams memory imageParams = Ethscriptions.CreateEthscriptionParams({
56+
ethscriptionId: IMAGE_ETSC_TX_HASH,
57+
contentUriSha: sha256(bytes(imageContent)),
58+
initialOwner: alice,
59+
content: bytes(imageContent),
60+
mimetype: "image/png",
61+
esip6: false,
62+
protocolParams: Ethscriptions.ProtocolParams({
63+
protocolName: "",
64+
operation: "",
65+
data: ""
66+
})
67+
});
68+
69+
vm.prank(alice);
70+
ethscriptions.createEthscription(imageParams);
71+
72+
// Create collection with esc:// reference to the image
73+
string memory escUri = string.concat(
74+
"esc://ethscriptions/",
75+
uint256(IMAGE_ETSC_TX_HASH).toHexString(),
76+
"/data"
77+
);
78+
79+
bytes32 collectionId = _createCollectionWithLogo(escUri);
80+
81+
// Get collection and check contractURI resolves the reference
82+
ERC721EthscriptionsCollectionManager.CollectionMetadata memory metadata =
83+
collectionsHandler.getCollection(collectionId);
84+
85+
// Stored value should be the esc:// URI
86+
assertEq(metadata.logoImageUri, escUri, "Should store esc:// URI");
87+
88+
// contractURI should resolve it to the media URI
89+
address collectionAddr = metadata.collectionContract;
90+
ERC721EthscriptionsCollection collection = ERC721EthscriptionsCollection(collectionAddr);
91+
string memory contractUri = collection.contractURI();
92+
93+
// Should contain a data URI (resolved from the referenced ethscription)
94+
assertTrue(contractUri.contains("data:"), "Should contain resolved data URI");
95+
}
96+
97+
function test_InvalidEthscriptionReferenceReturnsEmpty() public {
98+
// Reference to non-existent ethscription
99+
bytes32 fakeId = keccak256("nonexistent");
100+
string memory escUri = string.concat(
101+
"esc://ethscriptions/",
102+
uint256(fakeId).toHexString(),
103+
"/data"
104+
);
105+
106+
bytes32 collectionId = _createCollectionWithLogo(escUri);
107+
108+
ERC721EthscriptionsCollectionManager.CollectionMetadata memory metadata =
109+
collectionsHandler.getCollection(collectionId);
110+
address collectionAddr = metadata.collectionContract;
111+
ERC721EthscriptionsCollection collection = ERC721EthscriptionsCollection(collectionAddr);
112+
113+
// Should not revert, just return empty/placeholder
114+
string memory contractUri = collection.contractURI();
115+
assertTrue(bytes(contractUri).length > 0, "Should return contractURI without reverting");
116+
}
117+
118+
function test_MalformedEscURIReturnsEmpty() public {
119+
// Various malformed esc:// URIs
120+
string[] memory badUris = new string[](4);
121+
badUris[0] = "esc://ethscriptions/notahexid/data";
122+
badUris[1] = "esc://ethscriptions/0x123/data"; // Too short
123+
badUris[2] = "esc://ethscriptions/"; // Incomplete
124+
badUris[3] = "esc://wrong/0x1234567890123456789012345678901234567890123456789012345678901234/data";
125+
126+
for (uint i = 0; i < badUris.length; i++) {
127+
// Use unique collection ID for each iteration
128+
bytes32 uniqueCollectionId = keccak256(abi.encodePacked("malformed_test", i));
129+
bytes32 collectionId = _createCollectionWithLogoAndId(badUris[i], uniqueCollectionId);
130+
131+
ERC721EthscriptionsCollectionManager.CollectionMetadata memory metadata =
132+
collectionsHandler.getCollection(collectionId);
133+
address collectionAddr = metadata.collectionContract;
134+
ERC721EthscriptionsCollection collection = ERC721EthscriptionsCollection(collectionAddr);
135+
136+
// Should not revert
137+
string memory contractUri = collection.contractURI();
138+
assertTrue(bytes(contractUri).length > 0, "Should return contractURI without reverting");
139+
}
140+
}
141+
142+
// -------------------- Helpers --------------------
143+
144+
function _createCollectionWithLogo(string memory logoUri) private returns (bytes32) {
145+
return _createCollectionWithLogoAndId(logoUri, COLLECTION_TX_HASH);
146+
}
147+
148+
function _createCollectionWithLogoAndId(string memory logoUri, bytes32 collectionId) private returns (bytes32) {
149+
ERC721EthscriptionsCollectionManager.CollectionParams memory metadata =
150+
ERC721EthscriptionsCollectionManager.CollectionParams({
151+
name: "Test Collection",
152+
symbol: "TEST",
153+
maxSupply: 100,
154+
description: "Test collection",
155+
logoImageUri: logoUri,
156+
bannerImageUri: "",
157+
backgroundColor: "",
158+
websiteLink: "",
159+
twitterLink: "",
160+
discordLink: "",
161+
merkleRoot: bytes32(0)
162+
});
163+
164+
string memory collectionContent = string.concat(
165+
'data:application/json,',
166+
'{"p":"erc-721-ethscriptions-collection","op":"create_collection"}'
167+
);
168+
169+
Ethscriptions.CreateEthscriptionParams memory params = Ethscriptions.CreateEthscriptionParams({
170+
ethscriptionId: collectionId,
171+
contentUriSha: sha256(bytes(collectionContent)),
172+
initialOwner: alice,
173+
content: bytes(collectionContent),
174+
mimetype: "application/json",
175+
esip6: true, // Allow duplicate content URI
176+
protocolParams: Ethscriptions.ProtocolParams({
177+
protocolName: "erc-721-ethscriptions-collection",
178+
operation: "create_collection",
179+
data: abi.encode(metadata)
180+
})
181+
});
182+
183+
vm.prank(alice);
184+
ethscriptions.createEthscription(params);
185+
186+
return collectionId;
187+
}
188+
}

0 commit comments

Comments
 (0)