steem-ruby 0.1.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 +54 -0
- data/CONTRIBUTING.md +79 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +73 -0
- data/LICENSE +22 -0
- data/README.md +77 -0
- data/Rakefile +115 -0
- data/gource.sh +6 -0
- data/images/Anthony Martin.png +0 -0
- data/lib/steem/api.rb +190 -0
- data/lib/steem/base_error.rb +151 -0
- data/lib/steem/block_api.rb +45 -0
- data/lib/steem/broadcast.rb +1056 -0
- data/lib/steem/chain_config.rb +36 -0
- data/lib/steem/formatter.rb +14 -0
- data/lib/steem/jsonrpc.rb +98 -0
- data/lib/steem/mixins/retriable.rb +56 -0
- data/lib/steem/rpc/base_client.rb +154 -0
- data/lib/steem/rpc/thread_safe_client.rb +23 -0
- data/lib/steem/transaction_builder.rb +266 -0
- data/lib/steem/type/amount.rb +61 -0
- data/lib/steem/type/base_type.rb +10 -0
- data/lib/steem/utils.rb +17 -0
- data/lib/steem/version.rb +4 -0
- data/lib/steem.rb +35 -0
- data/steem-ruby.gemspec +37 -0
- metadata +390 -0
@@ -0,0 +1,36 @@
|
|
1
|
+
module Steem
|
2
|
+
module ChainConfig
|
3
|
+
EXPIRE_IN_SECS = 600
|
4
|
+
EXPIRE_IN_SECS_PROPOSAL = 24 * 60 * 60
|
5
|
+
|
6
|
+
NETWORKS_STEEM_CHAIN_ID = '0000000000000000000000000000000000000000000000000000000000000000'
|
7
|
+
NETWORKS_STEEM_ADDRESS_PREFIX = 'STM'
|
8
|
+
NETWORKS_STEEM_CORE_ASSET = ["0", 3, "@@000000021"] # STEEM
|
9
|
+
NETWORKS_STEEM_DEBT_ASSET = ["0", 3, "@@000000013"] # SBD
|
10
|
+
NETWORKS_STEEM_VEST_ASSET = ["0", 6, "@@000000037"] # VESTS
|
11
|
+
NETWORKS_STEEM_DEFAULT_NODE = 'https://api.steemit.com' # √
|
12
|
+
# NETWORKS_STEEM_DEFAULT_NODE = 'https://api.steemitstage.com' # √
|
13
|
+
# NETWORKS_STEEM_DEFAULT_NODE = 'https://api.steemitdev.com' # √
|
14
|
+
# NETWORKS_STEEM_DEFAULT_NODE = 'https://appbasetest.timcliff.com'
|
15
|
+
# NETWORKS_STEEM_DEFAULT_NODE = 'https://gtg.steem.house:8090'
|
16
|
+
# NETWORKS_STEEM_DEFAULT_NODE = 'https://api.steem.house' # √?
|
17
|
+
# NETWORKS_STEEM_DEFAULT_NODE = 'https://seed.bitcoiner.me'
|
18
|
+
# NETWORKS_STEEM_DEFAULT_NODE = 'https://steemd.minnowsupportproject.org'
|
19
|
+
# NETWORKS_STEEM_DEFAULT_NODE = 'https://steemd.privex.io'
|
20
|
+
# NETWORKS_STEEM_DEFAULT_NODE = 'https://rpc.steemliberator.com'
|
21
|
+
# NETWORKS_STEEM_DEFAULT_NODE = 'https://rpc.curiesteem.com'
|
22
|
+
# NETWORKS_STEEM_DEFAULT_NODE = 'https://rpc.buildteam.io'
|
23
|
+
# NETWORKS_STEEM_DEFAULT_NODE = 'https://steemd.pevo.science'
|
24
|
+
# NETWORKS_STEEM_DEFAULT_NODE = 'https://rpc.steemviz.com'
|
25
|
+
# NETWORKS_STEEM_DEFAULT_NODE = 'https://steemd.steemgigs.org'
|
26
|
+
|
27
|
+
NETWORKS_TEST_CHAIN_ID = '46d82ab7d8db682eb1959aed0ada039a6d49afa1602491f93dde9cac3e8e6c32'
|
28
|
+
NETWORKS_TEST_ADDRESS_PREFIX = 'TST'
|
29
|
+
NETWORKS_TEST_CORE_ASSET = ["0", 3, "@@000000021"] # TESTS
|
30
|
+
NETWORKS_TEST_DEBT_ASSET = ["0", 3, "@@000000013"] # TBD
|
31
|
+
NETWORKS_TEST_VEST_ASSET = ["0", 6, "@@000000037"] # VESTS
|
32
|
+
NETWORKS_TEST_DEFAULT_NODE = 'https://testnet.steemitdev.com'
|
33
|
+
|
34
|
+
NETWORK_CHAIN_IDS = [NETWORKS_STEEM_CHAIN_ID, NETWORKS_TEST_CHAIN_ID]
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
module Steem
|
2
|
+
# {Jsonrpc} allows you to inspect the available methods offered by a node.
|
3
|
+
# If a node runs a plugin you want, then all of the API methods it can exposes
|
4
|
+
# will automatically be available. This API is used internally to determine
|
5
|
+
# which APIs and methods are available on the node you specify.
|
6
|
+
#
|
7
|
+
# In theory, if a new plugin is created and enabled by the node, it will be
|
8
|
+
# available by this library without needing an update to its code.
|
9
|
+
class Jsonrpc < Api
|
10
|
+
API_METHODS = %i(get_signature get_methods)
|
11
|
+
|
12
|
+
def self.api_methods
|
13
|
+
@api_methods ||= {}
|
14
|
+
end
|
15
|
+
|
16
|
+
# Might help diagnose a cluster that has asymmetric plugin definitions.
|
17
|
+
def self.reset_api_methods
|
18
|
+
@api_methods = nil
|
19
|
+
end
|
20
|
+
|
21
|
+
def initialize(options = {})
|
22
|
+
self.class.api_name = :jsonrpc
|
23
|
+
@methods = API_METHODS
|
24
|
+
super
|
25
|
+
end
|
26
|
+
|
27
|
+
def get_api_methods(&block)
|
28
|
+
api_methods = self.class.api_methods[@rpc_client.uri.to_s]
|
29
|
+
|
30
|
+
if api_methods.nil?
|
31
|
+
get_methods do |result, error, rpc_id|
|
32
|
+
methods = result.map do |method|
|
33
|
+
method.split('.').map(&:to_sym)
|
34
|
+
end
|
35
|
+
|
36
|
+
api_methods = Hashie::Mash.new
|
37
|
+
|
38
|
+
methods.each do |api, method|
|
39
|
+
api_methods[api] ||= []
|
40
|
+
api_methods[api] << method
|
41
|
+
end
|
42
|
+
|
43
|
+
self.class.api_methods[@rpc_client.uri.to_s] = api_methods
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
if !!block
|
48
|
+
api_methods.each do |api, methods|
|
49
|
+
yield api, methods
|
50
|
+
end
|
51
|
+
else
|
52
|
+
return api_methods
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def get_all_signatures(&block)
|
57
|
+
request_body = []
|
58
|
+
method_names = []
|
59
|
+
method_map = {}
|
60
|
+
signatures = {}
|
61
|
+
offset = 0
|
62
|
+
|
63
|
+
get_api_methods do |api, methods|
|
64
|
+
request_body += methods.map do |method|
|
65
|
+
method_name = "#{api}.#{method}"
|
66
|
+
method_names << method_name
|
67
|
+
current_rpc_id = @rpc_client.rpc_id
|
68
|
+
offset += 1
|
69
|
+
method_map[current_rpc_id] = [api, method]
|
70
|
+
|
71
|
+
{
|
72
|
+
jsonrpc: '2.0',
|
73
|
+
id: current_rpc_id,
|
74
|
+
method: 'jsonrpc.get_signature',
|
75
|
+
params: {method: method_name}
|
76
|
+
}
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
@rpc_client.rpc_post(nil, nil, {request_body: request_body}) do |result, error, id|
|
81
|
+
api, method = method_map[id]
|
82
|
+
api = api.to_sym
|
83
|
+
method = method.to_sym
|
84
|
+
|
85
|
+
signatures[api] ||= {}
|
86
|
+
signatures[api][method] = result
|
87
|
+
end
|
88
|
+
|
89
|
+
if !!block
|
90
|
+
signatures.each do |api, methods|
|
91
|
+
yield api, methods
|
92
|
+
end
|
93
|
+
else
|
94
|
+
return signatures
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module Steem
|
2
|
+
module Retriable
|
3
|
+
# @private
|
4
|
+
MAX_RETRY_COUNT = 100
|
5
|
+
|
6
|
+
# @private
|
7
|
+
MAX_BACKOFF = 30
|
8
|
+
|
9
|
+
MAX_RETRY_ELAPSE = 300
|
10
|
+
|
11
|
+
RETRYABLE_EXCEPTIONS = [
|
12
|
+
NonCanonicalSignatureError, IncorrectRequestIdError,
|
13
|
+
IncorrectResponseIdError, Errno::EBADF, Errno::ECONNREFUSED,
|
14
|
+
JSON::ParserError, IOError, Net::OpenTimeout
|
15
|
+
]
|
16
|
+
|
17
|
+
# Expontential backoff.
|
18
|
+
#
|
19
|
+
# @private
|
20
|
+
def backoff
|
21
|
+
@backoff ||= 0.1
|
22
|
+
@backoff *= 2
|
23
|
+
if @backoff > MAX_BACKOFF
|
24
|
+
@backoff = 0.1
|
25
|
+
|
26
|
+
if Time.now.utc - @first_retry_at > MAX_RETRY_ELAPSE
|
27
|
+
@retry_count = nil
|
28
|
+
@first_retry_at = nil
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
sleep @backoff
|
33
|
+
end
|
34
|
+
|
35
|
+
def can_retry?(e = nil)
|
36
|
+
@retry_count ||= 0
|
37
|
+
@first_retry_at ||= Time.now.utc
|
38
|
+
|
39
|
+
return false if @retry_count >= MAX_RETRY_COUNT
|
40
|
+
|
41
|
+
@retry_count += 1
|
42
|
+
|
43
|
+
can_retry = case e
|
44
|
+
when *RETRYABLE_EXCEPTIONS
|
45
|
+
true
|
46
|
+
when RemoteNodeError
|
47
|
+
e.inspect.include?('Unable to acquire database lock')
|
48
|
+
else; false
|
49
|
+
end
|
50
|
+
|
51
|
+
backoff if can_retry
|
52
|
+
|
53
|
+
can_retry
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,154 @@
|
|
1
|
+
module Steem
|
2
|
+
module RPC
|
3
|
+
class BaseClient
|
4
|
+
include ChainConfig
|
5
|
+
|
6
|
+
attr_accessor :chain, :error_pipe
|
7
|
+
|
8
|
+
# @private
|
9
|
+
POST_HEADERS = {
|
10
|
+
'Content-Type' => 'application/json; charset=utf-8',
|
11
|
+
'User-Agent' => Steem::AGENT_ID
|
12
|
+
}
|
13
|
+
|
14
|
+
def initialize(options = {})
|
15
|
+
@chain = options[:chain] || :steem
|
16
|
+
@error_pipe = options[:error_pipe] || STDERR
|
17
|
+
@api_name = options[:api_name]
|
18
|
+
@url = case @chain
|
19
|
+
when :steem then options[:url] || NETWORKS_STEEM_DEFAULT_NODE
|
20
|
+
when :test then options[:url] || NETWORKS_TEST_DEFAULT_NODE
|
21
|
+
else; raise UnsupportedChainError, "Unsupported chain: #{@chain}"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def uri
|
26
|
+
@uri ||= URI.parse(@url)
|
27
|
+
end
|
28
|
+
|
29
|
+
def http
|
30
|
+
@http ||= Net::HTTP.new(uri.host, uri.port).tap do |http|
|
31
|
+
http.use_ssl = true
|
32
|
+
http.keep_alive_timeout = 2 # seconds
|
33
|
+
|
34
|
+
# WARNING This method opens a serious security hole. Never use this
|
35
|
+
# method in production code.
|
36
|
+
# http.set_debug_output(STDOUT) if !!ENV['DEBUG']
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def http_post
|
41
|
+
@http_post ||= Net::HTTP::Post.new(uri.request_uri, POST_HEADERS)
|
42
|
+
end
|
43
|
+
|
44
|
+
def put(api_name = @api_name, api_method = nil, options = {})
|
45
|
+
current_rpc_id = rpc_id
|
46
|
+
rpc_method_name = "#{api_name}.#{api_method}"
|
47
|
+
options ||= {}
|
48
|
+
request_body = defined?(options.delete) ? options.delete(:request_body) : []
|
49
|
+
request_body ||= []
|
50
|
+
|
51
|
+
request_body << {
|
52
|
+
jsonrpc: '2.0',
|
53
|
+
id: current_rpc_id,
|
54
|
+
method: rpc_method_name,
|
55
|
+
params: options
|
56
|
+
}
|
57
|
+
|
58
|
+
request_body
|
59
|
+
end
|
60
|
+
|
61
|
+
def evaluate_id(options = {})
|
62
|
+
debug = options[:debug] || ENV['DEBUG'] == 'true'
|
63
|
+
request = options[:request]
|
64
|
+
response = options[:response]
|
65
|
+
api_method = options[:api_method]
|
66
|
+
req_id = request[:id].to_i
|
67
|
+
res_id = !!response['id'] ? response['id'].to_i : nil
|
68
|
+
method = [@api_name, api_method].join('.')
|
69
|
+
|
70
|
+
if debug
|
71
|
+
req = JSON.pretty_generate(request)
|
72
|
+
res = JSON.parse(response) rescue response
|
73
|
+
res = JSON.pretty_generate(response) rescue response
|
74
|
+
|
75
|
+
error_pipe.puts '=' * 80
|
76
|
+
error_pipe.puts "Request:"
|
77
|
+
error_pipe.puts req
|
78
|
+
error_pipe.puts '=' * 80
|
79
|
+
error_pipe.puts "Response:"
|
80
|
+
error_pipe.puts res
|
81
|
+
error_pipe.puts '=' * 80
|
82
|
+
error_pipe.puts Thread.current.backtrace.join("\n")
|
83
|
+
end
|
84
|
+
|
85
|
+
error = response['error'].to_json if !!response['error']
|
86
|
+
|
87
|
+
if req_id != res_id
|
88
|
+
raise IncorrectResponseIdError, "#{method}: The json-rpc id did not match. Request was: #{req_id}, got: #{res_id.inspect}", error.nil? ? nil : error.to_json
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def http_request(request)
|
93
|
+
http.request(request)
|
94
|
+
end
|
95
|
+
|
96
|
+
def rpc_post(api_name = @api_name, api_method = nil, options = {}, &block)
|
97
|
+
request = http_post
|
98
|
+
|
99
|
+
request_body = if !!api_name && !!api_method
|
100
|
+
put(api_name, api_method, options)
|
101
|
+
elsif !!options && defined?(options.delete)
|
102
|
+
options.delete(:request_body)
|
103
|
+
end
|
104
|
+
|
105
|
+
request.body = if request_body.size == 1
|
106
|
+
request_body.first.to_json
|
107
|
+
else
|
108
|
+
request_body.to_json
|
109
|
+
end
|
110
|
+
|
111
|
+
response = http_request(request)
|
112
|
+
|
113
|
+
case response.code
|
114
|
+
when '200'
|
115
|
+
response = JSON[response.body]
|
116
|
+
response = case response
|
117
|
+
when Hash
|
118
|
+
Hashie::Mash.new(response).tap do |r|
|
119
|
+
evaluate_id(request: request_body.first, response: r, api_method: api_method)
|
120
|
+
end
|
121
|
+
when Array
|
122
|
+
Hashie::Array.new(response).tap do |r|
|
123
|
+
request_body.each_with_index do |req, index|
|
124
|
+
evaluate_id(request: req, response: r[index], api_method: api_method)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
else; response
|
128
|
+
end
|
129
|
+
|
130
|
+
if !!block
|
131
|
+
case response
|
132
|
+
when Hashie::Mash then yield response.result, response.error, response.id
|
133
|
+
when Hashie::Array
|
134
|
+
response.each do |r|
|
135
|
+
r = Hashie::Mash.new(r)
|
136
|
+
yield r.result, r.error, r.id
|
137
|
+
end
|
138
|
+
else; yield response
|
139
|
+
end
|
140
|
+
else
|
141
|
+
return response
|
142
|
+
end
|
143
|
+
else
|
144
|
+
raise UnknownError, "#{api_name}.#{api_method}: #{response.body}"
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def rpc_id
|
149
|
+
@rpc_id ||= 0
|
150
|
+
@rpc_id += 1
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Steem
|
2
|
+
module RPC
|
3
|
+
# {ThreadSafeClient} is the default RPC Client used by `steem-ruby.` It's
|
4
|
+
# perfect for simple requests. But for higher performance, it's better to
|
5
|
+
# override {BaseClient} and implement something other than {Net::HTTP}.
|
6
|
+
class ThreadSafeClient < BaseClient
|
7
|
+
SEMAPHORE = Mutex.new.freeze
|
8
|
+
|
9
|
+
def http_request(request)
|
10
|
+
response = SEMAPHORE.synchronize do
|
11
|
+
http.request(request)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def rpc_id
|
16
|
+
SEMAPHORE.synchronize do
|
17
|
+
@rpc_id ||= 0
|
18
|
+
@rpc_id += 1
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,266 @@
|
|
1
|
+
module Steem
|
2
|
+
# {TransactionBuilder} can be used to create a transaction that the
|
3
|
+
# {NetworkBroadcastApi} can broadcast to the rest of the platform. The main
|
4
|
+
# feature of this class is the ability to cryptographically sign the
|
5
|
+
# transaction so that it conforms to the consensus rules that are required by
|
6
|
+
# the blockchain.
|
7
|
+
#
|
8
|
+
# wif = '5JrvPrQeBBvCRdjv29iDvkwn3EQYZ9jqfAHzrCyUvfbEbRkrYFC'
|
9
|
+
# builder = Steem::TransactionBuilder.new(wif: wif)
|
10
|
+
# builder.put(vote: {
|
11
|
+
# voter: 'alice',
|
12
|
+
# author: 'bob',
|
13
|
+
# permlink: 'my-burgers',
|
14
|
+
# weight: 10000
|
15
|
+
# })
|
16
|
+
#
|
17
|
+
# trx = builder.transaction
|
18
|
+
# network_broadcast_api = Steem::NetworkBroadcastApi.new
|
19
|
+
# network_broadcast_api.broadcast_transaction_synchronous(trx: trx)
|
20
|
+
#
|
21
|
+
class TransactionBuilder
|
22
|
+
include Retriable
|
23
|
+
include ChainConfig
|
24
|
+
include Utils
|
25
|
+
|
26
|
+
attr_accessor :database_api, :block_api, :wif, :expiration, :operations
|
27
|
+
|
28
|
+
def initialize(options = {})
|
29
|
+
@database_api = options[:database_api] || Steem::DatabaseApi.new(options)
|
30
|
+
@block_api = options[:block_api] || Steem::BlockApi.new(options)
|
31
|
+
@wif = options[:wif]
|
32
|
+
@ref_block_num = options[:ref_block_num]
|
33
|
+
@ref_block_prefix = options[:ref_block_prefix]
|
34
|
+
@expiration = nil
|
35
|
+
@operations = options[:operations] || []
|
36
|
+
@extensions = []
|
37
|
+
@signatures = []
|
38
|
+
@chain = options[:chain] || :steem
|
39
|
+
@error_pipe = options[:error_pipe] || STDERR
|
40
|
+
@chain_id = case @chain
|
41
|
+
when :steem then NETWORKS_STEEM_CHAIN_ID
|
42
|
+
when :test then NETWORKS_TEST_CHAIN_ID
|
43
|
+
else; raise UnsupportedChainError, "Unsupported chain: #{@chain}"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def inspect
|
48
|
+
properties = %w(
|
49
|
+
ref_block_num ref_block_prefix expiration operations
|
50
|
+
extensions signatures
|
51
|
+
).map do |prop|
|
52
|
+
if !!(v = instance_variable_get("@#{prop}"))
|
53
|
+
"@#{prop}=#{v}"
|
54
|
+
end
|
55
|
+
end.compact.join(', ')
|
56
|
+
|
57
|
+
"#<#{self.class.name} [#{properties}]>"
|
58
|
+
end
|
59
|
+
|
60
|
+
def reset
|
61
|
+
@ref_block_num = nil
|
62
|
+
@ref_block_prefix = nil
|
63
|
+
@expiration = nil
|
64
|
+
@operations = []
|
65
|
+
@extensions = []
|
66
|
+
@signatures = []
|
67
|
+
|
68
|
+
self
|
69
|
+
end
|
70
|
+
|
71
|
+
def expired?
|
72
|
+
@expiration.nil? || @expiration < Time.now
|
73
|
+
end
|
74
|
+
|
75
|
+
# If the transaction can be prepared, this method will do so and set the
|
76
|
+
# expiration. Once the expiration is set, it will not re-prepare. If you
|
77
|
+
# call {#put}, the expiration is set {::Nil} so that it can be re-prepared.
|
78
|
+
#
|
79
|
+
# Usually, this method is called automatically by {#put} and/or {#transaction}.
|
80
|
+
#
|
81
|
+
# @return {TransactionBuilder}
|
82
|
+
def prepare
|
83
|
+
if expired?
|
84
|
+
catch :prepare_header do; begin
|
85
|
+
@database_api.get_dynamic_global_properties do |properties|
|
86
|
+
block_number = properties.last_irreversible_block_num
|
87
|
+
|
88
|
+
@block_api.get_block_header(block_num: block_number) do |result|
|
89
|
+
header = result.header
|
90
|
+
|
91
|
+
@ref_block_num = (block_number - 1) & 0xFFFF
|
92
|
+
@ref_block_prefix = unhexlify(header.previous[8..-1]).unpack('V*')[0]
|
93
|
+
@expiration = (Time.parse(properties.time + 'Z') + EXPIRE_IN_SECS).utc
|
94
|
+
end
|
95
|
+
end
|
96
|
+
rescue => e
|
97
|
+
if can_retry? e
|
98
|
+
@error_pipe.puts "#{e} ... retrying."
|
99
|
+
throw :prepare_header
|
100
|
+
else
|
101
|
+
raise e
|
102
|
+
end
|
103
|
+
end; end
|
104
|
+
end
|
105
|
+
|
106
|
+
self
|
107
|
+
end
|
108
|
+
|
109
|
+
# Sets operations all at once, then prepares.
|
110
|
+
def operations=(operations)
|
111
|
+
@operations = operations
|
112
|
+
prepare
|
113
|
+
@operations
|
114
|
+
end
|
115
|
+
|
116
|
+
# A quick and flexible way to append a new operation to the transaction.
|
117
|
+
# This method uses ducktyping to figure out how to form the operation.
|
118
|
+
#
|
119
|
+
# There are three main ways you can call this method. These assume that
|
120
|
+
# `op_type` is a {::Symbol} (or {::String}) representing the type of operation and `op` is the
|
121
|
+
# operation {::Hash}.
|
122
|
+
#
|
123
|
+
# put(op_type, op)
|
124
|
+
#
|
125
|
+
# ... or ...
|
126
|
+
#
|
127
|
+
# put(op_type => op)
|
128
|
+
#
|
129
|
+
# ... or ...
|
130
|
+
#
|
131
|
+
# put([op_type, op])
|
132
|
+
#
|
133
|
+
# You can also chain multiple operations:
|
134
|
+
#
|
135
|
+
# builder = Steem::TransactionBuilder.new
|
136
|
+
# builder.put(vote: vote1).put(vote: vote2)
|
137
|
+
# @return {TransactionBuilder}
|
138
|
+
def put(type, op = nil)
|
139
|
+
@expiration = nil
|
140
|
+
|
141
|
+
case type
|
142
|
+
when Symbol then @operations << [type, op]
|
143
|
+
when String then @operations << [type.to_sym, op]
|
144
|
+
when Hash then @operations << [type.keys.first.to_sym, type.values.first]
|
145
|
+
when Array then @operations << type
|
146
|
+
else
|
147
|
+
# don't know what to do with it, skipped
|
148
|
+
end
|
149
|
+
|
150
|
+
prepare
|
151
|
+
|
152
|
+
self
|
153
|
+
end
|
154
|
+
|
155
|
+
# If all of the required values are set, this returns a fully formed
|
156
|
+
# transaction that is ready to broadcast.
|
157
|
+
#
|
158
|
+
# @return
|
159
|
+
# {
|
160
|
+
# :ref_block_num => 18912,
|
161
|
+
# :ref_block_prefix => 575781536,
|
162
|
+
# :expiration => "2018-04-26T15:26:12",
|
163
|
+
# :extensions => [],
|
164
|
+
# :operations => [[:vote, {
|
165
|
+
# :voter => "alice",
|
166
|
+
# :author => "bob",
|
167
|
+
# :permlink => "my-burgers",
|
168
|
+
# :weight => 10000
|
169
|
+
# }
|
170
|
+
# ]],
|
171
|
+
# :signatures => ["1c45b65740b4b2c17c4bcf6bcc3f8d90ddab827d50532729fc3b8f163f2c465a532b0112ae4bf388ccc97b7c2e0bc570caadda78af48cf3c261037e65eefcd941e"]
|
172
|
+
# }
|
173
|
+
def transaction
|
174
|
+
prepare
|
175
|
+
sign
|
176
|
+
end
|
177
|
+
|
178
|
+
# Appends to the `signatures` array of the transaction, built from a
|
179
|
+
# serialized digest.
|
180
|
+
#
|
181
|
+
# @return {Hash | TransactionBuilder} The fully signed transaction if a `wif` is provided or the instance of the {TransactionBuilder} if a `wif` has not yet been provided.
|
182
|
+
def sign
|
183
|
+
return self unless !!@wif
|
184
|
+
return self if expired?
|
185
|
+
|
186
|
+
trx = {
|
187
|
+
ref_block_num: @ref_block_num,
|
188
|
+
ref_block_prefix: @ref_block_prefix,
|
189
|
+
expiration: @expiration.strftime('%Y-%m-%dT%H:%M:%S'),
|
190
|
+
operations: @operations,
|
191
|
+
extensions: @extensions,
|
192
|
+
signatures: @signatures
|
193
|
+
}
|
194
|
+
|
195
|
+
catch :serialize do; begin
|
196
|
+
@database_api.get_transaction_hex(trx: trx) do |result|
|
197
|
+
hex = @chain_id + result.hex[0..-4] # Why do we have to chop the last two bytes?
|
198
|
+
digest = unhexlify(hex)
|
199
|
+
digest_hex = Digest::SHA256.digest(digest)
|
200
|
+
private_key = Bitcoin::Key.from_base58 @wif
|
201
|
+
public_key_hex = private_key.pub
|
202
|
+
ec = Bitcoin::OpenSSL_EC
|
203
|
+
count = 0
|
204
|
+
sig = nil
|
205
|
+
|
206
|
+
loop do
|
207
|
+
count += 1
|
208
|
+
@error_pipe.puts "#{count} attempts to find canonical signature" if count % 40 == 0
|
209
|
+
sig = ec.sign_compact(digest_hex, private_key.priv, public_key_hex, false)
|
210
|
+
|
211
|
+
next if public_key_hex != ec.recover_compact(digest_hex, sig)
|
212
|
+
break if canonical? sig
|
213
|
+
end
|
214
|
+
|
215
|
+
trx[:signatures] = @signatures = [hexlify(sig)]
|
216
|
+
end
|
217
|
+
rescue => e
|
218
|
+
if can_retry? e
|
219
|
+
@error_pipe.puts "#{e} ... retrying."
|
220
|
+
throw :serialize
|
221
|
+
else
|
222
|
+
raise e
|
223
|
+
end
|
224
|
+
end; end
|
225
|
+
|
226
|
+
trx
|
227
|
+
end
|
228
|
+
|
229
|
+
# @return [Array] All public keys that could possibly sign for a given transaction.
|
230
|
+
def potential_signatures
|
231
|
+
@database_api.get_potential_signatures(trx: transaction) do |result|
|
232
|
+
result[:keys]
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
# This API will take a partially signed transaction and a set of public keys
|
237
|
+
# that the owner has the ability to sign for and return the minimal subset
|
238
|
+
# of public keys that should add signatures to the transaction.
|
239
|
+
#
|
240
|
+
# @return [Array] The minimal subset of public keys that should add signatures to the transaction.
|
241
|
+
def required_signatures
|
242
|
+
@database_api.get_required_signatures(trx: transaction) do |result|
|
243
|
+
result[:keys]
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
# @return [Boolean] True if the transaction has all of the required signatures.
|
248
|
+
def valid?
|
249
|
+
@database_api.verify_authority(trx: transaction) do |result|
|
250
|
+
result.valid
|
251
|
+
end
|
252
|
+
end
|
253
|
+
private
|
254
|
+
# See: https://github.com/steemit/steem/issues/1944
|
255
|
+
def canonical?(sig)
|
256
|
+
sig = sig.unpack('C*')
|
257
|
+
|
258
|
+
!(
|
259
|
+
((sig[0] & 0x80 ) != 0) || ( sig[0] == 0 ) ||
|
260
|
+
((sig[1] & 0x80 ) != 0) ||
|
261
|
+
((sig[32] & 0x80 ) != 0) || ( sig[32] == 0 ) ||
|
262
|
+
((sig[33] & 0x80 ) != 0)
|
263
|
+
)
|
264
|
+
end
|
265
|
+
end
|
266
|
+
end
|