tezos_client 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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