Skip to content

Commit 9e20f57

Browse files
committed
Refactor ERC721 Ethscriptions Collection Management
- Updated the collection creation process to use `max_supply` instead of `total_supply`. - Introduced a new operation `create_and_add_self` for creating collections with associated items. - Enhanced validation methods for collection metadata and item attributes, including optional merkle proof handling. - Adjusted ABI types and internal data structures to accommodate the new parameters. - Updated tests to reflect changes in the collection management logic and ensure compatibility with the new structure.
1 parent 09dbf24 commit 9e20f57

10 files changed

Lines changed: 680 additions & 637 deletions

app/models/erc721_ethscriptions_collection_parser.rb

Lines changed: 113 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,13 @@ class Erc721EthscriptionsCollectionParser
99
# Operation schemas defining exact structure and ABI encoding
1010
OPERATION_SCHEMAS = {
1111
'create_collection' => {
12-
keys: %w[name symbol total_supply description logo_image_uri banner_image_uri background_color website_link twitter_link discord_link],
13-
abi_type: '(string,string,uint256,string,string,string,string,string,string,string)',
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.
14+
abi_type: '(string,string,uint256,string,string,string,string,string,string,string,bytes32)',
1415
validators: {
1516
'name' => :string,
1617
'symbol' => :string,
17-
'total_supply' => :uint256,
18+
'max_supply' => :uint256,
1819
'description' => :string,
1920
'logo_image_uri' => :string,
2021
'banner_image_uri' => :string,
@@ -24,9 +25,19 @@ class Erc721EthscriptionsCollectionParser
2425
'discord_link' => :string
2526
}
2627
},
28+
'create_and_add_self' => {
29+
keys: %w[metadata item],
30+
# ((CollectionParams),(ItemData)) with ItemData including bytes32[] merkle_proof
31+
abi_type: '((string,string,uint256,string,string,string,string,string,string,string,bytes32),(uint256,string,bytes32,string,string,(string,string)[],bytes32[]))',
32+
validators: {
33+
'metadata' => :collection_metadata,
34+
'item' => :single_item
35+
}
36+
},
2737
'add_items_batch' => {
2838
keys: %w[collection_id items],
29-
abi_type: '(bytes32,(uint256,string,bytes32,string,string,(string,string)[])[])',
39+
# Includes per-item merkle_proof (bytes32[]) as the final tuple element
40+
abi_type: '(bytes32,(uint256,string,bytes32,string,string,(string,string)[],bytes32[])[])',
3041
validators: {
3142
'collection_id' => :bytes32,
3243
'items' => :items_array
@@ -42,7 +53,8 @@ class Erc721EthscriptionsCollectionParser
4253
},
4354
'edit_collection' => {
4455
keys: %w[collection_id description logo_image_uri banner_image_uri background_color website_link twitter_link discord_link],
45-
abi_type: '(bytes32,string,string,string,string,string,string,string)',
56+
# Contract includes a bytes32 merkleRoot; append zero when omitted
57+
abi_type: '(bytes32,string,string,string,string,string,string,string,bytes32)',
4658
validators: {
4759
'collection_id' => :bytes32,
4860
'description' => :string,
@@ -84,7 +96,8 @@ class Erc721EthscriptionsCollectionParser
8496
}.freeze
8597

8698
# Item keys for add_items_batch validation
87-
ITEM_KEYS = %w[item_index name ethscription_id background_color description attributes].freeze
99+
ITEM_KEYS_MIN = %w[item_index name ethscription_id background_color description attributes].freeze
100+
ITEM_KEYS_WITH_PROOF = %w[item_index name ethscription_id background_color description attributes merkle_proof].freeze
88101

89102
# Attribute keys for NFT metadata
90103
ATTRIBUTE_KEYS = %w[trait_type value].freeze
@@ -153,6 +166,8 @@ def encode_operation(operation, data, schema)
153166
values = case operation
154167
when 'create_collection'
155168
build_create_collection_values(validated_data)
169+
when 'create_and_add_self'
170+
build_create_and_add_self_values(validated_data)
156171
when 'add_items_batch'
157172
build_add_items_batch_values(validated_data)
158173
when 'remove_items'
@@ -243,25 +258,68 @@ def validate_items_array(value, field_name)
243258
end
244259
end
245260

261+
def validate_single_item(value, field_name)
262+
unless value.is_a?(Hash)
263+
raise ValidationError, "Expected object for #{field_name}"
264+
end
265+
validate_item(value)
266+
end
267+
268+
def validate_collection_metadata(value, field_name)
269+
unless value.is_a?(Hash)
270+
raise ValidationError, "Expected object for #{field_name}"
271+
end
272+
# Expected keys for metadata (merkle_root optional)
273+
expected_min_keys = %w[name symbol max_supply description logo_image_uri banner_image_uri background_color website_link twitter_link discord_link]
274+
unless value.keys == expected_min_keys || value.keys == (expected_min_keys + ['merkle_root'])
275+
raise ValidationError, "Invalid metadata keys or order"
276+
end
277+
278+
{
279+
name: validate_string(value['name'], 'name'),
280+
symbol: validate_string(value['symbol'], 'symbol'),
281+
maxSupply: validate_uint256(value['max_supply'], 'max_supply'),
282+
description: validate_string(value['description'], 'description'),
283+
logoImageUri: validate_string(value['logo_image_uri'], 'logo_image_uri'),
284+
bannerImageUri: validate_string(value['banner_image_uri'], 'banner_image_uri'),
285+
backgroundColor: validate_string(value['background_color'], 'background_color'),
286+
websiteLink: validate_string(value['website_link'], 'website_link'),
287+
twitterLink: validate_string(value['twitter_link'], 'twitter_link'),
288+
discordLink: validate_string(value['discord_link'], 'discord_link'),
289+
merkleRoot: value.key?('merkle_root') ? validate_bytes32(value['merkle_root'], 'merkle_root') : nil
290+
}
291+
end
292+
246293
def validate_item(item)
247294
unless item.is_a?(Hash)
248295
raise ValidationError, "Item must be an object"
249296
end
250297

251-
# Check exact key order
252-
unless item.keys == ITEM_KEYS
253-
raise ValidationError, "Invalid item keys or order. Expected: #{ITEM_KEYS.join(',')}, got: #{item.keys.join(',')}"
254-
end
298+
# Check exact key order; allow optional merkle_proof
299+
has_proof =
300+
if item.keys == ITEM_KEYS_WITH_PROOF
301+
true
302+
elsif item.keys == ITEM_KEYS_MIN
303+
false
304+
else
305+
expected = "[#{ITEM_KEYS_MIN.join(', ')}] or [#{ITEM_KEYS_WITH_PROOF.join(', ')}]"
306+
raise ValidationError, "Invalid item keys or order. Expected: #{expected}, got: [#{item.keys.join(', ')}]"
307+
end
255308

256309
# Validate each field - return in internal format for encoding
257-
{
310+
result = {
258311
itemIndex: validate_uint256(item['item_index'], 'item_index'),
259312
name: validate_string(item['name'], 'name'),
260313
ethscriptionId: validate_bytes32(item['ethscription_id'], 'ethscription_id'),
261314
backgroundColor: validate_string(item['background_color'], 'background_color'),
262315
description: validate_string(item['description'], 'description'),
263316
attributes: validate_attributes_array(item['attributes'], 'attributes')
264317
}
318+
319+
# Optional merkle proof; always include as bytes32[] (empty when omitted)
320+
result[:merkleProof] = has_proof ? validate_bytes32_array(item['merkle_proof'], 'merkle_proof') : []
321+
322+
result
265323
end
266324

267325
def validate_attributes_array(value, field_name)
@@ -297,15 +355,50 @@ def build_create_collection_values(data)
297355
[
298356
data['name'],
299357
data['symbol'],
300-
data['total_supply'],
358+
data['max_supply'],
301359
data['description'],
302360
data['logo_image_uri'],
303361
data['banner_image_uri'],
304362
data['background_color'],
305363
data['website_link'],
306364
data['twitter_link'],
307-
data['discord_link']
365+
data['discord_link'],
366+
# Append zero merkle root to satisfy contract struct shape
367+
["".ljust(64, '0')].pack('H*')
368+
]
369+
end
370+
371+
def build_create_and_add_self_values(data)
372+
meta = data['metadata']
373+
item = data['item']
374+
375+
# Metadata tuple with optional merkleRoot
376+
merkle_root = meta[:merkleRoot] || ["".ljust(64, '0')].pack('H*')
377+
metadata_tuple = [
378+
meta[:name],
379+
meta[:symbol],
380+
meta[:maxSupply],
381+
meta[:description],
382+
meta[:logoImageUri],
383+
meta[:bannerImageUri],
384+
meta[:backgroundColor],
385+
meta[:websiteLink],
386+
meta[:twitterLink],
387+
meta[:discordLink],
388+
merkle_root
389+
]
390+
391+
item_tuple = [
392+
item[:itemIndex],
393+
item[:name],
394+
item[:ethscriptionId],
395+
item[:backgroundColor],
396+
item[:description],
397+
item[:attributes],
398+
item[:merkleProof]
308399
]
400+
401+
[metadata_tuple, item_tuple]
309402
end
310403

311404
def build_add_items_batch_values(data)
@@ -317,7 +410,8 @@ def build_add_items_batch_values(data)
317410
item[:ethscriptionId],
318411
item[:backgroundColor],
319412
item[:description],
320-
item[:attributes]
413+
item[:attributes],
414+
item[:merkleProof]
321415
]
322416
end
323417

@@ -329,7 +423,7 @@ def build_remove_items_values(data)
329423
end
330424

331425
def build_edit_collection_values(data)
332-
[
426+
values = [
333427
data['collection_id'],
334428
data['description'],
335429
data['logo_image_uri'],
@@ -339,6 +433,10 @@ def build_edit_collection_values(data)
339433
data['twitter_link'],
340434
data['discord_link']
341435
]
436+
437+
# Append zero merkle root if not provided in payload (parser schema omits it)
438+
values << ["".ljust(64, '0')].pack('H*')
439+
values
342440
end
343441

344442
def build_edit_collection_item_values(data)

0 commit comments

Comments
 (0)