@@ -28,8 +28,8 @@ class Erc721EthscriptionsCollectionParser
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)) with ethscription_id in ItemData
32- abi_type : '((string,string,uint256,string,string,string,string,string,string,string,bytes32),(uint256,string,bytes32, string,string,(string,string)[],bytes32[]))' ,
31+ # ((CollectionParams),(ItemData)) - ItemData without ethscription_id (item refers to itself)
32+ 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 ,
3535 'item' => :single_item
@@ -38,7 +38,7 @@ class Erc721EthscriptionsCollectionParser
3838 # Legacy alias retained for backwards compatibility
3939 'create_and_add_self' => {
4040 keys : %w[ metadata item ] ,
41- abi_type : '((string,string,uint256,string,string,string,string,string,string,string,bytes32),(uint256,string,bytes32, string,string,(string,string)[],bytes32[]))' ,
41+ abi_type : '((string,string,uint256,string,string,string,string,string,string,string,bytes32),(uint256,string,string,string,(string,string)[],bytes32[]))' ,
4242 validators : {
4343 'metadata' => :collection_metadata ,
4444 'item' => :single_item
@@ -47,21 +47,12 @@ class Erc721EthscriptionsCollectionParser
4747 # New single-item add op; keep legacy batch below for compatibility
4848 'add_self_to_collection' => {
4949 keys : %w[ collection_id item ] ,
50- abi_type : '(bytes32,(uint256,string,bytes32, string,string,(string,string)[],bytes32[]))' ,
50+ abi_type : '(bytes32,(uint256,string,string,string,(string,string)[],bytes32[]))' ,
5151 validators : {
5252 'collection_id' => :bytes32 ,
5353 'item' => :single_item
5454 }
5555 } ,
56- 'add_items_batch' => {
57- keys : %w[ collection_id items ] ,
58- # Includes ethscription_id in each item
59- abi_type : '(bytes32,(uint256,string,bytes32,string,string,(string,string)[],bytes32[])[])' ,
60- validators : {
61- 'collection_id' => :bytes32 ,
62- 'items' => :items_array
63- }
64- } ,
6556 'remove_items' => {
6657 keys : %w[ collection_id ethscription_ids ] ,
6758 abi_type : '(bytes32,bytes32[])' ,
@@ -103,37 +94,29 @@ class Erc721EthscriptionsCollectionParser
10394 validators : {
10495 'collection_id' => :bytes32
10596 }
106- } ,
107- 'sync_ownership' => {
108- keys : %w[ collection_id ethscription_ids ] ,
109- abi_type : '(bytes32,bytes32[])' ,
110- validators : {
111- 'collection_id' => :bytes32 ,
112- 'ethscription_ids' => :bytes32_array
113- }
11497 }
11598 } . freeze
11699
117- # Item keys for validation (includes ethscription_id; item must refer to itself)
118- ITEM_KEYS_MIN = %w[ item_index name ethscription_id background_color description attributes ] . freeze
119- ITEM_KEYS_WITH_PROOF = %w[ item_index name ethscription_id background_color description attributes merkle_proof ] . freeze
100+ # Item keys for validation (ethscription_id removed - item refers to itself)
101+ # merkle_proof is always required (can be empty array)
102+ ITEM_KEYS = %w[ item_index name background_color description attributes merkle_proof ] . freeze
120103
121104 # Attribute keys for NFT metadata
122105 ATTRIBUTE_KEYS = %w[ trait_type value ] . freeze
123106
124107 class ValidationError < StandardError ; end
125108
126- DEFAULT_ITEMS_PATH = ENV [ 'COLLECTIONS_ITEMS_PATH' ] || 'items_by_ethscription.json'
127- DEFAULT_COLLECTIONS_PATH = ENV [ 'COLLECTIONS_META_PATH' ] || 'collections_by_name.json'
109+ DEFAULT_ITEMS_PATH = ENV [ 'COLLECTIONS_ITEMS_PATH' ] || Rails . root . join ( 'items_by_ethscription.json' )
110+ DEFAULT_COLLECTIONS_PATH = ENV [ 'COLLECTIONS_META_PATH' ] || Rails . root . join ( 'collections_by_name.json' )
128111
129- def self . extract ( content_uri , ethscription_id : nil , import_fallback : false , items_path : DEFAULT_ITEMS_PATH , collections_path : DEFAULT_COLLECTIONS_PATH , cutoff_number : nil )
130- new . extract ( content_uri , ethscription_id : ethscription_id , import_fallback : import_fallback , items_path : items_path , collections_path : collections_path , cutoff_number : cutoff_number )
112+ def self . extract ( content_uri , ethscription_id : nil )
113+ new . extract ( content_uri , ethscription_id : ethscription_id )
131114 end
132115
133- def extract ( content_uri , ethscription_id : nil , import_fallback : false , items_path : DEFAULT_ITEMS_PATH , collections_path : DEFAULT_COLLECTIONS_PATH , cutoff_number : nil )
134- # Import-aware path takes precedence if enabled and id is provided
135- if import_fallback && ethscription_id
136- if ( encoded = build_import_encoded_params ( ethscription_id . to_s . downcase , items_path : items_path , collections_path : collections_path , cutoff_number : cutoff_number ) )
116+ def extract ( content_uri , ethscription_id : nil )
117+ # Import-aware path takes precedence if id is provided
118+ if ethscription_id
119+ if ( encoded = build_import_encoded_params ( ethscription_id . to_hex ) )
137120 return encoded
138121 end
139122 end
@@ -169,7 +152,7 @@ def extract(content_uri, ethscription_id: nil, import_fallback: false, items_pat
169152
170153 # Remove protocol fields for encoding
171154 encoding_data = data . reject { |k , _ | k == 'p' || k == 'op' }
172-
155+
173156 # Validate field types and encode
174157 encoded_data = encode_operation ( operation , encoding_data , schema )
175158
@@ -181,31 +164,28 @@ def extract(content_uri, ethscription_id: nil, import_fallback: false, items_pat
181164 end
182165 end
183166
184- private
185-
186167 # -------------------- Import fallback --------------------
187168
188169 # Returns [protocol, operation, encoded_data] or nil
189- def build_import_encoded_params ( id , items_path :, collections_path :, cutoff_number : nil )
190- load_import_data ( items_path : items_path , collections_path : collections_path )
170+ def build_import_encoded_params ( id )
171+ data = self . class . load_import_data (
172+ items_path : DEFAULT_ITEMS_PATH ,
173+ collections_path : DEFAULT_COLLECTIONS_PATH
174+ )
191175
192- item = @__items_by_id [ id ]
176+ item = data [ :items_by_id ] [ id ]
193177 return nil unless item
194178
195- if cutoff_number && safe_uint ( item [ 'ethscription_number' ] ) > cutoff_number
196- return nil
197- end
198-
199179 coll_name = item [ 'collection_name' ]
200180 return nil unless coll_name
201181
202- leader_id = @__leader_by_collection [ coll_name ]
182+ leader_id = data [ :leader_by_collection ] [ coll_name ]
203183 return nil unless leader_id
204184
205- item_index = @__zero_index_by_id [ id ] || 0
185+ item_index = data [ :zero_index_by_id ] [ id ] || 0
206186
207187 if id == leader_id
208- metadata = @__collections_by_name [ coll_name ]
188+ metadata = data [ :collections_by_name ] [ coll_name ]
209189 return nil unless metadata
210190 operation = 'create_collection_and_add_self'
211191 schema = OPERATION_SCHEMAS [ operation ]
@@ -227,50 +207,53 @@ def build_import_encoded_params(id, items_path:, collections_path:, cutoff_numbe
227207 end
228208 end
229209
230- def load_import_data ( items_path :, collections_path :)
231- # Memoize parsed JSON and derived leader/index maps
232- if @__loaded_items_path == items_path && @__loaded_collections_path == collections_path && @__items_by_id
233- return
234- end
210+ class << self
211+ include Memery
235212
236- items = JSON . parse ( File . read ( items_path ) )
237- collections = JSON . parse ( File . read ( collections_path ) )
213+ def load_import_data ( items_path :, collections_path :)
214+ items = JSON . parse ( File . read ( items_path ) )
215+ collections = JSON . parse ( File . read ( collections_path ) )
238216
239- @__items_by_id = { }
240- items . each { |k , v | @__items_by_id [ k . to_s . downcase ] = v }
241- @__collections_by_name = collections
217+ items_by_id = { }
218+ items . each { |k , v | items_by_id [ k . to_s . downcase ] = v }
242219
243- # Group items by collection and derive leader (min ethscription_number)
244- groups = Hash . new { |h , k | h [ k ] = [ ] }
245- @__items_by_id . each do |iid , it |
246- cname = it [ 'collection_name' ]
247- next unless cname . is_a? ( String ) && !cname . empty?
248- num = safe_uint ( it [ 'ethscription_number' ] )
249- groups [ cname ] << [ iid , num ]
250- end
220+ # Group items by collection and derive leader (min ethscription_number)
221+ groups = Hash . new { |h , k | h [ k ] = [ ] }
222+ items_by_id . each do |iid , it |
223+ cname = it [ 'collection_name' ]
224+ next unless cname . is_a? ( String ) && !cname . empty?
225+ num = it [ 'ethscription_number' ] . to_i
226+ groups [ cname ] << [ iid , num ]
227+ end
251228
252- @__leader_by_collection = { }
253- groups . each do |cname , pairs |
254- next if pairs . empty?
255- @__leader_by_collection [ cname ] = pairs . min_by { |_id , num | num } [ 0 ]
256- end
229+ leader_by_collection = { }
230+ groups . each do |cname , pairs |
231+ next if pairs . empty?
232+ leader_by_collection [ cname ] = pairs . min_by { |_id , num | num } [ 0 ]
233+ end
257234
258- # Normalize item indices to zero-based
259- @__zero_index_by_id = { }
260- groups . each do |_cname , pairs |
261- explicit = pairs . map { |( iid , _ ) | [ iid , @__items_by_id [ iid ] [ 'index' ] ] }
262- explicit_indices = explicit . filter_map { |_iid , idx | idx if idx . is_a? ( Integer ) }
263- if explicit_indices . size == pairs . size
264- min_idx = explicit_indices . min
265- offset = ( min_idx == 0 ) ? 0 : 1
266- explicit . each { |iid , idx | @__zero_index_by_id [ iid ] = [ idx - offset , 0 ] . max }
267- else
268- pairs . sort_by { |_iid , num | num } . each_with_index { |( iid , _ ) , i | @__zero_index_by_id [ iid ] = i }
235+ # Normalize item indices to zero-based
236+ zero_index_by_id = { }
237+ groups . each do |_cname , pairs |
238+ explicit = pairs . map { |( iid , _ ) | [ iid , items_by_id [ iid ] [ 'index' ] ] }
239+ explicit_indices = explicit . filter_map { |_iid , idx | idx if idx . is_a? ( Integer ) }
240+ if explicit_indices . size == pairs . size
241+ min_idx = explicit_indices . min
242+ offset = ( min_idx == 0 ) ? 0 : 1
243+ explicit . each { |iid , idx | zero_index_by_id [ iid ] = [ idx - offset , 0 ] . max }
244+ else
245+ pairs . sort_by { |_iid , num | num } . each_with_index { |( iid , _ ) , i | zero_index_by_id [ iid ] = i }
246+ end
269247 end
270- end
271248
272- @__loaded_items_path = items_path
273- @__loaded_collections_path = collections_path
249+ {
250+ items_by_id : items_by_id ,
251+ collections_by_name : collections ,
252+ leader_by_collection : leader_by_collection ,
253+ zero_index_by_id : zero_index_by_id
254+ }
255+ end
256+ memoize :load_import_data
274257 end
275258
276259 # Build ordered JSON objects to match strict parser expectations
@@ -308,10 +291,10 @@ def build_item_object(item:, item_id:, item_index:)
308291 OrderedHash [
309292 'item_index' , safe_uint_string ( item_index ) ,
310293 'name' , safe_string ( item [ 'name' ] ) ,
311- 'ethscription_id' , to_bytes32_hex ( item_id ) ,
312294 'background_color' , safe_string ( item [ 'background_color' ] ) ,
313295 'description' , safe_string ( item [ 'description' ] ) ,
314- 'attributes' , attrs
296+ 'attributes' , attrs ,
297+ 'merkle_proof' , [ ]
315298 ]
316299 end
317300
@@ -370,8 +353,6 @@ def encode_operation(operation, data, schema)
370353 build_create_collection_values ( validated_data )
371354 when 'create_collection_and_add_self' , 'create_and_add_self'
372355 build_create_and_add_self_values ( validated_data )
373- when 'add_items_batch'
374- build_add_items_batch_values ( validated_data )
375356 when 'add_self_to_collection'
376357 build_add_self_to_collection_values ( validated_data )
377358 when 'remove_items'
@@ -382,14 +363,38 @@ def encode_operation(operation, data, schema)
382363 build_edit_collection_item_values ( validated_data )
383364 when 'lock_collection'
384365 build_lock_collection_values ( validated_data )
385- when 'sync_ownership'
386- build_sync_ownership_values ( validated_data )
387366 else
388367 raise ValidationError , "Unknown operation: #{ operation } "
389368 end
390369
391370 # Use ABI type from schema for encoding
392- Eth ::Abi . encode ( [ schema [ :abi_type ] ] , [ values ] )
371+ begin
372+ Eth ::Abi . encode ( [ schema [ :abi_type ] ] , [ values ] )
373+ rescue Encoding ::CompatibilityError => e
374+ Rails . logger . error "=== Collection ABI Encoding Error ==="
375+ Rails . logger . error "Error: #{ e . message } "
376+ Rails . logger . error "operation: #{ operation } "
377+ Rails . logger . error "schema abi_type: #{ schema [ :abi_type ] } "
378+ Rails . logger . error "values inspection:"
379+ log_encoding_details ( values )
380+ raise
381+ end
382+ end
383+
384+ def log_encoding_details ( obj , indent = 0 )
385+ prefix = " " * indent
386+ case obj
387+ when Array
388+ Rails . logger . error "#{ prefix } Array[#{ obj . size } ]:"
389+ obj . each_with_index do |item , idx |
390+ Rails . logger . error "#{ prefix } [#{ idx } ]:"
391+ log_encoding_details ( item , indent + 2 )
392+ end
393+ when String
394+ Rails . logger . error "#{ prefix } String: #{ obj . inspect [ 0 ..100 ] } , encoding: #{ obj . encoding . name } , bytesize: #{ obj . bytesize } "
395+ else
396+ Rails . logger . error "#{ prefix } #{ obj . class } : #{ obj . inspect [ 0 ..100 ] } "
397+ end
393398 end
394399
395400 def validate_fields ( data , validators )
@@ -415,7 +420,7 @@ def validate_string(value, field_name)
415420 unless value . is_a? ( String )
416421 raise ValidationError , "Field #{ field_name } must be a string, got #{ value . class . name } "
417422 end
418- value
423+ value . b
419424 end
420425
421426 def validate_uint256 ( value , field_name )
@@ -499,31 +504,21 @@ def validate_item(item)
499504 raise ValidationError , "Item must be an object"
500505 end
501506
502- # Check exact key order; allow optional merkle_proof
503- has_proof =
504- if item . keys == ITEM_KEYS_WITH_PROOF
505- true
506- elsif item . keys == ITEM_KEYS_MIN
507- false
508- else
509- expected = "[#{ ITEM_KEYS_MIN . join ( ', ' ) } ] or [#{ ITEM_KEYS_WITH_PROOF . join ( ', ' ) } ]"
510- raise ValidationError , "Invalid item keys or order. Expected: #{ expected } , got: [#{ item . keys . join ( ', ' ) } ]"
511- end
507+ # Check exact key order - merkle_proof is always required
508+ unless item . keys == ITEM_KEYS
509+ expected = "[#{ ITEM_KEYS . join ( ', ' ) } ]"
510+ raise ValidationError , "Invalid item keys or order. Expected: #{ expected } , got: [#{ item . keys . join ( ', ' ) } ]"
511+ end
512512
513513 # Validate each field - return in internal format for encoding
514- result = {
514+ {
515515 itemIndex : validate_uint256 ( item [ 'item_index' ] , 'item_index' ) ,
516516 name : validate_string ( item [ 'name' ] , 'name' ) ,
517- ethscriptionId : validate_bytes32 ( item [ 'ethscription_id' ] , 'ethscription_id' ) ,
518517 backgroundColor : validate_string ( item [ 'background_color' ] , 'background_color' ) ,
519518 description : validate_string ( item [ 'description' ] , 'description' ) ,
520- attributes : validate_attributes_array ( item [ 'attributes' ] , 'attributes' )
519+ attributes : validate_attributes_array ( item [ 'attributes' ] , 'attributes' ) ,
520+ merkleProof : validate_bytes32_array ( item [ 'merkle_proof' ] , 'merkle_proof' )
521521 }
522-
523- # Optional merkle proof; always include as bytes32[] (empty when omitted)
524- result [ :merkleProof ] = has_proof ? validate_bytes32_array ( item [ 'merkle_proof' ] , 'merkle_proof' ) : [ ]
525-
526- result
527522 end
528523
529524 def validate_attributes_array ( value , field_name )
@@ -595,7 +590,6 @@ def build_create_and_add_self_values(data)
595590 item_tuple = [
596591 item [ :itemIndex ] ,
597592 item [ :name ] ,
598- item [ :ethscriptionId ] ,
599593 item [ :backgroundColor ] ,
600594 item [ :description ] ,
601595 item [ :attributes ] ,
@@ -610,7 +604,6 @@ def build_add_self_to_collection_values(data)
610604 item_tuple = [
611605 item [ :itemIndex ] ,
612606 item [ :name ] ,
613- item [ :ethscriptionId ] ,
614607 item [ :backgroundColor ] ,
615608 item [ :description ] ,
616609 item [ :attributes ] ,
@@ -625,7 +618,6 @@ def build_add_items_batch_values(data)
625618 [
626619 item [ :itemIndex ] ,
627620 item [ :name ] ,
628- item [ :ethscriptionId ] ,
629621 item [ :backgroundColor ] ,
630622 item [ :description ] ,
631623 item [ :attributes ] ,
@@ -672,8 +664,4 @@ def build_lock_collection_values(data)
672664 # Single bytes32, not a tuple - but we need to return just the value
673665 data [ 'collection_id' ]
674666 end
675-
676- def build_sync_ownership_values ( data )
677- [ data [ 'collection_id' ] , data [ 'ethscription_ids' ] ]
678- end
679667end
0 commit comments