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