@@ -18,11 +18,14 @@ class EthscriptionTransaction < T::Struct
1818
1919 # Transfer operation fields
2020 prop :ethscription_id , T . nilable ( String )
21+ prop :transfer_ids , T . nilable ( T ::Array [ String ] )
2122 prop :transfer_from_address , T . nilable ( String )
2223 prop :transfer_to_address , T . nilable ( String )
2324 prop :enforced_previous_owner , T . nilable ( String )
24- prop :input_index , T . nilable ( Integer )
25- prop :log_index , T . nilable ( Integer )
25+
26+ # Unified source tracking
27+ prop :source_type , T . nilable ( Symbol ) # :input or :event
28+ prop :source_index , T . nilable ( Integer )
2629
2730 # Debug info (can be removed if not needed)
2831 prop :ethscription_operation , T . nilable ( String ) # 'create', 'transfer', 'transfer_with_previous_owner'
@@ -33,40 +36,9 @@ class EthscriptionTransaction < T::Struct
3336 GAS_LIMIT = 1_000_000_000
3437 TO_ADDRESS = SysConfig ::ETHSCRIPTIONS_ADDRESS
3538
36- class << self
37- def reset_seen_creates!
38- @seen_creates = Concurrent ::Set . new
39- end
40-
41- def add_seen_create ( tx_hash )
42- seen_creates . add ( tx_hash )
43- end
44-
45- # Access seen creates set - requires reset_seen_creates! to be called first
46- def seen_creates
47- raise "Must call reset_seen_creates! first" unless @seen_creates
48- @seen_creates
49- end
50- end
51-
52- # Dynamic source hash based on operation type
39+ # Dynamic source hash based on source type and index
5340 def source_hash
54- case ethscription_operation
55- when 'create'
56- # Creates are one-to-one with transactions
57- eth_transaction . transaction_hash
58- when 'transfer' , 'transfer_with_previous_owner'
59- # Transfers need unique hash within transaction
60- if input_index
61- compute_transfer_source_hash ( :input , input_index )
62- elsif log_index
63- compute_transfer_source_hash ( :event , log_index )
64- else
65- raise "Transfer must have either input_index or log_index"
66- end
67- else
68- raise "Unknown ethscription operation: #{ ethscription_operation } "
69- end
41+ compute_source_hash ( source_type , source_index )
7042 end
7143
7244
@@ -75,14 +47,18 @@ def self.create_ethscription(
7547 eth_transaction :,
7648 creator :,
7749 initial_owner :,
78- content_uri :
50+ content_uri :,
51+ source_type :,
52+ source_index :
7953 )
8054 new (
8155 from_address : Address20 . from_hex ( creator . is_a? ( String ) ? creator : creator . to_hex ) ,
8256 eth_transaction : eth_transaction ,
8357 creator : creator ,
8458 initial_owner : initial_owner ,
8559 content_uri : content_uri ,
60+ source_type : source_type &.to_sym ,
61+ source_index : source_index ,
8662 ethscription_operation : 'create'
8763 )
8864 end
@@ -94,8 +70,8 @@ def self.transfer_ethscription(
9470 to_address :,
9571 ethscription_id :,
9672 enforced_previous_owner : nil ,
97- input_index : nil ,
98- log_index : nil
73+ source_type : ,
74+ source_index :
9975 )
10076 operation_type = enforced_previous_owner ? 'transfer_with_previous_owner' : 'transfer'
10177
@@ -106,54 +82,55 @@ def self.transfer_ethscription(
10682 transfer_from_address : from_address ,
10783 transfer_to_address : to_address ,
10884 enforced_previous_owner : enforced_previous_owner ,
109- input_index : input_index ,
110- log_index : log_index ,
85+ source_type : source_type &. to_sym ,
86+ source_index : source_index ,
11187 ethscription_operation : operation_type
11288 )
11389 end
11490
115- # Instance method to compute transfer source hash
116- def compute_transfer_source_hash ( operation_type , index )
117- tag = ( operation_type == :input ) ? 0 : 1
118- payload = ByteString . from_bin (
119- eth_transaction . transaction_hash . to_bin +
120- Eth ::Util . zpad_int ( tag , 32 ) +
121- Eth ::Util . zpad_int ( index , 32 )
122- )
123- bin_val = Eth ::Util . keccak256 (
124- Eth ::Util . zpad_int ( 2 , 32 ) + Eth ::Util . keccak256 ( payload . to_bin )
91+ # Factory method for transferMultipleEthscriptions (inputs only)
92+ def self . transfer_multiple_ethscriptions (
93+ eth_transaction :,
94+ from_address :,
95+ to_address :,
96+ ethscription_ids :,
97+ source_type : :input ,
98+ source_index : 0
99+ )
100+ new (
101+ from_address : Address20 . from_hex ( from_address . is_a? ( String ) ? from_address : from_address . to_hex ) ,
102+ eth_transaction : eth_transaction ,
103+ transfer_ids : ethscription_ids ,
104+ transfer_from_address : from_address ,
105+ transfer_to_address : to_address ,
106+ source_type : source_type &.to_sym ,
107+ source_index : source_index ,
108+ ethscription_operation : 'transfer'
125109 )
126- Hash32 . from_bin ( bin_val )
127110 end
128111
129- # Check if this is a valid, unseen ethscription
130- def valid_and_unseen?
131- valid_ethscription? && !already_seen?
132- end
112+ # Unified source hash computation following Optimism pattern
113+ def compute_source_hash ( operation_source , index )
114+ raise "Operation must have source metadata" if operation_source . nil? || index . nil?
133115
134- # Mark this transaction as seen (for create deduplication)
135- def mark_as_seen!
136- if ethscription_operation == 'create'
137- self . class . add_seen_create ( source_hash )
138- end
139- end
116+ source_tag = operation_source . to_s # "input" or "event"
117+ source_tag_hash = Eth ::Util . keccak256 ( source_tag . bytes . pack ( 'C*' ) ) # Hash for constant width
140118
141- # Check if we've already seen this create transaction
142- def already_seen?
143- return false unless ethscription_operation == 'create'
144- self . class . seen_creates . include? ( source_hash )
145- end
119+ # Get function selector from input for operation type safety
120+ function_selector = input . to_bin [ 0 ...4 ]
146121
147- # Check if this is a valid ethscription
148- def valid_ethscription?
149- case ethscription_operation
150- when 'create'
151- valid_create?
152- when 'transfer' , 'transfer_with_previous_owner'
153- valid_transfer?
154- else
155- raise "Unknown ethscription operation: #{ ethscription_operation } "
156- end
122+ payload = ByteString . from_bin (
123+ eth_transaction . block_hash . to_bin +
124+ source_tag_hash + # 32 bytes (hashed source tag)
125+ function_selector + # 4 bytes (function selector)
126+ Eth ::Util . zpad_int ( index , 32 ) # 32 bytes (index)
127+ )
128+
129+ bin_val = Eth ::Util . keccak256 (
130+ Eth ::Util . zpad_int ( 0 , 32 ) + Eth ::Util . keccak256 ( payload . to_bin ) # Domain 0 like Optimism
131+ )
132+
133+ Hash32 . from_bin ( bin_val )
157134 end
158135
159136 def valid_create?
@@ -165,7 +142,21 @@ def valid_create?
165142
166143 def valid_transfer?
167144 # Basic field validation - if we extracted the data properly, ABI encoding should work
168- ethscription_id . present? &&
145+ case ethscription_operation
146+ when 'transfer'
147+ if transfer_ids
148+ # Multiple transfer (input-based)
149+ transfer_ids . is_a? ( Array ) && transfer_ids . any?
150+ else
151+ # Single transfer (event-based)
152+ ethscription_id . present?
153+ end
154+ when 'transfer_with_previous_owner'
155+ # Always single transfer (event-based only)
156+ ethscription_id . present?
157+ else
158+ false
159+ end &&
169160 transfer_from_address . present? &&
170161 transfer_to_address . present?
171162 end
@@ -178,7 +169,11 @@ def input
178169 when 'create'
179170 ByteString . from_bin ( build_create_calldata )
180171 when 'transfer'
181- ByteString . from_bin ( build_transfer_calldata )
172+ if transfer_ids && transfer_ids . any?
173+ ByteString . from_bin ( build_transfer_multiple_calldata )
174+ else
175+ ByteString . from_bin ( build_transfer_calldata )
176+ end
182177 when 'transfer_with_previous_owner'
183178 ByteString . from_bin ( build_transfer_with_previous_owner_calldata )
184179 else
@@ -282,6 +277,18 @@ def build_transfer_with_previous_owner_calldata
282277 ( function_sig + encoded ) . b
283278 end
284279
280+ def build_transfer_multiple_calldata
281+ # Get function selector as binary
282+ function_sig = Eth ::Util . keccak256 ( 'transferMultipleEthscriptions(bytes32[],address)' ) [ 0 ...4 ] . b
283+
284+ ids_bin = ( transfer_ids || [ ] ) . map { |id | hex_to_bin ( id ) }
285+ to_bin = address_to_bin ( transfer_to_address )
286+
287+ encoded = Eth ::Abi . encode ( [ 'bytes32[]' , 'address' ] , [ ids_bin , to_bin ] )
288+
289+ ( function_sig + encoded ) . b
290+ end
291+
285292 # Helper to convert hex string to binary
286293 def hex_to_bin ( hex_str )
287294 return nil unless hex_str
@@ -302,4 +309,4 @@ def address_to_bin(addr_str)
302309 clean_hex = clean_hex . rjust ( 40 , '0' ) [ -40 ..]
303310 [ clean_hex ] . pack ( 'H*' )
304311 end
305- end
312+ end
0 commit comments