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 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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NowPayments
4
+ VERSION = "1.0.0"
5
+ end
@@ -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: []