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
data/README.ru.md
ADDED
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
# Покупка энергии Tron через API
|
|
2
|
+
## Ruby SDK от TronZap.com
|
|
3
|
+
|
|
4
|
+
[English](README.md) | [Español](README.es.md) | [Português](README.pt-br.md) | **[Русский](README.ru.md)**
|
|
5
|
+
|
|
6
|
+
[](https://rubygems.org/gems/tronzap-sdk)
|
|
7
|
+
[](https://github.com/tron-energy-market/tronzap-sdk-ruby/actions/workflows/ci.yml)
|
|
8
|
+
[](LICENSE)
|
|
9
|
+
|
|
10
|
+
Официальный Ruby SDK для API TronZap.
|
|
11
|
+
Этот SDK позволяет легко интегрировать сервисы TronZap для аренды энергии TRON.
|
|
12
|
+
|
|
13
|
+
TronZap.com позволяет [покупать энергию TRON](https://tronzap.com/), существенно снижая комиссии при переводах USDT (TRC20).
|
|
14
|
+
|
|
15
|
+
👉 [Зарегистрируйтесь для получения API ключа](https://tronzap.com), чтобы начать использовать TronZap API и интегрировать его через SDK.
|
|
16
|
+
|
|
17
|
+
- Сайт: https://tronzap.com/
|
|
18
|
+
- Справочник API: https://docs.tronzap.com/
|
|
19
|
+
- RubyGems: https://rubygems.org/gems/tronzap-sdk
|
|
20
|
+
- Исходный код: https://github.com/tron-energy-market/tronzap-sdk-ruby
|
|
21
|
+
|
|
22
|
+
## Установка
|
|
23
|
+
|
|
24
|
+
Добавьте gem в Gemfile:
|
|
25
|
+
|
|
26
|
+
```ruby
|
|
27
|
+
gem "tronzap-sdk"
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
или установите его напрямую:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
gem install tronzap-sdk
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Требования
|
|
37
|
+
|
|
38
|
+
- Ruby 3.3 или новее
|
|
39
|
+
- Одна runtime-зависимость, `bigdecimal`. SDK не зависит от Rails.
|
|
40
|
+
|
|
41
|
+
## Быстрый старт
|
|
42
|
+
|
|
43
|
+
```ruby
|
|
44
|
+
require "tronzap"
|
|
45
|
+
|
|
46
|
+
client = Tronzap::Client.new(
|
|
47
|
+
api_token: "ваш_api_token",
|
|
48
|
+
api_secret: "ваш_api_secret"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
begin
|
|
52
|
+
balance = client.get_balance
|
|
53
|
+
puts "balance: #{balance.balance.to_s("F")} (deposit to #{balance.address})"
|
|
54
|
+
|
|
55
|
+
# Оцениваем, сколько энергии нужно для перевода USDT, и покупаем ровно столько.
|
|
56
|
+
estimate = client.estimate_energy(from_address: "TSenderAddress", to_address: "TRecipientAddress")
|
|
57
|
+
|
|
58
|
+
transaction = client.create_energy_transaction(
|
|
59
|
+
address: "TRecipientAddress",
|
|
60
|
+
energy: estimate.energy,
|
|
61
|
+
duration: 1,
|
|
62
|
+
external_id: "order-42",
|
|
63
|
+
activate_address: true
|
|
64
|
+
)
|
|
65
|
+
puts "transaction #{transaction.id} costs #{transaction.amount.to_s("F")} and is #{transaction.status}"
|
|
66
|
+
rescue Tronzap::Error => e
|
|
67
|
+
warn "TronZap call failed: #{e.message}"
|
|
68
|
+
end
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`require "tronzap"` загружает SDK. С Bundler его также загружает строка `gem "tronzap-sdk"` в Gemfile.
|
|
72
|
+
|
|
73
|
+
Запускаемый пример со всеми операциями находится в
|
|
74
|
+
[`examples/basic_usage.rb`](examples/basic_usage.rb):
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
export TRONZAP_API_TOKEN=ваш_api_token
|
|
78
|
+
export TRONZAP_API_SECRET=ваш_api_secret
|
|
79
|
+
export TRONZAP_BASE_URL=api.tronzap.com # необязательно
|
|
80
|
+
ruby -Ilib examples/basic_usage.rb
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
По умолчанию пример только читает данные и ничего не тратит. С
|
|
84
|
+
`TRONZAP_ALLOW_PURCHASES=1` он также вызывает endpoints, которые создают транзакции
|
|
85
|
+
и AML-проверки и списывают средства с баланса. Остальные необязательные переменные
|
|
86
|
+
описаны в комментарии в начале файла.
|
|
87
|
+
|
|
88
|
+
## Настройка
|
|
89
|
+
|
|
90
|
+
Клиент принимает два ключа из личного кабинета: API-токен передаётся как bearer
|
|
91
|
+
token, а API-секрет подписывает тело каждого запроса и никогда не передаётся.
|
|
92
|
+
Всё остальное необязательно. Настройки можно передать именованными аргументами,
|
|
93
|
+
в блоке или обоими способами; блок выполняется последним:
|
|
94
|
+
|
|
95
|
+
```ruby
|
|
96
|
+
client = Tronzap::Client.new(
|
|
97
|
+
api_token: api_token,
|
|
98
|
+
api_secret: api_secret,
|
|
99
|
+
base_url: "api.tronzap.com", # по умолчанию Tronzap::Configuration::DEFAULT_BASE_URL
|
|
100
|
+
timeout: 10, # секунды; по умолчанию 30
|
|
101
|
+
user_agent: "my-app/1.0"
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
client = Tronzap::Client.new do |config|
|
|
105
|
+
config.api_token = ENV.fetch("TRONZAP_API_TOKEN")
|
|
106
|
+
config.api_secret = ENV.fetch("TRONZAP_API_SECRET")
|
|
107
|
+
config.timeout = 10
|
|
108
|
+
end
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`base_url` принимает домен или полный URL: если схема не указана, используется
|
|
112
|
+
`https`, а завершающий слэш удаляется, поэтому `"api.tronzap.com"`,
|
|
113
|
+
`"api.tronzap.com/"` и `"https://api.tronzap.com"` равнозначны. Чтобы этого
|
|
114
|
+
избежать, укажите схему явно, например `"http://localhost:8080"` для локального
|
|
115
|
+
мока.
|
|
116
|
+
|
|
117
|
+
`timeout` действует на установку соединения и на каждое чтение и запись, а не на
|
|
118
|
+
запрос целиком.
|
|
119
|
+
|
|
120
|
+
SDK не хранит глобального состояния. Каждый клиент проверяет свои настройки при
|
|
121
|
+
создании и замораживает их, поэтому клиент неизменяем и безопасен для
|
|
122
|
+
использования из нескольких потоков. Создайте один клиент на набор ключей.
|
|
123
|
+
`inspect` никогда не показывает ключи.
|
|
124
|
+
|
|
125
|
+
### Свой HTTP-адаптер
|
|
126
|
+
|
|
127
|
+
Адаптер по умолчанию использует `Net::HTTP` из стандартной библиотеки, открывает
|
|
128
|
+
новое соединение для каждого запроса, всегда проверяет TLS-сертификаты и учитывает
|
|
129
|
+
переменную окружения `https_proxy`. Чтобы доверять частному центру сертификации,
|
|
130
|
+
передайте его PEM-файл:
|
|
131
|
+
|
|
132
|
+
```ruby
|
|
133
|
+
adapter = Tronzap::HttpAdapter::NetHttp.new(ca_file: "/etc/ssl/corporate-ca.pem")
|
|
134
|
+
client = Tronzap::Client.new(api_token: api_token, api_secret: api_secret, adapter: adapter)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Его можно заменить любым объектом с методом `call`. Он получает
|
|
138
|
+
`Tronzap::HttpAdapter::Request` (`http_method`, `url`, `headers`, `body`,
|
|
139
|
+
`timeout`) и возвращает `Tronzap::HttpAdapter::Response` (`status`, `headers`,
|
|
140
|
+
`body`). Отправляйте тело без изменений: оно подписывается побайтно.
|
|
141
|
+
|
|
142
|
+
```ruby
|
|
143
|
+
class FaradayAdapter
|
|
144
|
+
def initialize(connection)
|
|
145
|
+
@connection = connection
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def call(request)
|
|
149
|
+
response = @connection.post(request.url, request.body, request.headers) do |req|
|
|
150
|
+
req.options.timeout = request.timeout
|
|
151
|
+
end
|
|
152
|
+
Tronzap::HttpAdapter::Response.new(status: response.status, headers: response.headers.to_h,
|
|
153
|
+
body: response.body.to_s)
|
|
154
|
+
rescue Faraday::TimeoutError => e
|
|
155
|
+
raise Tronzap::TimeoutError, e.message
|
|
156
|
+
rescue Faraday::SSLError => e
|
|
157
|
+
raise Tronzap::SslError, e.message
|
|
158
|
+
rescue Faraday::ConnectionFailed => e
|
|
159
|
+
raise Tronzap::ConnectionError, e.message
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Стандартные сетевые ошибки Ruby, которые выбрасывает адаптер (`Timeout::Error`,
|
|
165
|
+
`OpenSSL::SSL::SSLError`, `SocketError`, `SystemCallError`, `IOError`),
|
|
166
|
+
автоматически сообщаются как подклассы `Tronzap::NetworkError`. Ошибки других
|
|
167
|
+
HTTP-библиотек адаптер должен преобразовать сам, как в примере выше.
|
|
168
|
+
|
|
169
|
+
## Доступные методы
|
|
170
|
+
|
|
171
|
+
| Метод | Endpoint | Описание |
|
|
172
|
+
|---|---|---|
|
|
173
|
+
| `get_services` | `/v1/services` | Доступные сервисы и цены |
|
|
174
|
+
| `get_balance` | `/v1/balance` | Текущий баланс аккаунта |
|
|
175
|
+
| `get_address_info(address)` | `/v1/address-info` | Ресурсы (энергия, bandwidth) и балансы (TRX, USDT) адреса |
|
|
176
|
+
| `estimate_energy(from_address:, to_address:, contract_address: nil)` | `/v1/estimate-energy` | Сколько энергии нужно для перевода и сколько она стоит |
|
|
177
|
+
| `calculate(address:, energy:, duration: 1)` | `/v1/calculate` | Стоимость покупки без создания транзакции |
|
|
178
|
+
| `create_energy_transaction(address:, energy:, duration: 1, external_id: nil, activate_address: false)` | `/v1/transaction/new` | Купить энергию |
|
|
179
|
+
| `create_bandwidth_transaction(address:, bandwidth:, external_id: nil)` | `/v1/transaction/new` | Купить bandwidth |
|
|
180
|
+
| `create_resource_bundle_transaction(address:, energy:, bandwidth:, duration: 1, external_id: nil, activate_address: false)` | `/v1/transaction/new` | Купить энергию и bandwidth одной транзакцией |
|
|
181
|
+
| `create_address_activation_transaction(address:, external_id: nil)` | `/v1/transaction/new` | Активировать адрес TRON |
|
|
182
|
+
| `check_transaction(id: nil, external_id: nil)` | `/v1/transaction/check` | Статус транзакции по id или внешнему id |
|
|
183
|
+
| `get_direct_recharge_info` | `/v1/direct-recharge-info` | Адрес и тарифы прямого пополнения |
|
|
184
|
+
| `get_aml_services` | `/v1/aml-checks` | AML-сервисы и цены |
|
|
185
|
+
| `create_aml_check(type:, network:, address:, transaction_hash: nil, direction: nil)` | `/v1/aml-checks/new` | Запустить AML-проверку |
|
|
186
|
+
| `check_aml_status(id)` | `/v1/aml-checks/check` | Статус и результат AML-проверки |
|
|
187
|
+
| `get_aml_history(page: 1, per_page: 10, status: nil)` | `/v1/aml-checks/history` | История AML-проверок с пагинацией |
|
|
188
|
+
|
|
189
|
+
Методы с параметрами принимают либо именованные аргументы, либо объект запроса из
|
|
190
|
+
`Tronzap::Requests`, поэтому запрос можно создать, проверить и передать дальше до
|
|
191
|
+
отправки:
|
|
192
|
+
|
|
193
|
+
```ruby
|
|
194
|
+
request = Tronzap::Requests::EnergyTransaction.new(address: "TRecipientAddress", energy: 65000)
|
|
195
|
+
client.create_energy_transaction(request)
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Запрос проверяется при создании, поэтому невалидный запрос вызывает
|
|
199
|
+
`ArgumentError` и никогда не доходит до API. Количества должны быть
|
|
200
|
+
положительными `Integer`. Значения по умолчанию совпадают с API: `duration` —
|
|
201
|
+
1 час, история AML начинается со страницы 1 по 10 элементов.
|
|
202
|
+
|
|
203
|
+
Результаты — неизменяемые объекты `Data` в `Tronzap::Responses` и
|
|
204
|
+
`Tronzap::Models`, а не хеши: `transaction.status`, `estimate.energy`. Коллекции
|
|
205
|
+
заморожены и никогда не бывают `nil`, а значения, которые API может не прислать,
|
|
206
|
+
равны `nil`.
|
|
207
|
+
|
|
208
|
+
### Покупка ресурсов
|
|
209
|
+
|
|
210
|
+
```ruby
|
|
211
|
+
# Энергия, при необходимости с активацией адреса в том же вызове.
|
|
212
|
+
client.create_energy_transaction(
|
|
213
|
+
address: "TRecipientAddress",
|
|
214
|
+
energy: 65000,
|
|
215
|
+
duration: 1, # часы; доступные сроки смотрите в get_services
|
|
216
|
+
external_id: "order-42",
|
|
217
|
+
activate_address: true
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
# Bandwidth.
|
|
221
|
+
client.create_bandwidth_transaction(address: "TRecipientAddress", bandwidth: 345, external_id: "bandwidth-1")
|
|
222
|
+
|
|
223
|
+
# Энергия и bandwidth вместе одной транзакцией.
|
|
224
|
+
client.create_resource_bundle_transaction(
|
|
225
|
+
address: "TRecipientAddress",
|
|
226
|
+
energy: 65000,
|
|
227
|
+
bandwidth: 345,
|
|
228
|
+
external_id: "bundle-1"
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
# Только активация.
|
|
232
|
+
client.create_address_activation_transaction(address: "TRecipientAddress", external_id: "activation-1")
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Цена энергии указана за единицу, цена bandwidth — за 1000 единиц: в
|
|
236
|
+
`get_services` `EnergyRate#price` × 65000 — это стоимость 65000 энергии, а 345
|
|
237
|
+
bandwidth при `BandwidthRate#price`, равном 1, стоят 0.345.
|
|
238
|
+
|
|
239
|
+
Сейчас API возвращает пакет ресурсов с `service`, равным `:energy`, а не
|
|
240
|
+
`:resource_bundle`. Состав покупки смотрите в `params.amounts`.
|
|
241
|
+
|
|
242
|
+
### Отслеживание транзакции
|
|
243
|
+
|
|
244
|
+
Транзакция проходит путь `:new` → `:pending` → `:success` или `:failed`:
|
|
245
|
+
|
|
246
|
+
```ruby
|
|
247
|
+
transaction = nil
|
|
248
|
+
loop do
|
|
249
|
+
sleep 2
|
|
250
|
+
transaction = client.check_transaction(external_id: "order-42")
|
|
251
|
+
break unless %i[new pending].include?(transaction.status)
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
puts "finished as #{transaction.status}, hash #{transaction.transaction_hash || "none"}"
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### AML-проверка
|
|
258
|
+
|
|
259
|
+
```ruby
|
|
260
|
+
check = client.create_aml_check(Tronzap::Requests::AmlCheck.for_address("TRX", "TAddressToScreen"))
|
|
261
|
+
# или Tronzap::Requests::AmlCheck.for_hash("BTC", "bc1RecipientAddress", "TX_HASH", direction: :withdrawal)
|
|
262
|
+
|
|
263
|
+
result = client.check_aml_status(check.id)
|
|
264
|
+
if result.status == :completed
|
|
265
|
+
puts "#{result.risk_level} #{result.risk_score.to_s("F")} #{result.risk_factors.size} factor(s)"
|
|
266
|
+
end
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
`risk_score` равен `nil`, пока проверка не завершится. У завершённой проверки score
|
|
270
|
+
может быть равен 0, и это не то же самое, что отсутствие результата.
|
|
271
|
+
|
|
272
|
+
## Обработка ошибок
|
|
273
|
+
|
|
274
|
+
Любой сбой вызова API — исключение `Tronzap::Error`. Перехватывайте подкласс, чтобы
|
|
275
|
+
обработать конкретный вид сбоя:
|
|
276
|
+
|
|
277
|
+
```
|
|
278
|
+
Tronzap::Error
|
|
279
|
+
├── Tronzap::ApiError — API ответил ненулевым code
|
|
280
|
+
├── Tronzap::HttpError — ответ не 2xx без payload API
|
|
281
|
+
│ ├── Tronzap::RateLimitError — HTTP 429
|
|
282
|
+
│ ├── Tronzap::UnauthorizedError — HTTP 401 или 403
|
|
283
|
+
│ └── Tronzap::ServerError — HTTP 5xx
|
|
284
|
+
├── Tronzap::InvalidResponseError — ответ 2xx, который SDK не смог прочитать
|
|
285
|
+
└── Tronzap::NetworkError — ответ не пришёл
|
|
286
|
+
├── Tronzap::ConnectionError — ошибка DNS, соединение отклонено
|
|
287
|
+
├── Tronzap::TimeoutError — запрос превысил timeout
|
|
288
|
+
└── Tronzap::SslError — сбой TLS-рукопожатия или сертификата
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
`ApiError`, `HttpError` и `InvalidResponseError` содержат HTTP-статус
|
|
292
|
+
(`status`) и сырое тело ответа (`response_body`). `ApiError` также содержит код
|
|
293
|
+
ошибки API (`code`), ключ ошибки (`error_key`) и ID запроса (`request_id`).
|
|
294
|
+
`RateLimitError#retry_after` хранит задержку из `Retry-After` в секундах, если API
|
|
295
|
+
её прислал.
|
|
296
|
+
|
|
297
|
+
Невалидные аргументы не являются сбоями API: они вызывают `ArgumentError` ещё до
|
|
298
|
+
отправки.
|
|
299
|
+
|
|
300
|
+
```ruby
|
|
301
|
+
begin
|
|
302
|
+
client.create_energy_transaction(address: "TRecipientAddress", energy: 65000)
|
|
303
|
+
rescue Tronzap::ApiError => e
|
|
304
|
+
# Ошибка уровня приложения: код точно говорит, что пошло не так.
|
|
305
|
+
case e.code
|
|
306
|
+
when Tronzap::ErrorCode::INVALID_TRON_ADDRESS
|
|
307
|
+
# Ключ может уточнить, например "invalid_tron_address.from_address"
|
|
308
|
+
warn "bad address: #{e.error_key}"
|
|
309
|
+
when Tronzap::ErrorCode::INSUFFICIENT_FUNDS
|
|
310
|
+
warn "top up the account"
|
|
311
|
+
when Tronzap::ErrorCode::ADDRESS_NOT_ACTIVATED
|
|
312
|
+
warn "activate the address first"
|
|
313
|
+
else
|
|
314
|
+
warn "api error #{e.code}: #{e.message} (request #{e.request_id || "-"})"
|
|
315
|
+
end
|
|
316
|
+
rescue Tronzap::RateLimitError => e
|
|
317
|
+
# Подождите и повторите, через e.retry_after секунд, если API его прислал.
|
|
318
|
+
rescue Tronzap::UnauthorizedError
|
|
319
|
+
# Неверный токен или подпись.
|
|
320
|
+
rescue Tronzap::TimeoutError, Tronzap::ServerError
|
|
321
|
+
# Временный сбой; можно повторить.
|
|
322
|
+
rescue Tronzap::NetworkError
|
|
323
|
+
# Сервер недоступен.
|
|
324
|
+
end
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
`request_id` — идентификатор, который API присваивает каждому запросу. Указывайте
|
|
328
|
+
его при обращении в поддержку.
|
|
329
|
+
|
|
330
|
+
Ошибка API важнее HTTP-статуса: часть сбоев API возвращает со статусом 2xx, а
|
|
331
|
+
часть — с 4xx или 5xx, поэтому читаемый payload с ненулевым кодом всегда
|
|
332
|
+
сообщается как `Tronzap::ApiError`, а не как `Tronzap::HttpError`.
|
|
333
|
+
|
|
334
|
+
### Коды ошибок API
|
|
335
|
+
|
|
336
|
+
| Код | Константа | Описание |
|
|
337
|
+
|------|----------|-------------|
|
|
338
|
+
| 1 | `AUTH_ERROR` | Ошибка аутентификации: неверный API-токен или подпись |
|
|
339
|
+
| 2 | `INVALID_SERVICE_OR_PARAMS` | Неверный сервис или параметры |
|
|
340
|
+
| 5 | `WALLET_NOT_FOUND` | Внутренний кошелёк не найден. Обратитесь в поддержку. |
|
|
341
|
+
| 6 | `INSUFFICIENT_FUNDS` | Недостаточно средств |
|
|
342
|
+
| 10 | `INVALID_TRON_ADDRESS` | Неверный адрес TRON |
|
|
343
|
+
| 11 | `INVALID_ENERGY_AMOUNT` | Неверное количество энергии |
|
|
344
|
+
| 12 | `INVALID_DURATION` | Неверная длительность |
|
|
345
|
+
| 20 | `TRANSACTION_NOT_FOUND` | Транзакция/подписка не найдена |
|
|
346
|
+
| 21 | `CANNOT_STOP_SUBSCRIPTION` | Невозможно остановить подписку |
|
|
347
|
+
| 24 | `ADDRESS_NOT_ACTIVATED` | Адрес не активирован |
|
|
348
|
+
| 25 | `ADDRESS_ALREADY_ACTIVATED` | Адрес уже активирован |
|
|
349
|
+
| 30 | `AML_CHECK_NOT_FOUND` | AML-проверка не найдена |
|
|
350
|
+
| 35 | `SERVICE_NOT_AVAILABLE` | Сервис недоступен |
|
|
351
|
+
| 50 | `INVALID_BANDWIDTH_AMOUNT` | Неверное количество bandwidth |
|
|
352
|
+
| 500 | `INTERNAL_SERVER_ERROR` | Внутренняя ошибка сервера: обратитесь в поддержку |
|
|
353
|
+
|
|
354
|
+
Константы находятся в `Tronzap::ErrorCode`. Код, который эта версия SDK не знает,
|
|
355
|
+
по-прежнему доступен как число через `ApiError#code`.
|
|
356
|
+
|
|
357
|
+
## Числовые поля и даты
|
|
358
|
+
|
|
359
|
+
Суммы и цены — `BigDecimal`, поэтому сохраняют ровно то значение, которое прислал
|
|
360
|
+
API. В одних ответах API кодирует деньги JSON-числом, в других — JSON-строкой; обе
|
|
361
|
+
формы читаются одинаково. Чтобы вывести значение без экспоненциальной записи,
|
|
362
|
+
используйте `to_s("F")`.
|
|
363
|
+
|
|
364
|
+
Даты — объекты `Tronzap::Models::Timestamp`: `value` — разобранный `Time`, `raw` —
|
|
365
|
+
текст ровно в том виде, в каком его прислал API. Поддерживаются все форматы,
|
|
366
|
+
которые использует API, а время без смещения читается как UTC. Нераспознанная дата
|
|
367
|
+
оставляет `value` равным `nil` и не ломает весь ответ.
|
|
368
|
+
|
|
369
|
+
Поля-перечисления — символы, например `:energy` или `:completed`. Значение,
|
|
370
|
+
которое API может добавить в будущем, например новый статус транзакции,
|
|
371
|
+
сообщается как `:unknown` и не приводит к ошибке. Известные значения перечислены
|
|
372
|
+
в `Tronzap::Models`.
|
|
373
|
+
|
|
374
|
+
## Тестирование
|
|
375
|
+
|
|
376
|
+
```bash
|
|
377
|
+
bundle install
|
|
378
|
+
bundle exec rspec
|
|
379
|
+
bundle exec rubocop
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
Тесты запускаются против локального HTTP-сервера: точное тело запроса и
|
|
383
|
+
подпись для каждого endpoint, ошибки API и HTTP, некорректный JSON, таймауты,
|
|
384
|
+
сетевые и TLS-сбои, а также конкурентное использование.
|
|
385
|
+
|
|
386
|
+
## Лицензия
|
|
387
|
+
|
|
388
|
+
Лицензия MIT (MIT). Подробнее в [файле лицензии](LICENSE).
|
|
389
|
+
|
|
390
|
+
## Поддержка
|
|
391
|
+
|
|
392
|
+
По вопросам поддержки пишите на [support@tronzap.com](mailto:support@tronzap.com).
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module Tronzap
|
|
7
|
+
# Client for the {https://docs.tronzap.com/ TronZap API}: buy TRON energy and bandwidth, activate addresses and
|
|
8
|
+
# run AML checks.
|
|
9
|
+
#
|
|
10
|
+
# A client is immutable and safe to share between threads. Create one per set of credentials.
|
|
11
|
+
#
|
|
12
|
+
# Methods that take parameters accept either a request object from {Requests} or the same values as keyword
|
|
13
|
+
# arguments. Invalid values raise +ArgumentError+ before anything is sent. Every failure of the call itself raises
|
|
14
|
+
# a {Tronzap::Error}: {ApiError} when the API rejected the request, {HttpError} for a non-2xx response without an
|
|
15
|
+
# API error code, {InvalidResponseError} for a response the SDK cannot read, and {NetworkError} when no response
|
|
16
|
+
# arrived.
|
|
17
|
+
#
|
|
18
|
+
# @example
|
|
19
|
+
# client = Tronzap::Client.new(api_token: ENV.fetch("TRONZAP_API_TOKEN"),
|
|
20
|
+
# api_secret: ENV.fetch("TRONZAP_API_SECRET"))
|
|
21
|
+
# transaction = client.create_energy_transaction(address: "TRecipientAddress", energy: 65000)
|
|
22
|
+
# transaction.status # => :new
|
|
23
|
+
class Client
|
|
24
|
+
# @return [Configuration] the frozen configuration
|
|
25
|
+
attr_reader :config
|
|
26
|
+
|
|
27
|
+
# @param options [Hash] the settings of {Configuration#initialize}
|
|
28
|
+
# @yieldparam config [Configuration] the configuration to change before the client is created
|
|
29
|
+
# @raise [ArgumentError] when a setting is invalid
|
|
30
|
+
def initialize(**)
|
|
31
|
+
config = Configuration.new(**)
|
|
32
|
+
yield config if block_given?
|
|
33
|
+
@config = config.finalize!
|
|
34
|
+
freeze
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Available services and their prices.
|
|
38
|
+
#
|
|
39
|
+
# @return [Responses::ServiceRates]
|
|
40
|
+
# @raise [Error]
|
|
41
|
+
def get_services
|
|
42
|
+
post("/v1/services", {}) { Responses::ServiceRates.from_api(_1) }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# The current account balance.
|
|
46
|
+
#
|
|
47
|
+
# @return [Responses::AccountBalance]
|
|
48
|
+
# @raise [Error]
|
|
49
|
+
def get_balance
|
|
50
|
+
post("/v1/balance", {}) { Responses::AccountBalance.from_api(_1) }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# The resources (energy, bandwidth) and token balances (TRX, USDT) of a TRON address.
|
|
54
|
+
#
|
|
55
|
+
# @param address [String] the TRON address
|
|
56
|
+
# @return [Responses::AddressInfo]
|
|
57
|
+
# @raise [ArgumentError, Error]
|
|
58
|
+
def get_address_info(address)
|
|
59
|
+
body = { "address" => Requests::Validation.string(address, "address") }
|
|
60
|
+
post("/v1/address-info", body) { Responses::AddressInfo.from_api(_1) }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# The energy a token transfer needs, and its cost.
|
|
64
|
+
#
|
|
65
|
+
# @overload estimate_energy(request)
|
|
66
|
+
# @param request [Requests::EstimateEnergy]
|
|
67
|
+
# @overload estimate_energy(from_address:, to_address:, contract_address: nil)
|
|
68
|
+
# @return [Responses::EnergyEstimate]
|
|
69
|
+
# @raise [ArgumentError, Error]
|
|
70
|
+
def estimate_energy(request = nil, **params)
|
|
71
|
+
body = build(Requests::EstimateEnergy, request, params).body
|
|
72
|
+
post("/v1/estimate-energy", body) { Responses::EnergyEstimate.from_api(_1) }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Prices an energy purchase without creating a transaction.
|
|
76
|
+
#
|
|
77
|
+
# @overload calculate(request)
|
|
78
|
+
# @param request [Requests::Calculate]
|
|
79
|
+
# @overload calculate(address:, energy:, duration: 1)
|
|
80
|
+
# @return [Responses::Calculation]
|
|
81
|
+
# @raise [ArgumentError, Error]
|
|
82
|
+
def calculate(request = nil, **params)
|
|
83
|
+
body = build(Requests::Calculate, request, params).body
|
|
84
|
+
post("/v1/calculate", body) { Responses::Calculation.from_api(_1) }
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Buys energy.
|
|
88
|
+
#
|
|
89
|
+
# @overload create_energy_transaction(request)
|
|
90
|
+
# @param request [Requests::EnergyTransaction]
|
|
91
|
+
# @overload create_energy_transaction(address:, energy:, duration: 1, external_id: nil, activate_address: false)
|
|
92
|
+
# @return [Responses::Transaction]
|
|
93
|
+
# @raise [ArgumentError, Error]
|
|
94
|
+
def create_energy_transaction(request = nil, **params)
|
|
95
|
+
create_transaction(build(Requests::EnergyTransaction, request, params))
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Buys bandwidth.
|
|
99
|
+
#
|
|
100
|
+
# @overload create_bandwidth_transaction(request)
|
|
101
|
+
# @param request [Requests::BandwidthTransaction]
|
|
102
|
+
# @overload create_bandwidth_transaction(address:, bandwidth:, external_id: nil)
|
|
103
|
+
# @return [Responses::Transaction]
|
|
104
|
+
# @raise [ArgumentError, Error]
|
|
105
|
+
def create_bandwidth_transaction(request = nil, **params)
|
|
106
|
+
create_transaction(build(Requests::BandwidthTransaction, request, params))
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Buys energy and bandwidth in one transaction.
|
|
110
|
+
#
|
|
111
|
+
# @overload create_resource_bundle_transaction(request)
|
|
112
|
+
# @param request [Requests::ResourceBundleTransaction]
|
|
113
|
+
# @overload create_resource_bundle_transaction(address:, energy:, bandwidth:, duration: 1, external_id: nil,
|
|
114
|
+
# activate_address: false)
|
|
115
|
+
# @return [Responses::Transaction]
|
|
116
|
+
# @raise [ArgumentError, Error]
|
|
117
|
+
def create_resource_bundle_transaction(request = nil, **params)
|
|
118
|
+
create_transaction(build(Requests::ResourceBundleTransaction, request, params))
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Activates a TRON address.
|
|
122
|
+
#
|
|
123
|
+
# @overload create_address_activation_transaction(request)
|
|
124
|
+
# @param request [Requests::AddressActivation]
|
|
125
|
+
# @overload create_address_activation_transaction(address:, external_id: nil)
|
|
126
|
+
# @return [Responses::Transaction]
|
|
127
|
+
# @raise [ArgumentError, Error]
|
|
128
|
+
def create_address_activation_transaction(request = nil, **params)
|
|
129
|
+
create_transaction(build(Requests::AddressActivation, request, params))
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# The status of a transaction, by its TronZap ID or by your external ID.
|
|
133
|
+
#
|
|
134
|
+
# @overload check_transaction(request)
|
|
135
|
+
# @param request [Requests::CheckTransaction]
|
|
136
|
+
# @overload check_transaction(id: nil, external_id: nil)
|
|
137
|
+
# @return [Responses::Transaction]
|
|
138
|
+
# @raise [ArgumentError, Error]
|
|
139
|
+
def check_transaction(request = nil, **params)
|
|
140
|
+
body = build(Requests::CheckTransaction, request, params).body
|
|
141
|
+
post("/v1/transaction/check", body) { Responses::Transaction.from_api(_1) }
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# The address to pay for a direct energy recharge and the rates energy is delivered at.
|
|
145
|
+
#
|
|
146
|
+
# @return [Responses::DirectRechargeInfo]
|
|
147
|
+
# @raise [Error]
|
|
148
|
+
def get_direct_recharge_info
|
|
149
|
+
post("/v1/direct-recharge-info", {}) { Responses::DirectRechargeInfo.from_api(_1) }
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# AML services and their prices.
|
|
153
|
+
#
|
|
154
|
+
# @return [Array<Responses::AmlService>]
|
|
155
|
+
# @raise [Error]
|
|
156
|
+
def get_aml_services
|
|
157
|
+
post("/v1/aml-checks", {}) { Responses::AmlService.list_from_api(_1) }
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Starts an AML screening of an address or a transaction.
|
|
161
|
+
#
|
|
162
|
+
# @overload create_aml_check(request)
|
|
163
|
+
# @param request [Requests::AmlCheck] see {Requests::AmlCheck.for_address} and {Requests::AmlCheck.for_hash}
|
|
164
|
+
# @overload create_aml_check(type:, network:, address:, transaction_hash: nil, direction: nil)
|
|
165
|
+
# @return [Responses::AmlCheck]
|
|
166
|
+
# @raise [ArgumentError, Error]
|
|
167
|
+
def create_aml_check(request = nil, **params)
|
|
168
|
+
body = build(Requests::AmlCheck, request, params).body
|
|
169
|
+
post("/v1/aml-checks/new", body) { Responses::AmlCheck.from_api(_1) }
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# The status and result of an AML check.
|
|
173
|
+
#
|
|
174
|
+
# @param id [String] the AML check ID
|
|
175
|
+
# @return [Responses::AmlCheck]
|
|
176
|
+
# @raise [ArgumentError, Error]
|
|
177
|
+
def check_aml_status(id)
|
|
178
|
+
body = { "id" => Requests::Validation.string(id, "id") }
|
|
179
|
+
post("/v1/aml-checks/check", body) { Responses::AmlCheck.from_api(_1) }
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# One page of past AML checks.
|
|
183
|
+
#
|
|
184
|
+
# @overload get_aml_history(request)
|
|
185
|
+
# @param request [Requests::AmlHistory]
|
|
186
|
+
# @overload get_aml_history(page: 1, per_page: 10, status: nil)
|
|
187
|
+
# @return [Responses::AmlHistory]
|
|
188
|
+
# @raise [ArgumentError, Error]
|
|
189
|
+
def get_aml_history(request = nil, **params)
|
|
190
|
+
body = build(Requests::AmlHistory, request, params).body
|
|
191
|
+
post("/v1/aml-checks/history", body) { Responses::AmlHistory.from_api(_1) }
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# @return [String] a description that leaves out the credentials
|
|
195
|
+
def inspect
|
|
196
|
+
"#<#{self.class.name} base_url=#{config.base_url.inspect}>"
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
private
|
|
200
|
+
|
|
201
|
+
def build(type, request, params)
|
|
202
|
+
raise ArgumentError, "pass either a #{type.name} or keyword arguments, not both" if request && !params.empty?
|
|
203
|
+
|
|
204
|
+
case request
|
|
205
|
+
when nil then type.new(**params)
|
|
206
|
+
when type then request
|
|
207
|
+
when Hash then type.new(**request.transform_keys(&:to_sym))
|
|
208
|
+
else raise ArgumentError, "expected a #{type.name}, got #{request.class}"
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def create_transaction(request)
|
|
213
|
+
post("/v1/transaction/new", request.body) { Responses::Transaction.from_api(_1) }
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def post(path, payload, &)
|
|
217
|
+
body = JSON.generate(payload)
|
|
218
|
+
request = HttpAdapter::Request.new(
|
|
219
|
+
http_method: :post,
|
|
220
|
+
url: "#{config.base_url}#{path}",
|
|
221
|
+
headers: headers(body),
|
|
222
|
+
body: body,
|
|
223
|
+
timeout: config.timeout
|
|
224
|
+
)
|
|
225
|
+
response = HttpAdapter.translate_errors { config.adapter.call(request) }
|
|
226
|
+
ResponseDecoder.decode(response, &)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def headers(body)
|
|
230
|
+
{
|
|
231
|
+
"Authorization" => "Bearer #{config.api_token}",
|
|
232
|
+
"X-Signature" => Digest::SHA256.hexdigest(body.b + config.api_secret.b),
|
|
233
|
+
"Content-Type" => "application/json",
|
|
234
|
+
"Accept" => "application/json",
|
|
235
|
+
"User-Agent" => config.user_agent
|
|
236
|
+
}.freeze
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|