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.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +23 -0
  3. data/LICENSE +21 -0
  4. data/README.es.md +396 -0
  5. data/README.md +392 -0
  6. data/README.pt-br.md +392 -0
  7. data/README.ru.md +392 -0
  8. data/lib/tronzap/client.rb +239 -0
  9. data/lib/tronzap/coerce.rb +127 -0
  10. data/lib/tronzap/configuration.rb +140 -0
  11. data/lib/tronzap/errors.rb +150 -0
  12. data/lib/tronzap/http_adapter.rb +168 -0
  13. data/lib/tronzap/models/activate_address_rate.rb +11 -0
  14. data/lib/tronzap/models/address_resources.rb +13 -0
  15. data/lib/tronzap/models/aml_risk_factor.rb +28 -0
  16. data/lib/tronzap/models/bandwidth_rate.rb +28 -0
  17. data/lib/tronzap/models/direct_recharge_rate.rb +38 -0
  18. data/lib/tronzap/models/energy_rate.rb +44 -0
  19. data/lib/tronzap/models/enums.rb +26 -0
  20. data/lib/tronzap/models/resource_amounts.rb +13 -0
  21. data/lib/tronzap/models/timestamp.rb +58 -0
  22. data/lib/tronzap/models/transaction_params.rb +46 -0
  23. data/lib/tronzap/requests/address_activation.rb +28 -0
  24. data/lib/tronzap/requests/aml_check.rb +68 -0
  25. data/lib/tronzap/requests/aml_history.rb +34 -0
  26. data/lib/tronzap/requests/bandwidth_transaction.rb +33 -0
  27. data/lib/tronzap/requests/calculate.rb +32 -0
  28. data/lib/tronzap/requests/check_transaction.rb +48 -0
  29. data/lib/tronzap/requests/energy_transaction.rb +42 -0
  30. data/lib/tronzap/requests/estimate_energy.rb +38 -0
  31. data/lib/tronzap/requests/resource_bundle_transaction.rb +51 -0
  32. data/lib/tronzap/requests/validation.rb +56 -0
  33. data/lib/tronzap/response_decoder.rb +115 -0
  34. data/lib/tronzap/responses/account_balance.rb +20 -0
  35. data/lib/tronzap/responses/address_info.rb +26 -0
  36. data/lib/tronzap/responses/aml_check.rb +56 -0
  37. data/lib/tronzap/responses/aml_history.rb +28 -0
  38. data/lib/tronzap/responses/aml_service.rb +30 -0
  39. data/lib/tronzap/responses/calculation.rb +40 -0
  40. data/lib/tronzap/responses/direct_recharge_info.rb +22 -0
  41. data/lib/tronzap/responses/energy_estimate.rb +44 -0
  42. data/lib/tronzap/responses/service_rates.rb +36 -0
  43. data/lib/tronzap/responses/transaction.rb +44 -0
  44. data/lib/tronzap/version.rb +6 -0
  45. data/lib/tronzap-sdk.rb +4 -0
  46. data/lib/tronzap.rb +47 -0
  47. 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
+ [![Gem Version](https://img.shields.io/gem/v/tronzap-sdk.svg)](https://rubygems.org/gems/tronzap-sdk)
7
+ [![CI](https://github.com/tron-energy-market/tronzap-sdk-ruby/actions/workflows/ci.yml/badge.svg)](https://github.com/tron-energy-market/tronzap-sdk-ruby/actions/workflows/ci.yml)
8
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](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