nowpayments-ruby 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/LICENSE +21 -0
- data/README.md +514 -0
- data/lib/nowpayments/client.rb +271 -0
- data/lib/nowpayments/constants.rb +27 -0
- data/lib/nowpayments/error.rb +21 -0
- data/lib/nowpayments/helpers.rb +44 -0
- data/lib/nowpayments/http.rb +94 -0
- data/lib/nowpayments/ipn.rb +63 -0
- data/lib/nowpayments/version.rb +5 -0
- data/lib/nowpayments.rb +22 -0
- metadata +74 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: b1798eb671822215a938ff5390a7f2ccb29a4f1551fdccb25b09f1871840c30f
|
|
4
|
+
data.tar.gz: 8056cd086f729d178d9c01d20ab73e6b7a9250122635e7d7b73ab9d400638443
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 16eee9ec92b349c824757e15311b43a3a2e61b4930c629a03558d74348897e7708e32b0daa716607b7ae831f2af3e1b725acdc63c291d5826d5db8014517c086
|
|
7
|
+
data.tar.gz: b5527e6b8a6dc3416f2957136c2362263daceb50530b6c1bee38bf4e05e899d52a975cdf0dca88c284ba5fb38430b54b7ce525d02d362f2d0c53ac7906fbc948
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Foisalislambd
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
# nowpayments-ruby
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<img src="https://img.shields.io/gem/v/nowpayments?color=6366f1&style=for-the-badge&logo=ruby" alt="gem version" />
|
|
5
|
+
<img src="https://img.shields.io/badge/license-MIT-22c55e?style=for-the-badge" alt="license" />
|
|
6
|
+
<img src="https://img.shields.io/badge/Ruby-2.7+-cc342d?style=for-the-badge&logo=ruby" alt="Ruby" />
|
|
7
|
+
</p>
|
|
8
|
+
|
|
9
|
+
<h1 align="center">nowpayments-ruby</h1>
|
|
10
|
+
<p align="center">
|
|
11
|
+
<strong>Full-featured Ruby SDK for NOWPayments</strong><br/>
|
|
12
|
+
Accept 300+ cryptocurrencies with auto-conversion to your wallet
|
|
13
|
+
</p>
|
|
14
|
+
|
|
15
|
+
<p align="center">
|
|
16
|
+
<a href="#-quick-start">Quick Start</a> •
|
|
17
|
+
<a href="#-features">Features</a> •
|
|
18
|
+
<a href="#-examples">Examples</a> •
|
|
19
|
+
<a href="#-api-reference">API</a> •
|
|
20
|
+
<a href="#-links">Links</a>
|
|
21
|
+
</p>
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## ✨ Features
|
|
26
|
+
|
|
27
|
+
| Feature | Support |
|
|
28
|
+
|---------|---------|
|
|
29
|
+
| **Payments** | Create, status, list, update estimate |
|
|
30
|
+
| **Invoices** | Create invoice + redirect flow |
|
|
31
|
+
| **Payouts** | Mass payout, verify 2FA, cancel scheduled |
|
|
32
|
+
| **Fiat Payouts** | Currencies, payment methods, list |
|
|
33
|
+
| **Subscriptions** | Plans, recurring payments, cancel |
|
|
34
|
+
| **Custody** | Sub-partners, transfers, deposit, write-off |
|
|
35
|
+
| **Conversions** | In-custody currency conversion |
|
|
36
|
+
| **IPN Webhooks** | HMAC signature verification |
|
|
37
|
+
| **Helpers** | `payment_complete?`, `status_label`, etc. |
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## 🚀 Quick Start
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
gem install nowpayments-ruby
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Or add to your Gemfile:
|
|
48
|
+
|
|
49
|
+
```ruby
|
|
50
|
+
gem "nowpayments-ruby"
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
```ruby
|
|
54
|
+
require "nowpayments"
|
|
55
|
+
|
|
56
|
+
np = NowPayments::Client.new(
|
|
57
|
+
api_key: ENV["NOWPAYMENTS_API_KEY"],
|
|
58
|
+
sandbox: true # false for production
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# Create payment
|
|
62
|
+
payment = np.create_payment(
|
|
63
|
+
price_amount: 29.99,
|
|
64
|
+
price_currency: "usd",
|
|
65
|
+
pay_currency: "btc",
|
|
66
|
+
order_id: "order-123"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
puts "Pay #{payment["pay_amount"]} #{(payment["pay_currency"] || "").to_s.upcase} → #{payment["pay_address"]}"
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## ⚙️ Config
|
|
75
|
+
|
|
76
|
+
| Option | Required | Default | Description |
|
|
77
|
+
|--------|----------|---------|-------------|
|
|
78
|
+
| `api_key` | Yes | — | From [Dashboard](https://account.nowpayments.io) |
|
|
79
|
+
| `sandbox` | No | `false` | Use sandbox API |
|
|
80
|
+
| `timeout` | No | `30000` | Request timeout (ms) |
|
|
81
|
+
| `ipn_secret` | No | — | For webhook verification |
|
|
82
|
+
| `base_url` | No | — | Override API URL |
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## 📁 Examples
|
|
87
|
+
|
|
88
|
+
All examples use the same setup. Set `NOWPAYMENTS_API_KEY` before running:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
export NOWPAYMENTS_API_KEY=your_api_key
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
| File | Description | Run |
|
|
95
|
+
|------|-------------|-----|
|
|
96
|
+
| `01_create_payment.rb` | Create payment, show address to customer | `ruby examples/01_create_payment.rb` |
|
|
97
|
+
| `02_check_status.rb` | Check payment status + helper labels | `ruby examples/02_check_status.rb PAYMENT_ID` |
|
|
98
|
+
| `03_list_payments.rb` | List payments with filters | `ruby examples/03_list_payments.rb` |
|
|
99
|
+
| `04_create_invoice.rb` | Create invoice (redirect flow) | `ruby examples/04_create_invoice.rb` |
|
|
100
|
+
| `05_estimate_and_min_amount.rb` | Price estimate + minimum amount | `ruby examples/05_estimate_and_min_amount.rb` |
|
|
101
|
+
| `06_get_currencies.rb` | Get available currencies | `ruby examples/06_get_currencies.rb` |
|
|
102
|
+
| `07_payout_flow.rb` | Payout: validate → create → verify | `ruby examples/07_payout_flow.rb` |
|
|
103
|
+
| `08_subscription.rb` | Subscription plans + create subscription | `ruby examples/08_subscription.rb` |
|
|
104
|
+
| `09_ipn_webhook.rb` | IPN signature verification demo | `ruby examples/09_ipn_webhook.rb` |
|
|
105
|
+
| `10_custody_and_balance.rb` | Balance, sub-partners, deposit payment | `ruby examples/10_custody_and_balance.rb` |
|
|
106
|
+
| `11_conversions.rb` | Create conversion (custody) | `ruby examples/11_conversions.rb` |
|
|
107
|
+
|
|
108
|
+
### Example: Create Payment
|
|
109
|
+
|
|
110
|
+
```ruby
|
|
111
|
+
# examples/01_create_payment.rb
|
|
112
|
+
require "nowpayments"
|
|
113
|
+
|
|
114
|
+
np = NowPayments::Client.new(api_key: ENV["NOWPAYMENTS_API_KEY"], sandbox: true)
|
|
115
|
+
|
|
116
|
+
payment = np.create_payment(
|
|
117
|
+
price_amount: 29.99,
|
|
118
|
+
price_currency: "usd",
|
|
119
|
+
pay_currency: "btc",
|
|
120
|
+
order_id: "order-#{Time.now.to_i}",
|
|
121
|
+
order_description: "Premium Plan",
|
|
122
|
+
ipn_callback_url: "https://yoursite.com/webhook"
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
puts "Payment created: #{payment["payment_id"]}"
|
|
126
|
+
puts "Pay #{payment["pay_amount"]} #{payment["pay_currency"].to_s.upcase} to:"
|
|
127
|
+
puts payment["pay_address"]
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Example: Check Payment Status
|
|
131
|
+
|
|
132
|
+
```ruby
|
|
133
|
+
# examples/02_check_status.rb
|
|
134
|
+
payment_id = ARGV[0] || "PASTE_PAYMENT_ID_HERE"
|
|
135
|
+
payment = np.get_payment_status(payment_id)
|
|
136
|
+
|
|
137
|
+
puts "Status: #{NowPayments::Helpers.status_label(payment["payment_status"])}"
|
|
138
|
+
puts "Amount: #{payment["pay_amount"]} #{payment["pay_currency"]}"
|
|
139
|
+
|
|
140
|
+
if NowPayments::Helpers.payment_complete?(payment["payment_status"])
|
|
141
|
+
puts "✅ Payment done! Fulfill the order."
|
|
142
|
+
else
|
|
143
|
+
puts "⏳ Waiting for payment..."
|
|
144
|
+
end
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Example: List Payments
|
|
148
|
+
|
|
149
|
+
```ruby
|
|
150
|
+
# examples/03_list_payments.rb
|
|
151
|
+
result = np.get_payments(
|
|
152
|
+
limit: 5,
|
|
153
|
+
page: 0,
|
|
154
|
+
sortBy: "created_at",
|
|
155
|
+
orderBy: "desc",
|
|
156
|
+
dateFrom: "2024-01-01",
|
|
157
|
+
dateTo: "2024-12-31"
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
puts "Total: #{result["total"]}"
|
|
161
|
+
puts "Page: #{result["page"] + 1} of #{result["pagesCount"]}"
|
|
162
|
+
result["data"].each_with_index do |p, i|
|
|
163
|
+
puts "#{i + 1}. #{p["payment_id"]} | #{p["payment_status"]} | #{p["price_amount"]} #{p["price_currency"]}"
|
|
164
|
+
end
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Example: Create Invoice
|
|
168
|
+
|
|
169
|
+
```ruby
|
|
170
|
+
# examples/04_create_invoice.rb
|
|
171
|
+
invoice = np.create_invoice(
|
|
172
|
+
price_amount: 49.99,
|
|
173
|
+
price_currency: "usd",
|
|
174
|
+
pay_currency: "btc",
|
|
175
|
+
order_id: "inv-#{Time.now.to_i}",
|
|
176
|
+
order_description: "Premium subscription",
|
|
177
|
+
success_url: "https://yoursite.com/success",
|
|
178
|
+
cancel_url: "https://yoursite.com/cancel"
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
puts "Redirect customer to: #{invoice["invoice_url"]}"
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Example: Estimate & Min Amount
|
|
185
|
+
|
|
186
|
+
```ruby
|
|
187
|
+
# examples/05_estimate_and_min_amount.rb
|
|
188
|
+
estimate = np.get_estimate_price(
|
|
189
|
+
amount: 100,
|
|
190
|
+
currency_from: "usd",
|
|
191
|
+
currency_to: "btc"
|
|
192
|
+
)
|
|
193
|
+
puts "100 USD ≈ #{estimate["estimated_amount"]} BTC"
|
|
194
|
+
|
|
195
|
+
min = np.get_min_amount(
|
|
196
|
+
currency_from: "usd",
|
|
197
|
+
currency_to: "btc",
|
|
198
|
+
fiat_equivalent: "usd"
|
|
199
|
+
)
|
|
200
|
+
puts "Min amount: #{min["min_amount"]} BTC (≈ #{min["fiat_equivalent"]} USD)"
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Example: Get Currencies
|
|
204
|
+
|
|
205
|
+
```ruby
|
|
206
|
+
# examples/06_get_currencies.rb
|
|
207
|
+
currencies = np.get_currencies["currencies"]
|
|
208
|
+
puts "Supported: #{currencies.first(15).join(", ")}..."
|
|
209
|
+
puts "Total: #{currencies.length}"
|
|
210
|
+
|
|
211
|
+
btc_info = np.get_currency("btc")
|
|
212
|
+
puts "BTC info: #{btc_info}"
|
|
213
|
+
|
|
214
|
+
full = np.get_full_currencies["currencies"]
|
|
215
|
+
btc_full = full.find { |c| c["code"]&.downcase == "btc" }
|
|
216
|
+
puts "BTC full: #{btc_full}"
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### Example: Payout Flow (JWT required)
|
|
220
|
+
|
|
221
|
+
```ruby
|
|
222
|
+
# examples/07_payout_flow.rb
|
|
223
|
+
# Env: NOWPAYMENTS_API_KEY, EMAIL, PASSWORD, PAYOUT_ADDRESS, VERIFICATION_CODE?
|
|
224
|
+
|
|
225
|
+
# 1. Validate address
|
|
226
|
+
np.validate_payout_address(address: payout_address, currency: "btc")
|
|
227
|
+
|
|
228
|
+
# 2. Get JWT
|
|
229
|
+
auth = np.get_auth_token(ENV["EMAIL"], ENV["PASSWORD"])
|
|
230
|
+
token = auth["token"]
|
|
231
|
+
|
|
232
|
+
# 3. Create payout
|
|
233
|
+
payout = np.create_payout(
|
|
234
|
+
{
|
|
235
|
+
withdrawals: [{ address: payout_address, currency: "btc", amount: 0.0001 }],
|
|
236
|
+
ipn_callback_url: "https://yoursite.com/payout-webhook"
|
|
237
|
+
},
|
|
238
|
+
token
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
# 4. Verify (2FA code from email)
|
|
242
|
+
np.verify_payout(payout["id"], ENV["VERIFICATION_CODE"], token)
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### Example: Subscriptions
|
|
246
|
+
|
|
247
|
+
```ruby
|
|
248
|
+
# examples/08_subscription.rb
|
|
249
|
+
plans = np.get_subscription_plans
|
|
250
|
+
puts "Plans: #{plans["result"]&.length || 0}"
|
|
251
|
+
|
|
252
|
+
plan = plans["result"]&.first
|
|
253
|
+
if plan
|
|
254
|
+
auth = np.get_auth_token(ENV["EMAIL"], ENV["PASSWORD"])
|
|
255
|
+
sub = np.create_subscription(
|
|
256
|
+
{ subscription_plan_id: plan["id"], email: "customer@example.com" },
|
|
257
|
+
auth["token"]
|
|
258
|
+
)
|
|
259
|
+
puts "Subscription: #{sub["result"]}"
|
|
260
|
+
end
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### Example: IPN Webhook
|
|
264
|
+
|
|
265
|
+
```ruby
|
|
266
|
+
# examples/09_ipn_webhook.rb
|
|
267
|
+
# In Sinatra/Rails: verify before processing
|
|
268
|
+
|
|
269
|
+
np = NowPayments::Client.new(api_key: "...", ipn_secret: ENV["IPN_SECRET"])
|
|
270
|
+
|
|
271
|
+
# In your webhook handler:
|
|
272
|
+
# if np.verify_ipn(request.body.read, request.env["HTTP_X_NOWPAYMENTS_SIG"])
|
|
273
|
+
# payload = JSON.parse(request.body.read)
|
|
274
|
+
# if NowPayments::Helpers.payment_complete?(payload["payment_status"])
|
|
275
|
+
# # Fulfill order
|
|
276
|
+
# end
|
|
277
|
+
# end
|
|
278
|
+
|
|
279
|
+
# Standalone verification
|
|
280
|
+
valid = NowPayments::IPN.verify_signature(payload, signature, ipn_secret)
|
|
281
|
+
NowPayments::IPN.create_signature(payload, ipn_secret) # For testing
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
### Example: Custody & Balance
|
|
285
|
+
|
|
286
|
+
```ruby
|
|
287
|
+
# examples/10_custody_and_balance.rb
|
|
288
|
+
balance = np.get_balance
|
|
289
|
+
puts "Balance: #{balance}"
|
|
290
|
+
|
|
291
|
+
partners = np.get_sub_partners
|
|
292
|
+
puts "Sub-partners: #{partners}"
|
|
293
|
+
|
|
294
|
+
# Deposit with payment (JWT required)
|
|
295
|
+
auth = np.get_auth_token(ENV["EMAIL"], ENV["PASSWORD"])
|
|
296
|
+
result = np.create_sub_partner_payment(
|
|
297
|
+
{ currency: "trx", amount: 50, sub_partner_id: ENV["SUB_PARTNER_ID"] },
|
|
298
|
+
auth["token"]
|
|
299
|
+
)
|
|
300
|
+
puts "Deposit: #{result["result"]["pay_address"]} #{result["result"]["pay_amount"]} TRX"
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
### Example: Conversions (JWT required)
|
|
304
|
+
|
|
305
|
+
```ruby
|
|
306
|
+
# examples/11_conversions.rb
|
|
307
|
+
auth = np.get_auth_token(ENV["EMAIL"], ENV["PASSWORD"])
|
|
308
|
+
token = auth["token"]
|
|
309
|
+
|
|
310
|
+
conv = np.create_conversion(
|
|
311
|
+
{ amount: 0.001, from_currency: "btc", to_currency: "usd" },
|
|
312
|
+
token
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
if conv["deposit_id"]
|
|
316
|
+
status = np.get_conversion_status(conv["deposit_id"], token)
|
|
317
|
+
puts "Status: #{status}"
|
|
318
|
+
end
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
---
|
|
322
|
+
|
|
323
|
+
## 📖 API Reference
|
|
324
|
+
|
|
325
|
+
### Auth & Status
|
|
326
|
+
|
|
327
|
+
```ruby
|
|
328
|
+
np.get_status
|
|
329
|
+
auth = np.get_auth_token("your@email.com", "password")
|
|
330
|
+
token = auth["token"]
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
### Currencies
|
|
334
|
+
|
|
335
|
+
```ruby
|
|
336
|
+
np.get_currencies
|
|
337
|
+
np.get_currencies(true) # fixed rate
|
|
338
|
+
np.get_full_currencies
|
|
339
|
+
np.get_merchant_coins
|
|
340
|
+
np.get_currency("btc")
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
### Payments
|
|
344
|
+
|
|
345
|
+
```ruby
|
|
346
|
+
payment = np.create_payment(
|
|
347
|
+
price_amount: 29.99,
|
|
348
|
+
price_currency: "usd",
|
|
349
|
+
pay_currency: "btc",
|
|
350
|
+
order_id: "order-123",
|
|
351
|
+
ipn_callback_url: "https://yoursite.com/webhook",
|
|
352
|
+
is_fixed_rate: true
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
np.get_payment_status(payment_id)
|
|
356
|
+
np.get_payments(limit: 10, page: 0, sortBy: "created_at", orderBy: "desc")
|
|
357
|
+
np.update_payment_estimate(payment_id)
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
### Estimate & Min Amount
|
|
361
|
+
|
|
362
|
+
```ruby
|
|
363
|
+
np.get_estimate_price(amount: 100, currency_from: "usd", currency_to: "btc")
|
|
364
|
+
np.get_min_amount(currency_from: "usd", currency_to: "btc", fiat_equivalent: "usd")
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
### Invoices
|
|
368
|
+
|
|
369
|
+
```ruby
|
|
370
|
+
invoice = np.create_invoice(
|
|
371
|
+
price_amount: 49.99,
|
|
372
|
+
price_currency: "usd",
|
|
373
|
+
order_id: "inv-001",
|
|
374
|
+
success_url: "https://yoursite.com/success",
|
|
375
|
+
cancel_url: "https://yoursite.com/cancel"
|
|
376
|
+
)
|
|
377
|
+
# invoice["invoice_url"] → redirect customer
|
|
378
|
+
|
|
379
|
+
np.create_invoice_payment(iid: invoice_id, pay_currency: "btc")
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
### Payouts (JWT required)
|
|
383
|
+
|
|
384
|
+
```ruby
|
|
385
|
+
np.validate_payout_address(address: "0x...", currency: "eth")
|
|
386
|
+
|
|
387
|
+
batch = np.create_payout(
|
|
388
|
+
{
|
|
389
|
+
ipn_callback_url: "https://yoursite.com/payout-webhook",
|
|
390
|
+
withdrawals: [
|
|
391
|
+
{ address: "TEmGw...", currency: "trx", amount: 200 },
|
|
392
|
+
{ address: "0x1EB...", currency: "eth", amount: 0.1 }
|
|
393
|
+
]
|
|
394
|
+
},
|
|
395
|
+
token
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
np.verify_payout(batch["id"], "123456", token)
|
|
399
|
+
np.cancel_payout(payout_id, token)
|
|
400
|
+
np.get_payout_status(payout_id, token)
|
|
401
|
+
np.get_payouts(batch_id: "...", limit: 10, page: 0)
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
### Fiat Payouts
|
|
405
|
+
|
|
406
|
+
```ruby
|
|
407
|
+
np.get_fiat_payouts_crypto_currencies({ provider: "transfi" }, token)
|
|
408
|
+
np.get_fiat_payouts_payment_methods({ provider: "transfi", currency: "usd" }, token)
|
|
409
|
+
np.get_fiat_payouts({ status: "FINISHED", limit: 10 }, token)
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
### Balance
|
|
413
|
+
|
|
414
|
+
```ruby
|
|
415
|
+
np.get_balance(token)
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
### Subscriptions
|
|
419
|
+
|
|
420
|
+
```ruby
|
|
421
|
+
np.get_subscription_plans(limit: 10, offset: 0)
|
|
422
|
+
np.get_subscription_plan(id)
|
|
423
|
+
np.update_subscription_plan(id, amount: 9.99, interval_day: "30")
|
|
424
|
+
np.create_subscription({ subscription_plan_id: 76215585, email: "user@example.com" }, token)
|
|
425
|
+
np.get_subscriptions(status: "PAID", limit: 10)
|
|
426
|
+
np.get_subscription(id)
|
|
427
|
+
np.delete_subscription(id, token)
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
### Custody / Sub-partners (JWT required)
|
|
431
|
+
|
|
432
|
+
```ruby
|
|
433
|
+
np.create_sub_partner("user-123", token)
|
|
434
|
+
np.create_sub_partner_payment({ currency: "trx", amount: 50, sub_partner_id: "1631380403" }, token)
|
|
435
|
+
np.get_sub_partners({ offset: 0, limit: 10 }, token)
|
|
436
|
+
np.get_sub_partner_balance(sub_partner_id)
|
|
437
|
+
np.create_transfer({ currency: "trx", amount: 0.3, from_id: 111, to_id: 222 }, token)
|
|
438
|
+
np.deposit({ currency: "trx", amount: 0.5, sub_partner_id: "111" }, token)
|
|
439
|
+
np.write_off({ currency: "trx", amount: 0.3, sub_partner_id: "111" }, token)
|
|
440
|
+
np.get_transfers({ status: "FINISHED", limit: 10 }, token)
|
|
441
|
+
np.get_transfer(id, token)
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
### Conversions (JWT required)
|
|
445
|
+
|
|
446
|
+
```ruby
|
|
447
|
+
np.create_conversion({ amount: 50, from_currency: "usdttrc20", to_currency: "usdterc20" }, token)
|
|
448
|
+
np.get_conversion_status(conversion_id, token)
|
|
449
|
+
np.get_conversions({ status: "FINISHED", limit: 10 }, token)
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
### IPN / Webhooks
|
|
453
|
+
|
|
454
|
+
```ruby
|
|
455
|
+
np = NowPayments::Client.new(api_key: "...", ipn_secret: "SECRET")
|
|
456
|
+
|
|
457
|
+
# In webhook handler (Sinatra/Rails)
|
|
458
|
+
if np.verify_ipn(request.body.read, request.env["HTTP_X_NOWPAYMENTS_SIG"])
|
|
459
|
+
payload = JSON.parse(request.body.read)
|
|
460
|
+
# Process payment
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
# Standalone
|
|
464
|
+
NowPayments.verify_ipn_signature(payload, signature, ipn_secret)
|
|
465
|
+
NowPayments.create_ipn_signature(payload, ipn_secret) # For testing
|
|
466
|
+
NowPayments::IPN.verify_signature(payload, signature, ipn_secret)
|
|
467
|
+
NowPayments::IPN.create_signature(payload, ipn_secret)
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
### Helper functions
|
|
471
|
+
|
|
472
|
+
```ruby
|
|
473
|
+
NowPayments::Helpers.payment_complete?(payment["payment_status"])
|
|
474
|
+
NowPayments::Helpers.payment_pending?(payment["payment_status"])
|
|
475
|
+
NowPayments::Helpers.status_label(payment["payment_status"])
|
|
476
|
+
NowPayments::Helpers.payment_summary(payment)
|
|
477
|
+
|
|
478
|
+
# Constants
|
|
479
|
+
NowPayments::PAYMENT_STATUSES
|
|
480
|
+
NowPayments::PAYMENT_DONE_STATUSES
|
|
481
|
+
NowPayments::PAYMENT_PENDING_STATUSES
|
|
482
|
+
NowPayments::PAYMENT_STATUS_LABELS
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
### Error handling
|
|
486
|
+
|
|
487
|
+
```ruby
|
|
488
|
+
begin
|
|
489
|
+
np.create_payment(...)
|
|
490
|
+
rescue NowPayments::NowPaymentsError => e
|
|
491
|
+
puts e.message
|
|
492
|
+
puts e.status_code
|
|
493
|
+
puts e.code
|
|
494
|
+
puts e.response
|
|
495
|
+
puts e.to_s
|
|
496
|
+
end
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
---
|
|
500
|
+
|
|
501
|
+
## 🔗 Links
|
|
502
|
+
|
|
503
|
+
| Link | URL |
|
|
504
|
+
|------|-----|
|
|
505
|
+
| **API Docs** | [Postman](https://documenter.getpostman.com/view/7907941/2s93JusNJt) |
|
|
506
|
+
| **Sandbox** | [Postman Sandbox](https://documenter.getpostman.com/view/7907941/T1LSCRHC) |
|
|
507
|
+
| **Help** | [nowpayments.io/help](https://nowpayments.io/help/payments/api) |
|
|
508
|
+
| **Dashboard** | [account.nowpayments.io](https://account.nowpayments.io) |
|
|
509
|
+
|
|
510
|
+
---
|
|
511
|
+
|
|
512
|
+
## 📄 License
|
|
513
|
+
|
|
514
|
+
MIT © [Foisalislambd](https://github.com/Foisalislambd)
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'http'
|
|
4
|
+
require_relative 'ipn'
|
|
5
|
+
require_relative 'constants'
|
|
6
|
+
|
|
7
|
+
module NowPayments
|
|
8
|
+
# Main client for NOWPayments API
|
|
9
|
+
# @see https://documenter.getpostman.com/view/7907941/2s93JusNJt
|
|
10
|
+
class Client
|
|
11
|
+
def initialize(api_key:, sandbox: false, base_url: nil, timeout: 30_000, ipn_secret: nil)
|
|
12
|
+
raise ArgumentError, 'NOWPayments API key is required. Get yours at https://account.nowpayments.io' if api_key.to_s.strip.empty?
|
|
13
|
+
|
|
14
|
+
@config = {
|
|
15
|
+
api_key: api_key.to_s.strip,
|
|
16
|
+
sandbox: sandbox,
|
|
17
|
+
base_url: base_url,
|
|
18
|
+
timeout: (timeout || 30_000) / 1000.0, # Faraday uses seconds
|
|
19
|
+
ipn_secret: ipn_secret
|
|
20
|
+
}
|
|
21
|
+
@http = Http.new(@config)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# --- Status & Auth ---
|
|
25
|
+
|
|
26
|
+
def get_status
|
|
27
|
+
@http.get('/v1/status')
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def get_auth_token(email, password)
|
|
31
|
+
@http.post('/v1/auth', { email: email, password: password })
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# --- Currencies ---
|
|
35
|
+
|
|
36
|
+
def get_currencies(fixed_rate = nil)
|
|
37
|
+
params = fixed_rate.nil? ? {} : { fixed_rate: fixed_rate }
|
|
38
|
+
@http.get('/v1/currencies', params)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def get_full_currencies
|
|
42
|
+
@http.get('/v1/full-currencies')
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def get_merchant_coins(fixed_rate = nil)
|
|
46
|
+
params = fixed_rate.nil? ? {} : { fixed_rate: fixed_rate }
|
|
47
|
+
@http.get('/v1/merchant/coins', params)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def get_currency(currency)
|
|
51
|
+
code = currency.to_s.strip
|
|
52
|
+
raise ArgumentError, 'Currency code is required (e.g. "btc", "eth")' if code.empty?
|
|
53
|
+
|
|
54
|
+
@http.get("/v1/currencies/#{code}")
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# --- Estimate & Min Amount ---
|
|
58
|
+
|
|
59
|
+
def get_estimate_price(amount:, currency_from:, currency_to:)
|
|
60
|
+
@http.get('/v1/estimate', {
|
|
61
|
+
amount: amount,
|
|
62
|
+
currency_from: currency_from,
|
|
63
|
+
currency_to: currency_to
|
|
64
|
+
})
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def get_min_amount(currency_from:, currency_to:, fiat_equivalent: nil, is_fixed_rate: nil, is_fee_paid_by_user: nil)
|
|
68
|
+
params = { currency_from: currency_from, currency_to: currency_to }
|
|
69
|
+
params[:fiat_equivalent] = fiat_equivalent unless fiat_equivalent.nil?
|
|
70
|
+
params[:is_fixed_rate] = is_fixed_rate unless is_fixed_rate.nil?
|
|
71
|
+
params[:is_fee_paid_by_user] = is_fee_paid_by_user unless is_fee_paid_by_user.nil?
|
|
72
|
+
@http.get('/v1/min-amount', params)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# --- Payments ---
|
|
76
|
+
|
|
77
|
+
def create_payment(params)
|
|
78
|
+
body = params.dup
|
|
79
|
+
# Alias fixed_rate → is_fixed_rate (Node.js compatibility, supports both symbol and string keys)
|
|
80
|
+
fixed_val = body[:fixed_rate] || body["fixed_rate"]
|
|
81
|
+
has_is_fixed = body.key?(:is_fixed_rate) || body.key?("is_fixed_rate")
|
|
82
|
+
if !fixed_val.nil? && !has_is_fixed
|
|
83
|
+
body[:is_fixed_rate] = fixed_val
|
|
84
|
+
body.delete(:fixed_rate)
|
|
85
|
+
body.delete("fixed_rate")
|
|
86
|
+
end
|
|
87
|
+
@http.post('/v1/payment', body)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def get_payment_status(payment_id)
|
|
91
|
+
raise ArgumentError, 'Payment ID is required' if payment_id.nil? || payment_id.to_s.strip.empty?
|
|
92
|
+
|
|
93
|
+
@http.get("/v1/payment/#{payment_id}")
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def get_payments(params = {})
|
|
97
|
+
@http.get('/v1/payment/', params)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def update_payment_estimate(payment_id)
|
|
101
|
+
@http.post("/v1/payment/#{payment_id}/update-merchant-estimate", {})
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# --- Invoices ---
|
|
105
|
+
|
|
106
|
+
def create_invoice(params)
|
|
107
|
+
@http.post('/v1/invoice', params)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def create_invoice_payment(params)
|
|
111
|
+
@http.post('/v1/invoice-payment', params)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# --- Payouts ---
|
|
115
|
+
|
|
116
|
+
def validate_payout_address(params)
|
|
117
|
+
@http.post('/v1/payout/validate-address', params)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def create_payout(params, jwt_token)
|
|
121
|
+
raise ArgumentError, 'JWT token is required for create_payout. Call get_auth_token first.' if jwt_token.to_s.strip.empty?
|
|
122
|
+
|
|
123
|
+
@http.post('/v1/payout', params, jwt_token)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def verify_payout(payout_id, verification_code, jwt_token)
|
|
127
|
+
raise ArgumentError, 'JWT token is required for verify_payout. Call get_auth_token first.' if jwt_token.to_s.strip.empty?
|
|
128
|
+
|
|
129
|
+
@http.post("/v1/payout/#{payout_id}/verify", { verification_code: verification_code }, jwt_token)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def cancel_payout(payout_id, jwt_token)
|
|
133
|
+
raise ArgumentError, 'JWT token is required for cancel_payout. Call get_auth_token first.' if jwt_token.to_s.strip.empty?
|
|
134
|
+
|
|
135
|
+
@http.post("/v1/payout/#{payout_id}/cancel", { payout_id: payout_id }, jwt_token)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def get_payout_status(payout_id, jwt_token = nil)
|
|
139
|
+
@http.get("/v1/payout/#{payout_id}", {}, jwt_token)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def get_payouts(params = {})
|
|
143
|
+
@http.get('/v1/payout', params)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# --- Fiat Payouts ---
|
|
147
|
+
|
|
148
|
+
def get_fiat_payouts_crypto_currencies(params = {}, jwt_token = nil)
|
|
149
|
+
@http.get('/v1/fiat-payouts/crypto-currencies', params, jwt_token)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def get_fiat_payouts_payment_methods(params = {}, jwt_token = nil)
|
|
153
|
+
@http.get('/v1/fiat-payouts/payment-methods', params, jwt_token)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def get_fiat_payouts(params = {}, jwt_token = nil)
|
|
157
|
+
@http.get('/v1/fiat-payouts', params, jwt_token)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# --- Balance ---
|
|
161
|
+
|
|
162
|
+
def get_balance(jwt_token = nil)
|
|
163
|
+
@http.get('/v1/balance', {}, jwt_token)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# --- Subscriptions ---
|
|
167
|
+
|
|
168
|
+
def get_subscriptions(params = {})
|
|
169
|
+
@http.get('/v1/subscriptions', params)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def get_subscription(id)
|
|
173
|
+
@http.get("/v1/subscriptions/#{id}")
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def delete_subscription(id, jwt_token = nil)
|
|
177
|
+
@http.delete("/v1/subscriptions/#{id}", jwt_token)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def get_subscription_plans(params = {})
|
|
181
|
+
@http.get('/v1/subscriptions/plans', params)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def get_subscription_plan(id)
|
|
185
|
+
@http.get("/v1/subscriptions/plans/#{id}")
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def update_subscription_plan(id, updates)
|
|
189
|
+
@http.patch("/v1/subscriptions/plans/#{id}", updates)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def create_subscription(params, jwt_token)
|
|
193
|
+
raise ArgumentError, 'JWT token is required for create_subscription. Call get_auth_token first.' if jwt_token.to_s.strip.empty?
|
|
194
|
+
|
|
195
|
+
@http.post('/v1/subscriptions', params, jwt_token)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# --- Sub-Partners / Custody ---
|
|
199
|
+
|
|
200
|
+
def create_sub_partner(name, jwt_token)
|
|
201
|
+
raise ArgumentError, 'JWT token is required for create_sub_partner. Call get_auth_token first.' if jwt_token.to_s.strip.empty?
|
|
202
|
+
|
|
203
|
+
@http.post('/v1/sub-partner/balance', { name: name }, jwt_token)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def create_sub_partner_payment(params, jwt_token)
|
|
207
|
+
raise ArgumentError, 'JWT token is required for create_sub_partner_payment. Call get_auth_token first.' if jwt_token.to_s.strip.empty?
|
|
208
|
+
|
|
209
|
+
@http.post('/v1/sub-partner/payment', params, jwt_token)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def get_sub_partners(params = {}, jwt_token = nil)
|
|
213
|
+
@http.get('/v1/sub-partner', params, jwt_token)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def get_sub_partner_balance(sub_partner_id)
|
|
217
|
+
@http.get("/v1/sub-partner/balance/#{sub_partner_id}")
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def get_transfers(params = {}, jwt_token = nil)
|
|
221
|
+
@http.get('/v1/sub-partner/transfers', params, jwt_token)
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def get_transfer(id, jwt_token = nil)
|
|
225
|
+
@http.get("/v1/sub-partner/transfer/#{id}", {}, jwt_token)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def create_transfer(params, jwt_token)
|
|
229
|
+
raise ArgumentError, 'JWT token is required for create_transfer. Call get_auth_token first.' if jwt_token.to_s.strip.empty?
|
|
230
|
+
|
|
231
|
+
@http.post('/v1/sub-partner/transfer', params, jwt_token)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def deposit(params, jwt_token)
|
|
235
|
+
raise ArgumentError, 'JWT token is required for deposit. Call get_auth_token first.' if jwt_token.to_s.strip.empty?
|
|
236
|
+
|
|
237
|
+
@http.post('/v1/sub-partner/deposit', params, jwt_token)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def write_off(params, jwt_token = nil)
|
|
241
|
+
@http.post('/v1/sub-partner/write-off', params, jwt_token)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# --- Conversions ---
|
|
245
|
+
|
|
246
|
+
def create_conversion(params, jwt_token)
|
|
247
|
+
raise ArgumentError, 'JWT token is required for create_conversion. Call get_auth_token first.' if jwt_token.to_s.strip.empty?
|
|
248
|
+
|
|
249
|
+
@http.post('/v1/conversion', params, jwt_token)
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def get_conversion_status(conversion_id, jwt_token)
|
|
253
|
+
raise ArgumentError, 'JWT token is required for get_conversion_status. Call get_auth_token first.' if jwt_token.to_s.strip.empty?
|
|
254
|
+
|
|
255
|
+
@http.get("/v1/conversion/#{conversion_id}", {}, jwt_token)
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def get_conversions(params = {}, jwt_token = nil)
|
|
259
|
+
@http.get('/v1/conversion', params, jwt_token)
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# --- IPN ---
|
|
263
|
+
|
|
264
|
+
def verify_ipn(payload, signature)
|
|
265
|
+
secret = @config[:ipn_secret]
|
|
266
|
+
raise ArgumentError, 'IPN secret not configured. Pass ipn_secret in constructor or use verify_ipn_signature with explicit secret.' if secret.to_s.strip.empty?
|
|
267
|
+
|
|
268
|
+
IPN.verify_signature(payload, signature, secret)
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module NowPayments
|
|
4
|
+
PRODUCTION_URL = "https://api.nowpayments.io"
|
|
5
|
+
SANDBOX_URL = "https://api-sandbox.nowpayments.io"
|
|
6
|
+
|
|
7
|
+
PAYMENT_STATUSES = %w[
|
|
8
|
+
waiting confirming confirmed spending sending
|
|
9
|
+
partially_paid finished failed refunded expired
|
|
10
|
+
].freeze
|
|
11
|
+
|
|
12
|
+
PAYMENT_DONE_STATUSES = %w[finished failed refunded expired].freeze
|
|
13
|
+
PAYMENT_PENDING_STATUSES = %w[waiting confirming confirmed spending sending partially_paid].freeze
|
|
14
|
+
|
|
15
|
+
PAYMENT_STATUS_LABELS = {
|
|
16
|
+
"waiting" => "Awaiting payment",
|
|
17
|
+
"confirming" => "Confirming",
|
|
18
|
+
"confirmed" => "Confirmed",
|
|
19
|
+
"spending" => "Processing",
|
|
20
|
+
"sending" => "Sending to wallet",
|
|
21
|
+
"partially_paid" => "Partially paid",
|
|
22
|
+
"finished" => "Completed",
|
|
23
|
+
"failed" => "Failed",
|
|
24
|
+
"refunded" => "Refunded",
|
|
25
|
+
"expired" => "Expired"
|
|
26
|
+
}.freeze
|
|
27
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module NowPayments
|
|
4
|
+
class NowPaymentsError < StandardError
|
|
5
|
+
attr_reader :status_code, :code, :response
|
|
6
|
+
|
|
7
|
+
def initialize(message, status_code = nil, code = nil, response = nil)
|
|
8
|
+
super(message)
|
|
9
|
+
@status_code = status_code
|
|
10
|
+
@code = code
|
|
11
|
+
@response = response
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def to_s
|
|
15
|
+
parts = [message]
|
|
16
|
+
parts << "(status: #{status_code})" if status_code
|
|
17
|
+
parts << "[#{code}]" if code
|
|
18
|
+
parts.join(" ")
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module NowPayments
|
|
4
|
+
# Human-friendly helpers for payment status and display
|
|
5
|
+
module Helpers
|
|
6
|
+
# Check if payment is complete (success or terminal state)
|
|
7
|
+
#
|
|
8
|
+
# @param status [String] Payment status
|
|
9
|
+
# @return [Boolean]
|
|
10
|
+
def self.payment_complete?(status)
|
|
11
|
+
PAYMENT_DONE_STATUSES.include?(status.to_s)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Check if payment is still pending (customer should pay)
|
|
15
|
+
#
|
|
16
|
+
# @param status [String] Payment status
|
|
17
|
+
# @return [Boolean]
|
|
18
|
+
def self.payment_pending?(status)
|
|
19
|
+
PAYMENT_PENDING_STATUSES.include?(status.to_s)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Get human-readable status label
|
|
23
|
+
#
|
|
24
|
+
# @param status [String] Payment status
|
|
25
|
+
# @return [String]
|
|
26
|
+
def self.status_label(status)
|
|
27
|
+
PAYMENT_STATUS_LABELS[status.to_s] || status.to_s
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Build a short summary for displaying to users
|
|
31
|
+
# e.g. "Awaiting payment: 0.001234 BTC → bc1q..."
|
|
32
|
+
#
|
|
33
|
+
# @param payment [Hash] Payment object with pay_amount, pay_currency, pay_address, payment_status
|
|
34
|
+
# @return [String]
|
|
35
|
+
def self.payment_summary(payment)
|
|
36
|
+
pay_amount = payment["pay_amount"] || payment[:pay_amount]
|
|
37
|
+
pay_currency = (payment["pay_currency"] || payment[:pay_currency] || "").to_s.upcase
|
|
38
|
+
pay_address = payment["pay_address"] || payment[:pay_address] || "…"
|
|
39
|
+
payment_status = payment["payment_status"] || payment[:payment_status]
|
|
40
|
+
label = PAYMENT_STATUS_LABELS[payment_status.to_s] || payment_status.to_s
|
|
41
|
+
"#{label}: #{pay_amount} #{pay_currency} → #{pay_address}"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "faraday"
|
|
4
|
+
require "faraday/json"
|
|
5
|
+
|
|
6
|
+
module NowPayments
|
|
7
|
+
class Http
|
|
8
|
+
def initialize(config)
|
|
9
|
+
@config = config
|
|
10
|
+
@conn = build_connection
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def get(path, params = {}, jwt_token = nil)
|
|
14
|
+
response = @conn.get(path) do |req|
|
|
15
|
+
req.params = params unless params.empty?
|
|
16
|
+
req.headers["Authorization"] = "Bearer #{jwt_token}" if jwt_token.to_s.strip != ""
|
|
17
|
+
end
|
|
18
|
+
handle_response(response)
|
|
19
|
+
rescue Faraday::Error => e
|
|
20
|
+
raise NowPaymentsError.new(
|
|
21
|
+
e.is_a?(Faraday::TimeoutError) ? "Request timed out. Check your connection or try again." : (e.message || "Network error. Check your connection."),
|
|
22
|
+
nil, e.class.name, e
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def post(path, body = nil, jwt_token = nil)
|
|
27
|
+
response = @conn.post(path) do |req|
|
|
28
|
+
req.body = body.nil? ? {} : body
|
|
29
|
+
req.headers["Authorization"] = "Bearer #{jwt_token}" if jwt_token.to_s.strip != ""
|
|
30
|
+
end
|
|
31
|
+
handle_response(response)
|
|
32
|
+
rescue Faraday::Error => e
|
|
33
|
+
raise NowPaymentsError.new(
|
|
34
|
+
e.is_a?(Faraday::TimeoutError) ? "Request timed out. Check your connection or try again." : (e.message || "Network error. Check your connection."),
|
|
35
|
+
nil, e.class.name, e
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def patch(path, body, jwt_token = nil)
|
|
40
|
+
response = @conn.patch(path) do |req|
|
|
41
|
+
req.body = body
|
|
42
|
+
req.headers["Authorization"] = "Bearer #{jwt_token}" if jwt_token.to_s.strip != ""
|
|
43
|
+
end
|
|
44
|
+
handle_response(response)
|
|
45
|
+
rescue Faraday::Error => e
|
|
46
|
+
raise NowPaymentsError.new(
|
|
47
|
+
e.is_a?(Faraday::TimeoutError) ? "Request timed out. Check your connection or try again." : (e.message || "Network error. Check your connection."),
|
|
48
|
+
nil, e.class.name, e
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def delete(path, jwt_token = nil)
|
|
53
|
+
response = @conn.delete(path) do |req|
|
|
54
|
+
req.headers["Authorization"] = "Bearer #{jwt_token}" if jwt_token.to_s.strip != ""
|
|
55
|
+
end
|
|
56
|
+
handle_response(response)
|
|
57
|
+
rescue Faraday::Error => e
|
|
58
|
+
raise NowPaymentsError.new(
|
|
59
|
+
e.is_a?(Faraday::TimeoutError) ? "Request timed out. Check your connection or try again." : (e.message || "Network error. Check your connection."),
|
|
60
|
+
nil, e.class.name, e
|
|
61
|
+
)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def build_connection
|
|
67
|
+
base_url = @config[:base_url] || (@config[:sandbox] ? SANDBOX_URL : PRODUCTION_URL)
|
|
68
|
+
timeout = @config[:timeout] || 30.0
|
|
69
|
+
|
|
70
|
+
Faraday.new(url: base_url) do |f|
|
|
71
|
+
f.request :json
|
|
72
|
+
f.response :json, content_type: /\bjson$/
|
|
73
|
+
f.options.timeout = timeout
|
|
74
|
+
f.headers["Content-Type"] = "application/json"
|
|
75
|
+
f.headers["x-api-key"] = @config[:api_key]
|
|
76
|
+
f.adapter Faraday.default_adapter
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def handle_response(response)
|
|
81
|
+
return response.body if response.success?
|
|
82
|
+
|
|
83
|
+
data = response.body
|
|
84
|
+
message = data.is_a?(Hash) ? (data["message"] || data["msg"] || data["error"]) : nil
|
|
85
|
+
message ||= response.reason_phrase || "Request failed"
|
|
86
|
+
raise NowPaymentsError.new(
|
|
87
|
+
message,
|
|
88
|
+
response.status,
|
|
89
|
+
data.is_a?(Hash) ? data["code"] : nil,
|
|
90
|
+
data
|
|
91
|
+
)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module NowPayments
|
|
4
|
+
# IPN (Instant Payment Notification) verification utilities
|
|
5
|
+
# Matches official docs: sort keys recursively, then HMAC-SHA512
|
|
6
|
+
# @see https://nowpayments.io/help/payments/api
|
|
7
|
+
module IPN
|
|
8
|
+
# Recursively sort object keys (matches NOWPayments IPN spec).
|
|
9
|
+
# Normalizes to string keys for consistent JSON output (matches Node.js JSON.parse behavior).
|
|
10
|
+
def self.sort_object(obj)
|
|
11
|
+
return obj unless obj.is_a?(Hash)
|
|
12
|
+
|
|
13
|
+
normalized = obj.transform_keys(&:to_s)
|
|
14
|
+
normalized.keys.sort.each_with_object({}) do |key, result|
|
|
15
|
+
val = normalized[key]
|
|
16
|
+
result[key] = val.is_a?(Hash) && !val.is_a?(Array) ? sort_object(val) : val
|
|
17
|
+
result
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Verify IPN callback signature from NOWPayments.
|
|
22
|
+
# Safe to call – handles invalid input gracefully.
|
|
23
|
+
#
|
|
24
|
+
# @param payload [String, Hash] Raw request body (string or parsed object)
|
|
25
|
+
# @param signature [String] Value from x-nowpayments-sig header
|
|
26
|
+
# @param ipn_secret [String] Your IPN Secret from Dashboard → Store Settings
|
|
27
|
+
# @return [Boolean] true if signature is valid, false otherwise
|
|
28
|
+
def self.verify_signature(payload, signature, ipn_secret)
|
|
29
|
+
return false if signature.nil? || signature.to_s.strip.empty?
|
|
30
|
+
return false if ipn_secret.nil? || ipn_secret.to_s.strip.empty?
|
|
31
|
+
|
|
32
|
+
obj = case payload
|
|
33
|
+
when String
|
|
34
|
+
JSON.parse(payload)
|
|
35
|
+
when Hash
|
|
36
|
+
payload
|
|
37
|
+
else
|
|
38
|
+
return false
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
json_string = JSON.generate(sort_object(obj))
|
|
42
|
+
computed_sig = OpenSSL::HMAC.hexdigest('SHA512', ipn_secret.strip, json_string)
|
|
43
|
+
|
|
44
|
+
sig_bytes = [signature].pack('H*')
|
|
45
|
+
computed_bytes = [computed_sig].pack('H*')
|
|
46
|
+
return false if sig_bytes.bytesize != computed_bytes.bytesize
|
|
47
|
+
|
|
48
|
+
OpenSSL::fixed_length_secure_compare(sig_bytes, computed_bytes)
|
|
49
|
+
rescue JSON::ParserError, ArgumentError
|
|
50
|
+
false
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Create IPN signature for testing (e.g., mocking callbacks)
|
|
54
|
+
#
|
|
55
|
+
# @param payload [Hash] Payload to sign
|
|
56
|
+
# @param ipn_secret [String] IPN secret
|
|
57
|
+
# @return [String] Hex-encoded HMAC-SHA512 signature
|
|
58
|
+
def self.create_signature(payload, ipn_secret)
|
|
59
|
+
json_string = JSON.generate(sort_object(payload))
|
|
60
|
+
OpenSSL::HMAC.hexdigest('SHA512', ipn_secret.strip, json_string)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
data/lib/nowpayments.rb
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "nowpayments/version"
|
|
4
|
+
require "nowpayments/constants"
|
|
5
|
+
require "nowpayments/error"
|
|
6
|
+
require "nowpayments/http"
|
|
7
|
+
require "nowpayments/ipn"
|
|
8
|
+
require "nowpayments/helpers"
|
|
9
|
+
require "nowpayments/client"
|
|
10
|
+
|
|
11
|
+
module NowPayments
|
|
12
|
+
class Error < StandardError; end
|
|
13
|
+
|
|
14
|
+
# Convenience aliases (Node.js style)
|
|
15
|
+
def self.verify_ipn_signature(payload, signature, ipn_secret)
|
|
16
|
+
IPN.verify_signature(payload, signature, ipn_secret)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.create_ipn_signature(payload, ipn_secret)
|
|
20
|
+
IPN.create_signature(payload, ipn_secret)
|
|
21
|
+
end
|
|
22
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: nowpayments-ruby
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Foisalislambd
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: faraday
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '1.0'
|
|
19
|
+
- - "<"
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: '3'
|
|
22
|
+
type: :runtime
|
|
23
|
+
prerelease: false
|
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
25
|
+
requirements:
|
|
26
|
+
- - ">="
|
|
27
|
+
- !ruby/object:Gem::Version
|
|
28
|
+
version: '1.0'
|
|
29
|
+
- - "<"
|
|
30
|
+
- !ruby/object:Gem::Version
|
|
31
|
+
version: '3'
|
|
32
|
+
description: Accept 300+ cryptocurrencies with auto-conversion. Payments, invoices,
|
|
33
|
+
payouts, subscriptions, custody, IPN webhooks.
|
|
34
|
+
email:
|
|
35
|
+
- ''
|
|
36
|
+
executables: []
|
|
37
|
+
extensions: []
|
|
38
|
+
extra_rdoc_files: []
|
|
39
|
+
files:
|
|
40
|
+
- LICENSE
|
|
41
|
+
- README.md
|
|
42
|
+
- lib/nowpayments.rb
|
|
43
|
+
- lib/nowpayments/client.rb
|
|
44
|
+
- lib/nowpayments/constants.rb
|
|
45
|
+
- lib/nowpayments/error.rb
|
|
46
|
+
- lib/nowpayments/helpers.rb
|
|
47
|
+
- lib/nowpayments/http.rb
|
|
48
|
+
- lib/nowpayments/ipn.rb
|
|
49
|
+
- lib/nowpayments/version.rb
|
|
50
|
+
homepage: https://github.com/Foisalislambd/nowpayments-ruby
|
|
51
|
+
licenses:
|
|
52
|
+
- MIT
|
|
53
|
+
metadata:
|
|
54
|
+
homepage_uri: https://github.com/Foisalislambd/nowpayments-ruby
|
|
55
|
+
source_code_uri: https://github.com/Foisalislambd/nowpayments-ruby
|
|
56
|
+
changelog_uri: https://github.com/Foisalislambd/nowpayments-ruby/blob/main/CHANGELOG.md
|
|
57
|
+
rdoc_options: []
|
|
58
|
+
require_paths:
|
|
59
|
+
- lib
|
|
60
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
61
|
+
requirements:
|
|
62
|
+
- - ">="
|
|
63
|
+
- !ruby/object:Gem::Version
|
|
64
|
+
version: 2.7.0
|
|
65
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
66
|
+
requirements:
|
|
67
|
+
- - ">="
|
|
68
|
+
- !ruby/object:Gem::Version
|
|
69
|
+
version: '0'
|
|
70
|
+
requirements: []
|
|
71
|
+
rubygems_version: 3.6.9
|
|
72
|
+
specification_version: 4
|
|
73
|
+
summary: Full-featured Ruby SDK for NOWPayments cryptocurrency payment API
|
|
74
|
+
test_files: []
|