kkhay 1.0.0 → 1.0.4

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ede43e8b5ffcadeb0e7530a569edad2b57f3bb323f30bdec176d392aadbf9247
4
- data.tar.gz: 18122defec338469717ffadc0aa4e4bef274c2755878d713311dd24d0c1298f8
3
+ metadata.gz: 65dfcca8ac26b07ae0023a248e95ca76d91969b27ed573cba5183cb08ce17476
4
+ data.tar.gz: a1df4eab611a02bdf56654389c6f671a83c6d0e71eff71f01f4134df84976f85
5
5
  SHA512:
6
- metadata.gz: 0c46a4d00454e5cab9a627b7e4a6d324ae77e9ef1615490202b15f82d2de621cce1711795f84889a70a7113ac39164347919723ddd5f02fe27fb4473c201fe43
7
- data.tar.gz: e7695e1e945d988b207b13a308d6598393b12a9665285a147231473d6ed14a4a096d0f7123d195f07289834fd2c921c8dd9c665c71ce1d4328af46f2c77fe8cb
6
+ metadata.gz: d8179fa57f292f15e16699a06ed0f1a1740d58987b6a2780d24bc563b3ff0c586baabae5fcf416a9c84652966e6ba09d71218a0ae8d083de39b3ae85f4dc96cb
7
+ data.tar.gz: 2934108cec17df46ad821619677171914a5f53498834fcc54e8bc77443555af1f90c9f7ae90b5a1536b4fbab1592def09480c8eb20482723f4fdb9ab368f50ca
data/README.md CHANGED
@@ -1,8 +1,23 @@
1
1
  # K Khay Ruby Gem 💎
2
2
 
3
- Official Ruby gem for the **[K Khay Sovereign Crypto Payment Gateway](https://kkhay.com)**.
3
+ [![Gem Version](https://badge.fury.io/rb/kkhay.svg)](https://badge.fury.io/rb/kkhay)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
5
+ [![Ruby](https://img.shields.io/badge/Ruby-%3E%3D3.0.0-red.svg)](https://www.ruby-lang.org/)
6
+ [![CI Status](https://img.shields.io/github/actions/workflow/status/boboaungdev/kkhay-ruby/ci.yml?branch=main&label=CI)](https://github.com/boboaungdev/kkhay-ruby/actions)
4
7
 
5
- Accept non-custodial and custodial crypto payments (USDT, USDC, BNB, ETH across BSC, Polygon, Arbitrum, Base, and Ethereum) directly in **Ruby on Rails**, Sinatra, Hanami, or standalone Ruby backends with **zero external dependencies**.
8
+ Official Ruby gem for the **[K Khay Gateway](https://kkhay.com)** — the sovereign cryptocurrency payment gateway.
9
+
10
+ Accept sovereign, non-custodial crypto payments (**USDT, USDC, BTC, ETH, SOL, TON, BNB, TRX, DOGE, XRP, LTC** across **190+ blockchain networks**) directly in **Ruby on Rails**, Sinatra, Hanami, or standalone Ruby backends with **zero external dependencies**.
11
+
12
+ ---
13
+
14
+ ## 🌟 Highlights
15
+
16
+ * **Zero External Dependencies**: Built strictly using Ruby's standard library (`net/http`, `openssl`, `json`, `uri`).
17
+ * **190+ Blockchain Networks**: Native support for EVM chains (BNB Chain, Polygon, Arbitrum, Base, Ethereum), Solana, TON, Tron, Bitcoin, XRPL, Dogecoin, and Litecoin.
18
+ * **Database Enum Alignment**: Strictly aligned with gateway invoice states (`WAITING`, `PAID`, `EXPIRED`, `CANCELED`).
19
+ * **Timing-Safe HMAC Verification**: Built-in webhook signature validation preventing timing attacks.
20
+ * **Rails Ready**: Seamless integration with Rails initializers and ActiveSupport.
6
21
 
7
22
  ---
8
23
 
@@ -14,13 +29,13 @@ Add to your application's `Gemfile`:
14
29
  gem "kkhay"
15
30
  ```
16
31
 
17
- And then execute:
32
+ And execute:
18
33
 
19
34
  ```bash
20
35
  bundle install
21
36
  ```
22
37
 
23
- Or install it directly:
38
+ Or install it directly via `gem`:
24
39
 
25
40
  ```bash
26
41
  gem install kkhay
@@ -45,11 +60,10 @@ end
45
60
  ```ruby
46
61
  client = Kkhay.client
47
62
 
63
+ # Dynamic multi-currency checkout:
48
64
  response = client.create_invoice(
49
- price_amount: 49.99,
50
- price_currency: "USD",
51
- pay_network: "bsc",
52
- pay_token: "USDT",
65
+ amount: 49.99,
66
+ currency: "USD",
53
67
  order_id: "ORDER-7721",
54
68
  title: "Pro Membership",
55
69
  customer_email: "buyer@example.com",
@@ -62,14 +76,33 @@ puts "Invoice ID: #{invoice[:id]}"
62
76
  puts "Hosted Checkout URL: #{invoice[:hostedUrl]}"
63
77
  ```
64
78
 
79
+ Or create an invoice locked to a specific cryptocurrency and chain:
80
+
81
+ ```ruby
82
+ response = client.create_invoice(
83
+ amount: 50.00,
84
+ currency: "USD",
85
+ pay_network: "solana",
86
+ pay_token: "USDC",
87
+ order_id: "ORDER-7722"
88
+ )
89
+ ```
90
+
65
91
  ### 3. Check Invoice Status
66
92
 
67
93
  ```ruby
68
94
  data = client.get_invoice("inv_9f81a7b2")
69
- puts "Status: #{data[:invoice][:status]}"
70
-
71
- data[:payments].each do |payment|
72
- puts "Tx: #{payment[:txHash]} (#{payment[:confirmations]} confirmations)"
95
+ status = data[:invoice][:status]
96
+
97
+ case status
98
+ when Kkhay::Status::PAID
99
+ puts "Payment confirmed on-chain!"
100
+ when Kkhay::Status::WAITING
101
+ puts "Waiting for customer payment..."
102
+ when Kkhay::Status::EXPIRED
103
+ puts "Invoice expired."
104
+ when Kkhay::Status::CANCELED
105
+ puts "Invoice canceled."
73
106
  end
74
107
  ```
75
108
 
@@ -77,20 +110,31 @@ end
77
110
 
78
111
  ## 🔐 Webhook / IPN Verification (Rails Controller)
79
112
 
113
+ Verify incoming webhook requests from K Khay Gateway with timing-safe HMAC-SHA256 signatures:
114
+
80
115
  ```ruby
116
+ # app/controllers/kkhay_webhooks_controller.rb
81
117
  class KkhayWebhooksController < ApplicationController
82
118
  skip_before_action :verify_authenticity_token
83
119
 
84
120
  def create
85
121
  payload = request.raw_post
86
122
  signature = request.headers["x-kkhay-signature"]
87
- secret = ENV["KKHAY_IPN_SECRET"]
123
+ secret = ENV["KKHAY_WEBHOOK_SECRET"]
88
124
 
89
125
  event = Kkhay::Webhook.parse_event(payload, signature, secret)
90
126
 
91
- if event[:event] == "payment.finished"
92
- order = Order.find_by(id: event[:order_id])
93
- order&.update!(status: "paid", tx_hash: event[:tx_hash])
127
+ case event[:event]
128
+ when "invoice.paid"
129
+ order = Order.find_by(id: event.dig(:data, :orderId))
130
+ order&.update!(
131
+ status: "paid",
132
+ tx_hash: event.dig(:data, :txHash),
133
+ amount_paid: event.dig(:data, :amountPaid)
134
+ )
135
+ when "invoice.expired"
136
+ order = Order.find_by(id: event.dig(:data, :orderId))
137
+ order&.update!(status: "expired")
94
138
  end
95
139
 
96
140
  head :ok
@@ -102,7 +146,37 @@ end
102
146
 
103
147
  ---
104
148
 
149
+ ## 🌐 Supported Cryptocurrencies & Networks
150
+
151
+ Supports 190+ blockchain networks and 500+ cryptocurrencies:
152
+
153
+ | Ecosystem | Networks | Supported Assets |
154
+ | :--- | :--- | :--- |
155
+ | **BNB Smart Chain** | BSC Mainnet, BSC Testnet | USDT, USDC, BNB, and BEP-20 tokens |
156
+ | **Polygon** | Polygon PoS, Amoy Testnet | USDT, USDC, POL, and ERC-20 tokens |
157
+ | **Arbitrum** | Arbitrum One, Nova, Sepolia | USDT, USDC, ETH, ARB |
158
+ | **Base** | Base Mainnet, Base Sepolia | USDC, ETH, and Base tokens |
159
+ | **Ethereum** | Ethereum Mainnet, Sepolia | USDT, USDC, ETH, and ERC-20 tokens |
160
+ | **Solana** | Solana Mainnet, Devnet | SOL, USDT, USDC, and SPL tokens |
161
+ | **TON** | TON Mainnet, Testnet | TON, USDT, and Jettons |
162
+ | **Tron** | Tron Mainnet, Shasta | TRX, USDT (TRC-20), USDC |
163
+ | **Bitcoin** | Bitcoin Mainnet, Testnet | BTC |
164
+ | **XRPL** | XRP Ledger Mainnet, Testnet | XRP |
165
+ | **Dogecoin** | Dogecoin Mainnet | DOGE |
166
+ | **Litecoin** | Litecoin Mainnet | LTC |
167
+
168
+ ---
169
+
170
+ ## ⚙️ Configuration Reference
171
+
172
+ | Option | Type | Default | Description |
173
+ | :--- | :--- | :--- | :--- |
174
+ | `api_key` | `String` | **Required** | Your K Khay Gateway API Key. |
175
+ | `base_url` | `String` | `"https://api.kkhay.com"` | Base Gateway API URL. |
176
+ | `timeout` | `Integer` | `30` | Request timeout in seconds. |
177
+
178
+ ---
179
+
105
180
  ## 📄 License
106
181
 
107
182
  MIT © [K Khay](https://kkhay.com)
108
-
data/lib/kkhay/client.rb CHANGED
@@ -8,9 +8,9 @@ module Kkhay
8
8
  class Client
9
9
  attr_reader :api_key, :base_url, :timeout
10
10
 
11
- # Initializes the K Khay Client
11
+ # Initializes the K Khay Gateway Client
12
12
  #
13
- # @param api_key [String] Your merchant API key
13
+ # @param api_key [String] Your gateway API key
14
14
  # @param base_url [String] Custom gateway URL (defaults to https://api.kkhay.com)
15
15
  # @param timeout [Integer] Timeout in seconds (defaults to 30)
16
16
  def initialize(api_key: nil, base_url: "https://api.kkhay.com", timeout: 30)
@@ -23,34 +23,55 @@ module Kkhay
23
23
 
24
24
  # Creates a new crypto payment invoice
25
25
  #
26
- # @param params [Hash] Invoice parameters (:price_amount, :pay_network, :pay_token, etc.)
27
- # @return [Hash] Parsed response containing invoice details and hosted_url
26
+ # @param params [Hash] Invoice parameters (:amount, :currency, :pay_network, :pay_token, etc.)
27
+ # @return [Hash] Parsed response containing invoice details and hostedUrl
28
28
  def create_invoice(params)
29
- price = params[:price_amount] || params["priceAmount"]
30
- network = params[:pay_network] || params["payNetwork"]
31
- token = params[:pay_token] || params["payToken"]
29
+ price = params[:price_amount] || params[:amount] || params["priceAmount"] || params["amount"]
30
+ raise ArgumentError, "create_invoice: :amount or :price_amount must be greater than 0" if price.nil? || price.to_f <= 0
32
31
 
33
- raise ArgumentError, "create_invoice: :price_amount must be greater than 0" if price.to_f <= 0
34
- raise ArgumentError, "create_invoice: :pay_network is required (e.g. 'bsc', 'polygon')" if network.to_s.empty?
35
- raise ArgumentError, "create_invoice: :pay_token is required (e.g. 'USDT', 'USDC')" if token.to_s.empty?
32
+ currency = (params[:price_currency] || params[:currency] || params["priceCurrency"] || params["currency"] || "USD").to_s.upcase
36
33
 
37
34
  payload = {
35
+ amount: price.to_f,
38
36
  priceAmount: price.to_f,
39
- payNetwork: network.to_s,
40
- payToken: token.to_s,
41
- priceCurrency: (params[:price_currency] || params["priceCurrency"] || "USD").to_s,
37
+ currency: currency,
38
+ priceCurrency: currency,
42
39
  }
43
40
 
44
- payload[:orderId] = (params[:order_id] || params["orderId"]).to_s if params[:order_id] || params["orderId"]
45
- payload[:title] = (params[:title] || params["title"]).to_s if params[:title] || params["title"]
46
- payload[:customerName] = (params[:customer_name] || params["customerName"]).to_s if params[:customer_name] || params["customerName"]
47
- payload[:customerEmail] = (params[:customer_email] || params["customerEmail"]).to_s if params[:customer_email] || params["customerEmail"]
48
- payload[:redirectUrl] = (params[:redirect_url] || params["redirectUrl"]).to_s if params[:redirect_url] || params["redirectUrl"]
49
- payload[:cancelUrl] = (params[:cancel_url] || params["cancelUrl"]).to_s if params[:cancel_url] || params["cancelUrl"]
50
- payload[:ipnCallbackUrl] = (params[:ipn_callback_url] || params["ipnCallbackUrl"]).to_s if params[:ipn_callback_url] || params["ipnCallbackUrl"]
51
- payload[:metadata] = params[:metadata] if params[:metadata]
41
+ network = params[:pay_network] || params[:network] || params["payNetwork"] || params["network"]
42
+ payload[:payNetwork] = network.to_s if network && !network.to_s.strip.empty?
52
43
 
53
- request("/v1/merchant/invoices", method: :post, body: payload)
44
+ token = params[:pay_token] || params[:token] || params["payToken"] || params["token"]
45
+ payload[:payToken] = token.to_s if token && !token.to_s.strip.empty?
46
+
47
+ order_id = params[:order_id] || params["orderId"]
48
+ payload[:orderId] = order_id.to_s if order_id
49
+
50
+ title = params[:title] || params["title"]
51
+ payload[:title] = title.to_s if title
52
+
53
+ description = params[:description] || params["description"]
54
+ payload[:description] = description.to_s if description
55
+
56
+ customer_name = params[:customer_name] || params["customerName"]
57
+ payload[:customerName] = customer_name.to_s if customer_name
58
+
59
+ customer_email = params[:customer_email] || params["customerEmail"]
60
+ payload[:customerEmail] = customer_email.to_s if customer_email
61
+
62
+ redirect_url = params[:redirect_url] || params[:return_url] || params["redirectUrl"] || params["returnUrl"]
63
+ payload[:redirectUrl] = redirect_url.to_s if redirect_url
64
+
65
+ cancel_url = params[:cancel_url] || params["cancelUrl"]
66
+ payload[:cancelUrl] = cancel_url.to_s if cancel_url
67
+
68
+ ipn_callback_url = params[:ipn_callback_url] || params[:callback_url] || params["ipnCallbackUrl"] || params["callbackUrl"]
69
+ payload[:ipnCallbackUrl] = ipn_callback_url.to_s if ipn_callback_url
70
+
71
+ metadata = params[:metadata]
72
+ payload[:metadata] = metadata if metadata
73
+
74
+ request("/v1/gateway/invoices", method: :post, body: payload)
54
75
  end
55
76
 
56
77
  # Retrieves an invoice by UUID
@@ -60,10 +81,10 @@ module Kkhay
60
81
  def get_invoice(invoice_id)
61
82
  raise ArgumentError, "get_invoice: invoice_id is required" if invoice_id.to_s.strip.empty?
62
83
 
63
- request("/v1/merchant/invoices/#{URI.encode_www_form_component(invoice_id.to_s.strip)}", method: :get)
84
+ request("/v1/gateway/invoices/#{URI.encode_www_form_component(invoice_id.to_s.strip)}", method: :get)
64
85
  end
65
86
 
66
- # Lists and paginates merchant invoices
87
+ # Lists and paginates gateway invoices
67
88
  #
68
89
  # @param query [Hash] Filtering and pagination options (:page, :limit, :status, :search)
69
90
  # @return [Hash] Paginated invoice records
@@ -75,7 +96,7 @@ module Kkhay
75
96
  query_params["search"] = query[:search] || query["search"] if query[:search] || query["search"]
76
97
 
77
98
  query_str = query_params.empty? ? "" : "?#{URI.encode_www_form(query_params)}"
78
- request("/v1/merchant/invoices#{query_str}", method: :get)
99
+ request("/v1/gateway/invoices#{query_str}", method: :get)
79
100
  end
80
101
 
81
102
  # Checks the health of the gateway
@@ -134,4 +155,3 @@ module Kkhay
134
155
  end
135
156
  end
136
157
  end
137
-
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kkhay
4
+ module Status
5
+ WAITING = "WAITING"
6
+ PAID = "PAID"
7
+ EXPIRED = "EXPIRED"
8
+ CANCELED = "CANCELED"
9
+
10
+ ALL = [WAITING, PAID, EXPIRED, CANCELED].freeze
11
+
12
+ def self.valid?(status)
13
+ ALL.include?(status.to_s.upcase)
14
+ end
15
+ end
16
+ end
17
+
data/lib/kkhay/version.rb CHANGED
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Kkhay
4
- VERSION = "1.0.0"
4
+ VERSION = "1.0.4"
5
5
  end
6
-
data/lib/kkhay/webhook.rb CHANGED
@@ -10,13 +10,13 @@ module Kkhay
10
10
  #
11
11
  # @param payload [String, Hash] The raw request body or hash
12
12
  # @param signature [String] The x-kkhay-signature header value
13
- # @param ipn_secret [String] Your merchant IPN Secret Key
13
+ # @param secret [String] Your gateway Webhook/IPN Secret Key
14
14
  # @return [Boolean] true if signature matches
15
- def verify_signature(payload, signature, ipn_secret)
16
- return false if signature.nil? || signature.to_s.strip.empty? || ipn_secret.nil? || ipn_secret.to_s.strip.empty?
15
+ def verify_signature(payload, signature, secret)
16
+ return false if signature.nil? || signature.to_s.strip.empty? || secret.nil? || secret.to_s.strip.empty?
17
17
 
18
18
  raw_body = payload.is_a?(String) ? payload : JSON.generate(payload)
19
- expected = OpenSSL::HMAC.hexdigest("SHA256", ipn_secret.to_s, raw_body)
19
+ expected = OpenSSL::HMAC.hexdigest("SHA256", secret.to_s, raw_body)
20
20
 
21
21
  secure_compare(expected, signature.to_s.strip)
22
22
  rescue StandardError
@@ -25,9 +25,13 @@ module Kkhay
25
25
 
26
26
  # Validates and parses incoming webhook payload into a Ruby Hash with symbolize_names.
27
27
  #
28
+ # @param raw_body [String] The raw request body
29
+ # @param signature [String] The x-kkhay-signature header value
30
+ # @param secret [String] Your gateway Webhook/IPN Secret Key
31
+ # @return [Hash] Parsed webhook payload
28
32
  # @raise [SignatureVerificationError] if signature is invalid
29
- def parse_event(raw_body, signature, ipn_secret)
30
- unless verify_signature(raw_body, signature, ipn_secret)
33
+ def parse_event(raw_body, signature, secret)
34
+ unless verify_signature(raw_body, signature, secret)
31
35
  raise SignatureVerificationError, "Invalid K Khay webhook signature: Request rejected."
32
36
  end
33
37
 
@@ -39,17 +43,22 @@ module Kkhay
39
43
  private
40
44
 
41
45
  def secure_compare(a, b)
42
- return false unless a.bytesize == b.bytesize
46
+ if OpenSSL.respond_to?(:fixed_length_secure_compare)
47
+ return false unless a.bytesize == b.bytesize
48
+
49
+ OpenSSL.fixed_length_secure_compare(a, b)
50
+ else
51
+ return false unless a.bytesize == b.bytesize
43
52
 
44
- l = a.unpack("C*")
45
- r = b.unpack("C*")
46
- res = 0
47
- l.each_with_index do |byte, i|
48
- res |= byte ^ r[i]
53
+ l = a.unpack("C*")
54
+ r = b.unpack("C*")
55
+ res = 0
56
+ l.each_with_index do |byte, i|
57
+ res |= byte ^ r[i]
58
+ end
59
+ res.zero?
49
60
  end
50
- res.zero?
51
61
  end
52
62
  end
53
63
  end
54
64
  end
55
-
data/lib/kkhay.rb CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "kkhay/version"
4
4
  require_relative "kkhay/errors"
5
+ require_relative "kkhay/status"
5
6
  require_relative "kkhay/webhook"
6
7
  require_relative "kkhay/client"
7
8
 
@@ -41,4 +42,3 @@ module Kkhay
41
42
  end
42
43
  end
43
44
  end
44
-
metadata CHANGED
@@ -1,19 +1,47 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kkhay
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - K Khay
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-10-06 00:00:00.000000000 Z
12
- dependencies: []
13
- description: Accept non-custodial and custodial crypto payments (USDT, USDC, BNB,
14
- ETH on BSC, Polygon, Arbitrum, Base, Ethereum) in Ruby on Rails applications.
11
+ date: 2026-10-26 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rake
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '13.0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '13.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: minitest
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '5.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '5.0'
41
+ description: Accept sovereign cryptocurrency payments (USDT, USDC, BTC, ETH, SOL,
42
+ TON, BNB, TRX, DOGE, XRP, LTC across 190+ chains) in Ruby and Rails applications.
15
43
  email:
16
- - dev@kkhay.com
44
+ - support@kkhay.com
17
45
  executables: []
18
46
  extensions: []
19
47
  extra_rdoc_files: []
@@ -23,6 +51,7 @@ files:
23
51
  - lib/kkhay.rb
24
52
  - lib/kkhay/client.rb
25
53
  - lib/kkhay/errors.rb
54
+ - lib/kkhay/status.rb
26
55
  - lib/kkhay/version.rb
27
56
  - lib/kkhay/webhook.rb
28
57
  homepage: https://kkhay.com
@@ -48,8 +77,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
48
77
  - !ruby/object:Gem::Version
49
78
  version: '0'
50
79
  requirements: []
51
- rubygems_version: 3.4.19
80
+ rubygems_version: 3.5.22
52
81
  signing_key:
53
82
  specification_version: 4
54
- summary: Official Ruby Gem for K Khay Sovereign Crypto Payment Gateway
83
+ summary: Official Ruby Gem for K Khay Gateway
55
84
  test_files: []