nowpayments 0.2.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/.env.example +4 -0
- data/CHANGELOG.md +122 -0
- data/LICENSE.txt +21 -0
- data/README.md +578 -0
- data/Rakefile +12 -0
- data/docs/API.md +922 -0
- data/examples/jwt_authentication_example.rb +254 -0
- data/examples/simple_demo.rb +72 -0
- data/examples/webhook_server.rb +85 -0
- data/lib/nowpayments/api/authentication.rb +66 -0
- data/lib/nowpayments/api/conversions.rb +42 -0
- data/lib/nowpayments/api/currencies.rb +32 -0
- data/lib/nowpayments/api/custody.rb +147 -0
- data/lib/nowpayments/api/estimation.rb +34 -0
- data/lib/nowpayments/api/fiat_payouts.rb +150 -0
- data/lib/nowpayments/api/invoices.rb +88 -0
- data/lib/nowpayments/api/payments.rb +93 -0
- data/lib/nowpayments/api/payouts.rb +107 -0
- data/lib/nowpayments/api/status.rb +15 -0
- data/lib/nowpayments/api/subscriptions.rb +93 -0
- data/lib/nowpayments/client.rb +91 -0
- data/lib/nowpayments/errors.rb +60 -0
- data/lib/nowpayments/middleware/error_handler.rb +33 -0
- data/lib/nowpayments/rack.rb +25 -0
- data/lib/nowpayments/version.rb +5 -0
- data/lib/nowpayments/webhook.rb +62 -0
- data/lib/nowpayments.rb +12 -0
- data/sig/nowpayments.rbs +4 -0
- metadata +92 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module NOWPayments
|
|
4
|
+
# Base error class for all NOWPayments errors
|
|
5
|
+
class Error < StandardError
|
|
6
|
+
attr_reader :status, :body, :headers
|
|
7
|
+
|
|
8
|
+
def initialize(env_or_message)
|
|
9
|
+
if env_or_message.is_a?(Hash)
|
|
10
|
+
@status = env_or_message[:status]
|
|
11
|
+
@body = env_or_message[:body]
|
|
12
|
+
@headers = env_or_message[:response_headers]
|
|
13
|
+
super(error_message)
|
|
14
|
+
else
|
|
15
|
+
super
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def error_message
|
|
22
|
+
if body.is_a?(Hash) && body["message"]
|
|
23
|
+
"#{self.class.name}: #{body["message"]} (HTTP #{status})"
|
|
24
|
+
elsif body.is_a?(String)
|
|
25
|
+
"#{self.class.name}: #{body} (HTTP #{status})"
|
|
26
|
+
else
|
|
27
|
+
"#{self.class.name}: HTTP #{status}"
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Connection-level errors
|
|
33
|
+
class ConnectionError < Error
|
|
34
|
+
def initialize(message)
|
|
35
|
+
@message = message
|
|
36
|
+
super
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# HTTP 400 - Bad Request
|
|
41
|
+
class BadRequestError < Error; end
|
|
42
|
+
|
|
43
|
+
# HTTP 401, 403 - Authentication/Authorization errors
|
|
44
|
+
class AuthenticationError < Error; end
|
|
45
|
+
|
|
46
|
+
# HTTP 404 - Resource Not Found
|
|
47
|
+
class NotFoundError < Error; end
|
|
48
|
+
|
|
49
|
+
# HTTP 429 - Rate Limit Exceeded
|
|
50
|
+
class RateLimitError < Error; end
|
|
51
|
+
|
|
52
|
+
# HTTP 500-599 - Server errors
|
|
53
|
+
class ServerError < Error; end
|
|
54
|
+
|
|
55
|
+
# Security/verification errors (e.g., invalid IPN signature)
|
|
56
|
+
class SecurityError < StandardError; end
|
|
57
|
+
|
|
58
|
+
# Client-side validation errors
|
|
59
|
+
class ValidationError < StandardError; end
|
|
60
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "faraday"
|
|
4
|
+
|
|
5
|
+
module NOWPayments
|
|
6
|
+
module Middleware
|
|
7
|
+
# Faraday middleware that converts HTTP errors into NOWPayments exceptions
|
|
8
|
+
class ErrorHandler < Faraday::Middleware
|
|
9
|
+
def on_complete(env)
|
|
10
|
+
case env[:status]
|
|
11
|
+
when 400
|
|
12
|
+
raise BadRequestError, env
|
|
13
|
+
when 401, 403
|
|
14
|
+
raise AuthenticationError, env
|
|
15
|
+
when 404
|
|
16
|
+
raise NotFoundError, env
|
|
17
|
+
when 429
|
|
18
|
+
raise RateLimitError, env
|
|
19
|
+
when 500..599
|
|
20
|
+
raise ServerError, env
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def call(env)
|
|
25
|
+
@app.call(env).on_complete do |response_env|
|
|
26
|
+
on_complete(response_env)
|
|
27
|
+
end
|
|
28
|
+
rescue Faraday::ConnectionFailed, Faraday::TimeoutError => e
|
|
29
|
+
raise ConnectionError, e.message
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module NOWPayments
|
|
4
|
+
# Rack/Rails integration helpers for webhook verification
|
|
5
|
+
module Rack
|
|
6
|
+
# Verify webhook from a Rack/Rails request object
|
|
7
|
+
# @param request [Rack::Request, ActionDispatch::Request] The request object
|
|
8
|
+
# @param ipn_secret [String] IPN secret key
|
|
9
|
+
# @return [Hash] Verified payload
|
|
10
|
+
# @raise [SecurityError] If verification fails
|
|
11
|
+
def self.verify_webhook(request, ipn_secret)
|
|
12
|
+
raw_body = request.body.read
|
|
13
|
+
request.body.rewind # Allow re-reading
|
|
14
|
+
|
|
15
|
+
# Try both header access methods (Rack vs Rails)
|
|
16
|
+
signature = request.get_header("HTTP_X_NOWPAYMENTS_SIG") if request.respond_to?(:get_header)
|
|
17
|
+
signature ||= request.headers["x-nowpayments-sig"] if request.respond_to?(:headers)
|
|
18
|
+
signature ||= request.env["HTTP_X_NOWPAYMENTS_SIG"]
|
|
19
|
+
|
|
20
|
+
raise SecurityError, "Missing x-nowpayments-sig header" unless signature
|
|
21
|
+
|
|
22
|
+
Webhook.verify!(raw_body, signature, ipn_secret)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "openssl"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module NOWPayments
|
|
7
|
+
# Webhook verification utilities for IPN (Instant Payment Notifications)
|
|
8
|
+
module Webhook
|
|
9
|
+
class << self
|
|
10
|
+
# Verify IPN signature
|
|
11
|
+
# @param raw_body [String] Raw POST body from webhook
|
|
12
|
+
# @param signature [String] x-nowpayments-sig header value
|
|
13
|
+
# @param secret [String] IPN secret key from dashboard
|
|
14
|
+
# @return [Hash] Verified, parsed payload
|
|
15
|
+
# @raise [SecurityError] If signature is invalid
|
|
16
|
+
def verify!(raw_body, signature, secret)
|
|
17
|
+
raise ArgumentError, "raw_body required" if raw_body.nil? || raw_body.empty?
|
|
18
|
+
raise ArgumentError, "signature required" if signature.nil? || signature.empty?
|
|
19
|
+
raise ArgumentError, "secret required" if secret.nil? || secret.empty?
|
|
20
|
+
|
|
21
|
+
parsed = JSON.parse(raw_body)
|
|
22
|
+
sorted_json = sort_keys_recursive(parsed)
|
|
23
|
+
expected_sig = generate_signature(sorted_json, secret)
|
|
24
|
+
|
|
25
|
+
raise SecurityError, "Invalid IPN signature - webhook verification failed" unless secure_compare(expected_sig, signature)
|
|
26
|
+
|
|
27
|
+
parsed
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
# Recursively sort Hash keys (including nested hashes and arrays)
|
|
33
|
+
# This is critical for proper HMAC signature verification
|
|
34
|
+
def sort_keys_recursive(obj)
|
|
35
|
+
case obj
|
|
36
|
+
when Hash
|
|
37
|
+
obj.sort.to_h.transform_values { |v| sort_keys_recursive(v) }
|
|
38
|
+
when Array
|
|
39
|
+
obj.map { |v| sort_keys_recursive(v) }
|
|
40
|
+
else
|
|
41
|
+
obj
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Generate HMAC-SHA512 signature
|
|
46
|
+
def generate_signature(sorted_json, secret)
|
|
47
|
+
json_string = JSON.generate(sorted_json, space: "", indent: "")
|
|
48
|
+
OpenSSL::HMAC.hexdigest("SHA512", secret, json_string)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Constant-time comparison to prevent timing attacks
|
|
52
|
+
def secure_compare(a, b)
|
|
53
|
+
return false unless a.bytesize == b.bytesize
|
|
54
|
+
|
|
55
|
+
l = a.unpack("C#{a.bytesize}")
|
|
56
|
+
res = 0
|
|
57
|
+
b.each_byte { |byte| res |= byte ^ l.shift }
|
|
58
|
+
res.zero?
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
data/lib/nowpayments.rb
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "nowpayments/version"
|
|
4
|
+
require_relative "nowpayments/errors"
|
|
5
|
+
require_relative "nowpayments/middleware/error_handler"
|
|
6
|
+
require_relative "nowpayments/client"
|
|
7
|
+
require_relative "nowpayments/webhook"
|
|
8
|
+
require_relative "nowpayments/rack"
|
|
9
|
+
|
|
10
|
+
module NOWPayments
|
|
11
|
+
class Error < StandardError; end
|
|
12
|
+
end
|
data/sig/nowpayments.rbs
ADDED
metadata
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: nowpayments
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.2.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Chayut Orapinpatipat
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2025-11-01 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: faraday
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '2.0'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '2.0'
|
|
27
|
+
description: A lightweight Ruby wrapper for the NOWPayments API that handles cryptocurrency
|
|
28
|
+
payments, invoices, and webhooks
|
|
29
|
+
email:
|
|
30
|
+
- chayut@canopusnet.com
|
|
31
|
+
executables: []
|
|
32
|
+
extensions: []
|
|
33
|
+
extra_rdoc_files: []
|
|
34
|
+
files:
|
|
35
|
+
- ".env.example"
|
|
36
|
+
- CHANGELOG.md
|
|
37
|
+
- LICENSE.txt
|
|
38
|
+
- README.md
|
|
39
|
+
- Rakefile
|
|
40
|
+
- docs/API.md
|
|
41
|
+
- examples/jwt_authentication_example.rb
|
|
42
|
+
- examples/simple_demo.rb
|
|
43
|
+
- examples/webhook_server.rb
|
|
44
|
+
- lib/nowpayments.rb
|
|
45
|
+
- lib/nowpayments/api/authentication.rb
|
|
46
|
+
- lib/nowpayments/api/conversions.rb
|
|
47
|
+
- lib/nowpayments/api/currencies.rb
|
|
48
|
+
- lib/nowpayments/api/custody.rb
|
|
49
|
+
- lib/nowpayments/api/estimation.rb
|
|
50
|
+
- lib/nowpayments/api/fiat_payouts.rb
|
|
51
|
+
- lib/nowpayments/api/invoices.rb
|
|
52
|
+
- lib/nowpayments/api/payments.rb
|
|
53
|
+
- lib/nowpayments/api/payouts.rb
|
|
54
|
+
- lib/nowpayments/api/status.rb
|
|
55
|
+
- lib/nowpayments/api/subscriptions.rb
|
|
56
|
+
- lib/nowpayments/client.rb
|
|
57
|
+
- lib/nowpayments/errors.rb
|
|
58
|
+
- lib/nowpayments/middleware/error_handler.rb
|
|
59
|
+
- lib/nowpayments/rack.rb
|
|
60
|
+
- lib/nowpayments/version.rb
|
|
61
|
+
- lib/nowpayments/webhook.rb
|
|
62
|
+
- sig/nowpayments.rbs
|
|
63
|
+
homepage: https://github.com/Sentia/nowpayments
|
|
64
|
+
licenses:
|
|
65
|
+
- MIT
|
|
66
|
+
metadata:
|
|
67
|
+
allowed_push_host: https://rubygems.org
|
|
68
|
+
homepage_uri: https://github.com/Sentia/nowpayments
|
|
69
|
+
source_code_uri: https://github.com/Sentia/nowpayments
|
|
70
|
+
changelog_uri: https://github.com/Sentia/nowpayments/blob/main/CHANGELOG.md
|
|
71
|
+
documentation_uri: https://rubydoc.info/gems/nowpayments
|
|
72
|
+
rubygems_mfa_required: 'true'
|
|
73
|
+
post_install_message:
|
|
74
|
+
rdoc_options: []
|
|
75
|
+
require_paths:
|
|
76
|
+
- lib
|
|
77
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - ">="
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: 3.2.0
|
|
82
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
83
|
+
requirements:
|
|
84
|
+
- - ">="
|
|
85
|
+
- !ruby/object:Gem::Version
|
|
86
|
+
version: '0'
|
|
87
|
+
requirements: []
|
|
88
|
+
rubygems_version: 3.5.11
|
|
89
|
+
signing_key:
|
|
90
|
+
specification_version: 4
|
|
91
|
+
summary: Ruby client for NOWPayments cryptocurrency payment processing API
|
|
92
|
+
test_files: []
|