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.
@@ -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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NOWPayments
4
+ VERSION = "0.2.0"
5
+ 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
@@ -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
@@ -0,0 +1,4 @@
1
+ module Nowpayments
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
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: []