Browse Source

Initial commit

master
Peppinux 1 year ago
commit
1766e72324
8 changed files with 456 additions and 0 deletions
  1. +21
    -0
      LICENSE
  2. +216
    -0
      README.md
  3. +14
    -0
      deromerchant.gemspec
  4. +3
    -0
      lib/deromerchant.rb
  5. +16
    -0
      lib/deromerchant/api_error.rb
  6. +131
    -0
      lib/deromerchant/client.rb
  7. +47
    -0
      lib/deromerchant/crypto_util.rb
  8. +8
    -0
      lib/deromerchant/webhook.rb

+ 21
- 0
LICENSE View File

@@ -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.

+ 216
- 0
README.md View File

@@ -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
```

+ 14
- 0
deromerchant.gemspec View File

@@ -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

+ 3
- 0
lib/deromerchant.rb View File

@@ -0,0 +1,3 @@
require_relative "./deromerchant/client.rb"
require_relative "./deromerchant/api_error.rb"
require_relative "./deromerchant/webhook.rb"

+ 16
- 0
lib/deromerchant/api_error.rb View File

@@ -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

+ 131
- 0
lib/deromerchant/client.rb View File

@@ -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

+ 47
- 0
lib/deromerchant/crypto_util.rb View File

@@ -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

+ 8
- 0
lib/deromerchant/webhook.rb View File

@@ -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

Loading…
Cancel
Save