tezos_client 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/.rspec +3 -0
- data/.rubocop.yml +9 -0
- data/.ruby-version +1 -0
- data/.travis.yml +21 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +147 -0
- data/LICENSE.txt +21 -0
- data/README.md +107 -0
- data/Rakefile +8 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/lib/tezos_client/client_interface/block_contextual.rb +16 -0
- data/lib/tezos_client/client_interface/client_wrapper.rb +37 -0
- data/lib/tezos_client/client_interface/contract.rb +17 -0
- data/lib/tezos_client/client_interface/key.rb +35 -0
- data/lib/tezos_client/client_interface/misc.rb +26 -0
- data/lib/tezos_client/client_interface.rb +25 -0
- data/lib/tezos_client/commands.rb +28 -0
- data/lib/tezos_client/crypto.rb +178 -0
- data/lib/tezos_client/currency_utils.rb +20 -0
- data/lib/tezos_client/encode_utils.rb +139 -0
- data/lib/tezos_client/liquidity_inteface/liquidity_wrapper.rb +34 -0
- data/lib/tezos_client/liquidity_interface.rb +127 -0
- data/lib/tezos_client/logger.rb +78 -0
- data/lib/tezos_client/operation.rb +125 -0
- data/lib/tezos_client/operations/origination_operation.rb +50 -0
- data/lib/tezos_client/operations/transaction_operation.rb +39 -0
- data/lib/tezos_client/rpc_interface/blocks.rb +34 -0
- data/lib/tezos_client/rpc_interface/context.rb +27 -0
- data/lib/tezos_client/rpc_interface/contracts.rb +27 -0
- data/lib/tezos_client/rpc_interface/helper.rb +100 -0
- data/lib/tezos_client/rpc_interface/monitor.rb +19 -0
- data/lib/tezos_client/rpc_interface/request_manager.rb +88 -0
- data/lib/tezos_client/rpc_interface.rb +29 -0
- data/lib/tezos_client/string_utils.rb +16 -0
- data/lib/tezos_client/version.rb +5 -0
- data/lib/tezos_client.rb +149 -0
- data/tezos_client.gemspec +48 -0
- data/travis-scripts/install-liquidity.sh +25 -0
- data/travis-scripts/install-opam.sh +4 -0
- data/travis-scripts/prepare-trusty.sh +16 -0
- 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
|