From 7e55b6ce66f85dfb65a15e5eed91b0d5d4105867 Mon Sep 17 00:00:00 2001 From: Tom Lehman Date: Wed, 12 Nov 2025 17:01:12 -0500 Subject: [PATCH 1/2] Enhance ERC721 Ethscriptions Collection Parser with Initial Owner Support - Updated the `Erc721EthscriptionsCollectionParser` to include `initial_owner` in the `create_collection` operation schema and ABI type. - Modified the `validate_and_encode` method to accept an `eth_transaction` parameter for determining the initial owner context. - Enhanced metadata handling to support ownership renouncement and validation of optional addresses. - Updated tests to reflect changes in the collection creation process, ensuring proper handling of the new `initial_owner` field. --- .../erc721_ethscriptions_collection_parser.rb | 75 ++++++++++++++----- app/models/ethscription_transaction.rb | 4 +- app/models/protocol_parser.rb | 14 ++-- .../src/ERC721EthscriptionsCollection.sol | 9 ++- .../ERC721EthscriptionsCollectionManager.sol | 28 +++++-- contracts/test/AddressPrediction.t.sol | 3 +- contracts/test/CollectionURIResolution.t.sol | 3 +- contracts/test/CollectionsManager.t.sol | 9 ++- contracts/test/CollectionsProtocol.t.sol | 9 ++- .../collections_protocol_e2e_spec.rb | 12 ++- spec/integration/collections_protocol_spec.rb | 12 ++- ...erc721_collections_import_fallback_spec.rb | 8 ++ ...21_ethscriptions_collection_parser_spec.rb | 22 +++--- spec/models/protocol_parser_spec.rb | 4 +- 14 files changed, 148 insertions(+), 64 deletions(-) diff --git a/app/models/erc721_ethscriptions_collection_parser.rb b/app/models/erc721_ethscriptions_collection_parser.rb index da79929..68b4f23 100644 --- a/app/models/erc721_ethscriptions_collection_parser.rb +++ b/app/models/erc721_ethscriptions_collection_parser.rb @@ -12,8 +12,8 @@ class Erc721EthscriptionsCollectionParser # Operation schemas defining exact structure and ABI encoding OPERATION_SCHEMAS = { 'create_collection' => { - keys: %w[name symbol max_supply description logo_image_uri banner_image_uri background_color website_link twitter_link discord_link merkle_root], - abi_type: '(string,string,uint256,string,string,string,string,string,string,string,bytes32)', + keys: %w[name symbol max_supply description logo_image_uri banner_image_uri background_color website_link twitter_link discord_link merkle_root initial_owner], + abi_type: '(string,string,uint256,string,string,string,string,string,string,string,bytes32,address)', validators: { 'name' => :string, 'symbol' => :string, @@ -25,14 +25,15 @@ class Erc721EthscriptionsCollectionParser 'website_link' => :string, 'twitter_link' => :string, 'discord_link' => :string, - 'merkle_root' => :bytes32 + 'merkle_root' => :bytes32, + 'initial_owner' => :optional_address } }, # New combined create op name used by the contract; keep legacy alias below 'create_collection_and_add_self' => { keys: %w[metadata item], - # ((CollectionParams),(ItemData)) - ItemData now includes contentHash as first field - abi_type: '((string,string,uint256,string,string,string,string,string,string,string,bytes32),(bytes32,uint256,string,string,string,(string,string)[],bytes32[]))', + # ((CollectionParams),(ItemData)) - CollectionParams now includes initialOwner + abi_type: '((string,string,uint256,string,string,string,string,string,string,string,bytes32,address),(bytes32,uint256,string,string,string,(string,string)[],bytes32[]))', validators: { 'metadata' => :collection_metadata, 'item' => :single_item @@ -41,7 +42,7 @@ class Erc721EthscriptionsCollectionParser # Legacy alias retained for backwards compatibility 'create_and_add_self' => { keys: %w[metadata item], - abi_type: '((string,string,uint256,string,string,string,string,string,string,string,bytes32),(bytes32,uint256,string,string,string,(string,string)[],bytes32[]))', + abi_type: '((string,string,uint256,string,string,string,string,string,string,string,bytes32,address),(bytes32,uint256,string,string,string,(string,string)[],bytes32[]))', validators: { 'metadata' => :collection_metadata, 'item' => :single_item @@ -132,21 +133,22 @@ class ValidationError < StandardError; end # New API: validate and encode protocol params # Unified interface - accepts all possible parameters, uses what it needs - def self.validate_and_encode(decoded_content:, operation:, params:, source:, ethscription_id: nil, **_extras) + def self.validate_and_encode(decoded_content:, operation:, params:, source:, ethscription_id: nil, eth_transaction: nil, **_extras) new.validate_and_encode( decoded_content: decoded_content, operation: operation, params: params, source: source, - ethscription_id: ethscription_id + ethscription_id: ethscription_id, + eth_transaction: eth_transaction ) end - def validate_and_encode(decoded_content:, operation:, params:, source:, ethscription_id: nil) + def validate_and_encode(decoded_content:, operation:, params:, source:, ethscription_id: nil, eth_transaction: nil) # Check import fallback first (if ethscription_id provided) if ethscription_id normalized_id = normalize_id(ethscription_id) - if normalized_id && (preplanned = build_import_encoded_params(normalized_id, decoded_content)) + if normalized_id && (preplanned = build_import_encoded_params(normalized_id, decoded_content, eth_transaction)) return preplanned end end @@ -217,7 +219,7 @@ def normalize_id(value) # -------------------- Import fallback -------------------- # Returns [protocol, operation, encoded_data] or nil - def build_import_encoded_params(id, decoded_content) + def build_import_encoded_params(id, decoded_content, eth_transaction = nil) data = self.class.load_import_data( items_path: DEFAULT_ITEMS_PATH, collections_path: DEFAULT_COLLECTIONS_PATH @@ -247,7 +249,7 @@ def build_import_encoded_params(id, decoded_content) operation = 'create_collection_and_add_self' schema = OPERATION_SCHEMAS[operation] encoding_data = { - 'metadata' => build_metadata_object(metadata), + 'metadata' => build_metadata_object(metadata, eth_transaction: eth_transaction), 'item' => build_item_object(item: item, item_index: item_index, content_hash: content_hash) } encoded_data = encode_operation(operation, encoding_data, schema, content_hash: content_hash) @@ -355,7 +357,7 @@ def load_import_data(items_path:, collections_path:) end # Build ordered JSON objects to match strict parser expectations - def build_metadata_object(meta) + def build_metadata_object(meta, eth_transaction: nil) name = safe_string(meta['name']) symbol = safe_string(meta['symbol'] || meta['slug'] || meta['name']) max_supply = safe_uint_string(meta['max_supply'] || meta['total_supply'] || 0) @@ -381,6 +383,24 @@ def build_metadata_object(meta) ] merkle_root = meta.fetch('merkle_root') result['merkle_root'] = to_bytes32_hex(merkle_root) + + # Handle initial_owner based on should_renounce flag + if meta['should_renounce'] == true + # address(0) means renounce ownership + result['initial_owner'] = '0x0000000000000000000000000000000000000000' + elsif meta['initial_owner'] + # Use explicitly specified initial owner + result['initial_owner'] = to_address_hex(meta['initial_owner']) + elsif eth_transaction && eth_transaction.respond_to?(:from_address) + # Use the transaction sender as the actual owner + result['initial_owner'] = to_address_hex(eth_transaction.from_address) + else + # No transaction context - this shouldn't happen in production + # For import, we always have the transaction + # Return nil to indicate we can't determine the owner + raise ValidationError, "Cannot determine initial owner without transaction context" + end + result end @@ -408,6 +428,12 @@ def to_bytes32_hex(val) h end + def to_address_hex(val) + h = safe_string(val).downcase + raise ValidationError, "Invalid address hex: #{val}" unless h.match?(/\A0x[0-9a-f]{40}\z/) + h + end + # Integer coercion helper for import computations def safe_uint(val) case val @@ -564,6 +590,14 @@ def validate_address(value, field_name) value.downcase end + def validate_optional_address(value, field_name) + unless value.is_a?(String) && value.match?(/\A0x[0-9a-f]{40}\z/i) + raise ValidationError, "Invalid address for #{field_name}: #{value}" + end + # Allow address(0) for renouncement + value.downcase + end + def validate_bytes32_array(value, field_name) unless value.is_a?(Array) raise ValidationError, "Expected array for #{field_name}" @@ -598,8 +632,8 @@ def validate_collection_metadata(value, field_name) unless value.is_a?(Hash) raise ValidationError, "Expected object for #{field_name}" end - # Expected keys for metadata (merkle_root optional) - expected_keys = %w[name symbol max_supply description logo_image_uri banner_image_uri background_color website_link twitter_link discord_link merkle_root] + # Expected keys for metadata (now includes initial_owner) + expected_keys = %w[name symbol max_supply description logo_image_uri banner_image_uri background_color website_link twitter_link discord_link merkle_root initial_owner] unless value.keys == expected_keys raise ValidationError, "Invalid metadata keys or order" end @@ -615,7 +649,8 @@ def validate_collection_metadata(value, field_name) websiteLink: validate_string(value['website_link'], 'website_link'), twitterLink: validate_string(value['twitter_link'], 'twitter_link'), discordLink: validate_string(value['discord_link'], 'discord_link'), - merkleRoot: validate_bytes32(value['merkle_root'], 'merkle_root') + merkleRoot: validate_bytes32(value['merkle_root'], 'merkle_root'), + initialOwner: validate_optional_address(value['initial_owner'], 'initial_owner') } end @@ -681,7 +716,8 @@ def build_create_collection_values(data) data['website_link'], data['twitter_link'], data['discord_link'], - data['merkle_root'] + data['merkle_root'], + data['initial_owner'] ] end @@ -689,7 +725,7 @@ def build_create_and_add_self_values(data, content_hash:) meta = data['metadata'] item = data['item'] - # Metadata tuple with optional merkleRoot + # Metadata tuple with merkleRoot and initialOwner merkle_root = meta[:merkleRoot] || ["".ljust(64, '0')].pack('H*') metadata_tuple = [ meta[:name], @@ -702,7 +738,8 @@ def build_create_and_add_self_values(data, content_hash:) meta[:websiteLink], meta[:twitterLink], meta[:discordLink], - merkle_root + merkle_root, + meta[:initialOwner] ] # Item tuple - contentHash comes first (keccak256 of ethscription content) diff --git a/app/models/ethscription_transaction.rb b/app/models/ethscription_transaction.rb index 5ccca59..55b2e85 100644 --- a/app/models/ethscription_transaction.rb +++ b/app/models/ethscription_transaction.rb @@ -180,10 +180,10 @@ def build_create_calldata esip6 = DataUri.esip6?(content_uri) || false # Extract protocol params - returns [protocol, operation, encoded_data] - # Pass the ethscription_id context so parsers can inject it when needed + # Pass the eth_transaction for context (includes from_address and transaction_hash) protocol, operation, encoded_data = ProtocolParser.for_calldata( content_uri, - ethscription_id: eth_transaction.transaction_hash + eth_transaction: eth_transaction ) # Hash the content for protocol uniqueness diff --git a/app/models/protocol_parser.rb b/app/models/protocol_parser.rb index 8bf0820..422ca91 100644 --- a/app/models/protocol_parser.rb +++ b/app/models/protocol_parser.rb @@ -11,7 +11,7 @@ class ProtocolParser 'erc-721-ethscriptions-collection' => Erc721EthscriptionsCollectionParser }.freeze - def self.extract(content_uri, ethscription_id: nil) + def self.extract(content_uri, eth_transaction: nil, ethscription_id: nil) # Parse data URI and extract protocol info parsed = parse_data_uri_and_protocol(content_uri) @@ -33,7 +33,8 @@ def self.extract(content_uri, ethscription_id: nil) operation: nil, params: {}, source: :json, - ethscription_id: ethscription_id + ethscription_id: ethscription_id, + eth_transaction: eth_transaction ) if encoded != DEFAULT_PARAMS @@ -61,7 +62,8 @@ def self.extract(content_uri, ethscription_id: nil) operation: parsed[:operation], params: parsed[:params], source: parsed[:source], - ethscription_id: ethscription_id + ethscription_id: ethscription_id, + eth_transaction: eth_transaction ) # Check if parsing succeeded @@ -83,8 +85,10 @@ def self.extract(content_uri, ethscription_id: nil) # Get protocol data formatted for L2 calldata # Returns [protocol, operation, encoded_data] for contract consumption - def self.for_calldata(content_uri, ethscription_id: nil) - result = extract(content_uri, ethscription_id: ethscription_id) + def self.for_calldata(content_uri, eth_transaction: nil, ethscription_id: nil) + # Support both for backward compatibility + ethscription_id ||= eth_transaction&.transaction_hash + result = extract(content_uri, eth_transaction: eth_transaction, ethscription_id: ethscription_id) if result.nil? # No protocol detected - return empty protocol params diff --git a/contracts/src/ERC721EthscriptionsCollection.sol b/contracts/src/ERC721EthscriptionsCollection.sol index c010f06..e656b7b 100644 --- a/contracts/src/ERC721EthscriptionsCollection.sol +++ b/contracts/src/ERC721EthscriptionsCollection.sol @@ -45,7 +45,14 @@ contract ERC721EthscriptionsCollection is ERC721EthscriptionsEnumerableUpgradeab bytes32 collectionId_ ) external initializer { __ERC721_init(name_, symbol_); - __Ownable_init(initialOwner_); + + if (initialOwner_ == address(0)) { + __Ownable_init(address(1)); + _transferOwnership(address(0)); + } else { + __Ownable_init(initialOwner_); + } + manager = ERC721EthscriptionsCollectionManager(msg.sender); collectionId = collectionId_; } diff --git a/contracts/src/ERC721EthscriptionsCollectionManager.sol b/contracts/src/ERC721EthscriptionsCollectionManager.sol index 27d55ce..560c259 100644 --- a/contracts/src/ERC721EthscriptionsCollectionManager.sol +++ b/contracts/src/ERC721EthscriptionsCollectionManager.sol @@ -31,6 +31,7 @@ contract ERC721EthscriptionsCollectionManager is IProtocolHandler { string twitterLink; string discordLink; bytes32 merkleRoot; + address initialOwner; } struct CollectionRecord { @@ -345,21 +346,20 @@ contract ERC721EthscriptionsCollectionManager is IProtocolHandler { // -------------------- Helpers -------------------- - function _createCollection(bytes32 collectionId, CollectionParams memory metadata) internal { - require(!collectionExists(collectionId), "Collection already exists"); + function _initializeCollection(Proxy collectionProxy, bytes32 collectionId, CollectionParams memory metadata) private { - Proxy collectionProxy = new Proxy{salt: collectionId}(address(this)); - collectionProxy.upgradeToAndCall(collectionsImplementation, abi.encodeWithSelector( ERC721EthscriptionsCollection.initialize.selector, metadata.name, metadata.symbol, - ethscriptions.ownerOf(collectionId), + metadata.initialOwner, collectionId )); - + collectionProxy.changeAdmin(Predeploys.PROXY_ADMIN); + } + function _storeCollectionData(bytes32 collectionId, address collectionContract, CollectionParams memory metadata) private { // Store string fields using DedupedBlobStore (, bytes32 nameRef) = DedupedBlobStore.storeMemory(bytes(metadata.name), collectionBlobStorage); (, bytes32 symbolRef) = DedupedBlobStore.storeMemory(bytes(metadata.symbol), collectionBlobStorage); @@ -372,7 +372,7 @@ contract ERC721EthscriptionsCollectionManager is IProtocolHandler { (, bytes32 discordLinkRef) = DedupedBlobStore.storeMemory(bytes(metadata.discordLink), collectionBlobStorage); collectionStore[collectionId] = CollectionRecord({ - collectionContract: address(collectionProxy), + collectionContract: collectionContract, locked: false, nameRef: nameRef, symbolRef: symbolRef, @@ -386,7 +386,19 @@ contract ERC721EthscriptionsCollectionManager is IProtocolHandler { discordLinkRef: discordLinkRef, merkleRoot: metadata.merkleRoot }); - + } + + function _createCollection(bytes32 collectionId, CollectionParams memory metadata) internal { + require(!collectionExists(collectionId), "Collection already exists"); + + Proxy collectionProxy = new Proxy{salt: collectionId}(address(this)); + + // Initialize the collection + _initializeCollection(collectionProxy, collectionId, metadata); + + // Store collection metadata + _storeCollectionData(collectionId, address(collectionProxy), metadata); + collectionAddressToId[address(collectionProxy)] = collectionId; collectionIds.push(collectionId); diff --git a/contracts/test/AddressPrediction.t.sol b/contracts/test/AddressPrediction.t.sol index c6e66d2..95d1911 100644 --- a/contracts/test/AddressPrediction.t.sol +++ b/contracts/test/AddressPrediction.t.sol @@ -73,7 +73,8 @@ contract AddressPredictionTest is TestSetup { websiteLink: "https://example.com", twitterLink: "", discordLink: "", - merkleRoot: bytes32(0) + merkleRoot: bytes32(0), + initialOwner: address(this) // Use test contract as owner }); // Manually compute predicted proxy address diff --git a/contracts/test/CollectionURIResolution.t.sol b/contracts/test/CollectionURIResolution.t.sol index 8001a0a..2a6ee99 100644 --- a/contracts/test/CollectionURIResolution.t.sol +++ b/contracts/test/CollectionURIResolution.t.sol @@ -170,7 +170,8 @@ contract CollectionURIResolutionTest is TestSetup { websiteLink: "", twitterLink: "", discordLink: "", - merkleRoot: bytes32(0) + merkleRoot: bytes32(0), + initialOwner: alice // Use alice as owner }); string memory collectionContent = string.concat( diff --git a/contracts/test/CollectionsManager.t.sol b/contracts/test/CollectionsManager.t.sol index 99bafab..10345a2 100644 --- a/contracts/test/CollectionsManager.t.sol +++ b/contracts/test/CollectionsManager.t.sol @@ -40,7 +40,8 @@ contract ERC721EthscriptionsCollectionManagerTest is TestSetup { websiteLink: "https://example.com", twitterLink: "https://twitter.com/test", discordLink: "https://discord.gg/test", - merkleRoot: bytes32(0) + merkleRoot: bytes32(0), + initialOwner: alice // Use alice as owner }); Ethscriptions.CreateEthscriptionParams memory params = Ethscriptions.CreateEthscriptionParams({ @@ -94,7 +95,8 @@ contract ERC721EthscriptionsCollectionManagerTest is TestSetup { websiteLink: "https://example.com", twitterLink: "", discordLink: "", - merkleRoot: bytes32(0) + merkleRoot: bytes32(0), + initialOwner: alice // Use alice as owner }); // Prepare item data @@ -1317,7 +1319,8 @@ contract ERC721EthscriptionsCollectionManagerTest is TestSetup { websiteLink: "", twitterLink: "", discordLink: "", - merkleRoot: merkleRoot + merkleRoot: merkleRoot, + initialOwner: alice // Use alice as owner }); string memory collectionContent = diff --git a/contracts/test/CollectionsProtocol.t.sol b/contracts/test/CollectionsProtocol.t.sol index c29b63d..b2d8b11 100644 --- a/contracts/test/CollectionsProtocol.t.sol +++ b/contracts/test/CollectionsProtocol.t.sol @@ -25,7 +25,8 @@ contract CollectionsProtocolTest is TestSetup { websiteLink: "", twitterLink: "", discordLink: "", - merkleRoot: bytes32(0) + merkleRoot: bytes32(0), + initialOwner: alice // Use alice as owner }); bytes memory encodedMetadata = abi.encode(metadata); @@ -92,7 +93,8 @@ contract CollectionsProtocolTest is TestSetup { websiteLink: "", twitterLink: "", discordLink: "", - merkleRoot: bytes32(0) + merkleRoot: bytes32(0), + initialOwner: alice // Use alice as owner }); bytes memory encodedProtocolData = abi.encode(metadata); @@ -154,7 +156,8 @@ contract CollectionsProtocolTest is TestSetup { websiteLink: "", twitterLink: "", discordLink: "", - merkleRoot: bytes32(0) + merkleRoot: bytes32(0), + initialOwner: alice // Use alice as owner }); bytes32 txHash = keccak256("call_test_tx"); diff --git a/spec/integration/collections_protocol_e2e_spec.rb b/spec/integration/collections_protocol_e2e_spec.rb index ec69ab9..bb8801f 100644 --- a/spec/integration/collections_protocol_e2e_spec.rb +++ b/spec/integration/collections_protocol_e2e_spec.rb @@ -94,7 +94,8 @@ def create_and_validate_ethscription(creator:, to:, data_uri:) "website_link" => "", "twitter_link" => "", "discord_link" => "", - "merkle_root" => zero_merkle_root + "merkle_root" => zero_merkle_root, + "initial_owner" => alice } tx_spec = create_input( @@ -179,7 +180,8 @@ def create_and_validate_ethscription(creator:, to:, data_uri:) "website_link" => "", "twitter_link" => "", "discord_link" => "", - "merkle_root" => zero_merkle_root + "merkle_root" => zero_merkle_root, + "initial_owner" => alice } # Create collection using the same pattern as the first test @@ -329,7 +331,8 @@ def create_and_validate_ethscription(creator:, to:, data_uri:) "website_link" => "", "twitter_link" => "", "discord_link" => "", - "merkle_root" => owner_merkle_root + "merkle_root" => owner_merkle_root, + "initial_owner" => alice } collection_spec = create_input( @@ -397,7 +400,8 @@ def create_and_validate_ethscription(creator:, to:, data_uri:) "website_link" => "", "twitter_link" => "", "discord_link" => "", - "merkle_root" => initial_merkle_root + "merkle_root" => initial_merkle_root, + "initial_owner" => alice } collection_spec = create_input( diff --git a/spec/integration/collections_protocol_spec.rb b/spec/integration/collections_protocol_spec.rb index bcedbd5..691e629 100644 --- a/spec/integration/collections_protocol_spec.rb +++ b/spec/integration/collections_protocol_spec.rb @@ -213,7 +213,8 @@ "website_link" => "", "twitter_link" => "", "discord_link" => "", - "merkle_root" => zero_merkle_root + "merkle_root" => zero_merkle_root, + "initial_owner" => alice } creation = expect_ethscription_success( @@ -261,7 +262,8 @@ "website_link" => "", "twitter_link" => "", "discord_link" => "", - "merkle_root" => zero_merkle_root + "merkle_root" => zero_merkle_root, + "initial_owner" => alice } creation = expect_ethscription_success( @@ -300,7 +302,8 @@ "website_link" => "", "twitter_link" => "", "discord_link" => "", - "merkle_root" => zero_merkle_root + "merkle_root" => zero_merkle_root, + "initial_owner" => alice } creation = expect_ethscription_success( @@ -438,7 +441,8 @@ "website_link" => "", "twitter_link" => "", "discord_link" => "", - "merkle_root" => zero_merkle_root + "merkle_root" => zero_merkle_root, + "initial_owner" => alice } expect_ethscription_success( diff --git a/spec/models/erc721_collections_import_fallback_spec.rb b/spec/models/erc721_collections_import_fallback_spec.rb index a88b55b..62ab735 100644 --- a/spec/models/erc721_collections_import_fallback_spec.rb +++ b/spec/models/erc721_collections_import_fallback_spec.rb @@ -59,8 +59,12 @@ end it 'builds create_collection_and_add_self for the leader via import fallback' do + # Create a mock eth_transaction with from_address + mock_tx = double('eth_transaction', from_address: '0x0000000000000000000000000000000000000001') + protocol, operation, encoded = ProtocolParser.for_calldata( 'data:,{}', + eth_transaction: mock_tx, ethscription_id: ByteString.from_hex(leader_id) ) expect(protocol).to eq('erc-721-ethscriptions-collection'.b) @@ -145,8 +149,12 @@ # This ethscription should be the leader of a collection in the live JSON files specific_id = '0x05aac415994e0e01e66c4970133a51a4cdcea1f3a967743b87e6eb08f2f4d9f9' + # Create a mock eth_transaction with from_address + mock_tx = double('eth_transaction', from_address: '0x0000000000000000000000000000000000000001') + protocol, operation, encoded = ProtocolParser.for_calldata( 'data:,{}', + eth_transaction: mock_tx, ethscription_id: ByteString.from_hex(specific_id) ) expect(protocol).to eq('erc-721-ethscriptions-collection'.b) diff --git a/spec/models/erc721_ethscriptions_collection_parser_spec.rb b/spec/models/erc721_ethscriptions_collection_parser_spec.rb index 212b6fe..bbebe0a 100644 --- a/spec/models/erc721_ethscriptions_collection_parser_spec.rb +++ b/spec/models/erc721_ethscriptions_collection_parser_spec.rb @@ -58,12 +58,12 @@ # @generic-compatible it 'validates uint256 format - no leading zeros' do # Valid - valid_json = %(data:,{"p":"erc-721-ethscriptions-collection","op":"create_collection","name":"Test","symbol":"TEST","max_supply":"1000","description":"","logo_image_uri":"","banner_image_uri":"","background_color":"","website_link":"","twitter_link":"","discord_link":"","merkle_root":"#{zero_merkle_root}"}) + valid_json = %(data:,{"p":"erc-721-ethscriptions-collection","op":"create_collection","name":"Test","symbol":"TEST","max_supply":"1000","description":"","logo_image_uri":"","banner_image_uri":"","background_color":"","website_link":"","twitter_link":"","discord_link":"","merkle_root":"#{zero_merkle_root}","initial_owner":"0x0000000000000000000000000000000000000001"}) result = ProtocolParser.for_calldata(valid_json) expect(result[0]).to eq('erc-721-ethscriptions-collection'.b) # Invalid - leading zero - invalid_json = %(data:,{"p":"erc-721-ethscriptions-collection","op":"create_collection","name":"Test","symbol":"TEST","max_supply":"01000","description":"","logo_image_uri":"","banner_image_uri":"","background_color":"","website_link":"","twitter_link":"","discord_link":"","merkle_root":"#{zero_merkle_root}"}) + invalid_json = %(data:,{"p":"erc-721-ethscriptions-collection","op":"create_collection","name":"Test","symbol":"TEST","max_supply":"01000","description":"","logo_image_uri":"","banner_image_uri":"","background_color":"","website_link":"","twitter_link":"","discord_link":"","merkle_root":"#{zero_merkle_root}","initial_owner":"0x0000000000000000000000000000000000000001"}) result = ProtocolParser.for_calldata(invalid_json) expect(result).to eq(default_params) end @@ -94,7 +94,7 @@ describe 'create_collection operation' do let(:valid_create_json) do - %(data:,{"p":"erc-721-ethscriptions-collection","op":"create_collection","name":"My Collection","symbol":"MYC","max_supply":"10000","description":"A test collection","logo_image_uri":"esc://logo","banner_image_uri":"esc://banner","background_color":"#FF5733","website_link":"https://example.com","twitter_link":"https://twitter.com/test","discord_link":"https://discord.gg/test","merkle_root":"#{zero_merkle_root}"}) + %(data:,{"p":"erc-721-ethscriptions-collection","op":"create_collection","name":"My Collection","symbol":"MYC","max_supply":"10000","description":"A test collection","logo_image_uri":"esc://logo","banner_image_uri":"esc://banner","background_color":"#FF5733","website_link":"https://example.com","twitter_link":"https://twitter.com/test","discord_link":"https://discord.gg/test","merkle_root":"#{zero_merkle_root}","initial_owner":"0x0000000000000000000000000000000000000001"}) end it 'encodes create_collection correctly' do @@ -123,7 +123,7 @@ end it 'handles empty optional fields' do - json = %(data:,{"p":"erc-721-ethscriptions-collection","op":"create_collection","name":"Test","symbol":"TST","max_supply":"100","description":"","logo_image_uri":"","banner_image_uri":"","background_color":"","website_link":"","twitter_link":"","discord_link":"","merkle_root":"#{zero_merkle_root}"}) + json = %(data:,{"p":"erc-721-ethscriptions-collection","op":"create_collection","name":"Test","symbol":"TST","max_supply":"100","description":"","logo_image_uri":"","banner_image_uri":"","background_color":"","website_link":"","twitter_link":"","discord_link":"","merkle_root":"#{zero_merkle_root}","initial_owner":"0x0000000000000000000000000000000000000001"}) result = ProtocolParser.for_calldata(json) expect(result[0]).to eq('erc-721-ethscriptions-collection'.b) @@ -145,7 +145,7 @@ # Value that exceeds uint256 max too_large = (2**256).to_s # One more than max - json = %(data:,{"p":"erc-721-ethscriptions-collection","op":"create_collection","name":"Test","symbol":"TST","max_supply":"#{too_large}","description":"","logo_image_uri":"","banner_image_uri":"","background_color":"","website_link":"","twitter_link":"","discord_link":"","merkle_root":"#{zero_merkle_root}"}) + json = %(data:,{"p":"erc-721-ethscriptions-collection","op":"create_collection","name":"Test","symbol":"TST","max_supply":"#{too_large}","description":"","logo_image_uri":"","banner_image_uri":"","background_color":"","website_link":"","twitter_link":"","discord_link":"","merkle_root":"#{zero_merkle_root}","initial_owner":"0x0000000000000000000000000000000000000001"}) result = ProtocolParser.for_calldata(json) # Should return default params due to validation failure @@ -156,7 +156,7 @@ # Maximum valid uint256 max_uint256 = (2**256 - 1).to_s - json = %(data:,{"p":"erc-721-ethscriptions-collection","op":"create_collection","name":"Test","symbol":"TST","max_supply":"#{max_uint256}","description":"","logo_image_uri":"","banner_image_uri":"","background_color":"","website_link":"","twitter_link":"","discord_link":"","merkle_root":"#{zero_merkle_root}"}) + json = %(data:,{"p":"erc-721-ethscriptions-collection","op":"create_collection","name":"Test","symbol":"TST","max_supply":"#{max_uint256}","description":"","logo_image_uri":"","banner_image_uri":"","background_color":"","website_link":"","twitter_link":"","discord_link":"","merkle_root":"#{zero_merkle_root}","initial_owner":"0x0000000000000000000000000000000000000001"}) result = ProtocolParser.for_calldata(json) # Should succeed with max value @@ -335,9 +335,9 @@ it 'preserves all data through encode/decode cycle' do test_cases = [ { - json: %(data:,{"p":"erc-721-ethscriptions-collection","op":"create_collection","name":"Test","symbol":"TST","max_supply":"100","description":"Desc","logo_image_uri":"logo","banner_image_uri":"banner","background_color":"#FFF","website_link":"http://test","twitter_link":"@test","discord_link":"discord","merkle_root":"#{zero_merkle_root}"}), - abi_type: '(string,string,uint256,string,string,string,string,string,string,string,bytes32)', - expected: ["Test", "TST", 100, "Desc", "logo", "banner", "#FFF", "http://test", "@test", "discord", [zero_merkle_root[2..]].pack('H*')] + json: %(data:,{"p":"erc-721-ethscriptions-collection","op":"create_collection","name":"Test","symbol":"TST","max_supply":"100","description":"Desc","logo_image_uri":"logo","banner_image_uri":"banner","background_color":"#FFF","website_link":"http://test","twitter_link":"@test","discord_link":"discord","merkle_root":"#{zero_merkle_root}","initial_owner":"0x0000000000000000000000000000000000000001"}), + abi_type: '(string,string,uint256,string,string,string,string,string,string,string,bytes32,address)', + expected: ["Test", "TST", 100, "Desc", "logo", "banner", "#FFF", "http://test", "@test", "discord", [zero_merkle_root[2..]].pack('H*'), "0x0000000000000000000000000000000000000001"] }, { json: 'data:,{"p":"erc-721-ethscriptions-collection","op":"lock_collection","collection_id":"0x' + 'a' * 64 + '"}', @@ -382,12 +382,12 @@ it 'rejects null values in string fields (no silent coercion)' do # Test null in create_collection string fields - json_with_null = %(data:,{"p":"erc-721-ethscriptions-collection","op":"create_collection","name":null,"symbol":"TEST","max_supply":"100","description":"","logo_image_uri":"","banner_image_uri":"","background_color":"","website_link":"","twitter_link":"","discord_link":"","merkle_root":"#{zero_merkle_root}"}) + json_with_null = %(data:,{"p":"erc-721-ethscriptions-collection","op":"create_collection","name":null,"symbol":"TEST","max_supply":"100","description":"","logo_image_uri":"","banner_image_uri":"","background_color":"","website_link":"","twitter_link":"","discord_link":"","merkle_root":"#{zero_merkle_root}","initial_owner":"0x0000000000000000000000000000000000000001"}) result = ProtocolParser.for_calldata(json_with_null) expect(result).to eq(default_params) # Test null in description field - json_with_null_desc = %(data:,{"p":"erc-721-ethscriptions-collection","op":"create_collection","name":"Test","symbol":"TEST","max_supply":"100","description":null,"logo_image_uri":"","banner_image_uri":"","background_color":"","website_link":"","twitter_link":"","discord_link":"","merkle_root":"#{zero_merkle_root}"}) + json_with_null_desc = %(data:,{"p":"erc-721-ethscriptions-collection","op":"create_collection","name":"Test","symbol":"TEST","max_supply":"100","description":null,"logo_image_uri":"","banner_image_uri":"","background_color":"","website_link":"","twitter_link":"","discord_link":"","merkle_root":"#{zero_merkle_root}","initial_owner":"0x0000000000000000000000000000000000000001"}) result = ProtocolParser.for_calldata(json_with_null_desc) expect(result).to eq(default_params) diff --git a/spec/models/protocol_parser_spec.rb b/spec/models/protocol_parser_spec.rb index 48f7341..bd033ac 100644 --- a/spec/models/protocol_parser_spec.rb +++ b/spec/models/protocol_parser_spec.rb @@ -57,7 +57,7 @@ context 'erc-721 collections protocol' do it 'parses a create_collection inscription' do - content_uri = %(data:,{"p":"erc-721-ethscriptions-collection","op":"create_collection","name":"My NFTs","symbol":"MNFT","max_supply":"100","description":"","logo_image_uri":"","banner_image_uri":"","background_color":"","website_link":"","twitter_link":"","discord_link":"","merkle_root":"#{zero_merkle_root}"}) + content_uri = %(data:,{"p":"erc-721-ethscriptions-collection","op":"create_collection","name":"My NFTs","symbol":"MNFT","max_supply":"100","description":"","logo_image_uri":"","banner_image_uri":"","background_color":"","website_link":"","twitter_link":"","discord_link":"","merkle_root":"#{zero_merkle_root}","initial_owner":"0x0000000000000000000000000000000000000001"}) result = described_class.extract(content_uri) @@ -139,7 +139,7 @@ end it 'returns encoded data for collections protocol' do - content_uri = %(data:,{"p":"erc-721-ethscriptions-collection","op":"create_collection","name":"My NFTs","symbol":"MNFT","max_supply":"42","description":"","logo_image_uri":"","banner_image_uri":"","background_color":"","website_link":"","twitter_link":"","discord_link":"","merkle_root":"#{zero_merkle_root}"}) + content_uri = %(data:,{"p":"erc-721-ethscriptions-collection","op":"create_collection","name":"My NFTs","symbol":"MNFT","max_supply":"42","description":"","logo_image_uri":"","banner_image_uri":"","background_color":"","website_link":"","twitter_link":"","discord_link":"","merkle_root":"#{zero_merkle_root}","initial_owner":"0x0000000000000000000000000000000000000001"}) protocol, operation, encoded = described_class.for_calldata(content_uri) From 76710a330d9974649be0d7d18c4dfbf0a5aaaf40 Mon Sep 17 00:00:00 2001 From: Tom Lehman Date: Wed, 12 Nov 2025 17:10:59 -0500 Subject: [PATCH 2/2] Refactor ERC721 Ethscriptions Collection Parser for Address Handling - Updated the `Erc721EthscriptionsCollectionParser` to convert `from_address` to a hexadecimal format using `to_hex` method for consistency. - Modified test cases to utilize `Address20.from_hex` for mock transactions, ensuring proper address handling in the context of collection creation. - Adjusted ABI encoding in tests to include the address type, reflecting the updated structure for collection operations. --- app/models/erc721_ethscriptions_collection_parser.rb | 2 +- spec/integration/ethscriptions_creation_spec.rb | 2 +- spec/models/erc721_collections_import_fallback_spec.rb | 8 ++++---- .../models/erc721_ethscriptions_collection_parser_spec.rb | 6 +++--- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/app/models/erc721_ethscriptions_collection_parser.rb b/app/models/erc721_ethscriptions_collection_parser.rb index 68b4f23..2d7919f 100644 --- a/app/models/erc721_ethscriptions_collection_parser.rb +++ b/app/models/erc721_ethscriptions_collection_parser.rb @@ -393,7 +393,7 @@ def build_metadata_object(meta, eth_transaction: nil) result['initial_owner'] = to_address_hex(meta['initial_owner']) elsif eth_transaction && eth_transaction.respond_to?(:from_address) # Use the transaction sender as the actual owner - result['initial_owner'] = to_address_hex(eth_transaction.from_address) + result['initial_owner'] = eth_transaction.from_address.to_hex else # No transaction context - this shouldn't happen in production # For import, we always have the transaction diff --git a/spec/integration/ethscriptions_creation_spec.rb b/spec/integration/ethscriptions_creation_spec.rb index 6bb9ea4..7bcc003 100644 --- a/spec/integration/ethscriptions_creation_spec.rb +++ b/spec/integration/ethscriptions_creation_spec.rb @@ -78,7 +78,7 @@ end it "accepts GZIP input post-ESIP-7" do - compressed_data_uri = Zlib.gzip("data:text/plain;charset=utf-8,Hello World") # Placeholder for GZIP data + compressed_data_uri = Zlib.gzip("data:text/plain;charset=utf-8,Hello Worldaaa") # Placeholder for GZIP data expect_ethscription_success( create_input( diff --git a/spec/models/erc721_collections_import_fallback_spec.rb b/spec/models/erc721_collections_import_fallback_spec.rb index 62ab735..65c0607 100644 --- a/spec/models/erc721_collections_import_fallback_spec.rb +++ b/spec/models/erc721_collections_import_fallback_spec.rb @@ -60,7 +60,7 @@ it 'builds create_collection_and_add_self for the leader via import fallback' do # Create a mock eth_transaction with from_address - mock_tx = double('eth_transaction', from_address: '0x0000000000000000000000000000000000000001') + mock_tx = double('eth_transaction', from_address: Address20.from_hex('0x0000000000000000000000000000000000000001')) protocol, operation, encoded = ProtocolParser.for_calldata( 'data:,{}', @@ -71,7 +71,7 @@ expect(operation).to eq('create_collection_and_add_self'.b) decoded = Eth::Abi.decode([ - '((string,string,uint256,string,string,string,string,string,string,string,bytes32),(bytes32,uint256,string,string,string,(string,string)[],bytes32[]))' + '((string,string,uint256,string,string,string,string,string,string,string,bytes32,address),(bytes32,uint256,string,string,string,(string,string)[],bytes32[]))' ], encoded)[0] metadata = decoded[0] @@ -150,7 +150,7 @@ specific_id = '0x05aac415994e0e01e66c4970133a51a4cdcea1f3a967743b87e6eb08f2f4d9f9' # Create a mock eth_transaction with from_address - mock_tx = double('eth_transaction', from_address: '0x0000000000000000000000000000000000000001') + mock_tx = double('eth_transaction', from_address: Address20.from_hex('0x0000000000000000000000000000000000000001')) protocol, operation, encoded = ProtocolParser.for_calldata( 'data:,{}', @@ -162,7 +162,7 @@ # Decode to verify it's properly formed decoded = Eth::Abi.decode([ - '((string,string,uint256,string,string,string,string,string,string,string,bytes32),(bytes32,uint256,string,string,string,(string,string)[],bytes32[]))' + '((string,string,uint256,string,string,string,string,string,string,string,bytes32,address),(bytes32,uint256,string,string,string,(string,string)[],bytes32[]))' ], encoded)[0] metadata = decoded[0] diff --git a/spec/models/erc721_ethscriptions_collection_parser_spec.rb b/spec/models/erc721_ethscriptions_collection_parser_spec.rb index bbebe0a..f4effd1 100644 --- a/spec/models/erc721_ethscriptions_collection_parser_spec.rb +++ b/spec/models/erc721_ethscriptions_collection_parser_spec.rb @@ -105,7 +105,7 @@ # Decode and verify decoded = Eth::Abi.decode( - ['(string,string,uint256,string,string,string,string,string,string,string,bytes32)'], + ['(string,string,uint256,string,string,string,string,string,string,string,bytes32,address)'], result[2] )[0] @@ -130,7 +130,7 @@ expect(result[1]).to eq('create_collection'.b) decoded = Eth::Abi.decode( - ['(string,string,uint256,string,string,string,string,string,string,string,bytes32)'], + ['(string,string,uint256,string,string,string,string,string,string,string,bytes32,address)'], result[2] )[0] @@ -164,7 +164,7 @@ expect(result[1]).to eq('create_collection'.b) decoded = Eth::Abi.decode( - ['(string,string,uint256,string,string,string,string,string,string,string,bytes32)'], + ['(string,string,uint256,string,string,string,string,string,string,string,bytes32,address)'], result[2] )[0]