tronzap-sdk 1.0.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/CHANGELOG.md +23 -0
- data/LICENSE +21 -0
- data/README.es.md +396 -0
- data/README.md +392 -0
- data/README.pt-br.md +392 -0
- data/README.ru.md +392 -0
- data/lib/tronzap/client.rb +239 -0
- data/lib/tronzap/coerce.rb +127 -0
- data/lib/tronzap/configuration.rb +140 -0
- data/lib/tronzap/errors.rb +150 -0
- data/lib/tronzap/http_adapter.rb +168 -0
- data/lib/tronzap/models/activate_address_rate.rb +11 -0
- data/lib/tronzap/models/address_resources.rb +13 -0
- data/lib/tronzap/models/aml_risk_factor.rb +28 -0
- data/lib/tronzap/models/bandwidth_rate.rb +28 -0
- data/lib/tronzap/models/direct_recharge_rate.rb +38 -0
- data/lib/tronzap/models/energy_rate.rb +44 -0
- data/lib/tronzap/models/enums.rb +26 -0
- data/lib/tronzap/models/resource_amounts.rb +13 -0
- data/lib/tronzap/models/timestamp.rb +58 -0
- data/lib/tronzap/models/transaction_params.rb +46 -0
- data/lib/tronzap/requests/address_activation.rb +28 -0
- data/lib/tronzap/requests/aml_check.rb +68 -0
- data/lib/tronzap/requests/aml_history.rb +34 -0
- data/lib/tronzap/requests/bandwidth_transaction.rb +33 -0
- data/lib/tronzap/requests/calculate.rb +32 -0
- data/lib/tronzap/requests/check_transaction.rb +48 -0
- data/lib/tronzap/requests/energy_transaction.rb +42 -0
- data/lib/tronzap/requests/estimate_energy.rb +38 -0
- data/lib/tronzap/requests/resource_bundle_transaction.rb +51 -0
- data/lib/tronzap/requests/validation.rb +56 -0
- data/lib/tronzap/response_decoder.rb +115 -0
- data/lib/tronzap/responses/account_balance.rb +20 -0
- data/lib/tronzap/responses/address_info.rb +26 -0
- data/lib/tronzap/responses/aml_check.rb +56 -0
- data/lib/tronzap/responses/aml_history.rb +28 -0
- data/lib/tronzap/responses/aml_service.rb +30 -0
- data/lib/tronzap/responses/calculation.rb +40 -0
- data/lib/tronzap/responses/direct_recharge_info.rb +22 -0
- data/lib/tronzap/responses/energy_estimate.rb +44 -0
- data/lib/tronzap/responses/service_rates.rb +36 -0
- data/lib/tronzap/responses/transaction.rb +44 -0
- data/lib/tronzap/version.rb +6 -0
- data/lib/tronzap-sdk.rb +4 -0
- data/lib/tronzap.rb +47 -0
- metadata +107 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tronzap
|
|
4
|
+
# Reads loosely typed JSON values into the types the models expose.
|
|
5
|
+
#
|
|
6
|
+
# @api private
|
|
7
|
+
module Coerce
|
|
8
|
+
# A value in the response does not have the expected shape.
|
|
9
|
+
class Error < StandardError; end
|
|
10
|
+
|
|
11
|
+
EMPTY_HASH = {}.freeze
|
|
12
|
+
EMPTY_ARRAY = [].freeze
|
|
13
|
+
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
# The API is backed by PHP, which encodes an empty associative array as [].
|
|
17
|
+
def object(value, name)
|
|
18
|
+
return EMPTY_HASH if value.nil? || value == EMPTY_ARRAY
|
|
19
|
+
raise Error, "#{name} is not an object" unless value.is_a?(Hash)
|
|
20
|
+
|
|
21
|
+
value
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def list(value, name, &)
|
|
25
|
+
return EMPTY_ARRAY if value.nil? || value == EMPTY_HASH
|
|
26
|
+
raise Error, "#{name} is not an array" unless value.is_a?(Array)
|
|
27
|
+
|
|
28
|
+
value.map(&).freeze
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def text(value, name)
|
|
32
|
+
case value
|
|
33
|
+
when nil then ""
|
|
34
|
+
when String then value
|
|
35
|
+
when Integer, true, false then value.to_s
|
|
36
|
+
when BigDecimal then value.to_s("F")
|
|
37
|
+
else raise Error, "#{name} is not a string"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def optional_text(value, name)
|
|
42
|
+
result = text(value, name)
|
|
43
|
+
result.empty? ? nil : result
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def integer(value, name)
|
|
47
|
+
return 0 if value.nil? || (value.is_a?(String) && value.strip.empty?)
|
|
48
|
+
return value if value.is_a?(Integer)
|
|
49
|
+
|
|
50
|
+
number = to_decimal(value, name)
|
|
51
|
+
raise Error, "#{name} is not an integer: #{value.inspect}" unless number.frac.zero?
|
|
52
|
+
|
|
53
|
+
number.to_i
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def decimal(value, name)
|
|
57
|
+
optional_decimal(value, name) || BigDecimal(0)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def optional_decimal(value, name)
|
|
61
|
+
return nil if value.nil? || (value.is_a?(String) && value.strip.empty?)
|
|
62
|
+
|
|
63
|
+
to_decimal(value, name)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Amounts arrive as JSON numbers from some endpoints and as strings from others.
|
|
67
|
+
def to_decimal(value, name)
|
|
68
|
+
case value
|
|
69
|
+
when BigDecimal then value
|
|
70
|
+
when Integer then BigDecimal(value)
|
|
71
|
+
when Float then BigDecimal(value.to_s)
|
|
72
|
+
when String then parse_decimal(value, name)
|
|
73
|
+
else raise Error, "#{name} is not a number: #{value.inspect}"
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def parse_decimal(value, name)
|
|
78
|
+
number = BigDecimal(value.strip)
|
|
79
|
+
raise Error, "#{name} is not a finite number: #{value.inspect}" unless number.finite?
|
|
80
|
+
|
|
81
|
+
number
|
|
82
|
+
rescue ArgumentError
|
|
83
|
+
raise Error, "#{name} is not a number: #{value.inspect}"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def boolean(value, name)
|
|
87
|
+
case value
|
|
88
|
+
when nil, false then false
|
|
89
|
+
when true then true
|
|
90
|
+
when Integer, BigDecimal then !value.zero?
|
|
91
|
+
when String then parse_boolean(value, name)
|
|
92
|
+
else raise Error, "#{name} is not a boolean: #{value.inspect}"
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def parse_boolean(value, name)
|
|
97
|
+
case value.strip
|
|
98
|
+
when "true", "1" then true
|
|
99
|
+
when "", "false", "0" then false
|
|
100
|
+
else raise Error, "#{name} is not a boolean: #{value.inspect}"
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def decimal_map(value, name)
|
|
105
|
+
object(value, name).to_h { |key, amount| [key.to_s.freeze, to_decimal(amount, "#{name}.#{key}")] }.freeze
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def timestamp(value, name)
|
|
109
|
+
case value
|
|
110
|
+
when nil then nil
|
|
111
|
+
when String then value.strip.empty? ? nil : Models::Timestamp.parse(value)
|
|
112
|
+
when Integer then Models::Timestamp.parse(value.to_s)
|
|
113
|
+
else raise Error, "#{name} is not a timestamp: #{value.inspect}"
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Values the API may add later are reported as +:unknown+ instead of failing the response.
|
|
118
|
+
def enum(value, allowed, name)
|
|
119
|
+
wire = text(value, name)
|
|
120
|
+
allowed.find { |symbol| symbol.name == wire } || :unknown
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def optional_enum(value, allowed, name)
|
|
124
|
+
optional_text(value, name) && enum(value, allowed, name)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
module Tronzap
|
|
6
|
+
# Settings of a {Client}. Every client owns its configuration: the client validates and freezes it when it is
|
|
7
|
+
# created, so there is no shared or global state.
|
|
8
|
+
#
|
|
9
|
+
# @example In the constructor
|
|
10
|
+
# Tronzap::Client.new(api_token: token, api_secret: secret, timeout: 10)
|
|
11
|
+
#
|
|
12
|
+
# @example In a block
|
|
13
|
+
# Tronzap::Client.new do |config|
|
|
14
|
+
# config.api_token = token
|
|
15
|
+
# config.api_secret = secret
|
|
16
|
+
# config.base_url = "api.tronzap.com"
|
|
17
|
+
# end
|
|
18
|
+
class Configuration
|
|
19
|
+
# The production API endpoint.
|
|
20
|
+
DEFAULT_BASE_URL = "https://api.tronzap.com"
|
|
21
|
+
|
|
22
|
+
# The default timeout, in seconds.
|
|
23
|
+
DEFAULT_TIMEOUT = 30
|
|
24
|
+
|
|
25
|
+
# @return [String] the API token from your TronZap dashboard, sent as a bearer token. Required.
|
|
26
|
+
attr_accessor :api_token
|
|
27
|
+
|
|
28
|
+
# @return [String] the API secret from your TronZap dashboard, used to sign every request body. It is never
|
|
29
|
+
# sent. Required.
|
|
30
|
+
attr_accessor :api_secret
|
|
31
|
+
|
|
32
|
+
# @return [String] the API endpoint. A bare host such as +api.tronzap.com+ gets the +https+ scheme and a
|
|
33
|
+
# trailing slash is removed. Give an explicit scheme to opt out, for example +http://localhost:8080+.
|
|
34
|
+
attr_accessor :base_url
|
|
35
|
+
|
|
36
|
+
# @return [Numeric] seconds allowed for connecting and for each read or write of a request
|
|
37
|
+
attr_accessor :timeout
|
|
38
|
+
|
|
39
|
+
# @return [String] the +User-Agent+ header
|
|
40
|
+
attr_accessor :user_agent
|
|
41
|
+
|
|
42
|
+
# @return [#call] the HTTP adapter, see {HttpAdapter}
|
|
43
|
+
attr_accessor :adapter
|
|
44
|
+
|
|
45
|
+
# @param api_token [String, nil]
|
|
46
|
+
# @param api_secret [String, nil]
|
|
47
|
+
# @param base_url [String]
|
|
48
|
+
# @param timeout [Numeric]
|
|
49
|
+
# @param user_agent [String]
|
|
50
|
+
# @param adapter [#call, nil] defaults to {HttpAdapter::NetHttp}
|
|
51
|
+
def initialize(api_token: nil, api_secret: nil, base_url: DEFAULT_BASE_URL, timeout: DEFAULT_TIMEOUT,
|
|
52
|
+
user_agent: "tronzap-sdk-ruby/#{VERSION}", adapter: nil)
|
|
53
|
+
@api_token = api_token
|
|
54
|
+
@api_secret = api_secret
|
|
55
|
+
@base_url = base_url
|
|
56
|
+
@timeout = timeout
|
|
57
|
+
@user_agent = user_agent
|
|
58
|
+
@adapter = adapter
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Validates and normalizes the settings, then freezes the configuration.
|
|
62
|
+
#
|
|
63
|
+
# @return [self]
|
|
64
|
+
# @raise [ArgumentError] when a setting is invalid
|
|
65
|
+
def finalize!
|
|
66
|
+
@api_token = credential(@api_token, "api_token")
|
|
67
|
+
@api_secret = credential(@api_secret, "api_secret")
|
|
68
|
+
@base_url = self.class.normalize_base_url(@base_url)
|
|
69
|
+
@timeout = validate_timeout(@timeout)
|
|
70
|
+
@user_agent = validate_user_agent(@user_agent)
|
|
71
|
+
@adapter = validate_adapter(@adapter || HttpAdapter::NetHttp.new)
|
|
72
|
+
freeze
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# @return [String] a description that leaves out the credentials
|
|
76
|
+
def inspect
|
|
77
|
+
"#<#{self.class.name} base_url=#{@base_url.inspect} timeout=#{@timeout.inspect} " \
|
|
78
|
+
"user_agent=#{@user_agent.inspect} adapter=#{@adapter.class.name}>"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# @param base_url [String]
|
|
82
|
+
# @return [String] the URL with a scheme and without a trailing slash
|
|
83
|
+
# @raise [ArgumentError] when it is not an http or https URL
|
|
84
|
+
def self.normalize_base_url(base_url)
|
|
85
|
+
raise ArgumentError, "base_url must be a String" unless base_url.is_a?(String)
|
|
86
|
+
|
|
87
|
+
normalized = base_url.strip
|
|
88
|
+
raise ArgumentError, "base_url must not be blank" if normalized.empty?
|
|
89
|
+
|
|
90
|
+
normalized = "https://#{normalized.delete_prefix("/")}" unless normalized.include?("://")
|
|
91
|
+
normalized = normalized.sub(%r{/+\z}, "")
|
|
92
|
+
check_url(normalized, base_url)
|
|
93
|
+
normalized
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def self.check_url(url, original)
|
|
97
|
+
uri = begin
|
|
98
|
+
URI.parse(url)
|
|
99
|
+
rescue URI::InvalidURIError
|
|
100
|
+
raise ArgumentError, "base_url is not a valid URL: #{original}"
|
|
101
|
+
end
|
|
102
|
+
raise ArgumentError, "base_url must use http or https: #{original}" unless %w[http https].include?(uri.scheme)
|
|
103
|
+
raise ArgumentError, "base_url has no host: #{original}" if uri.host.nil? || uri.host.empty?
|
|
104
|
+
return unless uri.query || uri.fragment
|
|
105
|
+
|
|
106
|
+
raise ArgumentError, "base_url must not have a query or fragment: #{original}"
|
|
107
|
+
end
|
|
108
|
+
private_class_method :check_url
|
|
109
|
+
|
|
110
|
+
private
|
|
111
|
+
|
|
112
|
+
def credential(value, name)
|
|
113
|
+
raise ArgumentError, "#{name} is required" unless value.is_a?(String) && !value.strip.empty?
|
|
114
|
+
|
|
115
|
+
value.dup.freeze
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def validate_timeout(value)
|
|
119
|
+
unless value.is_a?(Numeric) && value.real? && value.positive? && value.to_f.finite?
|
|
120
|
+
raise ArgumentError, "timeout must be a positive number of seconds, got #{value.inspect}"
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
value
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def validate_user_agent(value)
|
|
127
|
+
unless value.is_a?(String) && !value.strip.empty? && !value.match?(/[\r\n]/)
|
|
128
|
+
raise ArgumentError, "user_agent must be a non-blank single-line String"
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
value.dup.freeze
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def validate_adapter(value)
|
|
135
|
+
raise ArgumentError, "adapter must respond to #call" unless value.respond_to?(:call)
|
|
136
|
+
|
|
137
|
+
value
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tronzap
|
|
4
|
+
# The base class of every failure of a TronZap API call.
|
|
5
|
+
#
|
|
6
|
+
# Invalid arguments are not API failures: they raise +ArgumentError+ before anything is sent.
|
|
7
|
+
class Error < StandardError; end
|
|
8
|
+
|
|
9
|
+
# The API rejected the request with a non-zero error code.
|
|
10
|
+
#
|
|
11
|
+
# The API reports some errors with a 2xx status and others with 4xx or 5xx, so this error takes precedence over
|
|
12
|
+
# {HttpError} whenever the response carries an error code.
|
|
13
|
+
class ApiError < Error
|
|
14
|
+
# @return [Integer] the numeric error code, one of the {ErrorCode} constants or a code this SDK does not know
|
|
15
|
+
attr_reader :code
|
|
16
|
+
|
|
17
|
+
# @return [String, nil] the error key, for example +invalid_tron_address+ or
|
|
18
|
+
# +invalid_tron_address.from_address+
|
|
19
|
+
attr_reader :error_key
|
|
20
|
+
|
|
21
|
+
# @return [String, nil] the ID the API assigned to the request. Quote it when contacting support.
|
|
22
|
+
attr_reader :request_id
|
|
23
|
+
|
|
24
|
+
# @return [Integer] the HTTP status of the response
|
|
25
|
+
attr_reader :status
|
|
26
|
+
|
|
27
|
+
# @return [String] the raw response body
|
|
28
|
+
attr_reader :response_body
|
|
29
|
+
|
|
30
|
+
# @param message [String] the error message from the API
|
|
31
|
+
# @param code [Integer] the numeric error code
|
|
32
|
+
# @param status [Integer] the HTTP status of the response
|
|
33
|
+
# @param response_body [String] the raw response body
|
|
34
|
+
# @param error_key [String, nil] the error key
|
|
35
|
+
# @param request_id [String, nil] the request ID
|
|
36
|
+
def initialize(message, code:, status:, response_body:, error_key: nil, request_id: nil)
|
|
37
|
+
super(message)
|
|
38
|
+
@code = code
|
|
39
|
+
@status = status
|
|
40
|
+
@response_body = response_body
|
|
41
|
+
@error_key = error_key
|
|
42
|
+
@request_id = request_id
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# The API answered with a non-2xx status and no error code in the body.
|
|
47
|
+
class HttpError < Error
|
|
48
|
+
# @return [Integer] the HTTP status of the response
|
|
49
|
+
attr_reader :status
|
|
50
|
+
|
|
51
|
+
# @return [String] the raw response body
|
|
52
|
+
attr_reader :response_body
|
|
53
|
+
|
|
54
|
+
# @param message [String] the error message
|
|
55
|
+
# @param status [Integer] the HTTP status of the response
|
|
56
|
+
# @param response_body [String] the raw response body
|
|
57
|
+
def initialize(message, status:, response_body:)
|
|
58
|
+
super(message)
|
|
59
|
+
@status = status
|
|
60
|
+
@response_body = response_body
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# The API answered with HTTP 429: too many requests. Back off and retry.
|
|
65
|
+
class RateLimitError < HttpError
|
|
66
|
+
# @return [Float, nil] seconds the API asked to wait before retrying, from the +Retry-After+ header
|
|
67
|
+
attr_reader :retry_after
|
|
68
|
+
|
|
69
|
+
# @param message [String] the error message
|
|
70
|
+
# @param status [Integer] the HTTP status of the response
|
|
71
|
+
# @param response_body [String] the raw response body
|
|
72
|
+
# @param retry_after [Float, nil] seconds to wait before retrying
|
|
73
|
+
def initialize(message, status:, response_body:, retry_after: nil)
|
|
74
|
+
super(message, status: status, response_body: response_body)
|
|
75
|
+
@retry_after = retry_after
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# The API answered with HTTP 401 or 403: check the API token and secret.
|
|
80
|
+
class UnauthorizedError < HttpError; end
|
|
81
|
+
|
|
82
|
+
# The API answered with an HTTP 5xx status. Usually transient.
|
|
83
|
+
class ServerError < HttpError; end
|
|
84
|
+
|
|
85
|
+
# The API answered with a 2xx status, but the SDK could not read the response.
|
|
86
|
+
class InvalidResponseError < Error
|
|
87
|
+
# @return [Integer, nil] the HTTP status of the response, +nil+ when the body was rejected before the status was
|
|
88
|
+
# known
|
|
89
|
+
attr_reader :status
|
|
90
|
+
|
|
91
|
+
# @return [String] the raw response body, empty when it was too large to keep
|
|
92
|
+
attr_reader :response_body
|
|
93
|
+
|
|
94
|
+
# @param message [String] the error message
|
|
95
|
+
# @param status [Integer, nil] the HTTP status of the response
|
|
96
|
+
# @param response_body [String] the raw response body
|
|
97
|
+
def initialize(message, status:, response_body:)
|
|
98
|
+
super(message)
|
|
99
|
+
@status = status
|
|
100
|
+
@response_body = response_body
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# No response arrived: the request failed below the HTTP level.
|
|
105
|
+
class NetworkError < Error; end
|
|
106
|
+
|
|
107
|
+
# The connection could not be established: the host name did not resolve or the connection was refused.
|
|
108
|
+
class ConnectionError < NetworkError; end
|
|
109
|
+
|
|
110
|
+
# The request did not complete within the configured timeout.
|
|
111
|
+
class TimeoutError < NetworkError; end
|
|
112
|
+
|
|
113
|
+
# The TLS handshake failed, for example because the server certificate is not trusted.
|
|
114
|
+
class SslError < NetworkError; end
|
|
115
|
+
|
|
116
|
+
# Error codes the TronZap API reports in {ApiError#code}.
|
|
117
|
+
module ErrorCode
|
|
118
|
+
# Authentication error: check the API token and that the signature is calculated correctly.
|
|
119
|
+
AUTH_ERROR = 1
|
|
120
|
+
# Invalid service or parameters: check the service name and the parameters.
|
|
121
|
+
INVALID_SERVICE_OR_PARAMS = 2
|
|
122
|
+
# Wallet not found: verify the wallet address, or contact support if you believe this is an error.
|
|
123
|
+
WALLET_NOT_FOUND = 5
|
|
124
|
+
# Insufficient funds: top up the account or request a smaller amount.
|
|
125
|
+
INSUFFICIENT_FUNDS = 6
|
|
126
|
+
# Invalid TRON address: it should be a valid 34-character TRON address.
|
|
127
|
+
INVALID_TRON_ADDRESS = 10
|
|
128
|
+
# Invalid energy amount.
|
|
129
|
+
INVALID_ENERGY_AMOUNT = 11
|
|
130
|
+
# Invalid duration.
|
|
131
|
+
INVALID_DURATION = 12
|
|
132
|
+
# Transaction or subscription not found: check the transaction ID or external ID. The API reports this code
|
|
133
|
+
# under the key +subscription_not_found+.
|
|
134
|
+
TRANSACTION_NOT_FOUND = 20
|
|
135
|
+
# The subscription cannot be stopped right now.
|
|
136
|
+
CANNOT_STOP_SUBSCRIPTION = 21
|
|
137
|
+
# Address not activated: activate it first with an address activation transaction.
|
|
138
|
+
ADDRESS_NOT_ACTIVATED = 24
|
|
139
|
+
# Address already activated. No action is needed.
|
|
140
|
+
ADDRESS_ALREADY_ACTIVATED = 25
|
|
141
|
+
# AML check not found: check the ID or run the AML check again.
|
|
142
|
+
AML_CHECK_NOT_FOUND = 30
|
|
143
|
+
# The service is temporarily unavailable.
|
|
144
|
+
SERVICE_NOT_AVAILABLE = 35
|
|
145
|
+
# Invalid bandwidth amount.
|
|
146
|
+
INVALID_BANDWIDTH_AMOUNT = 50
|
|
147
|
+
# Internal server error. Contact support if it persists.
|
|
148
|
+
INTERNAL_SERVER_ERROR = 500
|
|
149
|
+
end
|
|
150
|
+
end
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "openssl"
|
|
5
|
+
require "zlib"
|
|
6
|
+
|
|
7
|
+
module Tronzap
|
|
8
|
+
# The transport a {Client} sends requests through.
|
|
9
|
+
#
|
|
10
|
+
# An adapter is any object with a +call+ method that takes a {Request} and returns a {Response} (or any object
|
|
11
|
+
# with +status+, +headers+ and +body+). The client builds the body, signs it and parses the response; the adapter
|
|
12
|
+
# only moves bytes. {NetHttp} is the default.
|
|
13
|
+
#
|
|
14
|
+
# Failures raised by an adapter are reported as {NetworkError} subclasses when they are standard Ruby network
|
|
15
|
+
# errors: +Timeout::Error+, +OpenSSL::SSL::SSLError+, +SocketError+, +SystemCallError+, +IOError+ and the
|
|
16
|
+
# +Net::HTTP+ protocol errors. An adapter built on another HTTP library should rescue that library's errors and
|
|
17
|
+
# raise {TimeoutError}, {ConnectionError}, {SslError} or {NetworkError} itself.
|
|
18
|
+
#
|
|
19
|
+
# @example An adapter on top of Faraday
|
|
20
|
+
# class FaradayAdapter
|
|
21
|
+
# def initialize(connection) = @connection = connection
|
|
22
|
+
#
|
|
23
|
+
# def call(request)
|
|
24
|
+
# response = @connection.post(request.url, request.body, request.headers) do |req|
|
|
25
|
+
# req.options.timeout = request.timeout
|
|
26
|
+
# end
|
|
27
|
+
# Tronzap::HttpAdapter::Response.new(status: response.status, headers: response.headers.to_h,
|
|
28
|
+
# body: response.body.to_s)
|
|
29
|
+
# rescue Faraday::TimeoutError => e
|
|
30
|
+
# raise Tronzap::TimeoutError, e.message
|
|
31
|
+
# rescue Faraday::SSLError => e
|
|
32
|
+
# raise Tronzap::SslError, e.message
|
|
33
|
+
# rescue Faraday::ConnectionFailed => e
|
|
34
|
+
# raise Tronzap::ConnectionError, e.message
|
|
35
|
+
# end
|
|
36
|
+
# end
|
|
37
|
+
module HttpAdapter
|
|
38
|
+
# An HTTP request to send.
|
|
39
|
+
#
|
|
40
|
+
# @!attribute [r] http_method
|
|
41
|
+
# @return [Symbol] the HTTP method, always +:post+
|
|
42
|
+
# @!attribute [r] url
|
|
43
|
+
# @return [String] the absolute URL
|
|
44
|
+
# @!attribute [r] headers
|
|
45
|
+
# @return [Hash{String => String}] the request headers
|
|
46
|
+
# @!attribute [r] body
|
|
47
|
+
# @return [String] the JSON body. It is signed byte for byte, so send it unchanged.
|
|
48
|
+
# @!attribute [r] timeout
|
|
49
|
+
# @return [Numeric] seconds allowed for connecting and for each read or write
|
|
50
|
+
Request = Data.define(:http_method, :url, :headers, :body, :timeout)
|
|
51
|
+
|
|
52
|
+
# An HTTP response.
|
|
53
|
+
#
|
|
54
|
+
# @!attribute [r] status
|
|
55
|
+
# @return [Integer] the HTTP status
|
|
56
|
+
# @!attribute [r] headers
|
|
57
|
+
# @return [Hash{String => String}] the response headers; names are matched case-insensitively
|
|
58
|
+
# @!attribute [r] body
|
|
59
|
+
# @return [String] the response body
|
|
60
|
+
Response = Data.define(:status, :headers, :body)
|
|
61
|
+
|
|
62
|
+
# The largest response body the default adapter reads.
|
|
63
|
+
MAX_RESPONSE_BYTES = 8 * 1024 * 1024
|
|
64
|
+
|
|
65
|
+
# The default adapter, on top of +Net::HTTP+ from the standard library.
|
|
66
|
+
#
|
|
67
|
+
# It opens a new connection for every request and keeps no state between requests, so it is safe to share
|
|
68
|
+
# across threads. TLS certificates are always verified. Proxies are taken from the +http_proxy+ and
|
|
69
|
+
# +https_proxy+ environment variables.
|
|
70
|
+
class NetHttp
|
|
71
|
+
# @param ca_file [String, nil] a PEM file with the certificates to trust instead of the system store
|
|
72
|
+
def initialize(ca_file: nil)
|
|
73
|
+
@ca_file = ca_file&.dup&.freeze
|
|
74
|
+
freeze
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# @param request [Request]
|
|
78
|
+
# @return [Response]
|
|
79
|
+
def call(request)
|
|
80
|
+
uri = URI.parse(request.url)
|
|
81
|
+
http = connection(uri, request.timeout)
|
|
82
|
+
post = Net::HTTP::Post.new(uri.request_uri, request.headers)
|
|
83
|
+
post.body = request.body
|
|
84
|
+
|
|
85
|
+
http.start do
|
|
86
|
+
http.request(post) do |response|
|
|
87
|
+
return Response.new(status: response.code.to_i, headers: headers(response), body: read(response))
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
private
|
|
93
|
+
|
|
94
|
+
def connection(uri, timeout)
|
|
95
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
96
|
+
http.use_ssl = uri.scheme == "https"
|
|
97
|
+
if http.use_ssl?
|
|
98
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
|
99
|
+
http.ca_file = @ca_file if @ca_file
|
|
100
|
+
end
|
|
101
|
+
http.open_timeout = timeout
|
|
102
|
+
http.read_timeout = timeout
|
|
103
|
+
http.write_timeout = timeout
|
|
104
|
+
http.ssl_timeout = timeout
|
|
105
|
+
http.max_retries = 0
|
|
106
|
+
http
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def headers(response)
|
|
110
|
+
response.each_header.to_h
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def read(response)
|
|
114
|
+
status = response.code.to_i
|
|
115
|
+
raise too_large(status) if response.content_length.to_i > MAX_RESPONSE_BYTES
|
|
116
|
+
|
|
117
|
+
body = +""
|
|
118
|
+
response.read_body do |chunk|
|
|
119
|
+
raise too_large(status) if body.bytesize + chunk.bytesize > MAX_RESPONSE_BYTES
|
|
120
|
+
|
|
121
|
+
body << chunk
|
|
122
|
+
end
|
|
123
|
+
body
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def too_large(status)
|
|
127
|
+
InvalidResponseError.new("Response body exceeds #{MAX_RESPONSE_BYTES} bytes",
|
|
128
|
+
status: status, response_body: "")
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
CONNECTION_ERRORS = [
|
|
133
|
+
SocketError, Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Errno::ENETUNREACH, Errno::EADDRNOTAVAIL
|
|
134
|
+
].freeze
|
|
135
|
+
private_constant :CONNECTION_ERRORS
|
|
136
|
+
|
|
137
|
+
NETWORK_ERRORS = [
|
|
138
|
+
OpenSSL::SSL::SSLError, Timeout::Error, IOError, SystemCallError, Net::ProtocolError, Net::HTTPBadResponse,
|
|
139
|
+
Zlib::Error, *CONNECTION_ERRORS
|
|
140
|
+
].freeze
|
|
141
|
+
private_constant :NETWORK_ERRORS
|
|
142
|
+
|
|
143
|
+
# Runs the block and reports standard Ruby network failures as {NetworkError} subclasses.
|
|
144
|
+
#
|
|
145
|
+
# @api private
|
|
146
|
+
def self.translate_errors
|
|
147
|
+
yield
|
|
148
|
+
rescue Tronzap::Error
|
|
149
|
+
raise
|
|
150
|
+
rescue *NETWORK_ERRORS => e
|
|
151
|
+
raise classify(e)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def self.classify(error)
|
|
155
|
+
case error
|
|
156
|
+
when OpenSSL::SSL::SSLError
|
|
157
|
+
SslError.new("TLS error: #{error.message}")
|
|
158
|
+
when Timeout::Error, Errno::ETIMEDOUT
|
|
159
|
+
TimeoutError.new("The request timed out: #{error.message}")
|
|
160
|
+
when *CONNECTION_ERRORS
|
|
161
|
+
ConnectionError.new("Connection failed: #{error.message}")
|
|
162
|
+
else
|
|
163
|
+
NetworkError.new("Network error: #{error.class}: #{error.message}")
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
private_class_method :classify
|
|
167
|
+
end
|
|
168
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tronzap
|
|
4
|
+
module Models
|
|
5
|
+
# The resources a TRON address has available.
|
|
6
|
+
#
|
|
7
|
+
# @!attribute [r] energy
|
|
8
|
+
# @return [Integer] available energy
|
|
9
|
+
# @!attribute [r] bandwidth
|
|
10
|
+
# @return [Integer] available bandwidth
|
|
11
|
+
AddressResources = Data.define(:energy, :bandwidth)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tronzap
|
|
4
|
+
module Models
|
|
5
|
+
# One factor that contributed to an AML risk score.
|
|
6
|
+
#
|
|
7
|
+
# @!attribute [r] name
|
|
8
|
+
# @return [String] machine-readable factor name
|
|
9
|
+
# @!attribute [r] label
|
|
10
|
+
# @return [String] human-readable factor label
|
|
11
|
+
# @!attribute [r] group
|
|
12
|
+
# @return [String] the group the factor belongs to
|
|
13
|
+
# @!attribute [r] score
|
|
14
|
+
# @return [BigDecimal] the factor's share of the risk score
|
|
15
|
+
AmlRiskFactor = Data.define(:name, :label, :group, :score) do
|
|
16
|
+
# @api private
|
|
17
|
+
def self.from_api(value)
|
|
18
|
+
data = Coerce.object(value, "risk factor")
|
|
19
|
+
new(
|
|
20
|
+
name: Coerce.text(data["name"], "name"),
|
|
21
|
+
label: Coerce.text(data["label"], "label"),
|
|
22
|
+
group: Coerce.text(data["group"], "group"),
|
|
23
|
+
score: Coerce.decimal(data["score"], "score")
|
|
24
|
+
)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tronzap
|
|
4
|
+
module Models
|
|
5
|
+
# A bandwidth price tier. Bandwidth is priced per 1000 units: 345 bandwidth at a {price} of 1 costs 0.345.
|
|
6
|
+
#
|
|
7
|
+
# @!attribute [r] duration
|
|
8
|
+
# @return [Integer] rental duration in hours
|
|
9
|
+
# @!attribute [r] min_amount
|
|
10
|
+
# @return [Integer] smallest bandwidth amount this tier applies to
|
|
11
|
+
# @!attribute [r] max_amount
|
|
12
|
+
# @return [Integer] largest bandwidth amount this tier applies to
|
|
13
|
+
# @!attribute [r] price
|
|
14
|
+
# @return [BigDecimal] price per 1000 units of bandwidth, in TRX
|
|
15
|
+
BandwidthRate = Data.define(:duration, :min_amount, :max_amount, :price) do
|
|
16
|
+
# @api private
|
|
17
|
+
def self.from_api(value)
|
|
18
|
+
data = Coerce.object(value, "bandwidth rate")
|
|
19
|
+
new(
|
|
20
|
+
duration: Coerce.integer(data["duration"], "duration"),
|
|
21
|
+
min_amount: Coerce.integer(data["min_amount"], "min_amount"),
|
|
22
|
+
max_amount: Coerce.integer(data["max_amount"], "max_amount"),
|
|
23
|
+
price: Coerce.decimal(data["price"], "price")
|
|
24
|
+
)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|