BTCPay Library Included
This commit is contained in:
@@ -24,7 +24,7 @@
|
|||||||
|
|
||||||
'summary': 'This module integrates BTCPAY - pay with Bitcoin - with Odoo v16.0',
|
'summary': 'This module integrates BTCPAY - pay with Bitcoin - with Odoo v16.0',
|
||||||
'author': 'Vandekul',
|
'author': 'Vandekul',
|
||||||
'website': 'https://github.com/vandekul',
|
'website': 'https://github.com/btcpayserver/odoo',
|
||||||
'category': 'Accounting/Payment Providers',
|
'category': 'Accounting/Payment Providers',
|
||||||
'version': '16.0',
|
'version': '16.0',
|
||||||
'license': 'GPL-3',
|
'license': 'GPL-3',
|
||||||
@@ -37,7 +37,6 @@
|
|||||||
'views/payment_btcpay_templates.xml',
|
'views/payment_btcpay_templates.xml',
|
||||||
'views/payment_provider_views.xml',
|
'views/payment_provider_views.xml',
|
||||||
'views/payment_transaction_views.xml',
|
'views/payment_transaction_views.xml',
|
||||||
|
|
||||||
'data/payment_provider_data.xml',
|
'data/payment_provider_data.xml',
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ from odoo.http import request
|
|||||||
from odoo.tools import html_escape
|
from odoo.tools import html_escape
|
||||||
|
|
||||||
import json
|
import json
|
||||||
from btcpay import BTCPayClient
|
from ..models.libs.client import BTCPayClient
|
||||||
|
|
||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
_logger = logging.getLogger(__name__)
|
||||||
@@ -71,7 +71,6 @@ class BTCPayController(http.Controller):
|
|||||||
|
|
||||||
@http.route('/payment/btcpay/ipn', type='json', auth='public', csrf=False)
|
@http.route('/payment/btcpay/ipn', type='json', auth='public', csrf=False)
|
||||||
def btcpay_ipn(self, **post):
|
def btcpay_ipn(self, **post):
|
||||||
|
|
||||||
""" BTCPay IPN. """
|
""" BTCPay IPN. """
|
||||||
_logger.info('BTCPAY IPN RECEIVED... ')
|
_logger.info('BTCPAY IPN RECEIVED... ')
|
||||||
data = json.loads(request.httprequest.data)
|
data = json.loads(request.httprequest.data)
|
||||||
|
|||||||
@@ -0,0 +1,167 @@
|
|||||||
|
"""btcpay.client
|
||||||
|
|
||||||
|
BTCPay API Client.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
import json
|
||||||
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from requests.exceptions import HTTPError
|
||||||
|
|
||||||
|
from . import crypto
|
||||||
|
|
||||||
|
|
||||||
|
class BTCPayClient:
|
||||||
|
def __init__(self, host, pem, insecure=False, tokens=None):
|
||||||
|
self.host = host
|
||||||
|
self.verify = not(insecure)
|
||||||
|
self.pem = pem
|
||||||
|
self.tokens = tokens or dict()
|
||||||
|
self.client_id = crypto.get_sin_from_pem(pem)
|
||||||
|
self.user_agent = 'btcpay-python'
|
||||||
|
self.s = requests.Session()
|
||||||
|
self.s.verify = self.verify
|
||||||
|
self.s.headers.update(
|
||||||
|
{'Content-Type': 'application/json',
|
||||||
|
'accept': 'application/json',
|
||||||
|
'X-accept-version': '2.0.0'})
|
||||||
|
|
||||||
|
def _create_signed_headers(self, uri, payload):
|
||||||
|
return {
|
||||||
|
"X-Identity": crypto.get_compressed_public_key_from_pem(self.pem),
|
||||||
|
"X-Signature": crypto.sign(uri + payload, self.pem)
|
||||||
|
}
|
||||||
|
|
||||||
|
def _signed_get_request(self, path, params=None, token=None):
|
||||||
|
token = token or list(self.tokens.values())[0]
|
||||||
|
params = params or dict()
|
||||||
|
params['token'] = token
|
||||||
|
|
||||||
|
uri = self.host + path
|
||||||
|
payload = '?' + urlencode(params)
|
||||||
|
headers = self._create_signed_headers(uri, payload)
|
||||||
|
r = self.s.get(uri, params=params, headers=headers)
|
||||||
|
r.raise_for_status()
|
||||||
|
return r.json()['data']
|
||||||
|
|
||||||
|
def _signed_post_request(self, path, payload, token=None):
|
||||||
|
token = token or list(self.tokens.values())[0]
|
||||||
|
uri = self.host + path
|
||||||
|
payload['token'] = token
|
||||||
|
payload = json.dumps(payload)
|
||||||
|
headers = self._create_signed_headers(uri, payload)
|
||||||
|
r = self.s.post(uri, headers=headers, data=payload)
|
||||||
|
if not r.ok:
|
||||||
|
if 400 <= r.status_code < 500:
|
||||||
|
http_error_msg = u'%s Client Error: \
|
||||||
|
%s for url: %s | body: %s' % (
|
||||||
|
r.status_code,
|
||||||
|
r.reason,
|
||||||
|
r.url,
|
||||||
|
r.text
|
||||||
|
)
|
||||||
|
elif 500 <= r.status_code < 600:
|
||||||
|
http_error_msg = u'%s Server Error: \
|
||||||
|
%s for url: %s | body: %s' % (
|
||||||
|
r.status_code,
|
||||||
|
r.reason,
|
||||||
|
r.url,
|
||||||
|
r.text
|
||||||
|
)
|
||||||
|
if http_error_msg:
|
||||||
|
raise HTTPError(http_error_msg, response=r)
|
||||||
|
return r.json()['data']
|
||||||
|
|
||||||
|
def _unsigned_request(self, path, payload=None):
|
||||||
|
uri = self.host + path
|
||||||
|
if payload:
|
||||||
|
payload = json.dumps(payload)
|
||||||
|
r = self.s.post(uri, data=payload)
|
||||||
|
else:
|
||||||
|
r = self.s.get(uri)
|
||||||
|
r.raise_for_status()
|
||||||
|
return r.json()['data']
|
||||||
|
|
||||||
|
def get_rates(self, crypto='BTC', store_id=None):
|
||||||
|
params = dict(
|
||||||
|
cryptoCode=crypto
|
||||||
|
)
|
||||||
|
if store_id:
|
||||||
|
params['storeID'] = store_id
|
||||||
|
return self._signed_get_request('/rates/', params=params)
|
||||||
|
|
||||||
|
def get_rate(self, currency, crypto='BTC', store_id=None):
|
||||||
|
rates = self.get_rates(crypto=crypto, store_id=store_id)
|
||||||
|
rate = [rate for rate in rates if rate['code'] == currency.upper()][0]
|
||||||
|
return rate['rate']
|
||||||
|
|
||||||
|
def create_invoice(self, payload, token=None):
|
||||||
|
try:
|
||||||
|
float(payload['price'])
|
||||||
|
except ValueError as e:
|
||||||
|
raise ValueError('Price must be a float') from e
|
||||||
|
return self._signed_post_request('/invoices/', payload, token=token)
|
||||||
|
|
||||||
|
def get_invoice(self, invoice_id, token=None):
|
||||||
|
return self._signed_get_request('/invoices/' + invoice_id, token=token)
|
||||||
|
|
||||||
|
def get_invoices(self, status=None, order_id=None, item_code=None, date_start=None, date_end=None, limit=None, offset=None, token=None):
|
||||||
|
params = dict()
|
||||||
|
if status is not None:
|
||||||
|
params['status'] = status
|
||||||
|
if order_id is not None:
|
||||||
|
params['orderId'] = order_id
|
||||||
|
if item_code is not None:
|
||||||
|
params['itemCode'] = item_code
|
||||||
|
if date_start is not None:
|
||||||
|
params['dateStart'] = date_start
|
||||||
|
if date_end is not None:
|
||||||
|
params['dateEnd'] = date_end
|
||||||
|
if limit is not None:
|
||||||
|
params['limit'] = limit
|
||||||
|
if offset is not None:
|
||||||
|
params['offset'] = offset
|
||||||
|
return self._signed_get_request('/invoices', params=params, token=token)
|
||||||
|
|
||||||
|
def pair_client(self, code):
|
||||||
|
if re.match(r'^\w{7,7}$', code) is None:
|
||||||
|
raise ValueError("pairing code is not legal")
|
||||||
|
payload = {'id': self.client_id, 'pairingCode': code}
|
||||||
|
data = self._unsigned_request('/tokens', payload)
|
||||||
|
data = data[0]
|
||||||
|
return {
|
||||||
|
data['facade']: data['token']
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create_client(cls, code, host):
|
||||||
|
pem = crypto.generate_privkey()
|
||||||
|
client = BTCPayClient(host=host, pem=pem)
|
||||||
|
token = client.pair_client(code)
|
||||||
|
return BTCPayClient(host=host, pem=pem, tokens=token)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create_tor_client(cls, code, host, proxy='socks5://127.0.0.1:9050'):
|
||||||
|
""" Useful for .onion services, the `proxy` input assumes the default
|
||||||
|
proxy header
|
||||||
|
"""
|
||||||
|
pem = crypto.generate_privkey()
|
||||||
|
client = BTCPayClient(host=host, pem=pem)
|
||||||
|
client.s.proxies = {
|
||||||
|
'http': proxy,
|
||||||
|
'https': proxy}
|
||||||
|
token = client.pair_client(code)
|
||||||
|
final_client = BTCPayClient(host=host, pem=pem, tokens=token)
|
||||||
|
final_client.s.proxies = {
|
||||||
|
'http': proxy,
|
||||||
|
'https': proxy}
|
||||||
|
return final_client
|
||||||
|
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return '{}({})'.format(
|
||||||
|
type(self).__name__,
|
||||||
|
self.host
|
||||||
|
)
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
"""btcpay.crypto
|
||||||
|
|
||||||
|
These are various crytography related utility functions borrowed from:
|
||||||
|
bitpay-python: https://github.com/bitpay/bitpay-python
|
||||||
|
"""
|
||||||
|
|
||||||
|
import binascii
|
||||||
|
import hashlib
|
||||||
|
|
||||||
|
from ecdsa import SigningKey, SECP256k1, VerifyingKey
|
||||||
|
from ecdsa import util as ecdsaUtil
|
||||||
|
|
||||||
|
|
||||||
|
def generate_privkey():
|
||||||
|
sk = SigningKey.generate(curve=SECP256k1)
|
||||||
|
pem = sk.to_pem()
|
||||||
|
pem = pem.decode('utf-8')
|
||||||
|
return pem
|
||||||
|
|
||||||
|
|
||||||
|
def get_sin_from_pem(pem):
|
||||||
|
public_key = get_compressed_public_key_from_pem(pem)
|
||||||
|
version = get_version_from_compressed_key(public_key)
|
||||||
|
checksum = get_checksum_from_version(version)
|
||||||
|
return base58encode(version + checksum)
|
||||||
|
|
||||||
|
|
||||||
|
def get_compressed_public_key_from_pem(pem):
|
||||||
|
vks = SigningKey.from_pem(pem).get_verifying_key().to_string()
|
||||||
|
bts = binascii.hexlify(vks)
|
||||||
|
compressed = compress_key(bts)
|
||||||
|
return compressed
|
||||||
|
|
||||||
|
|
||||||
|
def sign(message, pem):
|
||||||
|
message = message.encode()
|
||||||
|
sk = SigningKey.from_pem(pem)
|
||||||
|
signed = sk.sign(message, hashfunc=hashlib.sha256,
|
||||||
|
sigencode=ecdsaUtil.sigencode_der)
|
||||||
|
return binascii.hexlify(signed).decode()
|
||||||
|
|
||||||
|
|
||||||
|
def base58encode(hexastring):
|
||||||
|
chars = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
|
||||||
|
int_val = int(hexastring, 16)
|
||||||
|
encoded = encode58('', int_val, chars)
|
||||||
|
return encoded
|
||||||
|
|
||||||
|
|
||||||
|
def encode58(string, int_val, chars):
|
||||||
|
if int_val == 0:
|
||||||
|
return string
|
||||||
|
else:
|
||||||
|
(new_val, rem) = divmod(int_val, 58)
|
||||||
|
new_string = chars[rem] + string
|
||||||
|
return encode58(new_string, new_val, chars)
|
||||||
|
|
||||||
|
|
||||||
|
def get_checksum_from_version(version):
|
||||||
|
return sha_digest(sha_digest(version))[0:8]
|
||||||
|
|
||||||
|
|
||||||
|
def get_version_from_compressed_key(key):
|
||||||
|
sh2 = sha_digest(key)
|
||||||
|
rphash = hashlib.new('ripemd160')
|
||||||
|
rphash.update(binascii.unhexlify(sh2))
|
||||||
|
rp1 = rphash.hexdigest()
|
||||||
|
return '0F02' + rp1
|
||||||
|
|
||||||
|
|
||||||
|
def sha_digest(hexastring):
|
||||||
|
return hashlib.sha256(binascii.unhexlify(hexastring)).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def compress_key(bts):
|
||||||
|
intval = int(bts, 16)
|
||||||
|
prefix = find_prefix(intval)
|
||||||
|
return prefix + bts[0:64].decode('utf-8')
|
||||||
|
|
||||||
|
|
||||||
|
def find_prefix(intval):
|
||||||
|
if intval % 2 == 0:
|
||||||
|
prefix = '02'
|
||||||
|
else:
|
||||||
|
prefix = '03'
|
||||||
|
return prefix
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from odoo import _, api, fields, models
|
from odoo import _, api, fields, models
|
||||||
from btcpay import BTCPayClient
|
from .libs.client import BTCPayClient
|
||||||
from btcpay import crypto
|
from .libs import crypto
|
||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@ class PaymentProvider(models.Model):
|
|||||||
code = fields.Selection(
|
code = fields.Selection(
|
||||||
selection_add=[('btcpay', "BTCPay")], ondelete={'btcpay': 'set default'})
|
selection_add=[('btcpay', "BTCPay")], ondelete={'btcpay': 'set default'})
|
||||||
|
|
||||||
btcpay_location = fields.Char(string='Location', size=64, default='https://btcpay.evolus.net')
|
btcpay_location = fields.Char(string='Location', size=64, default='https://testnet.demo.btcpayserver.org')
|
||||||
btcpay_confirmationURL = fields.Char(string='Confirmation URL', help='Confirmation URL to return after Btcpay payment', default='http://yourdomain/shop/confirmation')
|
btcpay_confirmationURL = fields.Char(string='Confirmation URL', help='Confirmation URL to return after Btcpay payment', default='http://yourdomain/shop/confirmation')
|
||||||
|
|
||||||
btcpay_token = fields.Char(string='Token', help='Access Token to BTCPay')
|
btcpay_token = fields.Char(string='Token', help='Access Token to BTCPay')
|
||||||
@@ -22,9 +22,8 @@ class PaymentProvider(models.Model):
|
|||||||
btcpay_pairingCode = fields.Char(string='Pairing Code', help='Create paring Code in your BTCPay server and put here')
|
btcpay_pairingCode = fields.Char(string='Pairing Code', help='Create paring Code in your BTCPay server and put here')
|
||||||
|
|
||||||
def create(self, values_list):
|
def create(self, values_list):
|
||||||
|
|
||||||
if self.code == 'btcpay':
|
if self.code == 'btcpay':
|
||||||
values_list['btcpay_privateKey'] = crypto.generate_privkey()
|
values_list['btcpay_privateKey'] = crypto.generate_privakey()
|
||||||
|
|
||||||
return super(PaymentProvider, self).create(values_list)
|
return super(PaymentProvider, self).create(values_list)
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ from odoo import _, api, fields, models
|
|||||||
from odoo.exceptions import ValidationError
|
from odoo.exceptions import ValidationError
|
||||||
|
|
||||||
from odoo.addons.payment import utils as payment_utils
|
from odoo.addons.payment import utils as payment_utils
|
||||||
from ..controllers.main import BTCPayController
|
|
||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -18,9 +17,11 @@ class PaymentTransaction(models.Model):
|
|||||||
btcpay_txid = fields.Char("Transaction Id")
|
btcpay_txid = fields.Char("Transaction Id")
|
||||||
btcpay_status = fields.Char("Transaction Status")
|
btcpay_status = fields.Char("Transaction Status")
|
||||||
api_url = '/btcpay/checkout'
|
api_url = '/btcpay/checkout'
|
||||||
|
checkout_url = '/btcpay/checkout'
|
||||||
|
notify_url = 'payment/btcpay/ipn'
|
||||||
|
|
||||||
def _get_specific_rendering_values(self, processing_values):
|
def _get_specific_rendering_values(self, processing_values):
|
||||||
""" Override of payment to return Paypal-specific rendering values.
|
""" Override of payment to return Specific rendering values.
|
||||||
|
|
||||||
Note: self.ensure_one() from `_get_processing_values`
|
Note: self.ensure_one() from `_get_processing_values`
|
||||||
|
|
||||||
@@ -35,7 +36,7 @@ class PaymentTransaction(models.Model):
|
|||||||
return res
|
return res
|
||||||
|
|
||||||
base_url = self.provider_id.get_base_url()
|
base_url = self.provider_id.get_base_url()
|
||||||
_logger.info('Hola! API URL: %s', base_url)
|
_logger.info('Hola! API URL: %s', processing_values)
|
||||||
partner_first_name, partner_last_name = payment_utils.split_partner_name(self.partner_name)
|
partner_first_name, partner_last_name = payment_utils.split_partner_name(self.partner_name)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -52,8 +53,8 @@ class PaymentTransaction(models.Model):
|
|||||||
'lc': self.partner_lang,
|
'lc': self.partner_lang,
|
||||||
'state': self.partner_state_id.name,
|
'state': self.partner_state_id.name,
|
||||||
'zip_code': self.partner_zip,
|
'zip_code': self.partner_zip,
|
||||||
'api_url': BTCPayController._checkout_url,
|
'api_url': self.checkout_url,
|
||||||
'notify_url': base_url + BTCPayController._notify_url,
|
'notify_url': base_url + self.notify_url,
|
||||||
}
|
}
|
||||||
|
|
||||||
def _get_tx_from_notification_data(self, provider_code, notification_data):
|
def _get_tx_from_notification_data(self, provider_code, notification_data):
|
||||||
|
|||||||
@@ -24,8 +24,6 @@
|
|||||||
<div class="oe_span6">
|
<div class="oe_span6">
|
||||||
<p class='oe_mt32'>
|
<p class='oe_mt32'>
|
||||||
<ul>
|
<ul>
|
||||||
<li><strong>IMPORTANT:</strong> You must install btcpay library before install this module (pip3 install btcpay-python)</li>
|
|
||||||
https://stackoverflow.com/questions/72409563/unsupported-hash-type-ripemd160-with-hashlib-in-python
|
|
||||||
<li>Install BTCPay Module -> Website -> eCommerce -> Payment Acquirers -> BTCPay</li>
|
<li>Install BTCPay Module -> Website -> eCommerce -> Payment Acquirers -> BTCPay</li>
|
||||||
<li>Put your facace. Best option is 'merchant'.</li>
|
<li>Put your facace. Best option is 'merchant'.</li>
|
||||||
<li>Put the location as test or live url. Test example url: https://testnet.demo.btcpayserver.org</li>
|
<li>Put the location as test or live url. Test example url: https://testnet.demo.btcpayserver.org</li>
|
||||||
|
|||||||
Reference in New Issue
Block a user