Skip to content

Commit fa90c78

Browse files
committed
Refactor Protocol Handling and Update Dependencies
- Replaced `ProtocolExtractor` with a new `ProtocolParser` for improved protocol extraction and handling. - Updated the `Erc721EthscriptionsCollectionParser` to include `merkle_root` in various operations, enhancing metadata management. - Removed the deprecated `GenericProtocolExtractor` and its associated tests to streamline the codebase. - Updated dependencies in `Gemfile.lock` to ensure compatibility with the latest versions. - Enhanced integration tests to validate the new parser functionality and ensure proper handling of collection operations.
1 parent 466e4fc commit fa90c78

14 files changed

+138
-2204
lines changed

Gemfile.lock

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ GIT
1616

1717
GIT
1818
remote: https://github.com/0xfacet/facet_rails_common.git
19-
revision: 3d32c04defdd12c1f93b99d272338a4578895d1c
19+
revision: e733e3877d835f68e8671d12d6ef9c0d24025af4
2020
branch: lenient_base64
2121
specs:
2222
facet_rails_common (0.1.0)
@@ -103,9 +103,9 @@ GEM
103103
airbrake-ruby (6.2.2)
104104
rbtree3 (~> 0.6)
105105
awesome_print (1.9.2)
106-
base64 (0.2.0)
107-
benchmark (0.4.1)
108-
bigdecimal (3.2.3)
106+
base64 (0.3.0)
107+
benchmark (0.5.0)
108+
bigdecimal (3.3.1)
109109
bls12-381 (0.3.0)
110110
h2c (~> 0.2.0)
111111
bootsnap (1.17.0)
@@ -117,7 +117,7 @@ GEM
117117
tzinfo
118118
coderay (1.1.3)
119119
concurrent-ruby (1.3.5)
120-
connection_pool (2.4.1)
120+
connection_pool (2.5.4)
121121
crass (1.0.6)
122122
csv (3.3.5)
123123
date (3.4.1)
@@ -129,8 +129,7 @@ GEM
129129
dotenv-rails (2.8.1)
130130
dotenv (= 2.8.1)
131131
railties (>= 3.2)
132-
drb (2.2.0)
133-
ruby2_keywords
132+
drb (2.2.3)
134133
ecdsa (1.2.0)
135134
erubi (1.12.0)
136135
et-orbi (1.3.0)
@@ -156,7 +155,7 @@ GEM
156155
multi_xml (>= 0.5.2)
157156
httpx (1.6.2)
158157
http-2 (>= 1.0.0)
159-
i18n (1.14.1)
158+
i18n (1.14.7)
160159
concurrent-ruby (~> 1.0)
161160
io-console (0.7.1)
162161
irb (1.15.2)
@@ -183,7 +182,7 @@ GEM
183182
method_source (1.0.0)
184183
mini_mime (1.1.5)
185184
mini_portile2 (2.8.9)
186-
minitest (5.20.0)
185+
minitest (5.26.0)
187186
msgpack (1.7.2)
188187
multi_xml (0.7.2)
189188
bigdecimal (~> 3.1)
@@ -298,7 +297,6 @@ GEM
298297
json-schema (>= 2.2, < 6.0)
299298
railties (>= 5.2, < 8.1)
300299
rspec-core (>= 2.14)
301-
ruby2_keywords (0.0.5)
302300
rubyzip (2.4.1)
303301
scrypt (3.1.0)
304302
ffi-compiler (>= 1.0, < 2.0)
@@ -343,10 +341,10 @@ GEM
343341
thor (>= 1.2.0)
344342
yard-sorbet
345343
thor (1.4.0)
346-
timeout (0.4.1)
344+
timeout (0.4.4)
347345
tzinfo (2.0.6)
348346
concurrent-ruby (~> 1.0)
349-
uri (1.0.3)
347+
uri (1.1.1)
350348
useragent (0.16.11)
351349
webrick (1.8.1)
352350
websocket-driver (0.8.0)

app/models/erc721_ethscriptions_collection_parser.rb

Lines changed: 48 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Strict extractor for the ERC-721 Ethscriptions collection protocol with canonical JSON validation
1+
# Strict parser for the ERC-721 Ethscriptions collection protocol with canonical JSON validation
22
class Erc721EthscriptionsCollectionParser
33
# Default return for invalid input
44
DEFAULT_PARAMS = [''.b, ''.b, ''.b].freeze
@@ -9,8 +9,7 @@ class Erc721EthscriptionsCollectionParser
99
# Operation schemas defining exact structure and ABI encoding
1010
OPERATION_SCHEMAS = {
1111
'create_collection' => {
12-
keys: %w[name symbol max_supply description logo_image_uri banner_image_uri background_color website_link twitter_link discord_link],
13-
# Contract expects an extra bytes32 merkleRoot at the end. We append zero when omitted.
12+
keys: %w[name symbol max_supply description logo_image_uri banner_image_uri background_color website_link twitter_link discord_link merkle_root],
1413
abi_type: '(string,string,uint256,string,string,string,string,string,string,string,bytes32)',
1514
validators: {
1615
'name' => :string,
@@ -22,13 +21,14 @@ class Erc721EthscriptionsCollectionParser
2221
'background_color' => :string,
2322
'website_link' => :string,
2423
'twitter_link' => :string,
25-
'discord_link' => :string
24+
'discord_link' => :string,
25+
'merkle_root' => :bytes32
2626
}
2727
},
2828
# New combined create op name used by the contract; keep legacy alias below
2929
'create_collection_and_add_self' => {
3030
keys: %w[metadata item],
31-
# ((CollectionParams),(ItemData)) - ItemData without ethscription_id (item refers to itself)
31+
# ((CollectionParams),(ItemData)) - ItemData mirrors ItemData struct (no ethscriptionId field)
3232
abi_type: '((string,string,uint256,string,string,string,string,string,string,string,bytes32),(uint256,string,string,string,(string,string)[],bytes32[]))',
3333
validators: {
3434
'metadata' => :collection_metadata,
@@ -62,8 +62,7 @@ class Erc721EthscriptionsCollectionParser
6262
}
6363
},
6464
'edit_collection' => {
65-
keys: %w[collection_id description logo_image_uri banner_image_uri background_color website_link twitter_link discord_link],
66-
# Contract includes a bytes32 merkleRoot; append zero when omitted
65+
keys: %w[collection_id description logo_image_uri banner_image_uri background_color website_link twitter_link discord_link merkle_root],
6766
abi_type: '(bytes32,string,string,string,string,string,string,string,bytes32)',
6867
validators: {
6968
'collection_id' => :bytes32,
@@ -73,7 +72,8 @@ class Erc721EthscriptionsCollectionParser
7372
'background_color' => :string,
7473
'website_link' => :string,
7574
'twitter_link' => :string,
76-
'discord_link' => :string
75+
'discord_link' => :string,
76+
'merkle_root' => :bytes32
7777
}
7878
},
7979
'edit_collection_item' => {
@@ -97,8 +97,10 @@ class Erc721EthscriptionsCollectionParser
9797
}
9898
}.freeze
9999

100-
# Item keys for validation (ethscription_id removed - item refers to itself)
101-
# merkle_proof is always required (can be empty array)
100+
ZERO_BYTES32 = ["".ljust(64, '0')].pack('H*').freeze
101+
ZERO_HEX_BYTES32 = '0x' + '0' * 64
102+
103+
# Item keys for validation (merkle_proof always present, can be empty array)
102104
ITEM_KEYS = %w[item_index name background_color description attributes merkle_proof].freeze
103105

104106
# Attribute keys for NFT metadata
@@ -114,56 +116,50 @@ def self.extract(content_uri, ethscription_id: nil)
114116
end
115117

116118
def extract(content_uri, ethscription_id: nil)
117-
# Import-aware path takes precedence if id is provided
118119
if ethscription_id
119-
if (encoded = build_import_encoded_params(ethscription_id.to_hex))
120-
return encoded
120+
normalized_id = normalize_id(ethscription_id)
121+
if normalized_id && (preplanned = build_import_encoded_params(normalized_id))
122+
return preplanned
121123
end
122124
end
123125

124126
return DEFAULT_PARAMS unless valid_data_uri?(content_uri)
125127

126128
begin
127-
# Parse JSON (preserves key order)
128-
# Use DataUri to correctly handle optional parameters like ESIP6
129-
json_str = if content_uri.start_with?("data:,{")
130-
content_uri.sub(/\Adata:,/, '')
131-
else
132-
DataUri.new(content_uri).decoded_data
133-
end
134-
129+
json_str = DataUri.new(content_uri).decoded_data
130+
135131
# TODO: make sure this is safe
136132
data = JSON.parse(json_str)
137-
138-
# Must be an object
139133
return DEFAULT_PARAMS unless data.is_a?(Hash)
140-
141-
# Check protocol
142134
return DEFAULT_PARAMS unless data['p'] == 'erc-721-ethscriptions-collection'
143135

144-
# Get operation
145136
operation = data['op']
146137
return DEFAULT_PARAMS unless OPERATION_SCHEMAS.key?(operation)
147138

148-
# Validate exact key order (including p and op at start)
149139
schema = OPERATION_SCHEMAS[operation]
150140
expected_keys = ['p', 'op'] + schema[:keys]
151141
return DEFAULT_PARAMS unless data.keys == expected_keys
152142

153-
# Remove protocol fields for encoding
154143
encoding_data = data.reject { |k, _| k == 'p' || k == 'op' }
155-
156-
# Validate field types and encode
157144
encoded_data = encode_operation(operation, encoding_data, schema)
158-
159145
['erc-721-ethscriptions-collection'.b, operation.b, encoded_data.b]
160-
161146
rescue JSON::ParserError, ValidationError => e
162147
Rails.logger.debug "Collections extraction failed: #{e.message}" if defined?(Rails)
163148
DEFAULT_PARAMS
164149
end
165150
end
166151

152+
def normalize_id(value)
153+
case value
154+
when ByteString
155+
value.to_hex.downcase
156+
when String
157+
value.downcase
158+
else
159+
nil
160+
end
161+
end
162+
167163
# -------------------- Import fallback --------------------
168164

169165
# Returns [protocol, operation, encoded_data] or nil
@@ -185,13 +181,16 @@ def build_import_encoded_params(id)
185181
item_index = data[:zero_index_by_id][id] || 0
186182

187183
if id == leader_id
188-
metadata = data[:collections_by_name][coll_name]
189-
return nil unless metadata
184+
raw_metadata = data[:collections_by_name][coll_name]
185+
return nil unless raw_metadata
186+
metadata = raw_metadata.merge(
187+
'merkle_root' => raw_metadata['merkle_root'] || ZERO_HEX_BYTES32
188+
)
190189
operation = 'create_collection_and_add_self'
191190
schema = OPERATION_SCHEMAS[operation]
192191
encoding_data = {
193192
'metadata' => build_metadata_object(metadata),
194-
'item' => build_item_object(item: item, item_id: id, item_index: item_index)
193+
'item' => build_item_object(item: item, item_index: item_index)
195194
}
196195
encoded_data = encode_operation(operation, encoding_data, schema)
197196
['erc-721-ethscriptions-collection'.b, operation.b, encoded_data.b]
@@ -200,7 +199,7 @@ def build_import_encoded_params(id)
200199
schema = OPERATION_SCHEMAS[operation]
201200
encoding_data = {
202201
'collection_id' => to_bytes32_hex(leader_id),
203-
'item' => build_item_object(item: item, item_id: id, item_index: item_index)
202+
'item' => build_item_object(item: item, item_index: item_index)
204203
}
205204
encoded_data = encode_operation(operation, encoding_data, schema)
206205
['erc-721-ethscriptions-collection'.b, operation.b, encoded_data.b]
@@ -269,7 +268,7 @@ def build_metadata_object(meta)
269268
twitter_link = safe_string(meta['twitter_link'])
270269
discord_link = safe_string(meta['discord_link'])
271270

272-
OrderedHash[
271+
result = OrderedHash[
273272
'name', name,
274273
'symbol', symbol,
275274
'max_supply', max_supply,
@@ -281,20 +280,25 @@ def build_metadata_object(meta)
281280
'twitter_link', twitter_link,
282281
'discord_link', discord_link
283282
]
283+
merkle_root = meta.fetch('merkle_root')
284+
result['merkle_root'] = to_bytes32_hex(merkle_root)
285+
result
284286
end
285287

286-
def build_item_object(item:, item_id:, item_index:)
288+
def build_item_object(item:, item_index:)
287289
attrs = Array(item['attributes']).map do |a|
288290
OrderedHash['trait_type', safe_string(a['trait_type']), 'value', safe_string(a['value'])]
289291
end
290292

293+
proofs = item.key?('merkle_proof') ? Array(item['merkle_proof']) : []
294+
291295
OrderedHash[
292296
'item_index', safe_uint_string(item_index),
293297
'name', safe_string(item['name']),
294298
'background_color', safe_string(item['background_color']),
295299
'description', safe_string(item['description']),
296300
'attributes', attrs,
297-
'merkle_proof', []
301+
'merkle_proof', proofs
298302
]
299303
end
300304

@@ -479,8 +483,8 @@ def validate_collection_metadata(value, field_name)
479483
raise ValidationError, "Expected object for #{field_name}"
480484
end
481485
# Expected keys for metadata (merkle_root optional)
482-
expected_min_keys = %w[name symbol max_supply description logo_image_uri banner_image_uri background_color website_link twitter_link discord_link]
483-
unless value.keys == expected_min_keys || value.keys == (expected_min_keys + ['merkle_root'])
486+
expected_keys = %w[name symbol max_supply description logo_image_uri banner_image_uri background_color website_link twitter_link discord_link merkle_root]
487+
unless value.keys == expected_keys
484488
raise ValidationError, "Invalid metadata keys or order"
485489
end
486490

@@ -495,7 +499,7 @@ def validate_collection_metadata(value, field_name)
495499
websiteLink: validate_string(value['website_link'], 'website_link'),
496500
twitterLink: validate_string(value['twitter_link'], 'twitter_link'),
497501
discordLink: validate_string(value['discord_link'], 'discord_link'),
498-
merkleRoot: value.key?('merkle_root') ? validate_bytes32(value['merkle_root'], 'merkle_root') : nil
502+
merkleRoot: validate_bytes32(value['merkle_root'], 'merkle_root')
499503
}
500504
end
501505

@@ -504,13 +508,11 @@ def validate_item(item)
504508
raise ValidationError, "Item must be an object"
505509
end
506510

507-
# Check exact key order - merkle_proof is always required
508511
unless item.keys == ITEM_KEYS
509512
expected = "[#{ITEM_KEYS.join(', ')}]"
510513
raise ValidationError, "Invalid item keys or order. Expected: #{expected}, got: [#{item.keys.join(', ')}]"
511514
end
512515

513-
# Validate each field - return in internal format for encoding
514516
{
515517
itemIndex: validate_uint256(item['item_index'], 'item_index'),
516518
name: validate_string(item['name'], 'name'),
@@ -562,8 +564,7 @@ def build_create_collection_values(data)
562564
data['website_link'],
563565
data['twitter_link'],
564566
data['discord_link'],
565-
# Append zero merkle root to satisfy contract struct shape
566-
["".ljust(64, '0')].pack('H*')
567+
data['merkle_root']
567568
]
568569
end
569570

@@ -612,22 +613,6 @@ def build_add_self_to_collection_values(data)
612613
[data['collection_id'], item_tuple]
613614
end
614615

615-
def build_add_items_batch_values(data)
616-
# Transform items to array format for encoding
617-
items_array = data['items'].map do |item|
618-
[
619-
item[:itemIndex],
620-
item[:name],
621-
item[:backgroundColor],
622-
item[:description],
623-
item[:attributes],
624-
item[:merkleProof]
625-
]
626-
end
627-
628-
[data['collection_id'], items_array]
629-
end
630-
631616
def build_remove_items_values(data)
632617
[data['collection_id'], data['ethscription_ids']]
633618
end
@@ -644,8 +629,7 @@ def build_edit_collection_values(data)
644629
data['discord_link']
645630
]
646631

647-
# Append zero merkle root if not provided in payload (parser schema omits it)
648-
values << ["".ljust(64, '0')].pack('H*')
632+
values << data['merkle_root']
649633
values
650634
end
651635

app/models/ethscription_transaction.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ def build_create_calldata
181181

182182
# Extract protocol params - returns [protocol, operation, encoded_data]
183183
# Pass the ethscription_id context so parsers can inject it when needed
184-
protocol, operation, encoded_data = ProtocolExtractor.for_calldata(
184+
protocol, operation, encoded_data = ProtocolParser.for_calldata(
185185
content_uri,
186186
ethscription_id: eth_transaction.transaction_hash
187187
)

0 commit comments

Comments
 (0)