├── .gitignore ├── LICENSE ├── README.md ├── btcpay ├── __init__.py ├── client.py └── crypto.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | build/ 3 | *.egg-info/ 4 | __pycache__ 5 | jeff.py 6 | */venv/ 7 | data 8 | venv/ 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2017 Joe Black 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a 4 | copy of this software and associated documentation files (the "Software"), 5 | to deal in the Software without restriction, including without limitation 6 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | and/or sell copies of the Software, and to permit persons to whom the 8 | Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 16 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # btcpay-python 2 | 3 | ## Install 4 | ```shell 5 | pip3 install btcpay-python 6 | ``` 7 | If you were a user of the prior unofficial client library for Python, you would need to uninstall it first: 8 | ```shell 9 | pip3 uninstall btcpay 10 | pip3 install btcpay-python 11 | ``` 12 | This library is fully backward compatible with the prior unofficial library; no code changes are needed. 13 | 14 | ## Pairing to your server: 15 | To connect your website with your BTCPay server, you must first pair your application to BTCPay. To do this you will need to generate a pairing code as follows: 16 | 17 | 1. On your BTCPay server, browse to Stores > Store settings > Access tokens > Create new token 18 | 2. Fill in the form: 19 | ``` 20 | Label: 21 | Public key: leave blank 22 | ``` 23 | 3. Click save and then copy the 7 digit pairing code from the success page 24 | 25 | After you have the pairing code, you are ready to use the client library to create a client object. 26 | 27 | ## The "easy method" to create a new BTCPay client 28 | Use the pairing code obtained above as follows: 29 | ```python 30 | from btcpay import BTCPayClient 31 | 32 | client = BTCPayClient.create_client(host='https://btcpay.example.com', code=) 33 | ``` 34 | 35 | **NOTE THAT PAIRING CODES WORK ONLY ONCE.** This is because you only ever need to pair once. See the section later in this document regarding how to save a client object for later after pairing. 36 | 37 | ## Uses for the client object you just created above 38 | 39 | You'll probably only ever need the `create_invoice` and `get_invoice` methods, but the client object also has other methods, such as those for getting rate information. 40 | 41 | **Be sure to fully set up your store (including a derivation scheme in your store settings) otherwise these methods will fail.** 42 | 43 | The `get_invoice` method is very important. When BTCPay sends a payment notification (described [here in Bitpay's API docs](https://bitpay.com/docs/create-invoice)), it is unsigned and insecure. Being unsigned and insecure is necessary to maintain compatibility with software originally designed for Bitpay. You therefore cannot rely upon the data transmitted in the payment notification. 44 | 45 | Instead, take the `invoiceId` from the payment notification, and use it to securely fetch the paid invoice data from BTCPay using `get_invoice`. 46 | 47 | ### Create invoice 48 | See the Bitpay API documentation for a full listing of key-value pairs that can be passed to invoice creation: https://bitpay.com/api#resource-Invoices 49 | ```python 50 | new_invoice = client.create_invoice({"price": 20, "currency": "USD"}) 51 | ``` 52 | 53 | ### Get invoice 54 | ```python 55 | fetched_invoice = client.get_invoice() 56 | ``` 57 | The `fetched_invoice` above will be a dictionary of all invoice data from the Bitpay API. For instance, you can check the payment status with `fetched_invoice['status']`. 58 | 59 | This `get_invoice` method is very important. When BTCPay sends a payment notification (described [here in Bitpay's API docs](https://bitpay.com/docs/create-invoice)), it is unsigned and insecure. Being unsigned and insecure is necessary to maintain compatibility with software originally designed for Bitpay. You therefore cannot rely upon the data transmitted in the payment notification. 60 | 61 | Instead, take the `invoiceId` from the payment notification, and use it to securely fetch the paid invoice data from BTCPay using the `get_invoice` method above. 62 | 63 | ### Get a list of invoices matching certain parameters 64 | 65 | ```python 66 | invoice_list = client.get_invoices(status='confirmed') 67 | ``` 68 | You can search by `status`, `order_id`, `date_start`, `date_end`, etc. See the method for a full list of parameters that can be passed to the method. The method returns a list of dictionaries, with each dictionary containing the invoice data for each matching invoice. 69 | 70 | ### Get rates 71 | ```python 72 | client.get_rates() 73 | ``` 74 | This will fail if you have not set up default currency pairs in your store settings within BTCPay. 75 | 76 | ### Get specific rate 77 | ```python 78 | client.get_rate('USD') 79 | ``` 80 | 81 | ## Storing the client object for later 82 | 83 | After you create a client object, you must save the object to persistent storage if you wish for the pairing to persist beyond the limited time your code is in memory. 84 | 85 | You do not need to store any tokens or private keys. Simply `pickle` the client object and save it to your persistent storage method (Redis, SQLAlchemy/SQLite/PostgreSql, MongoDB, etc). I suggest not using `shelve` or a similar static file for storage, as concurrent access could corrupt the static file. 86 | 87 | When you need to call a method on the client object later, pull the client object from persistent storage, unpickle it, and perform any of the methods above on it which you may need. 88 | 89 | Note that the pairing code obtained from BTCPay may only be used once to create one client object. It is then forever burned. You may not recreate a client object by re-using the pairing code. For later use, the client object must either be retrieved from persistent storage (the easy way) or recreated using the pem and merchant token (the hard way). 90 | 91 | ## Creating a client the manual way (not necessary if you used the 'easy' method above) 92 | 93 | If you prefer to create the client object manually (as was the only way in the prior unofficial library), you can do so as follows. This is unnecessary for most developers and is preserved primarily to maintain backward compatibility with both the prior unofficial library and Bitpay. 94 | 95 | * Generate and save private key: 96 | ```python 97 | import btcpay.crypto 98 | privkey = btcpay.crypto.generate_privkey() 99 | ``` 100 | * Create client: 101 | ```python 102 | from btcpay import BTCPayClient 103 | client = BTCPayClient(host='http://hostname', pem=privkey) 104 | ``` 105 | * On BTCPay server > shop > access tokens > create new token, copy pairing code: 106 | * Pair client to server and save returned token: 107 | ```python 108 | client.pair_client() 109 | >>> {'merchant': "xdr9vw3v5wc0w90859v45"} 110 | ``` 111 | * Recreate client: 112 | ```python 113 | client = BTCPayClient( 114 | host='http://hostname', 115 | pem=privkey, 116 | tokens={'merchant': "xdr9vw3v5wc0w90859v45"} 117 | ) 118 | ``` 119 | -------------------------------------------------------------------------------- /btcpay/__init__.py: -------------------------------------------------------------------------------- 1 | from . import crypto, client 2 | from .client import BTCPayClient 3 | -------------------------------------------------------------------------------- /btcpay/client.py: -------------------------------------------------------------------------------- 1 | """btcpay.client 2 | 3 | BTCPay API Client. 4 | """ 5 | 6 | import re 7 | import json 8 | from urllib.parse import urlencode 9 | 10 | import requests 11 | from requests.exceptions import HTTPError 12 | 13 | from . import crypto 14 | 15 | 16 | class BTCPayClient: 17 | def __init__(self, host, pem, insecure=False, tokens=None): 18 | self.host = host 19 | self.verify = not(insecure) 20 | self.pem = pem 21 | self.tokens = tokens or dict() 22 | self.client_id = crypto.get_sin_from_pem(pem) 23 | self.user_agent = 'btcpay-python' 24 | self.s = requests.Session() 25 | self.s.verify = self.verify 26 | self.s.headers.update( 27 | {'Content-Type': 'application/json', 28 | 'accept': 'application/json', 29 | 'X-accept-version': '2.0.0'}) 30 | 31 | def _create_signed_headers(self, uri, payload): 32 | return { 33 | "X-Identity": crypto.get_compressed_public_key_from_pem(self.pem), 34 | "X-Signature": crypto.sign(uri + payload, self.pem) 35 | } 36 | 37 | def _signed_get_request(self, path, params=None, token=None): 38 | token = token or list(self.tokens.values())[0] 39 | params = params or dict() 40 | params['token'] = token 41 | 42 | uri = self.host + path 43 | payload = '?' + urlencode(params) 44 | headers = self._create_signed_headers(uri, payload) 45 | r = self.s.get(uri, params=params, headers=headers) 46 | r.raise_for_status() 47 | return r.json()['data'] 48 | 49 | def _signed_post_request(self, path, payload, token=None): 50 | token = token or list(self.tokens.values())[0] 51 | uri = self.host + path 52 | payload['token'] = token 53 | payload = json.dumps(payload) 54 | headers = self._create_signed_headers(uri, payload) 55 | r = self.s.post(uri, headers=headers, data=payload) 56 | if not r.ok: 57 | if 400 <= r.status_code < 500: 58 | http_error_msg = u'%s Client Error: \ 59 | %s for url: %s | body: %s' % ( 60 | r.status_code, 61 | r.reason, 62 | r.url, 63 | r.text 64 | ) 65 | elif 500 <= r.status_code < 600: 66 | http_error_msg = u'%s Server Error: \ 67 | %s for url: %s | body: %s' % ( 68 | r.status_code, 69 | r.reason, 70 | r.url, 71 | r.text 72 | ) 73 | if http_error_msg: 74 | raise HTTPError(http_error_msg, response=r) 75 | return r.json()['data'] 76 | 77 | def _unsigned_request(self, path, payload=None): 78 | uri = self.host + path 79 | if payload: 80 | payload = json.dumps(payload) 81 | r = self.s.post(uri, data=payload) 82 | else: 83 | r = self.s.get(uri) 84 | r.raise_for_status() 85 | return r.json()['data'] 86 | 87 | def get_rates(self, crypto='BTC', store_id=None): 88 | params = dict( 89 | cryptoCode=crypto 90 | ) 91 | if store_id: 92 | params['storeID'] = store_id 93 | return self._signed_get_request('/rates/', params=params) 94 | 95 | def get_rate(self, currency, crypto='BTC', store_id=None): 96 | rates = self.get_rates(crypto=crypto, store_id=store_id) 97 | rate = [rate for rate in rates if rate['code'] == currency.upper()][0] 98 | return rate['rate'] 99 | 100 | def create_invoice(self, payload, token=None): 101 | try: 102 | float(payload['price']) 103 | except ValueError as e: 104 | raise ValueError('Price must be a float') from e 105 | return self._signed_post_request('/invoices/', payload, token=token) 106 | 107 | def get_invoice(self, invoice_id, token=None): 108 | return self._signed_get_request('/invoices/' + invoice_id, token=token) 109 | 110 | def get_invoices(self, status=None, order_id=None, item_code=None, date_start=None, date_end=None, limit=None, offset=None, token=None): 111 | params = dict() 112 | if status is not None: 113 | params['status'] = status 114 | if order_id is not None: 115 | params['orderId'] = order_id 116 | if item_code is not None: 117 | params['itemCode'] = item_code 118 | if date_start is not None: 119 | params['dateStart'] = date_start 120 | if date_end is not None: 121 | params['dateEnd'] = date_end 122 | if limit is not None: 123 | params['limit'] = limit 124 | if offset is not None: 125 | params['offset'] = offset 126 | return self._signed_get_request('/invoices', params=params, token=token) 127 | 128 | def pair_client(self, code): 129 | if re.match(r'^\w{7,7}$', code) is None: 130 | raise ValueError("pairing code is not legal") 131 | payload = {'id': self.client_id, 'pairingCode': code} 132 | data = self._unsigned_request('/tokens', payload) 133 | data = data[0] 134 | return { 135 | data['facade']: data['token'] 136 | } 137 | 138 | @classmethod 139 | def create_client(cls, code, host): 140 | pem = crypto.generate_privkey() 141 | client = BTCPayClient(host=host, pem=pem) 142 | token = client.pair_client(code) 143 | return BTCPayClient(host=host, pem=pem, tokens=token) 144 | 145 | @classmethod 146 | def create_tor_client(cls, code, host, proxy='socks5://127.0.0.1:9050'): 147 | """ Useful for .onion services, the `proxy` input assumes the default 148 | proxy header 149 | """ 150 | pem = crypto.generate_privkey() 151 | client = BTCPayClient(host=host, pem=pem) 152 | client.s.proxies = { 153 | 'http': proxy, 154 | 'https': proxy} 155 | token = client.pair_client(code) 156 | final_client = BTCPayClient(host=host, pem=pem, tokens=token) 157 | final_client.s.proxies = { 158 | 'http': proxy, 159 | 'https': proxy} 160 | return final_client 161 | 162 | 163 | def __repr__(self): 164 | return '{}({})'.format( 165 | type(self).__name__, 166 | self.host 167 | ) 168 | -------------------------------------------------------------------------------- /btcpay/crypto.py: -------------------------------------------------------------------------------- 1 | """btcpay.crypto 2 | 3 | These are various crytography related utility functions borrowed from: 4 | bitpay-python: https://github.com/bitpay/bitpay-python 5 | """ 6 | 7 | import binascii 8 | import hashlib 9 | 10 | from ecdsa import SigningKey, SECP256k1, VerifyingKey 11 | from ecdsa import util as ecdsaUtil 12 | 13 | 14 | def generate_privkey(): 15 | sk = SigningKey.generate(curve=SECP256k1) 16 | pem = sk.to_pem() 17 | pem = pem.decode('utf-8') 18 | return pem 19 | 20 | 21 | def get_sin_from_pem(pem): 22 | public_key = get_compressed_public_key_from_pem(pem) 23 | version = get_version_from_compressed_key(public_key) 24 | checksum = get_checksum_from_version(version) 25 | return base58encode(version + checksum) 26 | 27 | 28 | def get_compressed_public_key_from_pem(pem): 29 | vks = SigningKey.from_pem(pem).get_verifying_key().to_string() 30 | bts = binascii.hexlify(vks) 31 | compressed = compress_key(bts) 32 | return compressed 33 | 34 | 35 | def sign(message, pem): 36 | message = message.encode() 37 | sk = SigningKey.from_pem(pem) 38 | signed = sk.sign(message, hashfunc=hashlib.sha256, 39 | sigencode=ecdsaUtil.sigencode_der) 40 | return binascii.hexlify(signed).decode() 41 | 42 | 43 | def base58encode(hexastring): 44 | chars = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' 45 | int_val = int(hexastring, 16) 46 | encoded = encode58('', int_val, chars) 47 | return encoded 48 | 49 | 50 | def encode58(string, int_val, chars): 51 | if int_val == 0: 52 | return string 53 | else: 54 | (new_val, rem) = divmod(int_val, 58) 55 | new_string = chars[rem] + string 56 | return encode58(new_string, new_val, chars) 57 | 58 | 59 | def get_checksum_from_version(version): 60 | return sha_digest(sha_digest(version))[0:8] 61 | 62 | 63 | def get_version_from_compressed_key(key): 64 | sh2 = sha_digest(key) 65 | rphash = hashlib.new('ripemd160') 66 | rphash.update(binascii.unhexlify(sh2)) 67 | rp1 = rphash.hexdigest() 68 | return '0F02' + rp1 69 | 70 | 71 | def sha_digest(hexastring): 72 | return hashlib.sha256(binascii.unhexlify(hexastring)).hexdigest() 73 | 74 | 75 | def compress_key(bts): 76 | intval = int(bts, 16) 77 | prefix = find_prefix(intval) 78 | return prefix + bts[0:64].decode('utf-8') 79 | 80 | 81 | def find_prefix(intval): 82 | if intval % 2 == 0: 83 | prefix = '02' 84 | else: 85 | prefix = '03' 86 | return prefix 87 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | 4 | setup( 5 | name="btcpay-python", 6 | packages=find_packages(), 7 | version="1.3.0", 8 | description="Accept bitcoin with BTCPay", 9 | url="https://github.com/btcpayserver/btcpay-python", 10 | download_url="https://github.com/btcpayserver/btcpay-python/archive/v1.3.0.tar.gz", 11 | license='MIT', 12 | keywords=["bitcoin", "payments", "crypto"], 13 | install_requires=[ 14 | "requests", 15 | "ecdsa" 16 | ], 17 | package_data={'': ['LICENSE']}, 18 | include_package_data=True, 19 | classifiers=[ 20 | "Programming Language :: Python :: 3.6", 21 | "Programming Language :: Python :: 3 :: Only", 22 | "Development Status :: 5 - Production/Stable", 23 | "Environment :: Web Environment", 24 | "Intended Audience :: Developers", 25 | "License :: OSI Approved :: MIT License", 26 | "Operating System :: OS Independent", 27 | "Topic :: Software Development :: Libraries :: Python Modules", 28 | "Topic :: Office/Business :: Financial" 29 | ] 30 | ) 31 | --------------------------------------------------------------------------------