-
Notifications
You must be signed in to change notification settings - Fork 25
Expand file tree
/
Copy pathstorage_reader.rb
More file actions
296 lines (255 loc) · 10.2 KB
/
Copy pathstorage_reader.rb
File metadata and controls
296 lines (255 loc) · 10.2 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
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
class StorageReader
ETHSCRIPTIONS_ADDRESS = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE'
# Define the nested ContentInfo struct
CONTENT_INFO_STRUCT = {
'components' => [
{ 'name' => 'contentUriHash', 'type' => 'bytes32' },
{ 'name' => 'contentSha', 'type' => 'bytes32' },
{ 'name' => 'mimetype', 'type' => 'string' },
{ 'name' => 'mediaType', 'type' => 'string' },
{ 'name' => 'mimeSubtype', 'type' => 'string' },
{ 'name' => 'esip6', 'type' => 'bool' }
],
'type' => 'tuple'
}
# Define the Ethscription struct ABI with nested ContentInfo
ETHSCRIPTION_STRUCT_ABI = {
'components' => [
CONTENT_INFO_STRUCT, # ContentInfo content
{ 'name' => 'creator', 'type' => 'address' },
{ 'name' => 'initialOwner', 'type' => 'address' },
{ 'name' => 'previousOwner', 'type' => 'address' },
{ 'name' => 'ethscriptionNumber', 'type' => 'uint256' },
{ 'name' => 'createdAt', 'type' => 'uint256' },
{ 'name' => 'l1BlockNumber', 'type' => 'uint64' },
{ 'name' => 'l2BlockNumber', 'type' => 'uint64' },
{ 'name' => 'l1BlockHash', 'type' => 'bytes32' }
],
'type' => 'tuple'
}
# Contract ABI - only the functions we need
CONTRACT_ABI = [
{
'name' => 'getEthscription',
'type' => 'function',
'stateMutability' => 'view',
'inputs' => [
{ 'name' => 'transactionHash', 'type' => 'bytes32' }
],
'outputs' => [
ETHSCRIPTION_STRUCT_ABI
]
},
{
'name' => 'getEthscriptionContent',
'type' => 'function',
'stateMutability' => 'view',
'inputs' => [
{ 'name' => 'transactionHash', 'type' => 'bytes32' }
],
'outputs' => [
{ 'name' => '', 'type' => 'bytes' }
]
},
{
'name' => 'ownerOf',
'type' => 'function',
'stateMutability' => 'view',
'inputs' => [
{ 'name' => 'transactionHash', 'type' => 'bytes32' }
],
'outputs' => [
{ 'name' => '', 'type' => 'address' }
]
},
{
'name' => 'totalSupply',
'type' => 'function',
'stateMutability' => 'view',
'inputs' => [],
'outputs' => [
{ 'name' => '', 'type' => 'uint256' }
]
},
{
'name' => 'getEthscriptionWithContent',
'type' => 'function',
'stateMutability' => 'view',
'inputs' => [
{ 'name' => 'txHash', 'type' => 'bytes32' }
],
'outputs' => [
{ 'name' => 'ethscription', **ETHSCRIPTION_STRUCT_ABI },
{ 'name' => 'content', 'type' => 'bytes' }
]
}
]
class << self
def get_ethscription_with_content(tx_hash, block_tag: 'latest')
# Single call to get both ethscription and content
tx_hash_bytes32 = format_bytes32(tx_hash)
# Build function signature and encode parameters
function_sig = Eth::Util.keccak256('getEthscriptionWithContent(bytes32)')[0...4]
# Encode the parameter (bytes32 is already 32 bytes)
calldata = function_sig + [tx_hash_bytes32].pack('H*')
# Make the eth_call
result = eth_call('0x' + calldata.unpack1('H*'), block_tag)
# When contract returns 0x/0x0, the ethscription doesn't exist (not an error, just not found)
return nil if result == '0x' || result == '0x0'
# If result is nil, that's an RPC/network error
raise StandardError, "RPC call failed for ethscription #{tx_hash}" if result.nil?
# Decode the tuple: (Ethscription, bytes)
types = ['((bytes32,bytes32,string,string,string,bool),address,address,address,uint256,uint256,uint64,uint64,bytes32)', 'bytes']
decoded = Eth::Abi.decode(types, result)
# Extract ethscription struct and content
ethscription_data = decoded[0]
content_data = decoded[1]
content_info = ethscription_data[0] # Nested ContentInfo struct
{
# ContentInfo fields
content_uri_hash: '0x' + content_info[0].unpack1('H*'),
content_sha: '0x' + content_info[1].unpack1('H*'),
mimetype: content_info[2],
media_type: content_info[3],
mime_subtype: content_info[4],
esip6: content_info[5],
# Main Ethscription fields
creator: Eth::Address.new(ethscription_data[1]).to_s,
initial_owner: Eth::Address.new(ethscription_data[2]).to_s,
previous_owner: Eth::Address.new(ethscription_data[3]).to_s,
ethscription_number: ethscription_data[4],
created_at: ethscription_data[5],
l1_block_number: ethscription_data[6],
l2_block_number: ethscription_data[7],
l1_block_hash: '0x' + ethscription_data[8].unpack1('H*'),
# Content
content: content_data
}
rescue => e
Rails.logger.error "Failed to get ethscription with content #{tx_hash}: #{e.message}"
Rails.logger.error e.backtrace.join("\n") if Rails.env.development?
raise e
end
def get_ethscription(tx_hash, block_tag: 'latest')
# Ensure tx_hash is properly formatted as bytes32
tx_hash_bytes32 = format_bytes32(tx_hash)
# Build function signature and encode parameters
function_sig = Eth::Util.keccak256('getEthscription(bytes32)')[0...4]
# Encode the parameter (bytes32 is already 32 bytes)
calldata = function_sig + [tx_hash_bytes32].pack('H*')
# Make the eth_call
result = eth_call('0x' + calldata.unpack1('H*'), block_tag)
# Deterministic not-found from contract returns 0x/0x0
return nil if result == '0x' || result == '0x0'
# Nil indicates an RPC/network failure
raise StandardError, "RPC call failed for ethscription #{tx_hash}" if result.nil?
# Decode using Eth::Abi
# Updated types for nested struct: ContentInfo is a tuple within the main tuple
types = ['((bytes32,bytes32,string,string,string,bool),address,address,address,uint256,uint256,uint64,uint64,bytes32)']
decoded = Eth::Abi.decode(types, result)
# The struct is returned as an array within an array
ethscription_data = decoded[0]
content_info = ethscription_data[0] # Nested ContentInfo struct
{
# ContentInfo fields
content_uri_hash: '0x' + content_info[0].unpack1('H*'),
content_sha: '0x' + content_info[1].unpack1('H*'),
mimetype: content_info[2],
media_type: content_info[3],
mime_subtype: content_info[4],
esip6: content_info[5],
# Main Ethscription fields
creator: Eth::Address.new(ethscription_data[1]).to_s,
initial_owner: Eth::Address.new(ethscription_data[2]).to_s,
previous_owner: Eth::Address.new(ethscription_data[3]).to_s,
ethscription_number: ethscription_data[4],
created_at: ethscription_data[5],
l1_block_number: ethscription_data[6],
l2_block_number: ethscription_data[7],
l1_block_hash: '0x' + ethscription_data[8].unpack1('H*')
}
rescue EthRpcClient::ExecutionRevertedError => e
# Contract reverted - ethscription doesn't exist
Rails.logger.debug "Ethscription #{tx_hash} doesn't exist (contract reverted): #{e.message}"
nil
end
def get_ethscription_content(tx_hash, block_tag: 'latest')
# Ensure tx_hash is properly formatted as bytes32
tx_hash_bytes32 = format_bytes32(tx_hash)
# Build function signature and encode parameters
function_sig = Eth::Util.keccak256('getEthscriptionContent(bytes32)')[0...4]
# Encode the parameter (bytes32 is already 32 bytes)
calldata = function_sig + [tx_hash_bytes32].pack('H*')
# Make the eth_call
result = eth_call('0x' + calldata.unpack1('H*'), block_tag)
return nil if result.nil? || result == '0x' || result == '0x0'
# Decode using Eth::Abi - returns bytes
decoded = Eth::Abi.decode(['bytes'], result)
# Return the raw bytes content
decoded[0]
rescue EthRpcClient::ExecutionRevertedError => e
# Contract reverted - ethscription doesn't exist
Rails.logger.debug "Ethscription content #{tx_hash} doesn't exist (contract reverted): #{e.message}"
nil
end
def get_owner(token_id, block_tag: 'latest')
# Build function signature
function_sig = Eth::Util.keccak256('ownerOf(bytes32)')[0...4]
# Token ID is the transaction hash as uint256
token_id_bytes32 = format_bytes32(token_id)
# Encode the parameter
calldata = function_sig + [token_id_bytes32].pack('H*')
# Make the eth_call
result = eth_call('0x' + calldata.unpack1('H*'), block_tag)
# Some nodes return 0x when the call yields no data
return nil if result == '0x'
# Nil indicates an RPC/network failure
raise StandardError, "RPC call failed for ownerOf #{token_id}" if result.nil?
# Decode the result - ownerOf returns a single address
decoded = Eth::Abi.decode(['address'], result)
Eth::Address.new(decoded[0]).to_s
rescue EthRpcClient::ExecutionRevertedError => e
# Contract reverted - token doesn't exist
Rails.logger.debug "Token #{token_id} doesn't exist (contract reverted): #{e.message}"
nil
end
def get_total_supply(block_tag: 'latest')
# Build function signature
function_sig = Eth::Util.keccak256('totalSupply()')[0...4]
# No parameters for totalSupply
calldata = '0x' + function_sig.unpack1('H*')
# Make the eth_call
result = eth_call(calldata, block_tag)
return 0 if result.nil? || result == '0x'
# Decode the result
decoded = Eth::Abi.decode(['uint256'], result)
decoded[0]
rescue => e
Rails.logger.error "Failed to get total supply: #{e.message}"
0
end
private
def eth_call(calldata, block_tag = 'latest')
# calldata should be a hex string starting with 0x
EthRpcClient.l2.call('eth_call', [{
to: ETHSCRIPTIONS_ADDRESS,
data: calldata
}, block_tag])
end
def format_bytes32(hex_value)
# Remove 0x prefix if present and ensure it's 32 bytes
clean_hex = hex_value.to_s.delete_prefix('0x')
# Pad or truncate to 32 bytes
if clean_hex.length > 64
clean_hex[0...64]
else
clean_hex.rjust(64, '0')
end
end
def format_uint256(hex_value)
# Convert hex to integer (transaction hash as uint256)
clean_hex = hex_value.to_s.delete_prefix('0x')
clean_hex.to_i(16)
end
end
end