@@ -0,0 +1,21 @@ | |||
MIT License | |||
Copyright (c) 2020 Peppinux | |||
Permission is hereby granted, free of charge, to any person obtaining a copy | |||
of this software and associated documentation files (the "Software"), to deal | |||
in the Software without restriction, including without limitation the rights | |||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |||
copies of the Software, and to permit persons to whom the Software is | |||
furnished to do so, subject to the following conditions: | |||
The above copyright notice and this permission notice shall be included in all | |||
copies or substantial portions of the Software. | |||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |||
SOFTWARE. |
@@ -0,0 +1,216 @@ | |||
# DERO Merchant Ruby SDK | |||
Library with bindings for the [DERO Merchant REST API](https://merchant.dero.io/docs) for accepting DERO payments on a Ruby backend. | |||
## Requirements | |||
- A store registered on your [DERO Merchant Dashboard](https://merchant.dero.io/dashboard) to receive an API Key and a Secret Key, required to send requests to the API. | |||
- A Ruby web server. | |||
## Installation | |||
`gem install dero-merchant-ruby-sdk` | |||
## Usage | |||
### Import | |||
`require "deromerchant"` | |||
### Setup | |||
```ruby | |||
dm_client = DeroMerchant::Client.new( | |||
"API_KEY_OF_YOUR_STORE_GOES_HERE", # REQUIRED | |||
"SECRET_KEY_OF_YOUR_STORE_GOES_HERE", # REQUIRED | |||
scheme: "https", # OPTIONAL. Default: https | |||
host: "merchant.dero.io", # OPTIONAL. Default: merchant.dero.io | |||
api_version: "v1" # OPTIONAL. Default: v1 | |||
) | |||
begin | |||
res = dm_client.ping() | |||
puts(res) # {"ping"=>"pong"} | |||
rescue DeroMerchant::APIError => api_err | |||
# Error returned by the API. Probably invalid API Key. | |||
puts(api_err) | |||
rescue => exception | |||
# Somethign went wrong while sending the request. | |||
# The server is offline or bad scheme/host/api version were provided. | |||
puts(exception) | |||
end | |||
``` | |||
### Create a Payment | |||
```ruby | |||
begin | |||
# payment = dm_client.create_payment("USD", 1) // USD value will be converted to DERO | |||
# payment = dm_client.create_payment("EUR", 100) // Same thing goes for EUR and other currencies supported by the CoinGecko API V3 | |||
payment = dm_client.create_payment("DERO", 10) | |||
puts(payment) | |||
=begin | |||
Hash | |||
{ | |||
"paymentID"=>"ba5a517df8506a9f55b24d18bb66d316d8df6ed93376c6414c1876d7421764b9", | |||
"status"=>"pending", | |||
"currency"=>"DERO", | |||
"currencyAmount"=>10, | |||
"exchangeRate"=>1, | |||
"deroAmount"=>"10.000000000000", | |||
"atomicDeroAmount"=>10000000000000, | |||
"integratedAddress"=>"dETin8HwLs94N6j8zASZjD8htBbQTkhUuicZEYKBG6zQENd8mrhopv3YqaeP3Q9q1RMLHX3PvF4F4Xy1cN3Rndq7daiU7JSmXpBET9APnksErnJCXaBriPySALsG8JWrUt571tRDA4Q1Cb", | |||
"creationTime"=>"2020-08-07T14:10:13.775959Z", | |||
"ttl"=>60 | |||
} | |||
=end | |||
rescue DeroMerchant::APIError => api_err | |||
# Handle API Error | |||
rescue => exception | |||
# Handle exception | |||
end | |||
``` | |||
### Get a Payment from its ID | |||
```ruby | |||
begin | |||
payment_id = "ba5a517df8506a9f55b24d18bb66d316d8df6ed93376c6414c1876d7421764b9" | |||
payment = dm_client.get_payment(payment_id) | |||
puts(payment) | |||
=begin | |||
Hash | |||
{ | |||
"paymentID"=>"ba5a517df8506a9f55b24d18bb66d316d8df6ed93376c6414c1876d7421764b9", | |||
"status"=>"pending", | |||
"currency"=>"DERO", | |||
"currencyAmount"=>10, | |||
"exchangeRate"=>1, | |||
"deroAmount"=>"10.000000000000", | |||
"atomicDeroAmount"=>10000000000000, | |||
"integratedAddress"=>"dETin8HwLs94N6j8zASZjD8htBbQTkhUuicZEYKBG6zQENd8mrhopv3YqaeP3Q9q1RMLHX3PvF4F4Xy1cN3Rndq7daiU7JSmXpBET9APnksErnJCXaBriPySALsG8JWrUt571tRDA4Q1Cb", | |||
"creationTime"=>"2020-08-07T14:10:13.775959Z", | |||
"ttl"=>55 | |||
} | |||
=end | |||
rescue DeroMerchant::APIError => api_err | |||
# Handle API Error | |||
rescue => exception | |||
# Handle exception | |||
end | |||
``` | |||
### Get an array of Payments from their IDs | |||
```ruby | |||
begin | |||
payment_ids = ["ba5a517df8506a9f55b24d18bb66d316d8df6ed93376c6414c1876d7421764b9", "95f28cb0a70a10f42e1e748d825cc72a110bae317205d6a4c1c74d8bf8927a24"] | |||
payments = dm_client.get_payments(payment_ids) | |||
puts(payments) | |||
=begin | |||
Hashes | |||
{ | |||
"paymentID"=>"ba5a517df8506a9f55b24d18bb66d316d8df6ed93376c6414c1876d7421764b9", | |||
"status"=>"pending", | |||
"currency"=>"DERO", | |||
"currencyAmount"=>10, | |||
"exchangeRate"=>1, | |||
"deroAmount"=>"10.000000000000", | |||
"atomicDeroAmount"=>10000000000000, | |||
"integratedAddress"=>"dETin8HwLs94N6j8zASZjD8htBbQTkhUuicZEYKBG6zQENd8mrhopv3YqaeP3Q9q1RMLHX3PvF4F4Xy1cN3Rndq7daiU7JSmXpBET9APnksErnJCXaBriPySALsG8JWrUt571tRDA4Q1Cb", | |||
"creationTime"=>"2020-08-07T14:10:13.775959Z", | |||
"ttl"=>51 | |||
} | |||
{ | |||
"paymentID"=>"95f28cb0a70a10f42e1e748d825cc72a110bae317205d6a4c1c74d8bf8927a24", | |||
"status"=>"pending", | |||
"currency"=>"DERO", | |||
"currencyAmount"=>10, | |||
"exchangeRate"=>1, | |||
"deroAmount"=>"10.000000000000", | |||
"atomicDeroAmount"=>10000000000000, | |||
"integratedAddress"=>"dETin8HwLs94N6j8zASZjD8htBbQTkhUuicZEYKBG6zQENd8mrhopv3YqaeP3Q9q1RMLHX3PvF4F4Xy1cN3Rndq7daiU3CDnpe22gezRV3eibbGX4drSePTPo1ye8wrH2c6b6YwysZLssQ", | |||
"creationTime"=>"2020-08-07T14:14:38.441926Z", | |||
"ttl"=>56 | |||
} | |||
=end | |||
rescue DeroMerchant::APIError => api_err | |||
# Handle API Error | |||
rescue => exception | |||
# Handle exception | |||
end | |||
``` | |||
### Get an array of filtered Payments | |||
_Not detailed because this endpoint was created for an internal usecase._ | |||
```ruby | |||
begin | |||
res = dm_client.get_filtered_payments({ | |||
"limit" => int, | |||
"page" => int, | |||
"sort_by" => string, | |||
"order_by" => string, | |||
"filter_status" => string, | |||
"filter_currency" => string | |||
} | |||
) | |||
puts(res) # Hash | |||
rescue DeroMerchant::APIError => api_err | |||
# Handle API Error | |||
rescue => exception | |||
# Handle exception | |||
end | |||
``` | |||
### Get Pay helper page URL | |||
```ruby | |||
payment_id = "ba5a517df8506a9f55b24d18bb66d316d8df6ed93376c6414c1876d7421764b9" | |||
pay_url = dm_client.get_pay_helper_url(payment_id) | |||
puts(pay_url) # https://merchant.dero.io/pay/ba5a517df8506a9f55b24d18bb66d316d8df6ed93376c6414c1876d7421764b9 | |||
``` | |||
### Verify Webhook Signature | |||
When using Webhooks to receive Payment status updates, it is highly suggested to verify the HTTP requests are actually sent by the DERO Merchant server thorugh the X-Signature header. | |||
**Example using Rails** | |||
app/controllers/webhooks_controller.rb | |||
```ruby | |||
require 'deromerchant' # Need to add deromerchant and httparty to Gemfile and run 'bundle install' before | |||
class WebhooksController < ApplicationController | |||
skip_before_action :verify_authenticity_token | |||
WEBHOOK_SECRET_KEY = "WEBHOOK_SECRET_KEY_OF_YOUR_STORE_GOES_HERE" | |||
def dero_merchant_example | |||
req_body = request.body.read() | |||
req_signature = request.headers["X-Signature"] | |||
valid = DeroMerchant::verify_webhook_signature(req_body, req_signature, WEBHOOK_SECRET_KEY) | |||
if valid | |||
# Signature was verified. As such, as long Webhook Secret Key was stored securely, request should be trusted. | |||
# Proceed with updating the status of the order on your store associated to req_json["paymentID"] accordingly to req_json["status"] | |||
req_json = JSON.parse(req_body) | |||
puts req_json | |||
=begin | |||
Hash | |||
{ | |||
"paymentID"=>"38ad8cf0c5da388fe9b5b44f6641619659c99df6cdece60c6e202acd78e895b1", | |||
"status"=>"paid" | |||
} | |||
=end | |||
else | |||
# Signature of the body provided in the request does not match the signature of the body generated using webhook_secret_key. | |||
# As such, REQUEST SHOULD NOT BE TRUSTED. | |||
# This could also mean a wrong WEBHOOK_SECRET_KEY was provided as a param, so be extra careful when copying the value from the Dashboard. | |||
end | |||
end | |||
end | |||
``` | |||
config/routes.rb | |||
```ruby | |||
Rails.application.routes.draw do | |||
# ... | |||
post 'dero_merchant_webhook_example', to: 'webhooks#dero_merchant_example' | |||
# ... | |||
end | |||
``` |
@@ -0,0 +1,14 @@ | |||
Gem::Specification.new do |s| | |||
s.name = "deromerchant" | |||
s.version = "1.0.0" | |||
s.date = "2020-08-05" | |||
s.summary = "Ruby SDK for DERO Merchant REST API" | |||
s.description = "Library with bindings for the DERO Merchant REST API" | |||
s.authors = ["Peppinux"] | |||
s.files = `git ls-files`.split("\n") | |||
s.homepage = "https://github.com/Peppinux/dero-merchant-ruby-sdk" | |||
s.license = "MIT" | |||
s.required_ruby_version = ">= 2.5" | |||
s.add_runtime_dependency("httparty", "~> 0.18", ">= 0.18.1") | |||
end |
@@ -0,0 +1,3 @@ | |||
require_relative "./deromerchant/client.rb" | |||
require_relative "./deromerchant/api_error.rb" | |||
require_relative "./deromerchant/webhook.rb" |
@@ -0,0 +1,16 @@ | |||
module DeroMerchant | |||
# Exception that represents the error object returned by the API. | |||
class APIError < StandardError | |||
# Error code returned by the API. | |||
attr_reader :code | |||
# Error message returned by the API. | |||
attr_reader :message | |||
def initialize(code, message) | |||
@code = code | |||
@message = message | |||
msg = "DeroMerchant Client: API Error: #{@code}: #{@message}" | |||
super(msg) | |||
end | |||
end | |||
end |
@@ -0,0 +1,131 @@ | |||
require "httparty" | |||
require "net/http" | |||
require "json" | |||
require_relative "./crypto_util" | |||
require_relative "./api_error" | |||
module DeroMerchant | |||
# Dero Merchant Client. Has methods to interact with the Dero Merchant REST API. | |||
class Client | |||
DEFAULT_SCHEME = "https" | |||
DEFAULT_HOST = "merchant.dero.io" | |||
DEFAULT_API_VERSION = "v1" | |||
# Number of the seconds before a connection to the API times out. | |||
attr_accessor :timeout | |||
def initialize(api_key, secret_key, scheme:DEFAULT_SCHEME, host:DEFAULT_HOST, api_version:DEFAULT_API_VERSION) | |||
@scheme = scheme | |||
@host = host | |||
@api_version = api_version | |||
@base_url = "#{@scheme}://#{@host}/api/#{@api_version}" | |||
@timeout = 10 | |||
@api_key = api_key | |||
@secret_key = secret_key | |||
end | |||
# Sends a request to the API. | |||
def send_request(method, endpoint, query_params: {}, payload: {}, sign_body: false) | |||
url = @base_url + endpoint | |||
headers = { | |||
"User-Agent" => "DeroMerchant_Client_Ruby/1.0", | |||
"X-API-Key" => @api_key | |||
} | |||
res = nil | |||
if method == Net::HTTP::Get | |||
res = HTTParty.get(url, :headers => headers, :query => query_params, timeout: @timeout) | |||
elsif method == Net::HTTP::Post | |||
headers["Content-Type"] = "application/json" | |||
headers["Accept"] = "applciation/json" | |||
json_payload = JSON.generate(payload) | |||
if sign_body == true | |||
signature = DeroMerchant::CryptoUtil::sign_message(json_payload, @secret_key) | |||
headers["X-Signature"] = signature | |||
end | |||
res = HTTParty.post(url, body: json_payload, :headers => headers, :query => query_params, timeout: @timeout) | |||
else | |||
raise ArgumentError.new("method must be Net::HTTP::Get or Net::HTTP::Post") | |||
end | |||
res_json = nil | |||
begin | |||
res_json = JSON.parse(res.body) | |||
rescue | |||
# Do nothing | |||
end | |||
if res.code < 200 || res.code > 299 | |||
if res_json != nil | |||
raise DeroMerchant::APIError.new(res_json["error"]["code"], res_json["error"]["message"]) | |||
else | |||
if res.code == 404 | |||
raise StandardError.new("DeroMerchant Client: error 404: page #{res.request.last_uri.to_s} not found") | |||
else | |||
raise StandardError.new("DeroMerchant Client: error #{res.status_code} returned by #{res.request.last_uri.to_s}") | |||
end | |||
end | |||
else | |||
return res_json | |||
end | |||
end | |||
# Pings the API. | |||
def ping() | |||
return self.send_request( | |||
Net::HTTP::Get, | |||
"/ping" | |||
) | |||
end | |||
# Creates a new payment. | |||
def create_payment(currency, amount) | |||
return self.send_request( | |||
Net::HTTP::Post, | |||
"/payment", | |||
payload: { | |||
"currency" => currency, | |||
"amount" => amount | |||
}, | |||
sign_body: true | |||
) | |||
end | |||
# Gets a payment from its ID. | |||
def get_payment(payment_id) | |||
return self.send_request( | |||
Net::HTTP::Get, | |||
"/payment/#{payment_id}" | |||
) | |||
end | |||
# Gets payments from their IDs. | |||
def get_payments(payment_ids) | |||
return self.send_request( | |||
Net::HTTP::Post, | |||
"/payments", | |||
payload: payment_ids | |||
) | |||
end | |||
# Gets filtered payments. | |||
def get_filtered_payments(opts = {}) | |||
return self.send_request( | |||
Net::HTTP::Get, | |||
"/payments", | |||
query_params: opts | |||
) | |||
end | |||
# Gets the URL of the Pay helper page of the payment ID. | |||
def get_pay_helper_url(payment_id) | |||
return "#{@scheme}://#{@host}/pay/#{payment_id}" | |||
end | |||
end | |||
end |
@@ -0,0 +1,47 @@ | |||
require "openssl" | |||
module DeroMerchant | |||
# Cryptography utility functions for generation/verification of HMACs. | |||
module CryptoUtil | |||
# Returns the SHA256 hex encoded signature of the message. | |||
def self.sign_message(message, key) | |||
return OpenSSL::HMAC.hexdigest("SHA256", [key].pack("H*"), message) | |||
end | |||
# Returns whether the signature of the message is valid or not. | |||
def self.valid_mac(message, message_mac, key) | |||
signed_message = self.sign_message(message, key) | |||
return self.secure_compare(message_mac, signed_message) | |||
end | |||
# Credits for this function go to Rails' module: | |||
# https://github.com/rails/rails/blob/fbe2433be6e052a1acac63c7faf287c52ed3c5ba/activesupport/lib/active_support/security_utils.rb | |||
# released under MIT License. | |||
# | |||
# Constant time string comparison, for fixed length strings. | |||
# | |||
# The values compared should be of fixed length, such as strings | |||
# that have already been processed by HMAC. Raises in case of length mismatch. | |||
def self.fixed_length_secure_compare(a, b) | |||
raise ArgumentError, "string length mismatch." unless a.bytesize == b.bytesize | |||
l = a.unpack "C#{a.bytesize}" | |||
res = 0 | |||
b.each_byte { |byte| res |= byte ^ l.shift } | |||
res == 0 | |||
end | |||
# Credits for this function go to Rails' module: | |||
# https://github.com/rails/rails/blob/fbe2433be6e052a1acac63c7faf287c52ed3c5ba/activesupport/lib/active_support/security_utils.rb | |||
# released under MIT License. | |||
# | |||
# Constant time string comparison, for variable length strings. | |||
# | |||
# The values are first processed by SHA256, so that we don't leak length info | |||
# via timing attacks. | |||
def self.secure_compare(a, b) | |||
fixed_length_secure_compare(::Digest::SHA256.digest(a), ::Digest::SHA256.digest(b)) && a == b | |||
end | |||
end | |||
end |
@@ -0,0 +1,8 @@ | |||
require_relative "./crypto_util" | |||
module DeroMerchant | |||
# Returns the validity of the signature of a webhook request. | |||
def self.verify_webhook_signature(req_body, req_signature, webhook_secret_key) | |||
return DeroMerchant::CryptoUtil::valid_mac(req_body, req_signature, webhook_secret_key) | |||
end | |||
end |