-
Notifications
You must be signed in to change notification settings - Fork 25
Expand file tree
/
Copy pathprotocol_parser.rb
More file actions
216 lines (176 loc) · 6.8 KB
/
Copy pathprotocol_parser.rb
File metadata and controls
216 lines (176 loc) · 6.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
# Unified protocol parser that delegates to specific protocol parsers
class ProtocolParser
# Default return value for all parsers - unified 3-element format
DEFAULT_PARAMS = [''.b, ''.b, ''.b].freeze
# Protocol name to parser class mapping
PROTOCOL_PARSERS = {
'erc-20' => Erc20FixedDenominationParser,
'erc-20-fixed-denomination' => Erc20FixedDenominationParser,
'erc-721-ethscriptions-collection' => Erc721EthscriptionsCollectionParser
}.freeze
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)
if parsed.nil?
# If we have an ethscription_id, try import fallback for collections regardless of content
if ethscription_id
# Get decoded content for import fallback
decoded_content = nil
if content_uri.is_a?(String) && DataUri.valid?(content_uri)
data_uri = DataUri.new(content_uri)
decoded_content = data_uri.decoded_data
end
# Try collections parser for import fallback with any content
# Ensure decoded_content is binary to avoid encoding issues
encoded = Erc721EthscriptionsCollectionParser.validate_and_encode(
decoded_content: (decoded_content || '').b,
operation: nil,
params: {},
source: :json,
ethscription_id: ethscription_id,
eth_transaction: eth_transaction
)
if encoded != DEFAULT_PARAMS
protocol, operation, encoded_data = encoded
return {
type: :erc721_ethscriptions_collection,
protocol: protocol,
operation: operation,
params: nil,
encoded_params: encoded_data
}
end
end
return nil
end
# Direct routing - no "try" needed since we know the protocol
parser_class = PROTOCOL_PARSERS[parsed[:protocol_name]]
return nil unless parser_class
# Call the same method on all parsers with unified interface
encoded = parser_class.validate_and_encode(
decoded_content: parsed[:decoded_content],
operation: parsed[:operation],
params: parsed[:params],
source: parsed[:source],
ethscription_id: ethscription_id,
eth_transaction: eth_transaction
)
# Check if parsing succeeded
return nil if encoded == DEFAULT_PARAMS
protocol, operation, encoded_data = encoded
# Derive type from parser class name
type = parser_class.name.underscore.sub(/_parser$/, '').to_sym
{
type: type,
protocol: protocol,
operation: operation,
params: nil,
encoded_params: encoded_data
}
end
# Get protocol data formatted for L2 calldata
# Returns [protocol, operation, encoded_data] for contract consumption
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
DEFAULT_PARAMS
else
# All parsers return the same format, so we can just extract directly
[result[:protocol], result[:operation], result[:encoded_params]]
end
end
private
# Parse data URI and extract protocol information from JSON body or headers
# Returns hash with: decoded_content, protocol_name, operation, params, source
# Note: content_hash removed - parsers compute their own if needed
def self.parse_data_uri_and_protocol(content_uri)
return nil unless content_uri.is_a?(String)
return nil unless DataUri.valid?(content_uri)
data_uri = DataUri.new(content_uri)
decoded_content = data_uri.decoded_data
return nil unless decoded_content.is_a?(String)
# Try to extract protocol from JSON body
json_protocol = extract_json_protocol(decoded_content)
# Try to extract protocol from headers
header_protocol = extract_header_protocol(data_uri)
# Fail if both present (ambiguous)
return nil if json_protocol && header_protocol
# Get protocol info from whichever source exists
protocol_info = json_protocol || header_protocol
return nil unless protocol_info
{
decoded_content: decoded_content,
protocol_name: protocol_info[:protocol],
operation: protocol_info[:operation],
params: protocol_info[:params],
source: protocol_info[:source]
}
end
# Extract protocol info from JSON body
def self.extract_json_protocol(decoded_content)
return nil unless decoded_content.lstrip.start_with?('{')
data = JSON.parse(decoded_content)
return nil unless data.is_a?(Hash)
protocol = data['p'] || data['protocol']
operation = data['op'] || data['operation'] || data['type']
return nil unless protocol.is_a?(String) && operation.is_a?(String)
# Return protocol info with full JSON data as params
{
protocol: protocol,
operation: operation,
params: data,
source: :json
}
rescue JSON::ParserError
nil
end
# Extract protocol info from data URI headers (;p=...;op=...;d=...)
def self.extract_header_protocol(data_uri)
params_map = parse_parameters(data_uri.parameters)
# Must have exactly one 'p' and exactly one 'op'
p_values = params_map['p']
op_values = params_map['op']
return nil unless p_values&.length == 1 && op_values&.length == 1
protocol = p_values.first
operation = op_values.first
# Validate protocol and operation format (lowercase, alphanumeric + dash/underscore, 1-50 chars)
return nil unless protocol.match?(/\A[a-z0-9\-_]{1,50}\z/)
return nil unless operation.match?(/\A[a-z0-9\-_]{1,50}\z/)
# Optional data parameter (d= or data=) with base64-encoded JSON
d_values = (params_map['d'] || []) + (params_map['data'] || [])
return nil if d_values.length > 1 # Only zero or one allowed
params_hash = {}
if d_values.length == 1
begin
raw = Base64.strict_decode64(d_values.first)
parsed = JSON.parse(raw)
params_hash = parsed if parsed.is_a?(Hash)
rescue ArgumentError, JSON::ParserError
return nil # Invalid base64 or JSON
end
end
{
protocol: protocol,
operation: operation,
params: params_hash,
source: :header
}
end
# Parse data URI parameters into a hash of arrays (supporting multiple values per key)
def self.parse_parameters(parameters)
map = Hash.new { |h, k| h[k] = [] }
parameters.each do |seg|
next if seg.to_s.empty?
if (eq = seg.index('='))
key = seg[0...eq].strip.downcase
val = seg[(eq + 1)..].to_s.strip
map[key] << val
end
# Ignore bare flags (e.g., base64, rule=esip6)
end
map
end
end