substrate-nft-tracker 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/erc721.json ADDED
@@ -0,0 +1,348 @@
1
+ [
2
+ {
3
+ "inputs": [
4
+ {
5
+ "internalType": "string",
6
+ "name": "name_",
7
+ "type": "string"
8
+ },
9
+ {
10
+ "internalType": "string",
11
+ "name": "symbol_",
12
+ "type": "string"
13
+ }
14
+ ],
15
+ "stateMutability": "nonpayable",
16
+ "type": "constructor"
17
+ },
18
+ {
19
+ "anonymous": false,
20
+ "inputs": [
21
+ {
22
+ "indexed": true,
23
+ "internalType": "address",
24
+ "name": "owner",
25
+ "type": "address"
26
+ },
27
+ {
28
+ "indexed": true,
29
+ "internalType": "address",
30
+ "name": "approved",
31
+ "type": "address"
32
+ },
33
+ {
34
+ "indexed": true,
35
+ "internalType": "uint256",
36
+ "name": "tokenId",
37
+ "type": "uint256"
38
+ }
39
+ ],
40
+ "name": "Approval",
41
+ "type": "event"
42
+ },
43
+ {
44
+ "anonymous": false,
45
+ "inputs": [
46
+ {
47
+ "indexed": true,
48
+ "internalType": "address",
49
+ "name": "owner",
50
+ "type": "address"
51
+ },
52
+ {
53
+ "indexed": true,
54
+ "internalType": "address",
55
+ "name": "operator",
56
+ "type": "address"
57
+ },
58
+ {
59
+ "indexed": false,
60
+ "internalType": "bool",
61
+ "name": "approved",
62
+ "type": "bool"
63
+ }
64
+ ],
65
+ "name": "ApprovalForAll",
66
+ "type": "event"
67
+ },
68
+ {
69
+ "anonymous": false,
70
+ "inputs": [
71
+ {
72
+ "indexed": true,
73
+ "internalType": "address",
74
+ "name": "from",
75
+ "type": "address"
76
+ },
77
+ {
78
+ "indexed": true,
79
+ "internalType": "address",
80
+ "name": "to",
81
+ "type": "address"
82
+ },
83
+ {
84
+ "indexed": true,
85
+ "internalType": "uint256",
86
+ "name": "tokenId",
87
+ "type": "uint256"
88
+ }
89
+ ],
90
+ "name": "Transfer",
91
+ "type": "event"
92
+ },
93
+ {
94
+ "inputs": [
95
+ {
96
+ "internalType": "address",
97
+ "name": "to",
98
+ "type": "address"
99
+ },
100
+ {
101
+ "internalType": "uint256",
102
+ "name": "tokenId",
103
+ "type": "uint256"
104
+ }
105
+ ],
106
+ "name": "approve",
107
+ "outputs": [],
108
+ "stateMutability": "nonpayable",
109
+ "type": "function"
110
+ },
111
+ {
112
+ "inputs": [
113
+ {
114
+ "internalType": "address",
115
+ "name": "owner",
116
+ "type": "address"
117
+ }
118
+ ],
119
+ "name": "balanceOf",
120
+ "outputs": [
121
+ {
122
+ "internalType": "uint256",
123
+ "name": "",
124
+ "type": "uint256"
125
+ }
126
+ ],
127
+ "stateMutability": "view",
128
+ "type": "function"
129
+ },
130
+ {
131
+ "inputs": [
132
+ {
133
+ "internalType": "uint256",
134
+ "name": "tokenId",
135
+ "type": "uint256"
136
+ }
137
+ ],
138
+ "name": "getApproved",
139
+ "outputs": [
140
+ {
141
+ "internalType": "address",
142
+ "name": "",
143
+ "type": "address"
144
+ }
145
+ ],
146
+ "stateMutability": "view",
147
+ "type": "function"
148
+ },
149
+ {
150
+ "inputs": [
151
+ {
152
+ "internalType": "address",
153
+ "name": "owner",
154
+ "type": "address"
155
+ },
156
+ {
157
+ "internalType": "address",
158
+ "name": "operator",
159
+ "type": "address"
160
+ }
161
+ ],
162
+ "name": "isApprovedForAll",
163
+ "outputs": [
164
+ {
165
+ "internalType": "bool",
166
+ "name": "",
167
+ "type": "bool"
168
+ }
169
+ ],
170
+ "stateMutability": "view",
171
+ "type": "function"
172
+ },
173
+ {
174
+ "inputs": [],
175
+ "name": "name",
176
+ "outputs": [
177
+ {
178
+ "internalType": "string",
179
+ "name": "",
180
+ "type": "string"
181
+ }
182
+ ],
183
+ "stateMutability": "view",
184
+ "type": "function"
185
+ },
186
+ {
187
+ "inputs": [
188
+ {
189
+ "internalType": "uint256",
190
+ "name": "tokenId",
191
+ "type": "uint256"
192
+ }
193
+ ],
194
+ "name": "ownerOf",
195
+ "outputs": [
196
+ {
197
+ "internalType": "address",
198
+ "name": "",
199
+ "type": "address"
200
+ }
201
+ ],
202
+ "stateMutability": "view",
203
+ "type": "function"
204
+ },
205
+ {
206
+ "inputs": [
207
+ {
208
+ "internalType": "address",
209
+ "name": "from",
210
+ "type": "address"
211
+ },
212
+ {
213
+ "internalType": "address",
214
+ "name": "to",
215
+ "type": "address"
216
+ },
217
+ {
218
+ "internalType": "uint256",
219
+ "name": "tokenId",
220
+ "type": "uint256"
221
+ }
222
+ ],
223
+ "name": "safeTransferFrom",
224
+ "outputs": [],
225
+ "stateMutability": "nonpayable",
226
+ "type": "function"
227
+ },
228
+ {
229
+ "inputs": [
230
+ {
231
+ "internalType": "address",
232
+ "name": "from",
233
+ "type": "address"
234
+ },
235
+ {
236
+ "internalType": "address",
237
+ "name": "to",
238
+ "type": "address"
239
+ },
240
+ {
241
+ "internalType": "uint256",
242
+ "name": "tokenId",
243
+ "type": "uint256"
244
+ },
245
+ {
246
+ "internalType": "bytes",
247
+ "name": "_data",
248
+ "type": "bytes"
249
+ }
250
+ ],
251
+ "name": "safeTransferFrom",
252
+ "outputs": [],
253
+ "stateMutability": "nonpayable",
254
+ "type": "function"
255
+ },
256
+ {
257
+ "inputs": [
258
+ {
259
+ "internalType": "address",
260
+ "name": "operator",
261
+ "type": "address"
262
+ },
263
+ {
264
+ "internalType": "bool",
265
+ "name": "approved",
266
+ "type": "bool"
267
+ }
268
+ ],
269
+ "name": "setApprovalForAll",
270
+ "outputs": [],
271
+ "stateMutability": "nonpayable",
272
+ "type": "function"
273
+ },
274
+ {
275
+ "inputs": [
276
+ {
277
+ "internalType": "bytes4",
278
+ "name": "interfaceId",
279
+ "type": "bytes4"
280
+ }
281
+ ],
282
+ "name": "supportsInterface",
283
+ "outputs": [
284
+ {
285
+ "internalType": "bool",
286
+ "name": "",
287
+ "type": "bool"
288
+ }
289
+ ],
290
+ "stateMutability": "view",
291
+ "type": "function"
292
+ },
293
+ {
294
+ "inputs": [],
295
+ "name": "symbol",
296
+ "outputs": [
297
+ {
298
+ "internalType": "string",
299
+ "name": "",
300
+ "type": "string"
301
+ }
302
+ ],
303
+ "stateMutability": "view",
304
+ "type": "function"
305
+ },
306
+ {
307
+ "inputs": [
308
+ {
309
+ "internalType": "uint256",
310
+ "name": "tokenId",
311
+ "type": "uint256"
312
+ }
313
+ ],
314
+ "name": "tokenURI",
315
+ "outputs": [
316
+ {
317
+ "internalType": "string",
318
+ "name": "",
319
+ "type": "string"
320
+ }
321
+ ],
322
+ "stateMutability": "view",
323
+ "type": "function"
324
+ },
325
+ {
326
+ "inputs": [
327
+ {
328
+ "internalType": "address",
329
+ "name": "from",
330
+ "type": "address"
331
+ },
332
+ {
333
+ "internalType": "address",
334
+ "name": "to",
335
+ "type": "address"
336
+ },
337
+ {
338
+ "internalType": "uint256",
339
+ "name": "tokenId",
340
+ "type": "uint256"
341
+ }
342
+ ],
343
+ "name": "transferFrom",
344
+ "outputs": [],
345
+ "stateMutability": "nonpayable",
346
+ "type": "function"
347
+ }
348
+ ]
data/lib/nft_helper.rb ADDED
@@ -0,0 +1,106 @@
1
+ class SubstrateEvmNftHelper
2
+ attr_reader :client
3
+ def initialize(client)
4
+ @client = client
5
+ end
6
+
7
+ def track_nft(start_from)
8
+ latest_block_number = @client.get_latest_block_number
9
+ Tracker::logger.debug "The lasest block number is #{latest_block_number}"
10
+
11
+ while true
12
+ begin
13
+ if start_from <= latest_block_number
14
+ Tracker::logger.info "Scan block #{start_from}"
15
+ erc721_events, erc1155_events = get_evm_nft_events(start_from)
16
+ Tracker::logger.debug "#{erc721_events.length} erc721 events scanned"
17
+ Tracker::logger.debug "#{erc1155_events.length} erc1155 events scanned"
18
+ yield erc721_events, erc1155_events
19
+ start_from += 1
20
+ else
21
+ Tracker::logger.debug "Sleep 12 seconds."
22
+ sleep(12)
23
+ latest_block_number = @client.get_latest_block_number
24
+ Tracker::logger.debug "The lasest block number is #{latest_block_number}"
25
+ end
26
+ rescue => ex
27
+ Tracker::logger.error ex.message
28
+ Tracker::logger.error ex.backtrace.join("\n")
29
+ Tracker::logger.debug "Sleep 30 seconds."
30
+ sleep(30)
31
+ end
32
+ end
33
+ end
34
+
35
+ def get_evm_nft_events(block_number)
36
+ # ERC721
37
+ erc721_transfer_topic = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"
38
+
39
+ # ERC1155
40
+ erc1155_transfer_single = "0xc3d58168c5ae7397731d063d5bbf3d657854427343f4c083240f7aacaa2d0f62"
41
+ erc1155_transfer_batch = "0x4a39dc06d4c0dbc64b70af90fd698a233a518aa5d07e595d983b8c0526c8f7fb"
42
+
43
+ erc721_events = []
44
+ erc1155_events = []
45
+ @client.get_events_by_block_number(block_number).each do |event|
46
+ module_name = event[:module_metadata][:name]
47
+ event_name = event[:event_metadata][:name]
48
+ if module_name == "EVM" && event_name == "Log"
49
+
50
+ event[:params].each do |param|
51
+ if param[:name] == "Log"
52
+ address = param[:value][:address]
53
+ topics = param[:value][:topics]
54
+ data = param[:value][:data]
55
+
56
+ if topics[0] == erc721_transfer_topic
57
+ if topics.length == 4
58
+ from = "0x#{topics[1][26..]}"
59
+ to = "0x#{topics[2][26..]}"
60
+ token_id = topics[3].to_i(16)
61
+
62
+ erc721_events << {address: address, from: from, to: to, token_id: token_id}
63
+ end
64
+ elsif topics[0] == erc1155_transfer_single
65
+ if topics.length == 4
66
+ operator = "0x#{topics[1][26..]}"
67
+ from = "0x#{topics[2][26..]}"
68
+ to = "0x#{topics[3][26..]}"
69
+ token_id = data[0...66].to_i(16)
70
+ amount = data[66...130].to_i(16)
71
+
72
+ erc1155_events << {address: address, from: from, to: to, token_id: token_id, amount: amount}
73
+ end
74
+ elsif topics[0] == erc1155_transfer_batch
75
+ if topics.length == 4
76
+ operator = "0x#{topics[1][26..]}"
77
+ from = "0x#{topics[2][26..]}"
78
+ to = "0x#{topics[3][26..]}"
79
+ token_ids, amounts = get_token_ids_and_amounts(data)
80
+
81
+ token_ids.each_with_index do |token_id, i|
82
+ amount = amounts[i]
83
+ erc1155_events << {address: address, from: from, to: to, token_id: token_id, amount: amount}
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
89
+
90
+ end
91
+ end
92
+
93
+ [erc721_events, erc1155_events]
94
+ end
95
+
96
+ private
97
+
98
+ def get_token_ids_and_amounts(data)
99
+ chunks = data[2..].scan(/.{1,64}/)[2..]
100
+ len = chunks.length
101
+ token_ids = chunks[1...len/2].map {|c| c.to_i(16) }
102
+ amounts = chunks[len/2+1...len].map {|c| c.to_i(16) }
103
+ [token_ids, amounts]
104
+ end
105
+ end
106
+