eth-custom 0.5.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. checksums.yaml +7 -0
  2. data/.github/dependabot.yml +18 -0
  3. data/.github/workflows/codeql.yml +48 -0
  4. data/.github/workflows/docs.yml +26 -0
  5. data/.github/workflows/spec.yml +52 -0
  6. data/.gitignore +43 -0
  7. data/.gitmodules +3 -0
  8. data/.rspec +4 -0
  9. data/.yardopts +1 -0
  10. data/AUTHORS.txt +29 -0
  11. data/CHANGELOG.md +218 -0
  12. data/Gemfile +17 -0
  13. data/LICENSE.txt +202 -0
  14. data/README.md +347 -0
  15. data/Rakefile +6 -0
  16. data/bin/console +10 -0
  17. data/bin/setup +9 -0
  18. data/codecov.yml +6 -0
  19. data/eth.gemspec +51 -0
  20. data/lib/eth/abi/event.rb +137 -0
  21. data/lib/eth/abi/type.rb +178 -0
  22. data/lib/eth/abi.rb +446 -0
  23. data/lib/eth/address.rb +106 -0
  24. data/lib/eth/api.rb +223 -0
  25. data/lib/eth/chain.rb +157 -0
  26. data/lib/eth/client/http.rb +63 -0
  27. data/lib/eth/client/ipc.rb +50 -0
  28. data/lib/eth/client.rb +499 -0
  29. data/lib/eth/constant.rb +71 -0
  30. data/lib/eth/contract/event.rb +42 -0
  31. data/lib/eth/contract/function.rb +57 -0
  32. data/lib/eth/contract/function_input.rb +38 -0
  33. data/lib/eth/contract/function_output.rb +37 -0
  34. data/lib/eth/contract/initializer.rb +47 -0
  35. data/lib/eth/contract.rb +143 -0
  36. data/lib/eth/eip712.rb +184 -0
  37. data/lib/eth/key/decrypter.rb +146 -0
  38. data/lib/eth/key/encrypter.rb +207 -0
  39. data/lib/eth/key.rb +167 -0
  40. data/lib/eth/rlp/decoder.rb +114 -0
  41. data/lib/eth/rlp/encoder.rb +78 -0
  42. data/lib/eth/rlp/sedes/big_endian_int.rb +66 -0
  43. data/lib/eth/rlp/sedes/binary.rb +97 -0
  44. data/lib/eth/rlp/sedes/list.rb +84 -0
  45. data/lib/eth/rlp/sedes.rb +74 -0
  46. data/lib/eth/rlp.rb +63 -0
  47. data/lib/eth/signature.rb +163 -0
  48. data/lib/eth/solidity.rb +75 -0
  49. data/lib/eth/tx/eip1559.rb +337 -0
  50. data/lib/eth/tx/eip2930.rb +329 -0
  51. data/lib/eth/tx/legacy.rb +297 -0
  52. data/lib/eth/tx.rb +322 -0
  53. data/lib/eth/unit.rb +49 -0
  54. data/lib/eth/util.rb +235 -0
  55. data/lib/eth/version.rb +20 -0
  56. data/lib/eth.rb +35 -0
  57. metadata +184 -0
data/lib/eth/abi.rb ADDED
@@ -0,0 +1,446 @@
1
+ # Copyright (c) 2016-2022 The Ruby-Eth Contributors
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ # -*- encoding : ascii-8bit -*-
16
+
17
+ require "konstructor"
18
+
19
+ require "eth/abi/event"
20
+ require "eth/abi/type"
21
+
22
+ # Provides the {Eth} module.
23
+ module Eth
24
+
25
+ # Provides a Ruby implementation of the Ethereum Application Binary Interface (ABI).
26
+ # ref: https://docs.soliditylang.org/en/develop/abi-spec.html
27
+ module Abi
28
+ extend self
29
+
30
+ # Provides a special encoding error if anything fails to encode.
31
+ class EncodingError < StandardError; end
32
+
33
+ # Provides a special decoding error if anything fails to decode.
34
+ class DecodingError < StandardError; end
35
+
36
+ # Provides a special out-of-bounds error for values.
37
+ class ValueOutOfBounds < StandardError; end
38
+
39
+ # Encodes Application Binary Interface (ABI) data. It accepts multiple
40
+ # arguments and encodes using the head/tail mechanism.
41
+ #
42
+ # @param types [Array] types to be ABI-encoded.
43
+ # @param args [Array] values to be ABI-encoded.
44
+ # @return [String] the encoded ABI data.
45
+ def encode(types, args)
46
+
47
+ # parse all types
48
+ parsed_types = types.map { |t| Type.parse(t) }
49
+
50
+ # prepare the "head"
51
+ head_size = (0...args.size)
52
+ .map { |i| parsed_types[i].size or 32 }
53
+ .reduce(0, &:+)
54
+ head, tail = "", ""
55
+
56
+ # encode types and arguments
57
+ args.each_with_index do |arg, i|
58
+ if parsed_types[i].is_dynamic?
59
+ head += encode_type Type.size_type, head_size + tail.size
60
+ tail += encode_type parsed_types[i], arg
61
+ else
62
+ head += encode_type parsed_types[i], arg
63
+ end
64
+ end
65
+
66
+ # return the encoded ABI blob
67
+ return "#{head}#{tail}"
68
+ end
69
+
70
+ # Encodes a specific value, either static or dynamic.
71
+ #
72
+ # @param type [Eth::Abi::Type] type to be encoded.
73
+ # @param arg [String|Number] value to be encoded.
74
+ # @return [String] the encoded type.
75
+ # @raise [EncodingError] if value does not match type.
76
+ def encode_type(type, arg)
77
+ if %w(string bytes).include? type.base_type and type.sub_type.empty? and type.dimensions.empty?
78
+ raise EncodingError, "Argument must be a String" unless arg.instance_of? String
79
+
80
+ # encodes strings and bytes
81
+ size = encode_type Type.size_type, arg.size
82
+ padding = Constant::BYTE_ZERO * (Util.ceil32(arg.size) - arg.size)
83
+ return "#{size}#{arg}#{padding}"
84
+ elsif type.is_dynamic?
85
+ raise EncodingError, "Argument must be an Array" unless arg.instance_of? Array
86
+
87
+ # encodes dynamic-sized arrays
88
+ head, tail = "", ""
89
+ head += encode_type Type.size_type, arg.size
90
+ nested_sub = type.nested_sub
91
+ nested_sub_size = type.nested_sub.size
92
+
93
+ # calculate offsets
94
+ if %w(string bytes).include?(type.base_type) && type.sub_type.empty?
95
+ offset = 0
96
+ arg.size.times do |i|
97
+ if i == 0
98
+ offset = arg.size * 32
99
+ else
100
+ number_of_words = ((arg[i - 1].size + 32 - 1) / 32).floor
101
+ total_bytes_length = number_of_words * 32
102
+ offset += total_bytes_length + 32
103
+ end
104
+
105
+ head += encode_type Type.size_type, offset
106
+ end
107
+ end
108
+
109
+ arg.size.times do |i|
110
+ head += encode_type nested_sub, arg[i]
111
+ end
112
+ return "#{head}#{tail}"
113
+ else
114
+ if type.dimensions.empty?
115
+
116
+ # encode a primitive type
117
+ return encode_primitive_type type, arg
118
+ else
119
+
120
+ # encode static-size arrays
121
+ return arg.map { |x| encode_type(type.nested_sub, x) }.join
122
+ end
123
+ end
124
+ end
125
+
126
+ # Encodes primitive types.
127
+ #
128
+ # @param type [Eth::Abi::Type] type to be encoded.
129
+ # @param arg [String|Number] value to be encoded.
130
+ # @return [String] the encoded primitive type.
131
+ # @raise [EncodingError] if value does not match type.
132
+ # @raise [ValueOutOfBounds] if value is out of bounds for type.
133
+ # @raise [EncodingError] if encoding fails for type.
134
+ def encode_primitive_type(type, arg)
135
+ case type.base_type
136
+ when "uint"
137
+ return encode_uint arg, type
138
+ when "bool"
139
+ return encode_bool arg
140
+ when "int"
141
+ return encode_int arg, type
142
+ when "ureal", "ufixed"
143
+ return encode_ufixed arg, type
144
+ when "real", "fixed"
145
+ return encode_fixed arg, type
146
+ when "string", "bytes"
147
+ return encode_bytes arg, type
148
+ when "hash"
149
+ return encode_hash arg, type
150
+ when "address"
151
+ return encode_address arg
152
+ else
153
+ raise EncodingError, "Unhandled type: #{type.base_type} #{type.sub_type}"
154
+ end
155
+ end
156
+
157
+ # Decodes Application Binary Interface (ABI) data. It accepts multiple
158
+ # arguments and decodes using the head/tail mechanism.
159
+ #
160
+ # @param types [Array] the ABI to be decoded.
161
+ # @param data [String] ABI data to be decoded.
162
+ # @return [Array] the decoded ABI data.
163
+ def decode(types, data)
164
+
165
+ # accept hex abi but decode it first
166
+ data = Util.hex_to_bin data if Util.is_hex? data
167
+
168
+ # parse all types
169
+ parsed_types = types.map { |t| Type.parse(t) }
170
+
171
+ # prepare output data
172
+ outputs = [nil] * types.size
173
+ start_positions = [nil] * types.size + [data.size]
174
+ pos = 0
175
+ parsed_types.each_with_index do |t, i|
176
+ if t.is_dynamic?
177
+
178
+ # record start position for dynamic type
179
+ start_positions[i] = Util.deserialize_big_endian_to_int(data[pos, 32])
180
+ j = i - 1
181
+ while j >= 0 and start_positions[j].nil?
182
+ start_positions[j] = start_positions[i]
183
+ j -= 1
184
+ end
185
+ pos += 32
186
+ else
187
+
188
+ # get data directly for static types
189
+ outputs[i] = data[pos, t.size]
190
+ pos += t.size
191
+ end
192
+ end
193
+
194
+ # add start position equal the length of the entire data
195
+ j = types.size - 1
196
+ while j >= 0 and start_positions[j].nil?
197
+ start_positions[j] = start_positions[types.size]
198
+ j -= 1
199
+ end
200
+ raise DecodingError, "Not enough data for head" unless pos <= data.size
201
+
202
+ # add dynamic types
203
+ parsed_types.each_with_index do |t, i|
204
+ if t.is_dynamic?
205
+ offset, next_offset = start_positions[i, 2]
206
+ outputs[i] = data[offset...next_offset]
207
+ end
208
+ end
209
+
210
+ # return the decoded ABI types and data
211
+ return parsed_types.zip(outputs).map { |(type, out)| decode_type(type, out) }
212
+ end
213
+
214
+ # Decodes a specific value, either static or dynamic.
215
+ #
216
+ # @param type [Eth::Abi::Type] type to be decoded.
217
+ # @param arg [String] encoded type data string.
218
+ # @return [String] the decoded data for the type.
219
+ # @raise [DecodingError] if decoding fails for type.
220
+ def decode_type(type, arg)
221
+ if %w(string bytes).include?(type.base_type) and type.sub_type.empty?
222
+ l = Util.deserialize_big_endian_to_int arg[0, 32]
223
+ data = arg[32..-1]
224
+ raise DecodingError, "Wrong data size for string/bytes object" unless data.size == Util.ceil32(l)
225
+
226
+ # decoded strings and bytes
227
+ return data[0, l]
228
+ elsif type.is_dynamic?
229
+ l = Util.deserialize_big_endian_to_int arg[0, 32]
230
+ nested_sub = type.nested_sub
231
+
232
+ # ref https://github.com/ethereum/tests/issues/691
233
+ raise NotImplementedError, "Decoding dynamic arrays with nested dynamic sub-types is not implemented for ABI." if nested_sub.is_dynamic?
234
+
235
+ # decoded dynamic-sized arrays
236
+ return (0...l).map { |i| decode_type(nested_sub, arg[32 + nested_sub.size * i, nested_sub.size]) }
237
+ elsif !type.dimensions.empty?
238
+ l = type.dimensions.last[0]
239
+ nested_sub = type.nested_sub
240
+
241
+ # decoded static-size arrays
242
+ return (0...l).map { |i| decode_type(nested_sub, arg[nested_sub.size * i, nested_sub.size]) }
243
+ else
244
+
245
+ # decoded primitive types
246
+ return decode_primitive_type type, arg
247
+ end
248
+ end
249
+
250
+ # Decodes primitive types.
251
+ #
252
+ # @param type [Eth::Abi::Type] type to be decoded.
253
+ # @param data [String] encoded primitive type data string.
254
+ # @return [String] the decoded data for the type.
255
+ # @raise [DecodingError] if decoding fails for type.
256
+ def decode_primitive_type(type, data)
257
+ case type.base_type
258
+ when "address"
259
+
260
+ # decoded address with 0x-prefix
261
+ return "0x#{Util.bin_to_hex data[12..-1]}"
262
+ when "string", "bytes"
263
+ if type.sub_type.empty?
264
+ size = Util.deserialize_big_endian_to_int data[0, 32]
265
+
266
+ # decoded dynamic-sized array
267
+ return data[32..-1][0, size]
268
+ else
269
+
270
+ # decoded static-sized array
271
+ return data[0, type.sub_type.to_i]
272
+ end
273
+ when "hash"
274
+
275
+ # decoded hash
276
+ return data[(32 - type.sub_type.to_i), type.sub_type.to_i]
277
+ when "uint"
278
+
279
+ # decoded unsigned integer
280
+ return Util.deserialize_big_endian_to_int data
281
+ when "int"
282
+ u = Util.deserialize_big_endian_to_int data
283
+ i = u >= 2 ** (type.sub_type.to_i - 1) ? (u - 2 ** type.sub_type.to_i) : u
284
+
285
+ # decoded integer
286
+ return i
287
+ when "ureal", "ufixed"
288
+ high, low = type.sub_type.split("x").map(&:to_i)
289
+
290
+ # decoded unsigned fixed point numeric
291
+ return Util.deserialize_big_endian_to_int(data) * 1.0 / 2 ** low
292
+ when "real", "fixed"
293
+ high, low = type.sub_type.split("x").map(&:to_i)
294
+ u = Util.deserialize_big_endian_to_int data
295
+ i = u >= 2 ** (high + low - 1) ? (u - 2 ** (high + low)) : u
296
+
297
+ # decoded fixed point numeric
298
+ return i * 1.0 / 2 ** low
299
+ when "bool"
300
+
301
+ # decoded boolean
302
+ return data[-1] == Constant::BYTE_ONE
303
+ else
304
+ raise DecodingError, "Unknown primitive type: #{type.base_type}"
305
+ end
306
+ end
307
+
308
+ # Build event signature string from ABI interface.
309
+ #
310
+ # @param interface [Hash] ABI event interface.
311
+ # @return [String] interface signature string.
312
+ def signature(interface)
313
+ name = interface.fetch("name")
314
+ inputs = interface.fetch("inputs", [])
315
+ types = inputs.map { |i| i.fetch("type") }
316
+ "#{name}(#{types.join(",")})"
317
+ end
318
+
319
+ private
320
+
321
+ # Properly encodes unsigned integers.
322
+ def encode_uint(arg, type)
323
+ raise ArgumentError, "Don't know how to handle this input." unless arg.is_a? Numeric
324
+ raise ValueOutOfBounds, "Number out of range: #{arg}" if arg > Constant::UINT_MAX or arg < Constant::UINT_MIN
325
+ real_size = type.sub_type.to_i
326
+ i = arg.to_i
327
+ raise ValueOutOfBounds, arg unless i >= 0 and i < 2 ** real_size
328
+ return Util.zpad_int i
329
+ end
330
+
331
+ # Properly encodes signed integers.
332
+ def encode_int(arg, type)
333
+ raise ArgumentError, "Don't know how to handle this input." unless arg.is_a? Numeric
334
+ raise ValueOutOfBounds, "Number out of range: #{arg}" if arg > Constant::INT_MAX or arg < Constant::INT_MIN
335
+ real_size = type.sub_type.to_i
336
+ i = arg.to_i
337
+ raise ValueOutOfBounds, arg unless i >= -2 ** (real_size - 1) and i < 2 ** (real_size - 1)
338
+ return Util.zpad_int(i % 2 ** type.sub_type.to_i)
339
+ end
340
+
341
+ # Properly encodes booleans.
342
+ def encode_bool(arg)
343
+ raise EncodingError, "Argument is not bool: #{arg}" unless arg.instance_of? TrueClass or arg.instance_of? FalseClass
344
+ return Util.zpad_int(arg ? 1 : 0)
345
+ end
346
+
347
+ # Properly encodes unsigned fixed-point numbers.
348
+ def encode_ufixed(arg, type)
349
+ raise ArgumentError, "Don't know how to handle this input." unless arg.is_a? Numeric
350
+ high, low = type.sub_type.split("x").map(&:to_i)
351
+ raise ValueOutOfBounds, arg unless arg >= 0 and arg < 2 ** high
352
+ return Util.zpad_int((arg * 2 ** low).to_i)
353
+ end
354
+
355
+ # Properly encodes signed fixed-point numbers.
356
+ def encode_fixed(arg, type)
357
+ raise ArgumentError, "Don't know how to handle this input." unless arg.is_a? Numeric
358
+ high, low = type.sub_type.split("x").map(&:to_i)
359
+ raise ValueOutOfBounds, arg unless arg >= -2 ** (high - 1) and arg < 2 ** (high - 1)
360
+ i = (arg * 2 ** low).to_i
361
+ return Util.zpad_int(i % 2 ** (high + low))
362
+ end
363
+
364
+ # Properly encodes byte-strings.
365
+ def encode_bytes(arg, type)
366
+ raise EncodingError, "Expecting String: #{arg}" unless arg.instance_of? String
367
+ arg = handle_hex_string arg, type
368
+
369
+ if type.sub_type.empty?
370
+ size = Util.zpad_int arg.size
371
+ padding = Constant::BYTE_ZERO * (Util.ceil32(arg.size) - arg.size)
372
+
373
+ # variable length string/bytes
374
+ return "#{size}#{arg}#{padding}"
375
+ else
376
+ raise ValueOutOfBounds, arg unless arg.size <= type.sub_type.to_i
377
+ padding = Constant::BYTE_ZERO * (32 - arg.size)
378
+
379
+ # fixed length string/bytes
380
+ return "#{arg}#{padding}"
381
+ end
382
+ end
383
+
384
+ # Properly encodes hash-strings.
385
+ def encode_hash(arg, type)
386
+ size = type.sub_type.to_i
387
+ raise EncodingError, "Argument too long: #{arg}" unless size > 0 and size <= 32
388
+ if arg.is_a? Integer
389
+
390
+ # hash from integer
391
+ return Util.zpad_int arg
392
+ elsif arg.size == size
393
+
394
+ # hash from encoded hash
395
+ return Util.zpad arg, 32
396
+ elsif arg.size == size * 2
397
+
398
+ # hash from hexa-decimal hash
399
+ return Util.zpad_hex arg
400
+ else
401
+ raise EncodingError, "Could not parse hash: #{arg}"
402
+ end
403
+ end
404
+
405
+ # Properly encodes addresses.
406
+ def encode_address(arg)
407
+ if arg.is_a? Integer
408
+
409
+ # address from integer
410
+ return Util.zpad_int arg
411
+ elsif arg.size == 20
412
+
413
+ # address from encoded address
414
+ return Util.zpad arg, 32
415
+ elsif arg.size == 40
416
+
417
+ # address from hexa-decimal address with 0x prefix
418
+ return Util.zpad_hex arg
419
+ elsif arg.size == 42 and arg[0, 2] == "0x"
420
+
421
+ # address from hexa-decimal address
422
+ return Util.zpad_hex arg[2..-1]
423
+ else
424
+ raise EncodingError, "Could not parse address: #{arg}"
425
+ end
426
+ end
427
+
428
+ # The ABI encoder needs to be able to determine between a hex `"123"`
429
+ # and a binary `"123"` string.
430
+ def handle_hex_string(arg, type)
431
+ if Util.is_prefixed? arg or
432
+ (arg.size === type.sub_type.to_i * 2 and Util.is_hex? arg)
433
+
434
+ # There is no way telling whether a string is hex or binary with certainty
435
+ # in Ruby. Therefore, we assume a `0x` prefix to indicate a hex string.
436
+ # Additionally, if the string size is exactly the double of the expected
437
+ # binary size, we can assume a hex value.
438
+ return Util.hex_to_bin arg
439
+ else
440
+
441
+ # Everything else will be assumed binary or raw string.
442
+ return arg.b
443
+ end
444
+ end
445
+ end
446
+ end
@@ -0,0 +1,106 @@
1
+ # Copyright (c) 2016-2022 The Ruby-Eth Contributors
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ # Provides the {Eth} module.
16
+ module Eth
17
+
18
+ # The {Eth::Address} class to handle checksummed Ethereum addresses.
19
+ class Address
20
+
21
+ # Provides a special checksum error if EIP-55 is violated.
22
+ class CheckSumError < StandardError; end
23
+
24
+ # The prefixed and checksummed Ethereum address.
25
+ attr_reader :address
26
+
27
+ # Constructor of the {Eth::Address} class. Creates a new hex
28
+ # prefixed address.
29
+ #
30
+ # @param address [String] hex string representing an ethereum address.
31
+ def initialize(address)
32
+ unless Util.is_hex? address
33
+ raise CheckSumError, "Unknown address type #{address}!"
34
+ end
35
+ @address = Util.prefix_hex address
36
+ unless self.valid?
37
+ raise CheckSumError, "Invalid address provided #{address}"
38
+ end
39
+ end
40
+
41
+ # Checks that the address is valid.
42
+ #
43
+ # @return [Boolean] true if valid address.
44
+ def valid?
45
+ if !matches_any_format?
46
+ false
47
+ elsif not_checksummed?
48
+ true
49
+ else
50
+ checksum_matches?
51
+ end
52
+ end
53
+
54
+ # Generate a checksummed address.
55
+ #
56
+ # @return [String] prefixed hexstring representing an checksummed address.
57
+ def checksummed
58
+ raise CheckSumError, "Invalid address: #{address}" unless matches_any_format?
59
+
60
+ cased = unprefixed.chars.zip(checksum.chars).map do |char, check|
61
+ check.match(/[0-7]/) ? char.downcase : char.upcase
62
+ end
63
+
64
+ Util.prefix_hex cased.join
65
+ end
66
+
67
+ alias :to_s :checksummed
68
+
69
+ private
70
+
71
+ # Checks whether the address checksum matches.
72
+ def checksum_matches?
73
+ address == checksummed
74
+ end
75
+
76
+ # Checks whether the address is not checksummed.
77
+ def not_checksummed?
78
+ all_uppercase? || all_lowercase?
79
+ end
80
+
81
+ # Checks whether the address is all upper-case.
82
+ def all_uppercase?
83
+ address.match /(?:0[xX])[A-F0-9]{40}/
84
+ end
85
+
86
+ # Checks whether the address is all lower-case.
87
+ def all_lowercase?
88
+ address.match /(?:0[xX])[a-f0-9]{40}/
89
+ end
90
+
91
+ # Checks whether the address matches any known format.
92
+ def matches_any_format?
93
+ address.match /\A(?:0[xX])[a-fA-F0-9]{40}\z/
94
+ end
95
+
96
+ # Computes the checksum of the address.
97
+ def checksum
98
+ Util.bin_to_hex Util.keccak256 unprefixed.downcase
99
+ end
100
+
101
+ # Removes the hex prefix.
102
+ def unprefixed
103
+ Util.remove_hex_prefix address
104
+ end
105
+ end
106
+ end