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.
- 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
|