Browse Source

Initial commit

master
Peppinux 2 years ago
commit
58eb94d391
9 changed files with 526 additions and 0 deletions
  1. +21
    -0
      LICENSE
  2. +203
    -0
      README.md
  3. +16
    -0
      deromerchant/__init__.py
  4. +25
    -0
      deromerchant/api_error.py
  5. +181
    -0
      deromerchant/client.py
  6. +37
    -0
      deromerchant/crypto_util.py
  7. +16
    -0
      deromerchant/webhook.py
  8. +1
    -0
      requirements.txt
  9. +26
    -0
      setup.py

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

+ 203
- 0
README.md View File

@@ -0,0 +1,203 @@
# DERO Merchant Python SDK
Library with bindings for the [DERO Merchant REST API](https://merchant.dero.io/docs) for accepting DERO payments on a Python 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 Python web server.

## Installation
`pip install dero-merchant-python-sdk`

## Usage
### Import
`import deromerchant`

### Setup
```python
dm_client = deromerchant.Client(
"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
)

try:
res = dm_client.ping()
print(res) # {"ping":"pong"}
except deromerchant.APIError as api_err:
# Error returned by the API. Probably invalid API Key.
print(api_err)
except Exception as err:
# Somethign went wrong while sending the request.
# The server is offline or bad scheme/host/api version were provided.
print(err)
```

### Create a Payment
```python
try:
# 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)

print(payment)
"""
Dictionary
{
'paymentID': '9ad7fd4ccd85035b30bb8f3f4bea058d50fb38c6b12aca83bedc2bbc21a3d1b1',
'status': 'pending',
'currency': 'DERO',
'currencyAmount': 10,
'exchangeRate': 1,
'deroAmount': '10.000000000000',
'atomicDeroAmount': 10000000000000,
'integratedAddress': 'dETin8HwLs94N6j8zASZjD8htBbQTkhUuicZEYKBG6zQENd8mrhopv3YqaeP3Q9q1RMLHX3PvF4F4Xy1cN3Rndq7daiU3kG58ZaPFPqhm3i2KCg9Jc2nRSb3n8A8NFpz9mWp7D4kJcC2dY',
'creationTime': '2020-08-02T22:14:56.119235Z',
'ttl': 60
}
"""
except deromerchant.APIError as api_err:
# Handle API Error
except Exception as err:
# Handle error
```

### Get a Payment from its ID
```python
try:
payment_id = "9ad7fd4ccd85035b30bb8f3f4bea058d50fb38c6b12aca83bedc2bbc21a3d1b1"
payment = dm_client.get_payment(payment_id)

print(payment)
"""
Dictionary
{
'paymentID': '9ad7fd4ccd85035b30bb8f3f4bea058d50fb38c6b12aca83bedc2bbc21a3d1b1',
'status': 'pending',
'currency': 'DERO',
'currencyAmount': 10,
'exchangeRate': 1,
'deroAmount': '10.000000000000',
'atomicDeroAmount': 10000000000000,
'integratedAddress': 'dETin8HwLs94N6j8zASZjD8htBbQTkhUuicZEYKBG6zQENd8mrhopv3YqaeP3Q9q1RMLHX3PvF4F4Xy1cN3Rndq7daiU3kG58ZaPFPqhm3i2KCg9Jc2nRSb3n8A8NFpz9mWp7D4kJcC2dY',
'creationTime': '2020-08-02T22:14:56.119235Z',
'ttl': 55
}
"""
except deromerchant.APIError as api_err:
# Handle API Error
except Exception as err:
# Handle error
```

### Get an array of Payments from their IDs
```python
try:
payment_ids = ["9ad7fd4ccd85035b30bb8f3f4bea058d50fb38c6b12aca83bedc2bbc21a3d1b1", "7d3dadd862344b2792a591d92391e49fdb15c3b0db6fe73b901000c54c97922c"]
payments = dm_client.get_payments(payment_ids)

print(payments)
"""
List of dictionaries
[
{
'paymentID': '7d3dadd862344b2792a591d92391e49fdb15c3b0db6fe73b901000c54c97922c',
'status': 'error',
'currency': 'USD',
'currencyAmount': 10,
'exchangeRate': 1.18,
'deroAmount': '8.474576271186',
'atomicDeroAmount': 8474576271186,
'integratedAddress': 'dETin8HwLs94N6j8zASZjD8htBbQTkhUuicZEYKBG6zQENd8mrhopv3YqaeP3Q9q1RMLHX3PvF4F4Xy1cN3Rndq7daiTzQYtH859kKAUCwhfgvRQM3BVMsaEvKibuCxfRoMD6k9MK7wtBk',
'creationTime': '2020-08-02T14:59:54.259882Z',
'ttl': 0
},
{
'paymentID': '9ad7fd4ccd85035b30bb8f3f4bea058d50fb38c6b12aca83bedc2bbc21a3d1b1',
'status': 'pending',
'currency': 'DERO',
'currencyAmount': 10,
'exchangeRate': 1,
'deroAmount': '10.000000000000',
'atomicDeroAmount': 10000000000000,
'integratedAddress': 'dETin8HwLs94N6j8zASZjD8htBbQTkhUuicZEYKBG6zQENd8mrhopv3YqaeP3Q9q1RMLHX3PvF4F4Xy1cN3Rndq7daiU3kG58ZaPFPqhm3i2KCg9Jc2nRSb3n8A8NFpz9mWp7D4kJcC2dY',
'creationTime': '2020-08-02T22:14:56.119235Z',
'ttl': 52
}
]
"""
except deromerchant.APIError as api_err:
# Handle API Error
except Exception as err:
# Handle error
```

### Get an array of filtered Payments
_Not detailed because this endpoint was created for an internal usecase._
```python
try:
res = dm_client.get_filtered_payments(
limit=None,
page=None,
sort_by=None,
order_by=None,
filter_status=None,
filter_currency=None
)

print(res) # Dictionary
except deromerchant.APIError as api_err:
# Handle API Error
except Exception as err:
# Handle error
```

### Get Pay helper page URL
```python
payment_id = "9ad7fd4ccd85035b30bb8f3f4bea058d50fb38c6b12aca83bedc2bbc21a3d1b1"
pay_url = dm_client.get_pay_helper_url(payment_id)

print(pay_url) # https://merchant.dero.io/pay/9ad7fd4ccd85035b30bb8f3f4bea058d50fb38c6b12aca83bedc2bbc21a3d1b1
```

### 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 Flask**
```python
import deromerchant
import json
from flask import Flask, request
app = Flask(__name__)

WEBHOOK_SECRET_KEY = "WEBHOOK_SECRET_KEY_OF_YOUR_STORE_GOES_HERE"

@app.route("/dero_merchant_webhook_example", methods=["POST"])
def hello_world():
try:
req_json = request.get_json() # Dictionary
req_body = json.dumps(req_json, separators=(",", ":")) # String, required by verify_webhook_signature
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"]
print(req_json)
"""
Dictionary
{
'status': 'paid',
'paymentID': '38ad8cf0c5da388fe9b5b44f6641619659c99df6cdece60c6e202acd78e895b1'
}
"""
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.
except Exception as err:
# Handle error
return ("", 204) # No response needed
```

+ 16
- 0
deromerchant/__init__.py View File

@@ -0,0 +1,16 @@
"""Python SDK for DERO Merchant REST API

This module allows developers to interact with the DERO Merchant REST API.

Requires:
requests (module)

Exports:
Client (class): exposes methods to send requests to the API.
APIError (Exception): representation of the error object returned by the API.
verify_webhook_signature (function): verifies the signature of requests to the webhook.
"""

from .client import Client
from .api_error import APIError
from .webhook import verify_webhook_signature

+ 25
- 0
deromerchant/api_error.py View File

@@ -0,0 +1,25 @@
class APIError(Exception):
"""Exception that represents the error object returned by the API.

Attributes:
code: Integer of the error code returned by the API
message: String of the error message returned by the API
"""

def __init__(self, code: int, message: str):
"""Inits APIError with code and message
Args:
code: Integer of the error code returned by the API
message: String of the error message returned by the API
"""
self.code = code
self.message = message

def __str__(self):
"""Describes the exception.
Returns:
A string with the description of the error.
"""
return f'API Error {self.code}: {self.message}'

+ 181
- 0
deromerchant/client.py View File

@@ -0,0 +1,181 @@
import requests
import json
from .crypto_util import sign_message
from .api_error import APIError

DEFAULT_SCHEME = "https"
DEFAULT_HOST = "merchant.dero.io"
DEFAULT_API_VERSION = "v1"

class Client:
"""Dero Merchant Client. Has methods to interact with the Dero Merchant REST API.

Attributes:
__scheme: String of the scheme of the URL of the API.
__host: String of the host of the URL of the API.
__api_version: String of the version of the API.
__base_url: URL of the API.
timeout: Integer of the seconds before a connection to the API times out.
__api_key: String of the API Key of the Client.
__secret_key: String of the Secret Key of the Client.
"""
def __init__(self, api_key: str, secret_key: str, scheme=DEFAULT_SCHEME, host=DEFAULT_HOST, api_version=DEFAULT_API_VERSION):
"""Inits Client with API Key, Secret Key, scheme, host and API Version

Args:
api_key: String of the API Key of the Client.
secret_key: String of the Secret Key of the Client.
scheme: String of the scheme of the URL of the API.
host: String of the host of the URL of the API.
api_version: String of the version of the API.
"""

self.__scheme = scheme
self.__host = host
self.__api_version = api_version
self.__base_url = f'{self.__scheme}://{self.__host}/api/{self.__api_version}'
self.timeout = 10

self.__api_key = api_key
self.__secret_key = secret_key
def __send_request(self, method: str, endpoint: str, query_params: dict=None, payload=None, sign_body=False) -> dict:
"""Sends a request to the API.
Args:
method: String of the method of the request.
endpoint: String of the endpoint of the request.
query_params: Dictionary of the query params to send in the request.
payload: Payload to send as the JSON body of the request.
sign_body: Bool of whether the request body has to be signed or not.

Returns:
A dictionary of the JSON response.

Raises:
Exception: An error occured send the request.
APIError: An error was returned as a response by the API.
"""
url = self.__base_url + endpoint
headers = {
"User-Agent": "DeroMerchant_Client_Python/1.0",
"X-API-Key": self.__api_key,
}

json_payload = None
json_res = None

try:
if payload is not None:
headers["Content-Type"] = "application/json"
headers["Accept"] = "applciation/json"

json_payload = json.dumps(payload, separators=(",", ":"))

if sign_body is not False:
signature = sign_message(json_payload, self.__secret_key)
headers["X-Signature"] = signature

res = requests.request(method, url, timeout=self.timeout, params=query_params, headers=headers, data=json_payload)
if "application/json" in res.headers.get("Content-Type"):
json_res = res.json()

if (res.status_code < 200) or (res.status_code > 299):
if (json_res is not None) and ("error" in json_res):
raise APIError(json_res["error"]["code"], json_res["error"]["message"])
else:
if res.status_code == 404:
raise Exception(f'error 404: page {res.url} not found')
else:
raise Exception(f'error {res.status_code} returned by {res.url}')
except Exception as err:
raise Exception(f'DeroMerchant Client: {err}')
else:
return json_res

def ping(self) -> dict:
"""Pings the API.

Returns:
A dictionary with the JSON response.
"""
return self.__send_request(
method="GET",
endpoint="/ping"
)

def create_payment(self, currency: str, amount: float) -> dict:
"""Creates a new payment.

Args:
currency: String of the currency of the payment.
amount: Float of the amount of the payment.

Returns:
A dictionary with the JSON response of the newly created Payment.
"""
return self.__send_request(
method="POST",
endpoint="/payment",
payload={
"currency": currency,
"amount": amount,
},
sign_body=True
)

def get_payment(self, payment_id: str) -> dict:
"""Gets a payment from its ID.

Args:
payment_id: String of the payment ID.

Returns:
A dictionary with the JSON response of the requested Payment.
"""
return self.__send_request(
method="GET",
endpoint=f'/payment/{payment_id}'
)

def get_payments(self, payment_ids: list) -> list:
"""Gets payments from their IDs.

Args:
payment_ids: List of strings of the payment IDs.
Returns:
A list containing dictionaries with the JSON response of the requested payments.
"""
return self.__send_request(
method="POST",
endpoint="/payments",
payload=payment_ids
)

def get_filtered_payments(self, limit: int=None, page: int=None, sort_by: str=None, order_by: str=None, filter_status:str=None, filter_currency: str=None) -> dict:
"""Gets filtered payments"""
return self.__send_request(
method="GET",
endpoint="/payments",
query_params={
"limit": limit,
"page": page,
"sort_by": sort_by,
"order_by": order_by,
"filter_status": filter_status,
"filter_currency": filter_currency,
}
)

def get_pay_helper_url(self, payment_id: str) -> str:
"""Gets the URL of the Pay helper page of the payment ID.

Args:
payment_id: String of the payment ID.

Returns:
String with the URL of the Pay helper page of the payment ID.
"""
return f'{self.__scheme}://{self.__host}/pay/{payment_id}'

+ 37
- 0
deromerchant/crypto_util.py View File

@@ -0,0 +1,37 @@
import hmac
import hashlib
import binascii

def sign_message(message: str, key: str) -> str:
"""Signs a message with a key.

Args:
message:
String of the message we want to sign.
key:
String of the SHA256 hex encoded key we want to use to sign the message.
Returns:
A string containing the SHA256 hex encoded signature of the message.
"""

key_bytes = binascii.unhexlify(key)
message_bytes = message.encode()
return hmac.new(key_bytes, message_bytes, hashlib.sha256).hexdigest()

def valid_mac(message: str, message_mac: str, key: str) -> bool:
"""Verifies if the signature of a message is valid.

Args:
message:
String of the message we want to verify.
message_mac:
String of the SHA256 hex encoded signature of the message we want to verify.
key:
String of the SHA256 hex encoded key used to sign the message.

Returns:
A bool with the validity of the signature.
"""
signed_message = sign_message(message, key)
return hmac.compare_digest(bytearray.fromhex(message_mac), bytearray.fromhex(signed_message))

+ 16
- 0
deromerchant/webhook.py View File

@@ -0,0 +1,16 @@
from .crypto_util import valid_mac

def verify_webhook_signature(req_body: str, req_signature: str, webhook_secret_key: str) -> bool:
"""Verifies the signature of a webhook request.

Args:
req_body:
A string of the body of the webhook request.
req_signature:
A string of the SHA256 hex encoded signature of the body of the webhook request.
webhook_secret_key:
A string of the SHA256 hex encoded webhook secret key.
Returns:
A bool with the validity of the signature.
"""
return valid_mac(req_body, req_signature, webhook_secret_key)

+ 1
- 0
requirements.txt View File

@@ -0,0 +1 @@
requests

+ 26
- 0
setup.py View File

@@ -0,0 +1,26 @@
import setuptools

with open("README.md") as f:
readme = f.read()

with open("LICENSE") as f:
license = f.read()

setuptools.setup(
name="dero-merchant-python-sdk",
version="1.0.0",
author="Peppinux",
description="Python SDK for DERO Merchant REST API",
long_description=readme,
long_description_content_type="text/markdown",
url="https://github.com/peppinux/dero-merchant-python-sdk",
license=license,
packages=setuptools.find_packages(),
classifiers=[
"Programming Language :: Python :: 3.6",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Intended Audience :: Developers",
],
python_requires=">=3.6",
)

Loading…
Cancel
Save