Rename the module for publish on odoo marketplace. (#12)
* Rename the module for publish on odoo marketplace. * More renaming needed.
This commit is contained in:
@@ -0,0 +1 @@
|
||||
from . import payment_provider, payment_transaction
|
||||
@@ -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
|
||||
@@ -0,0 +1,51 @@
|
||||
import logging
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from .libs.client import BTCPayClient
|
||||
from .libs import crypto
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PaymentProvider(models.Model):
|
||||
_inherit = 'payment.provider'
|
||||
|
||||
code = fields.Selection(
|
||||
selection_add=[('btcpayserver', "BTCPay")], ondelete={'btcpayserver': 'set default'})
|
||||
|
||||
btcpay_location = fields.Char(string='BTCPay Server URL', size=64, help='URL where your BTCPay Server instance is reachable (where you log into your BTCPay Server).', default='https://testnet.demo.btcpayserver.org')
|
||||
btcpay_pairingCode = fields.Char(string='Pairing Code', help='Create paring Code in your BTCPay server and put here')
|
||||
|
||||
btcpay_token = fields.Char(string='Token', help='Access Token to BTCPay. Leave empty, will be autogenerated during pairing.')
|
||||
btcpay_privateKey = fields.Text(string='Private Key', help='Private Key for BTCPay Client. Leave empty, will be autogenerated during pairing.')
|
||||
btcpay_facade = fields.Char(string='Facade', help='Token facade type: merchant/pos/payroll. Keep merchant', default='merchant')
|
||||
|
||||
def create(self, values_list):
|
||||
if self.code == 'btcpayserver':
|
||||
values_list['btcpay_privateKey'] = crypto.generate_privakey()
|
||||
|
||||
return super(PaymentProvider, self).create(values_list)
|
||||
|
||||
@api.onchange('btcpay_pairingCode')
|
||||
def _onchange_pairingCode(self):
|
||||
if not self.btcpay_token and self.code == 'btcpayserver' and not self.btcpay_pairingCode == '':
|
||||
#_logger.info("ONCHANGE PAIRING CODE***SELF: %s %s %s", self.btcpay_location, self.btcpay_privateKey, self.btcpay_pairingCode)
|
||||
self.btcpay_privateKey = crypto.generate_privkey()
|
||||
client = BTCPayClient(host=self.btcpay_location, pem=self.btcpay_privateKey)
|
||||
token = client.pair_client(self.btcpay_pairingCode)
|
||||
self.btcpay_token = token.get(self.btcpay_facade)
|
||||
|
||||
@api.onchange('btcpay_token')
|
||||
def _onchange_token(self):
|
||||
if self.code == 'btcpayserver':
|
||||
self.btcpay_pairingCode = ''
|
||||
#_logger.info("ONCHANGE TOKEN")
|
||||
|
||||
|
||||
@api.onchange('btcpay_location')
|
||||
def _onchange_location(self):
|
||||
if self.code == 'btcpayserver':
|
||||
self.btcpay_token = ''
|
||||
#_logger.info("ONCHANGE LOCATION ***SELF: %s %s %s", self.btcpay_location, self.btcpay_privateKey, self.btcpay_pairingCode)
|
||||
self.btcpay_privateKey = ''
|
||||
self.btcpay_pairingCode = ''
|
||||
@@ -0,0 +1,135 @@
|
||||
import logging
|
||||
import pprint
|
||||
from werkzeug import urls
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
from odoo.addons.payment import utils as payment_utils
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PaymentTransaction(models.Model):
|
||||
_inherit = 'payment.transaction'
|
||||
|
||||
btcpay_invoiceId = fields.Char("Invoice Id")
|
||||
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 Specific rendering values.
|
||||
|
||||
Note: self.ensure_one() from `_get_processing_values`
|
||||
|
||||
:param dict processing_values: The generic and specific processing values of the transaction
|
||||
:return: The dict of provider-specific processing values
|
||||
:rtype: dict
|
||||
"""
|
||||
|
||||
res = super()._get_specific_rendering_values(processing_values)
|
||||
|
||||
if self.provider_code != 'btcpayserver':
|
||||
return res
|
||||
|
||||
base_url = self.provider_id.get_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 {
|
||||
'address1': self.partner_address,
|
||||
'amount': self.amount,
|
||||
'city': self.partner_city,
|
||||
'country': self.partner_country_id.code,
|
||||
'currency_code': self.currency_id.name,
|
||||
'email': self.partner_email,
|
||||
'first_name': partner_first_name,
|
||||
'item_name': f"{self.company_id.name}: {self.reference}",
|
||||
'item_number': self.reference,
|
||||
'last_name': partner_last_name,
|
||||
'lc': self.partner_lang,
|
||||
'state': self.partner_state_id.name,
|
||||
'zip_code': self.partner_zip,
|
||||
'api_url': self.checkout_url,
|
||||
'notify_url': base_url + self.notify_url,
|
||||
}
|
||||
|
||||
def _get_tx_from_notification_data(self, provider_code, notification_data):
|
||||
""" Override of payment to find the transaction based on BTCPay data.
|
||||
|
||||
:param str provider_code: The code of the provider that handled the transaction
|
||||
:param dict notification_data: The notification data sent by the provider
|
||||
:return: The transaction if found
|
||||
:rtype: recordset of `payment.transaction`
|
||||
:raise: ValidationError if the data match no transaction
|
||||
"""
|
||||
tx = super()._get_tx_from_notification_data(provider_code, notification_data)
|
||||
_logger.info('GET TX FROM NOTIFICATION Notification_data %s', pprint.pformat(notification_data))
|
||||
if provider_code != 'btcpayserver' or len(tx) == 1:
|
||||
return tx
|
||||
|
||||
reference = notification_data.get('reference')
|
||||
tx = self.search([('reference', '=', reference), ('provider_code', '=', 'btcpayserver')])
|
||||
if not tx:
|
||||
raise ValidationError(
|
||||
"BTCPay: " + _("No transaction found matching reference %s.", reference)
|
||||
)
|
||||
return tx
|
||||
|
||||
def _handle_notification_data(self, provider_code, notification_data):
|
||||
""" Match the transaction with the notification data, update its state and return it.
|
||||
|
||||
:param str provider_code: The code of the provider handling the transaction.
|
||||
:param dict notification_data: The notification data sent by the provider.
|
||||
:return: The transaction.
|
||||
:rtype: recordset of `payment.transaction`
|
||||
"""
|
||||
tx = self._get_tx_from_notification_data(provider_code, notification_data)
|
||||
tx._process_notification_data(notification_data)
|
||||
tx._execute_callback()
|
||||
return tx
|
||||
|
||||
def _process_notification_data(self, notification_data):
|
||||
""" Override of payment to process the transaction based on BTCPay data.
|
||||
|
||||
Note: self.ensure_one()
|
||||
|
||||
:param dict notification_data: The notification data sent by the provider
|
||||
:return: None
|
||||
:raise: ValidationError if inconsistent data were received
|
||||
"""
|
||||
super()._process_notification_data(notification_data)
|
||||
if self.provider_code != 'btcpayserver':
|
||||
return
|
||||
|
||||
_logger.info("_process_notification_data %s", pprint.pformat(notification_data))
|
||||
txn_id = notification_data.get('reference')
|
||||
if not all(txn_id):
|
||||
raise ValidationError(
|
||||
"BTCPay: " + _("Missing value for txn_id (%(txn_id)s)).", txn_id=txn_id))
|
||||
|
||||
self.provider_reference = txn_id
|
||||
self.btcpay_txid = notification_data.get('txid')
|
||||
self.btcpay_status = notification_data.get('status')
|
||||
|
||||
if self.btcpay_status in ['paid','processing']:
|
||||
self._set_pending(state_message=notification_data.get('pending_reason'))
|
||||
elif self.btcpay_status in ['confirmed', 'complete']:
|
||||
self._set_done()
|
||||
confirmed_orders = self._check_amount_and_confirm_order()
|
||||
confirmed_orders._send_order_confirmation_mail()
|
||||
elif self.btcpay_status in ['new']:
|
||||
self.btcpay_invoiceId = notification_data.get('invoiceID')
|
||||
elif self.btcpay_status in ['cancel','cancelled']:
|
||||
self._set_canceled()
|
||||
elif self.btcpay_status in ['invalid']:
|
||||
_logger.info(
|
||||
"received data with invalid payment status (%s) for transaction with reference %s",
|
||||
self.btcpay_status, self.reference
|
||||
)
|
||||
self._set_error(
|
||||
"BTCPay: " + _("Received data with invalid payment status: %s", self.btcpay_status)
|
||||
)
|
||||
Reference in New Issue
Block a user