sequence-sdk 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +191 -0
- data/README.md +41 -0
- data/lib/chain/account.rb +80 -0
- data/lib/chain/asset.rb +85 -0
- data/lib/chain/balance.rb +40 -0
- data/lib/chain/batch_response.rb +21 -0
- data/lib/chain/client.rb +71 -0
- data/lib/chain/client_module.rb +11 -0
- data/lib/chain/connection.rb +206 -0
- data/lib/chain/constants.rb +3 -0
- data/lib/chain/dev_utils.rb +14 -0
- data/lib/chain/errors.rb +80 -0
- data/lib/chain/key.rb +46 -0
- data/lib/chain/page.rb +25 -0
- data/lib/chain/query.rb +79 -0
- data/lib/chain/response_object.rb +116 -0
- data/lib/chain/stats.rb +31 -0
- data/lib/chain/transaction.rb +256 -0
- data/lib/chain/unspent_output.rb +72 -0
- data/lib/chain/version.rb +3 -0
- data/lib/chain.rb +3 -0
- metadata +202 -0
@@ -0,0 +1,206 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'net/http'
|
3
|
+
require 'net/https'
|
4
|
+
require 'openssl'
|
5
|
+
require 'thread'
|
6
|
+
|
7
|
+
require_relative './batch_response'
|
8
|
+
require_relative './errors'
|
9
|
+
require_relative './version'
|
10
|
+
|
11
|
+
module Chain
|
12
|
+
class Connection
|
13
|
+
|
14
|
+
# Parameters to the retry exponential backoff function.
|
15
|
+
MAX_RETRIES = 10
|
16
|
+
RETRY_BASE_DELAY_MS = 40
|
17
|
+
RETRY_MAX_DELAY_MS = 4000
|
18
|
+
|
19
|
+
NETWORK_ERRORS = [
|
20
|
+
InvalidRequestIDError,
|
21
|
+
SocketError,
|
22
|
+
EOFError,
|
23
|
+
IOError,
|
24
|
+
Timeout::Error,
|
25
|
+
Errno::ECONNABORTED,
|
26
|
+
Errno::ECONNRESET,
|
27
|
+
Errno::ETIMEDOUT,
|
28
|
+
Errno::EHOSTUNREACH,
|
29
|
+
Errno::ECONNREFUSED,
|
30
|
+
]
|
31
|
+
|
32
|
+
def initialize(opts)
|
33
|
+
@opts = opts
|
34
|
+
@url = URI(@opts[:url])
|
35
|
+
@access_token = @opts[:access_token] || @url.userinfo
|
36
|
+
@http_mutex = Mutex.new
|
37
|
+
end
|
38
|
+
|
39
|
+
# Returns a copy of the configuration options
|
40
|
+
def opts
|
41
|
+
@opts.dup
|
42
|
+
end
|
43
|
+
|
44
|
+
def request(path, body = {})
|
45
|
+
_request_with_retries(path, body)[:body]
|
46
|
+
end
|
47
|
+
|
48
|
+
def batch_request(path, body = {}, &translate)
|
49
|
+
res = _request_with_retries(path, body)
|
50
|
+
body = res[:body]
|
51
|
+
response = res[:response]
|
52
|
+
|
53
|
+
successes = {}
|
54
|
+
errors = {}
|
55
|
+
|
56
|
+
body.each_with_index do |item, i|
|
57
|
+
if !!item['code']
|
58
|
+
errors[i] = APIError.new(item, response)
|
59
|
+
else
|
60
|
+
if translate
|
61
|
+
successes[i] = translate.call(item)
|
62
|
+
else
|
63
|
+
successes[i] = item
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
BatchResponse.new(
|
69
|
+
successes: successes,
|
70
|
+
errors: errors,
|
71
|
+
response: response,
|
72
|
+
)
|
73
|
+
end
|
74
|
+
|
75
|
+
def singleton_batch_request(path, body = {}, &translate)
|
76
|
+
batch = batch_request(path, body, &translate)
|
77
|
+
|
78
|
+
if batch.size != 1
|
79
|
+
raise "Invalid response, expected a single response object but got #{batch.items.size}"
|
80
|
+
end
|
81
|
+
|
82
|
+
raise batch.errors.values.first if batch.errors.size == 1
|
83
|
+
|
84
|
+
batch.successes.values.first
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
def _request_with_retries(path, body)
|
90
|
+
attempts = 0
|
91
|
+
|
92
|
+
begin
|
93
|
+
attempts += 1
|
94
|
+
|
95
|
+
# If this is a retry and not the first attempt, sleep before making the
|
96
|
+
# retry request.
|
97
|
+
sleep(backoff_delay(attempts)) if attempts > 1
|
98
|
+
|
99
|
+
_single_request(path, body)
|
100
|
+
rescue *NETWORK_ERRORS => e
|
101
|
+
raise e if attempts > MAX_RETRIES
|
102
|
+
retry
|
103
|
+
rescue APIError => e
|
104
|
+
raise e if attempts > MAX_RETRIES
|
105
|
+
retry if e.retriable?
|
106
|
+
raise e
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def _single_request(path, body)
|
111
|
+
@http_mutex.synchronize do
|
112
|
+
# Timeout configuration
|
113
|
+
[:open_timeout, :read_timeout, :ssl_timeout].each do |k|
|
114
|
+
next unless @opts.key?(k)
|
115
|
+
http.send "#{k}=", @opts[k]
|
116
|
+
end
|
117
|
+
|
118
|
+
full_path = @url.request_uri.chomp('/')
|
119
|
+
full_path += (path[0] == '/') ? path : '/' + path
|
120
|
+
|
121
|
+
req = Net::HTTP::Post.new(full_path)
|
122
|
+
req['Accept'] = 'application/json'
|
123
|
+
req['Content-Type'] = 'application/json'
|
124
|
+
req['User-Agent'] = 'chain-sdk-ruby/' + Chain::VERSION
|
125
|
+
req.body = JSON.dump(body)
|
126
|
+
|
127
|
+
if @access_token
|
128
|
+
user, pass = @access_token.split(':')
|
129
|
+
req.basic_auth(user, pass)
|
130
|
+
end
|
131
|
+
|
132
|
+
response = http.request(req)
|
133
|
+
|
134
|
+
req_id = response['Chain-Request-ID']
|
135
|
+
unless req_id.is_a?(String) && req_id.size > 0
|
136
|
+
raise InvalidRequestIDError.new(response)
|
137
|
+
end
|
138
|
+
|
139
|
+
status = Integer(response.code)
|
140
|
+
parsed_body = nil
|
141
|
+
|
142
|
+
if status != 204 # No Content
|
143
|
+
begin
|
144
|
+
parsed_body = JSON.parse(response.body)
|
145
|
+
rescue JSON::JSONError
|
146
|
+
raise JSONError.new(req_id, response)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
if status / 100 != 2
|
151
|
+
klass = status == 401 ? UnauthorizedError : APIError
|
152
|
+
raise klass.new(parsed_body, response)
|
153
|
+
end
|
154
|
+
|
155
|
+
{body: parsed_body, response: response}
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
private
|
160
|
+
|
161
|
+
MILLIS_TO_SEC = 0.001
|
162
|
+
|
163
|
+
def backoff_delay(attempt)
|
164
|
+
max = RETRY_BASE_DELAY_MS * 2**(attempt-1)
|
165
|
+
max = [max, RETRY_MAX_DELAY_MS].min
|
166
|
+
millis = rand(max) + 1
|
167
|
+
millis * MILLIS_TO_SEC
|
168
|
+
end
|
169
|
+
|
170
|
+
def http
|
171
|
+
return @http if @http
|
172
|
+
|
173
|
+
args = [@url.host, @url.port]
|
174
|
+
|
175
|
+
# Proxy configuration
|
176
|
+
if @opts.key?(:proxy_addr)
|
177
|
+
args += [@opts[:proxy_addr], @opts[:proxy_port]]
|
178
|
+
if @opts.key?(:proxy_user)
|
179
|
+
args += [@opts[:proxy_user], @opts[:proxy_pass]]
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
@http = Net::HTTP.new(*args)
|
184
|
+
|
185
|
+
@http.set_debug_output($stdout) if ENV['DEBUG']
|
186
|
+
if @url.scheme == 'https'
|
187
|
+
@http.use_ssl = true
|
188
|
+
@http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
189
|
+
if @opts.key?(:ssl_params)
|
190
|
+
ssl_params = @opts[:ssl_params]
|
191
|
+
if ssl_params.key?(:ca_file)
|
192
|
+
@http.ca_file = ssl_params[:ca_file]
|
193
|
+
end
|
194
|
+
if ssl_params.key?(:cert)
|
195
|
+
@http.cert = ssl_params[:cert]
|
196
|
+
end
|
197
|
+
if ssl_params.key?(:key)
|
198
|
+
@http.key = ssl_params[:key]
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
@http
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'securerandom'
|
2
|
+
|
3
|
+
require_relative './client_module'
|
4
|
+
|
5
|
+
module Chain
|
6
|
+
class DevUtils
|
7
|
+
class ClientModule < Chain::ClientModule
|
8
|
+
# Deletes all data in the ledger. (development ledgers only)
|
9
|
+
def reset
|
10
|
+
client.conn.request('/reset', client_token: SecureRandom.uuid)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
data/lib/chain/errors.rb
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
module Chain
|
2
|
+
|
3
|
+
# Base class for all errors raised by the Chain SDK.
|
4
|
+
class BaseError < StandardError; end
|
5
|
+
|
6
|
+
# InvalidRequestIDError arises when an HTTP response is received, but it does
|
7
|
+
# not contain headers that are included in all Chain API responses. This
|
8
|
+
# could arise due to a badly-configured proxy, or other upstream network
|
9
|
+
# issues.
|
10
|
+
class InvalidRequestIDError < BaseError
|
11
|
+
attr_accessor :response
|
12
|
+
|
13
|
+
def initialize(response)
|
14
|
+
super "Response HTTP header field Chain-Request-ID is unset. There may be network issues. Please check your local network settings."
|
15
|
+
self.response = response
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# JSONError should be very rare, and will only arise if there is a bug in the
|
20
|
+
# Chain API, or if the upstream server is spoofing common Chain API response
|
21
|
+
# headers.
|
22
|
+
class JSONError < BaseError
|
23
|
+
attr_accessor :request_id
|
24
|
+
attr_accessor :response
|
25
|
+
|
26
|
+
def initialize(request_id, response)
|
27
|
+
super "Error decoding JSON response. Request-ID: #{request_id}"
|
28
|
+
self.request_id = request_id
|
29
|
+
self.response = response
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# APIError describes errors that are codified by the Chain API. They have
|
34
|
+
# an error code, a message, and an optional detail field that provides
|
35
|
+
# additional context for the error.
|
36
|
+
class APIError < BaseError
|
37
|
+
RETRIABLE_STATUS_CODES = [
|
38
|
+
408, # Request Timeout
|
39
|
+
429, # Too Many Requests
|
40
|
+
500, # Internal Server Error
|
41
|
+
502, # Bad Gateway
|
42
|
+
503, # Service Unavailable
|
43
|
+
504, # Gateway Timeout
|
44
|
+
509, # Bandwidth Limit Exceeded
|
45
|
+
]
|
46
|
+
|
47
|
+
attr_accessor :code, :chain_message, :detail, :data, :temporary, :request_id, :response
|
48
|
+
|
49
|
+
def initialize(body, response)
|
50
|
+
self.code = body['code']
|
51
|
+
self.chain_message = body['message']
|
52
|
+
self.detail = body['detail']
|
53
|
+
self.temporary = body['temporary']
|
54
|
+
|
55
|
+
self.response = response
|
56
|
+
self.request_id = response['Chain-Request-ID'] if response
|
57
|
+
|
58
|
+
super self.class.format_error_message(code, chain_message, detail, request_id)
|
59
|
+
end
|
60
|
+
|
61
|
+
def retriable?
|
62
|
+
temporary || (response && RETRIABLE_STATUS_CODES.include?(Integer(response.code)))
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.format_error_message(code, message, detail, request_id)
|
66
|
+
tokens = []
|
67
|
+
tokens << "Code: #{code}" if code.is_a?(String) && code.size > 0
|
68
|
+
tokens << "Message: #{message}"
|
69
|
+
tokens << "Detail: #{detail}" if detail.is_a?(String) && detail.size > 0
|
70
|
+
tokens << "Request-ID: #{request_id}"
|
71
|
+
tokens.join(' ')
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# UnauthorizedError is a special case of APIError, and is raised when the
|
76
|
+
# response status code is 401. This is a common error case, so a discrete
|
77
|
+
# exception type is provided for convenience.
|
78
|
+
class UnauthorizedError < APIError; end
|
79
|
+
|
80
|
+
end
|
data/lib/chain/key.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
require_relative './client_module'
|
2
|
+
require_relative './connection'
|
3
|
+
require_relative './query'
|
4
|
+
require_relative './response_object'
|
5
|
+
|
6
|
+
module Chain
|
7
|
+
class Key < ResponseObject
|
8
|
+
# @!attribute [r] alias
|
9
|
+
# User specified, unique identifier of the key.
|
10
|
+
# @return [String]
|
11
|
+
attrib :alias
|
12
|
+
|
13
|
+
# @!attribute [r] id
|
14
|
+
# Unique identifier of the key, based on the public key material itself.
|
15
|
+
# @return [String]
|
16
|
+
attrib :id
|
17
|
+
|
18
|
+
class ClientModule < Chain::ClientModule
|
19
|
+
|
20
|
+
# Creates a key object.
|
21
|
+
# @param [Hash] opts Parameters for MockHSM key creation.
|
22
|
+
# @option opts [String] alias User specified, unique identifier.
|
23
|
+
# @return [Key]
|
24
|
+
def create(opts = {})
|
25
|
+
Key.new(client.conn.request('create-key', opts))
|
26
|
+
end
|
27
|
+
|
28
|
+
# @param [Hash] opts Filtering information
|
29
|
+
# @option opts [Array<String>] aliases Optional list of requested aliases, max 200.
|
30
|
+
# @return [Query]
|
31
|
+
def query(opts = {})
|
32
|
+
Query.new(client, opts)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
class Query < Chain::Query
|
37
|
+
def fetch(query)
|
38
|
+
client.conn.request('list-keys', query)
|
39
|
+
end
|
40
|
+
|
41
|
+
def translate(obj)
|
42
|
+
Key.new(obj)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
data/lib/chain/page.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
require_relative './response_object'
|
2
|
+
|
3
|
+
module Chain
|
4
|
+
class Page < ResponseObject
|
5
|
+
# @!attribute [r] items
|
6
|
+
# List of items.
|
7
|
+
# @return [Array]
|
8
|
+
attrib :items
|
9
|
+
|
10
|
+
# @!attribute [r] next
|
11
|
+
# Query object to request next page of items
|
12
|
+
# @return [Hash]
|
13
|
+
attrib :next
|
14
|
+
|
15
|
+
# @!attribute [r] last_page
|
16
|
+
# Indicator of whether there are more pages to load
|
17
|
+
# @return [Boolean]
|
18
|
+
attrib :last_page
|
19
|
+
|
20
|
+
def initialize(raw_attribs, translate)
|
21
|
+
super(raw_attribs)
|
22
|
+
@items = @items.map { |i| translate.call(i) }
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/lib/chain/query.rb
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
require_relative './page'
|
2
|
+
|
3
|
+
module Chain
|
4
|
+
class Query
|
5
|
+
include ::Enumerable
|
6
|
+
|
7
|
+
# @return [Client]
|
8
|
+
attr_reader :client
|
9
|
+
|
10
|
+
# @return [Hash]
|
11
|
+
attr_reader :query
|
12
|
+
|
13
|
+
def initialize(client, query = {})
|
14
|
+
@client = client
|
15
|
+
@query = query
|
16
|
+
end
|
17
|
+
|
18
|
+
# Iterate through objects in response, fetching the next page of results
|
19
|
+
# from the API as needed.
|
20
|
+
#
|
21
|
+
# Implements required method
|
22
|
+
# {https://ruby-doc.org/core/Enumerable.html Enumerable#each}.
|
23
|
+
# @return [void]
|
24
|
+
def each
|
25
|
+
pages.each do |page|
|
26
|
+
page.items.each do |item, index|
|
27
|
+
yield item
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def pages
|
33
|
+
PageQuery.new(client, query, method(:fetch), method(:translate))
|
34
|
+
end
|
35
|
+
|
36
|
+
# @abstract
|
37
|
+
def fetch(query)
|
38
|
+
raise NotImplementedError
|
39
|
+
end
|
40
|
+
|
41
|
+
# Overwrite to translate API response data to a different Ruby object.
|
42
|
+
# @abstract
|
43
|
+
def translate(response_object)
|
44
|
+
raise NotImplementedError
|
45
|
+
end
|
46
|
+
|
47
|
+
alias_method :all, :to_a
|
48
|
+
|
49
|
+
class PageQuery
|
50
|
+
include ::Enumerable
|
51
|
+
|
52
|
+
def initialize(client, query, fetch, translate)
|
53
|
+
@client = client
|
54
|
+
@query = query
|
55
|
+
@fetch = fetch
|
56
|
+
@translate = translate
|
57
|
+
end
|
58
|
+
|
59
|
+
def each
|
60
|
+
page = nil
|
61
|
+
|
62
|
+
loop do
|
63
|
+
page = Page.new(@fetch.call(@query), @translate)
|
64
|
+
@query = page.next
|
65
|
+
|
66
|
+
yield page
|
67
|
+
|
68
|
+
break if page.last_page
|
69
|
+
|
70
|
+
# The second predicate (empty?) *should* be redundant, but we check it
|
71
|
+
# anyway as a defensive measure.
|
72
|
+
break if page.items.empty?
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
alias_method :all, :to_a
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'time'
|
3
|
+
|
4
|
+
module Chain
|
5
|
+
class ResponseObject
|
6
|
+
def initialize(raw_attribs)
|
7
|
+
raw_attribs.each do |k, v|
|
8
|
+
next unless self.class.has_attrib?(k)
|
9
|
+
self[k] = self.class.translate(k, v) unless v.nil?
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_h
|
14
|
+
self.class.attrib_opts.keys.reduce({}) do |memo, name|
|
15
|
+
memo[name] = instance_variable_get("@#{name}")
|
16
|
+
memo
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_json(opts = nil)
|
21
|
+
h = to_h.reduce({}) do |memo, (k, v)|
|
22
|
+
memo[k] = self.class.detranslate(k, v)
|
23
|
+
memo
|
24
|
+
end
|
25
|
+
|
26
|
+
h.to_json
|
27
|
+
end
|
28
|
+
|
29
|
+
def [](attrib_name)
|
30
|
+
attrib_name = attrib_name.to_sym
|
31
|
+
raise KeyError.new("key not found: #{attrib_name}") unless self.class.attrib_opts.key?(attrib_name)
|
32
|
+
|
33
|
+
instance_variable_get "@#{attrib_name}"
|
34
|
+
end
|
35
|
+
|
36
|
+
def []=(attrib_name, value)
|
37
|
+
attrib_name = attrib_name.to_sym
|
38
|
+
raise KeyError.new("key not found: #{attrib_name}") unless self.class.attrib_opts.key?(attrib_name)
|
39
|
+
|
40
|
+
instance_variable_set "@#{attrib_name}", value
|
41
|
+
end
|
42
|
+
|
43
|
+
# @!visibility private
|
44
|
+
def self.attrib_opts
|
45
|
+
@attrib_opts ||= {}
|
46
|
+
end
|
47
|
+
|
48
|
+
# @!visibility private
|
49
|
+
def self.attrib(attrib_name, opts = {}, &translate)
|
50
|
+
opts[:translate] = translate
|
51
|
+
attrib_opts[attrib_name.to_sym] = opts
|
52
|
+
attr_accessor attrib_name
|
53
|
+
end
|
54
|
+
|
55
|
+
# @!visibility private
|
56
|
+
def self.has_attrib?(attrib_name)
|
57
|
+
attrib_opts.key?(attrib_name.to_sym)
|
58
|
+
end
|
59
|
+
|
60
|
+
# @!visibility private
|
61
|
+
def self.translate(attrib_name, raw_value)
|
62
|
+
attrib_name = attrib_name.to_sym
|
63
|
+
opts = attrib_opts[attrib_name]
|
64
|
+
|
65
|
+
return Time.parse(raw_value) if opts[:rfc3339_time]
|
66
|
+
return raw_value if opts[:translate].nil?
|
67
|
+
|
68
|
+
begin
|
69
|
+
opts[:translate].call raw_value
|
70
|
+
rescue => e
|
71
|
+
raise TranslateError.new(attrib_name, raw_value, e)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# @!visibility private
|
76
|
+
def self.detranslate(attrib_name, raw_value)
|
77
|
+
opts = attrib_opts.fetch(attrib_name, {})
|
78
|
+
|
79
|
+
if opts[:rfc3339_time]
|
80
|
+
begin
|
81
|
+
return raw_value.to_datetime.rfc3339
|
82
|
+
rescue => e
|
83
|
+
raise DetranslateError.new(attrib_name, raw_value, e)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
raw_value
|
88
|
+
end
|
89
|
+
|
90
|
+
class TranslateError < StandardError
|
91
|
+
attr_reader :attrib_name
|
92
|
+
attr_reader :raw_value
|
93
|
+
attr_reader :source
|
94
|
+
|
95
|
+
def initialize(attrib_name, raw_value, source)
|
96
|
+
super "Error translating attrib #{attrib_name}: #{source}"
|
97
|
+
@attrib_name = attrib_name
|
98
|
+
@raw_value = raw_value
|
99
|
+
@source = source
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
class DetranslateError < StandardError
|
104
|
+
attr_reader :attrib_name
|
105
|
+
attr_reader :raw_value
|
106
|
+
attr_reader :source
|
107
|
+
|
108
|
+
def initialize(attrib_name, raw_value, source)
|
109
|
+
super "Error de-translating attrib #{attrib_name}: #{source}"
|
110
|
+
@attrib_name = attrib_name
|
111
|
+
@raw_value = raw_value
|
112
|
+
@source = source
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
data/lib/chain/stats.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
require_relative './client_module'
|
2
|
+
require_relative './response_object'
|
3
|
+
require_relative './query'
|
4
|
+
|
5
|
+
module Chain
|
6
|
+
class Stats < ResponseObject
|
7
|
+
|
8
|
+
# @!attribute [r] asset_count
|
9
|
+
# Total number of assets in the ledger.
|
10
|
+
# @return [Integer]
|
11
|
+
attrib :asset_count
|
12
|
+
|
13
|
+
# @!attribute [r] account_count
|
14
|
+
# Total number of accounts in the ledger.
|
15
|
+
# @return [Integer]
|
16
|
+
attrib :account_count
|
17
|
+
|
18
|
+
# @!attribute [r] tx_count
|
19
|
+
# Total number of transactions in the ledger.
|
20
|
+
# @return [Integer]
|
21
|
+
attrib :tx_count
|
22
|
+
|
23
|
+
class ClientModule < Chain::ClientModule
|
24
|
+
# @return [Stats]
|
25
|
+
def get
|
26
|
+
Stats.new(client.conn.request('stats'))
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|