tezos_client 0.2.0

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 (45) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +14 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +9 -0
  5. data/.ruby-version +1 -0
  6. data/.travis.yml +21 -0
  7. data/CODE_OF_CONDUCT.md +74 -0
  8. data/Gemfile +8 -0
  9. data/Gemfile.lock +147 -0
  10. data/LICENSE.txt +21 -0
  11. data/README.md +107 -0
  12. data/Rakefile +8 -0
  13. data/bin/console +15 -0
  14. data/bin/setup +8 -0
  15. data/lib/tezos_client/client_interface/block_contextual.rb +16 -0
  16. data/lib/tezos_client/client_interface/client_wrapper.rb +37 -0
  17. data/lib/tezos_client/client_interface/contract.rb +17 -0
  18. data/lib/tezos_client/client_interface/key.rb +35 -0
  19. data/lib/tezos_client/client_interface/misc.rb +26 -0
  20. data/lib/tezos_client/client_interface.rb +25 -0
  21. data/lib/tezos_client/commands.rb +28 -0
  22. data/lib/tezos_client/crypto.rb +178 -0
  23. data/lib/tezos_client/currency_utils.rb +20 -0
  24. data/lib/tezos_client/encode_utils.rb +139 -0
  25. data/lib/tezos_client/liquidity_inteface/liquidity_wrapper.rb +34 -0
  26. data/lib/tezos_client/liquidity_interface.rb +127 -0
  27. data/lib/tezos_client/logger.rb +78 -0
  28. data/lib/tezos_client/operation.rb +125 -0
  29. data/lib/tezos_client/operations/origination_operation.rb +50 -0
  30. data/lib/tezos_client/operations/transaction_operation.rb +39 -0
  31. data/lib/tezos_client/rpc_interface/blocks.rb +34 -0
  32. data/lib/tezos_client/rpc_interface/context.rb +27 -0
  33. data/lib/tezos_client/rpc_interface/contracts.rb +27 -0
  34. data/lib/tezos_client/rpc_interface/helper.rb +100 -0
  35. data/lib/tezos_client/rpc_interface/monitor.rb +19 -0
  36. data/lib/tezos_client/rpc_interface/request_manager.rb +88 -0
  37. data/lib/tezos_client/rpc_interface.rb +29 -0
  38. data/lib/tezos_client/string_utils.rb +16 -0
  39. data/lib/tezos_client/version.rb +5 -0
  40. data/lib/tezos_client.rb +149 -0
  41. data/tezos_client.gemspec +48 -0
  42. data/travis-scripts/install-liquidity.sh +25 -0
  43. data/travis-scripts/install-opam.sh +4 -0
  44. data/travis-scripts/prepare-trusty.sh +16 -0
  45. metadata +242 -0
@@ -0,0 +1,178 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "base58"
4
+ require "rbnacl"
5
+ require "digest"
6
+
7
+ class TezosClient
8
+ module Crypto
9
+ using StringUtils
10
+
11
+ PREFIXES = {
12
+ tz1: [6, 161, 159],
13
+ tz2: [6, 161, 161],
14
+ tz3: [6, 161, 164],
15
+ KT: [2, 90, 121],
16
+ edpk: [13, 15, 37, 217],
17
+ edsk2: [13, 15, 58, 7],
18
+ spsk: [17, 162, 224, 201],
19
+ p2sk: [16, 81, 238, 189],
20
+ sppk: [3, 254, 226, 86],
21
+ p2pk: [3, 178, 139, 127],
22
+ edsk: [43, 246, 78, 7],
23
+ edsig: [9, 245, 205, 134, 18],
24
+ spsig1: [13, 115, 101, 19, 63],
25
+ p2sig: [54, 240, 44, 52],
26
+ sig: [4, 130, 43],
27
+ Net: [87, 82, 0],
28
+ nce: [69, 220, 169],
29
+ b: [1, 52],
30
+ o: [5, 116],
31
+ Lo: [133, 233],
32
+ LLo: [29, 159, 109],
33
+ P: [2, 170],
34
+ Co: [79, 179],
35
+ id: [153, 103]
36
+ }.freeze
37
+
38
+ WATERMARK = {
39
+ block: "01",
40
+ endorsement: "02",
41
+ generic: "03"
42
+ }.freeze
43
+
44
+ def hex_prefix(type)
45
+ PREFIXES[type].pack("C*").to_hex
46
+ end
47
+
48
+ def decode_base58(base58_val)
49
+ bin_val = Base58.base58_to_binary(base58_val, :bitcoin)
50
+ bin_val.to_hex
51
+ end
52
+
53
+ def encode_base58(hex_val)
54
+ bin_val = hex_val.to_bin
55
+ Base58.binary_to_base58(bin_val, :bitcoin)
56
+ end
57
+
58
+ def checksum(hex)
59
+ b = hex.to_bin
60
+ Digest::SHA256.hexdigest(Digest::SHA256.digest(b))[0...8]
61
+ end
62
+
63
+ def get_prefix_and_payload(str)
64
+ PREFIXES.keys.each do |prefix|
65
+ if str.start_with? hex_prefix(prefix)
66
+ return prefix, str[(hex_prefix(prefix).size) .. -1]
67
+ end
68
+ end
69
+ end
70
+
71
+ def decode_tz(str)
72
+ decoded = decode_base58 str
73
+
74
+ unless checksum(decoded[0...-8]) != decoded[0...-8]
75
+ raise "invalid checksum for #{str}"
76
+ end
77
+
78
+ prefix, payload = get_prefix_and_payload(decoded[0...-8])
79
+
80
+ yield(prefix, payload) if block_given?
81
+
82
+ payload
83
+ end
84
+
85
+ def encode_tz(prefix, str)
86
+ prefixed = hex_prefix(prefix) + str
87
+ checksum = checksum(prefixed)
88
+
89
+ encode_base58(prefixed + checksum)
90
+ end
91
+
92
+ def secret_key_to_public_key(secret_key)
93
+ signing_key = signing_key(secret_key)
94
+ verify_key = signing_key.verify_key
95
+ hex_pubkey = verify_key.to_s.to_hex
96
+
97
+ encode_tz(:edpk, hex_pubkey)
98
+ end
99
+
100
+ def public_key_to_address(public_key)
101
+ public_key = decode_tz(public_key) do |type, _key|
102
+ raise "invalid public key: #{public_key} " unless type == :edpk
103
+ end
104
+
105
+ hash = RbNaCl::Hash::Blake2b.digest(public_key, digest_size: 20)
106
+ hex_hash = hash.to_hex
107
+
108
+ encode_tz(:tz1, hex_hash)
109
+ end
110
+
111
+ def generate_key
112
+ signing_key = RbNaCl::SigningKey.generate.to_bytes.to_hex
113
+
114
+ secret_key = encode_tz(:edsk2, signing_key)
115
+ public_key = secret_key_to_public_key(secret_key)
116
+ address = public_key_to_address(public_key)
117
+
118
+ {
119
+ secret_key: secret_key,
120
+ public_key: public_key,
121
+ address: address
122
+ }
123
+ end
124
+
125
+ def signing_key(secret_key)
126
+ secret_key = decode_tz(secret_key) do |type, _key|
127
+ raise "invalid secret key: #{secret_key} " unless type == :edsk2
128
+ end
129
+
130
+ RbNaCl::SigningKey.new(secret_key.to_bin)
131
+ end
132
+
133
+ def sign_bytes(secret_key:, data:, watermark: nil)
134
+ watermarked_data = if watermark.nil?
135
+ data
136
+ else
137
+ WATERMARK[watermark] + data
138
+ end
139
+
140
+ hash = RbNaCl::Hash::Blake2b.digest(watermarked_data.to_bin, digest_size: 32)
141
+
142
+ signing_key = signing_key(secret_key)
143
+ bin_signature = signing_key.sign(hash)
144
+
145
+ edsig = encode_tz(:edsig, bin_signature.to_hex)
146
+ signed_data = data + bin_signature.to_hex
147
+
148
+ if block_given?
149
+ yield(edsig, signed_data)
150
+ else
151
+ edsig
152
+ end
153
+ end
154
+
155
+ def operation_id(signed_operation_hex)
156
+ hash = RbNaCl::Hash::Blake2b.digest(
157
+ signed_operation_hex.to_bin,
158
+ digest_size: 32
159
+ )
160
+ encode_tz(:o, hash.to_hex)
161
+ end
162
+
163
+
164
+ def sign_operation(secret_key:, operation_hex:)
165
+ sign_bytes(secret_key: secret_key,
166
+ data: operation_hex,
167
+ watermark: :generic) do |edsig, signed_data|
168
+ op_id = operation_id(signed_data)
169
+
170
+ if block_given?
171
+ yield(edsig, signed_data, op_id)
172
+ else
173
+ edsig
174
+ end
175
+ end
176
+ end
177
+ end
178
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bigdecimal"
4
+ require "bigdecimal/util"
5
+
6
+ class TezosClient
7
+ module CurrencyUtils
8
+ TEZOS_SATOSHI = 1000000.0
9
+
10
+ refine Numeric do
11
+ def from_satoshi
12
+ self.to_d / TEZOS_SATOSHI
13
+ end
14
+
15
+ def to_satoshi
16
+ (self * TEZOS_SATOSHI).to_i
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ class TezosClient
4
+ module EncodeUtils
5
+ class ArgsEncoder
6
+ attr_accessor :expr, :popen, :sopen, :escaped, :pl, :ret
7
+
8
+
9
+ def initialize(expr)
10
+ @expr = expr.gsub(/(?:@[a-z_]+)|(?:#.*$)/m, "")
11
+ .gsub(/\s+/, " ")
12
+ .strip
13
+ initialize_statuses
14
+ initialize_ret
15
+ end
16
+
17
+ def initialize_statuses
18
+ @popen = false
19
+ @sopen = false
20
+ @escaped = false
21
+ @pl = 0
22
+ @val = ""
23
+ end
24
+
25
+ def initialize_ret
26
+ @ret = {
27
+ prim: nil,
28
+ args: []
29
+ }
30
+ end
31
+
32
+ def treat_val
33
+ unless @val.empty?
34
+ if @val == @val.to_i.to_s
35
+ if !ret[:prim]
36
+ @ret = { "int" => @val }
37
+ else
38
+ @ret[:args] << { "int" => @val }
39
+ end
40
+ elsif ret[:prim]
41
+ @ret[:args] << ArgsEncoder.new(@val).encode
42
+ else
43
+ @ret[:prim] = @val
44
+ end
45
+ @val = ""
46
+ end
47
+ end
48
+
49
+ def treat_double_quote(char)
50
+ return false unless char == '"'
51
+
52
+ if @sopen
53
+ @sopen = false
54
+ if !ret[:prim]
55
+ @ret = { "string" => @val }
56
+ else
57
+ @ret[:args] << { "string" => @val }
58
+ end
59
+ @val = ""
60
+ else
61
+ @sopen = true
62
+ end
63
+ true
64
+ end
65
+
66
+ def treat_parenthesis(char)
67
+ case char
68
+ when "("
69
+ @val += char if @popen
70
+ @popen = true
71
+ @pl += 1
72
+ true
73
+ when ")"
74
+ raise "closing parenthesis while none was opened #{val}" unless popen
75
+ @pl -= 1
76
+ if pl.zero?
77
+ @ret[:args] << ArgsEncoder.new(@val).encode
78
+ @val = ""
79
+ @popen = false
80
+ else
81
+ @val += char
82
+ end
83
+ true
84
+ else
85
+ false
86
+ end
87
+ end
88
+
89
+ def treat_escape(char)
90
+ if escaped
91
+ @val += char
92
+ @escaped = false
93
+ true
94
+ elsif char == "\\"
95
+ @escaped = true
96
+ true
97
+ end
98
+ false
99
+ end
100
+
101
+ def treat_char(char, is_last_char)
102
+ return if treat_escape(char)
103
+
104
+ unless popen || sopen
105
+ if is_last_char || char == " "
106
+ @val += char if is_last_char
107
+ treat_val
108
+ return
109
+ end
110
+ end
111
+
112
+ unless popen
113
+ return if treat_double_quote(char)
114
+ end
115
+
116
+ return if treat_parenthesis(char)
117
+
118
+ @val += char
119
+ end
120
+
121
+ def encode
122
+ expr.each_char.with_index do |char, i|
123
+ is_last_char = (i == (expr.length - 1))
124
+ treat_char(char, is_last_char)
125
+ end
126
+
127
+ if sopen
128
+ raise ArgumentError, "string '#{@val}' has not been closed"
129
+ end
130
+
131
+ ret
132
+ end
133
+ end
134
+
135
+ def encode_args(expr)
136
+ ArgsEncoder.new(expr).encode
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ class TezosClient
4
+ class LiquidityInterface
5
+ # Wrapper used to call the tezos-client binary
6
+ module LiquidityWrapper
7
+ def call_liquidity(command)
8
+ cmd = "#{liquidity_cmd} #{command}"
9
+ log cmd
10
+ Open3.popen3(cmd) do |_stdin, stdout, stderr, wait_thr|
11
+ err = stderr.read
12
+ status = wait_thr.value.exitstatus
13
+
14
+ if status != 0
15
+ raise "command '#{cmd}' existed with status #{status}: #{err}"
16
+ end
17
+
18
+ log err
19
+ output = stdout.read
20
+
21
+ if block_given?
22
+ yield(output)
23
+ else
24
+ output
25
+ end
26
+ end
27
+ end
28
+
29
+ def liquidity_cmd
30
+ "liquidity --tezos-node #{tezos_node}"
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "liquidity_inteface/liquidity_wrapper"
4
+
5
+ class TezosClient
6
+ class LiquidityInterface
7
+ include Logger
8
+ include LiquidityWrapper
9
+
10
+ def initialize(rpc_node_address: "127.0.0.1", rpc_node_port: 8732)
11
+ @rpc_node_address = rpc_node_address
12
+ @rpc_node_port = rpc_node_port
13
+ end
14
+
15
+ def format_params(params)
16
+ params = [params] if params.is_a? String
17
+ params.map { |s| "'#{s}'" }.join(" ")
18
+ end
19
+
20
+ def initial_storage(args)
21
+ from = args.fetch :from
22
+ script = args.fetch :script
23
+ init_params = args.fetch :init_params
24
+ init_params = format_params(init_params)
25
+
26
+ with_tempfile(".json") do |json_file|
27
+ call_liquidity "--source #{from} --json #{script} -o #{json_file.path} --init-storage #{init_params}"
28
+ JSON.parse json_file.read.strip
29
+ end
30
+ end
31
+
32
+ def with_tempfile(extension)
33
+ file = Tempfile.new(["script", extension])
34
+ yield(file)
35
+
36
+ ensure
37
+ file.unlink
38
+ end
39
+
40
+ def with_file_copy(source_file_path)
41
+ source_file = File.open(source_file_path, "r")
42
+ source_extention = File.extname(source_file_path)
43
+
44
+ file_copy_path = nil
45
+
46
+ res = with_tempfile(source_extention) do |file_copy|
47
+ file_copy.write(source_file.read)
48
+ file_copy_path = file_copy.path
49
+ file_copy.close
50
+ yield(file_copy_path)
51
+ end
52
+
53
+ res
54
+ ensure
55
+ File.delete(file_copy_path) if File.exists? file_copy_path
56
+ end
57
+
58
+ def json_scripts(args)
59
+ with_file_copy(args[:script]) do |script_copy_path|
60
+ json_init_script_path = "#{script_copy_path}.initializer.tz.json"
61
+ json_contract_script_path = "#{script_copy_path}.tz.json"
62
+
63
+ call_liquidity "--json #{script_copy_path}"
64
+
65
+ json_contract_script_file = File.open(json_contract_script_path)
66
+ json_contract_script = JSON.parse(json_contract_script_file.read)
67
+ json_contract_script_file.close
68
+
69
+ if File.exists? json_init_script_path
70
+ json_init_script_file = File.open(json_init_script_path)
71
+ json_init_script = JSON.parse(json_init_script_file.read)
72
+ json_init_script_file.close
73
+ end
74
+
75
+ if block_given?
76
+ yield(json_init_script, json_contract_script)
77
+ else
78
+ return json_init_script, json_contract_script
79
+ end
80
+
81
+ ensure
82
+ [json_init_script_path, json_contract_script_path].each do |file_path|
83
+ File.delete file_path if File.exists? file_path
84
+ end
85
+ end
86
+ end
87
+
88
+ def origination_script(args)
89
+ storage = initial_storage(args)
90
+ _json_init_script, json_contract_script = json_scripts(args)
91
+
92
+ {
93
+ code: json_contract_script,
94
+ storage: storage
95
+ }
96
+ end
97
+
98
+ def forge_deploy(args)
99
+ amount = args.fetch(:amount, 0)
100
+ spendable = args.fetch(:spendable, false)
101
+ delegatable = args.fetch(:delegatable, false)
102
+ source = args.fetch :from
103
+ script = args.fetch :script
104
+ init_params = args.fetch :init_params
105
+
106
+ res = call_liquidity "--source #{source} #{spendable ? '--spendable' : ''} #{delegatable ? '--delegatable' : ''} --amount #{amount}tz #{script} --forge-deploy '#{init_params}'"
107
+ res.strip
108
+ end
109
+
110
+ def tezos_node
111
+ "#{@rpc_node_address}:#{@rpc_node_port}"
112
+ end
113
+
114
+ def get_storage(script:, contract_address:)
115
+ res = call_liquidity "#{script} --get-storage #{contract_address}"
116
+ res.strip
117
+ end
118
+
119
+ def call_parameters(script:, parameters:)
120
+ parameters = format_params(parameters)
121
+ with_tempfile(".json") do |json_file|
122
+ res = call_liquidity "--json -o #{json_file.path} #{script} --data #{parameters}"
123
+ JSON.parse res
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+ require "active_support/concern"
3
+
4
+ class TezosClient
5
+ module Logger
6
+ extend ActiveSupport::Concern
7
+ @@logger = nil
8
+ @@env_logger = nil
9
+
10
+ def log(out)
11
+ return unless self.class.logger
12
+ self.class.logger << out + "\n"
13
+ end
14
+
15
+ class_methods do
16
+ # Setup the log for TezosClient calls.
17
+ # Value should be a logger but can can be stdout, stderr, or a filename.
18
+ # You can also configure logging by the environment variable TEZOSCLIENT_LOG.
19
+ def logger=(log)
20
+ @@logger = create_logger log
21
+ end
22
+
23
+ class StdOutLogger
24
+ def <<(obj)
25
+ STDOUT.puts obj
26
+ end
27
+ end
28
+
29
+ class StdErrLogger
30
+ def <<(obj)
31
+ STDERR.puts obj
32
+ end
33
+ end
34
+
35
+ class FileLogger
36
+ attr_writer :target_file
37
+
38
+ def initialize(target_file)
39
+ @target_file = target_file
40
+ end
41
+
42
+ def <<(obj)
43
+ File.open(@target_file, "a") { |f| f.puts obj }
44
+ end
45
+ end
46
+
47
+ # Create a log that respond to << like a logger
48
+ # param can be 'stdout', 'stderr', a string (then we will log to that file) or a logger (then we return it)
49
+ def create_logger(param)
50
+ return unless param
51
+
52
+ if param.is_a? String
53
+ if param == "stdout"
54
+ StdOutLogger.new
55
+ elsif param == "stderr"
56
+ StdErrLogger.new
57
+ else
58
+ FileLogger.new(param)
59
+ end
60
+ else
61
+ param
62
+ end
63
+ end
64
+
65
+ def env_logger
66
+ if @@env_logger
67
+ @@env_logger
68
+ elsif ENV["TEZOSCLIENT_LOG"]
69
+ @@env_logger = create_logger ENV["TEZOSCLIENT_LOG"]
70
+ end
71
+ end
72
+
73
+ def logger
74
+ @@logger || env_logger
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,125 @@
1
+
2
+ class TezosClient
3
+
4
+ class Operation
5
+ include Crypto
6
+ using CurrencyUtils
7
+
8
+ attr_accessor :liquidity_interface,
9
+ :rpc_interface,
10
+ :base_58_signature,
11
+ :signed_hex,
12
+ :from,
13
+ :operation_args,
14
+ :rpc_args
15
+
16
+ def initialize(liquidity_interface:, rpc_interface:, **args)
17
+ @liquidity_interface = liquidity_interface
18
+ @rpc_interface = rpc_interface
19
+ @from = args.fetch(:from) { raise ArgumentError, "Argument :from missing" }
20
+ @secret_key = args.fetch(:secret_key)
21
+ @init_args = args
22
+ @signed = false
23
+ @operation_args = {}
24
+ initialize_operation_args
25
+ @rpc_args = rpc_interface.operation(@operation_args)
26
+ end
27
+
28
+ def initialize_operation_args
29
+ raise NotImplementedError.new("#{self.class.name}##{__method__} is an abstract method.")
30
+ end
31
+
32
+ def operation_kind
33
+ raise NotImplementedError.new("#{self.class.name}##{__method__} is an abstract method.")
34
+ end
35
+
36
+ def branch
37
+ rpc_interface.head_hash
38
+ end
39
+
40
+ def counter
41
+ @init_args.fetch(:counter) { rpc_interface.contract_counter(from) + 1 }
42
+ end
43
+
44
+ def protocol
45
+ rpc_interface.protocols[0]
46
+ end
47
+
48
+ def simulate_and_update_limits
49
+ run_result = run
50
+
51
+ @operation_args[:gas_limit] = run_result[:consumed_gas] + 0.01
52
+ @operation_args[:storage_limit] = run_result[:consumed_storage]
53
+ end
54
+
55
+ def to_hex
56
+ rpc_interface.forge_operation(operation_args)
57
+ end
58
+
59
+ def sign
60
+ sign_operation(
61
+ secret_key: @secret_key,
62
+ operation_hex: to_hex
63
+ ) do |base_58_signature, signed_hex, _op_id|
64
+ @base_58_signature = base_58_signature
65
+ @signed_hex = signed_hex
66
+ end
67
+
68
+ @signed = true
69
+ end
70
+
71
+ def test_and_broadcast
72
+ # simulate operations and adjust gas limits
73
+ simulate_and_update_limits
74
+ sign
75
+ operation_result = preapply
76
+ op_id = broadcast
77
+ {
78
+ operation_id: op_id,
79
+ operation_result: operation_result
80
+ }
81
+ end
82
+
83
+ def run
84
+ rpc_response = rpc_interface.run_operation(**operation_args, signature: RANDOM_SIGNATURE)
85
+
86
+ operation_result = ensure_applied!(rpc_response)
87
+
88
+ consumed_storage = operation_result.fetch(:paid_storage_size_diff, "0").to_i.from_satoshi
89
+ consumed_gas = operation_result.fetch(:consumed_gas, "0").to_i.from_satoshi
90
+
91
+ {
92
+ status: :applied,
93
+ consumed_gas: consumed_gas,
94
+ consumed_storage: consumed_storage,
95
+ operation_result: operation_result
96
+ }
97
+ end
98
+
99
+ def preapply
100
+ raise "can not preapply unsigned operations" unless @signed
101
+
102
+ res = rpc_interface.preapply_operation(
103
+ **operation_args,
104
+ signature: base_58_signature,
105
+ protocol: protocol)
106
+
107
+ ensure_applied!(res)
108
+ end
109
+
110
+ def broadcast
111
+ raise "can not preapply unsigned operations" unless @signed
112
+ rpc_interface.broadcast_operation(signed_hex)
113
+ end
114
+
115
+
116
+ private
117
+
118
+ def ensure_applied!(rpc_response)
119
+ operation_result = rpc_response[:metadata][:operation_result]
120
+ status = operation_result[:status]
121
+ raise "Operation status != 'applied': #{status}\n #{rpc_response.pretty_inspect}" if status != "applied"
122
+ operation_result
123
+ end
124
+ end
125
+ end