From 7930e3eb30c1940be78c67fce921bc4280cfb211 Mon Sep 17 00:00:00 2001 From: vandekul Date: Mon, 13 Nov 2023 14:40:43 +0100 Subject: [PATCH] BTCPay Library Included --- payment_btcpay/__manifest__.py | 3 +- payment_btcpay/controllers/main.py | 3 +- payment_btcpay/models/libs/__init__.py | 0 payment_btcpay/models/libs/client.py | 167 +++++++++++++++++++ payment_btcpay/models/libs/crypto.py | 86 ++++++++++ payment_btcpay/models/payment_provider.py | 9 +- payment_btcpay/models/payment_transaction.py | 11 +- payment_btcpay/static/description/index.html | 2 - 8 files changed, 265 insertions(+), 16 deletions(-) create mode 100644 payment_btcpay/models/libs/__init__.py create mode 100644 payment_btcpay/models/libs/client.py create mode 100644 payment_btcpay/models/libs/crypto.py diff --git a/payment_btcpay/__manifest__.py b/payment_btcpay/__manifest__.py index 1045f5a..3572a94 100644 --- a/payment_btcpay/__manifest__.py +++ b/payment_btcpay/__manifest__.py @@ -24,7 +24,7 @@ 'summary': 'This module integrates BTCPAY - pay with Bitcoin - with Odoo v16.0', 'author': 'Vandekul', - 'website': 'https://github.com/vandekul', + 'website': 'https://github.com/btcpayserver/odoo', 'category': 'Accounting/Payment Providers', 'version': '16.0', 'license': 'GPL-3', @@ -37,7 +37,6 @@ 'views/payment_btcpay_templates.xml', 'views/payment_provider_views.xml', 'views/payment_transaction_views.xml', - 'data/payment_provider_data.xml', ], diff --git a/payment_btcpay/controllers/main.py b/payment_btcpay/controllers/main.py index b31000a..d0cbb1d 100644 --- a/payment_btcpay/controllers/main.py +++ b/payment_btcpay/controllers/main.py @@ -33,7 +33,7 @@ from odoo.http import request from odoo.tools import html_escape import json -from btcpay import BTCPayClient +from ..models.libs.client import BTCPayClient _logger = logging.getLogger(__name__) @@ -71,7 +71,6 @@ class BTCPayController(http.Controller): @http.route('/payment/btcpay/ipn', type='json', auth='public', csrf=False) def btcpay_ipn(self, **post): - """ BTCPay IPN. """ _logger.info('BTCPAY IPN RECEIVED... ') data = json.loads(request.httprequest.data) diff --git a/payment_btcpay/models/libs/__init__.py b/payment_btcpay/models/libs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/payment_btcpay/models/libs/client.py b/payment_btcpay/models/libs/client.py new file mode 100644 index 0000000..669367e --- /dev/null +++ b/payment_btcpay/models/libs/client.py @@ -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 + ) diff --git a/payment_btcpay/models/libs/crypto.py b/payment_btcpay/models/libs/crypto.py new file mode 100644 index 0000000..e4ef6cf --- /dev/null +++ b/payment_btcpay/models/libs/crypto.py @@ -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 diff --git a/payment_btcpay/models/payment_provider.py b/payment_btcpay/models/payment_provider.py index aaefbc5..1c07d2e 100644 --- a/payment_btcpay/models/payment_provider.py +++ b/payment_btcpay/models/payment_provider.py @@ -1,8 +1,8 @@ import logging from odoo import _, api, fields, models -from btcpay import BTCPayClient -from btcpay import crypto +from .libs.client import BTCPayClient +from .libs import crypto _logger = logging.getLogger(__name__) @@ -13,7 +13,7 @@ class PaymentProvider(models.Model): code = fields.Selection( 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_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') def create(self, values_list): - 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) diff --git a/payment_btcpay/models/payment_transaction.py b/payment_btcpay/models/payment_transaction.py index 71ce4d6..76aea38 100644 --- a/payment_btcpay/models/payment_transaction.py +++ b/payment_btcpay/models/payment_transaction.py @@ -6,7 +6,6 @@ from odoo import _, api, fields, models from odoo.exceptions import ValidationError from odoo.addons.payment import utils as payment_utils -from ..controllers.main import BTCPayController _logger = logging.getLogger(__name__) @@ -18,9 +17,11 @@ class PaymentTransaction(models.Model): btcpay_txid = fields.Char("Transaction Id") btcpay_status = fields.Char("Transaction Status") api_url = '/btcpay/checkout' + checkout_url = '/btcpay/checkout' + notify_url = 'payment/btcpay/ipn' 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` @@ -35,7 +36,7 @@ class PaymentTransaction(models.Model): return res 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) return { @@ -52,8 +53,8 @@ class PaymentTransaction(models.Model): 'lc': self.partner_lang, 'state': self.partner_state_id.name, 'zip_code': self.partner_zip, - 'api_url': BTCPayController._checkout_url, - 'notify_url': base_url + BTCPayController._notify_url, + 'api_url': self.checkout_url, + 'notify_url': base_url + self.notify_url, } def _get_tx_from_notification_data(self, provider_code, notification_data): diff --git a/payment_btcpay/static/description/index.html b/payment_btcpay/static/description/index.html index e5171fd..5a91406 100644 --- a/payment_btcpay/static/description/index.html +++ b/payment_btcpay/static/description/index.html @@ -24,8 +24,6 @@