├── .gitignore ├── .noserc ├── .travis.yml ├── CHANGELOG.md ├── GUIDE.md ├── LICENSE.txt ├── MANIFEST ├── README.md ├── bin ├── activate ├── activate.csh ├── activate.fish ├── activate_this.py ├── easy_install ├── easy_install-3.4 ├── pip ├── pip3 ├── pip3.4 ├── python ├── python3 └── python3.4 ├── bitpay ├── __init__.py ├── client.py ├── exceptions.py └── key_utils.py ├── examples ├── test_merchant_facade.py └── test_payouts.py ├── features ├── creating_invoices.feature ├── pairing.feature ├── retrieving_invoices.feature └── steps │ └── pair_steps.py ├── include └── python3.4m ├── requirements.txt ├── setup.cfg ├── setup.py ├── tasks └── set_constants.sh └── test ├── client_test.py └── key_utils_test.py /.gitignore: -------------------------------------------------------------------------------- 1 | temp/ 2 | # Byte-compiled / optimized / DLL files 3 | __pycache__/ 4 | *.py[cod] 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .cache 41 | nosetests.xml 42 | coverage.xml 43 | 44 | # Translations 45 | *.mo 46 | *.pot 47 | 48 | # Django stuff: 49 | *.log 50 | 51 | # Sphinx documentation 52 | docs/_build/ 53 | 54 | # PyBuilder 55 | target/ 56 | 57 | cover/ 58 | -------------------------------------------------------------------------------- /.noserc: -------------------------------------------------------------------------------- 1 | [nosetests] 2 | verbosity=3 3 | with-doctest=1 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | - "3.4" 5 | - "3.5" 6 | - "3.6" 7 | install: 8 | - pip install -r requirements.txt 9 | script: 10 | - nosetests --with-coverage --cover-package=bitpay --cover-html 11 | after_success: 12 | - coveralls 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. 3 | This project adheres to [Semantic Versioning](http://semver.org/). 4 | 5 | ## [2.5.1906] - 2019-06-24 6 | ### Added 7 | - Feature: Python 2.7 support 8 | 9 | ### Changed 10 | - Tests adjusted for v2.7 11 | -------------------------------------------------------------------------------- /GUIDE.md: -------------------------------------------------------------------------------- 1 | # Using the BitPay Python Client Library 2 | 3 | For detailed instructions, see https://support.bitpay.com/hc/en-us/articles/115003001203-How-do-I-configure-and-use-the-BitPay-Python-Library- 4 | 5 | 6 | ## Prerequisites 7 | You must have BitPay or test.bitpay merchant account to use this library. [Signing up for a merchant account](https://bitpay.com/start) is free. 8 | More information about setting up a test.bitpay merchant account and a testnet bitcoin wallet can be found here: https://bitpay.com/docs/testing 9 | 10 | ## Quick Start 11 | ### Installation 12 | 13 | BitPay's python library was developed in Python 3.7. The recommended method of installion is using pip. 14 | 15 | `pip3 install 'bitpay'` 16 | 17 | ### Basic Usage 18 | 19 | The bitpay library allows authenticating with BitPay, creating invoices, and retrieving invoices. 20 | 21 | #### Pairing with Bitpay.com 22 | 23 | Before pairing with BitPay.com, you'll need to log in to your BitPay account and navigate to /api-tokens. Generate a new pairing code and use it in the next step. You can try out various functions using the Python REPL. In this example, it's assumed that we are working against the bitpay test server and have generated the pairing code "abcdefg". 24 | 25 | > from bitpay.client import Client 26 | > client = Client(api_uri="https://test.bitpay.com") #if api_uri is not passed, it defaults to "https://bitpay.com" 27 | > client.pair_pos_client("abcdefg") 28 | 29 | #### To create an invoice with a paired client: 30 | 31 | Using the same web client from the last step: 32 | 33 | > client.create_invoice({"price": 20, "currency": "USD", "token": client.tokens['pos']}) 34 | 35 | That will return the invoice as JSON. Other parameters can be sent, see the [BitPay REST API documentation](https://bitpay.com/api#resource-Invoices) for details. 36 | 37 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 BitPay, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /MANIFEST: -------------------------------------------------------------------------------- 1 | # file GENERATED by distutils, do NOT edit 2 | setup.cfg 3 | setup.py 4 | bitpay/__init__.py 5 | bitpay/client.py 6 | bitpay/exceptions.py 7 | bitpay/key_utils.py 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # This repository is no longer maintained and it will be archived by the end of 2021, we are working on a complete new SDK that will be available soon [Here](https://github.com/bitpay/python-bitpay-client) 2 | -------------------------------------------------------------------------------- /bin/activate: -------------------------------------------------------------------------------- 1 | # This file must be used with "source bin/activate" *from bash* 2 | # you cannot run it directly 3 | 4 | deactivate () { 5 | unset pydoc 6 | 7 | # reset old environment variables 8 | if [ -n "$_OLD_VIRTUAL_PATH" ] ; then 9 | PATH="$_OLD_VIRTUAL_PATH" 10 | export PATH 11 | unset _OLD_VIRTUAL_PATH 12 | fi 13 | if [ -n "$_OLD_VIRTUAL_PYTHONHOME" ] ; then 14 | PYTHONHOME="$_OLD_VIRTUAL_PYTHONHOME" 15 | export PYTHONHOME 16 | unset _OLD_VIRTUAL_PYTHONHOME 17 | fi 18 | 19 | # This should detect bash and zsh, which have a hash command that must 20 | # be called to get it to forget past commands. Without forgetting 21 | # past commands the $PATH changes we made may not be respected 22 | if [ -n "$BASH" -o -n "$ZSH_VERSION" ] ; then 23 | hash -r 2>/dev/null 24 | fi 25 | 26 | if [ -n "$_OLD_VIRTUAL_PS1" ] ; then 27 | PS1="$_OLD_VIRTUAL_PS1" 28 | export PS1 29 | unset _OLD_VIRTUAL_PS1 30 | fi 31 | 32 | unset VIRTUAL_ENV 33 | if [ ! "$1" = "nondestructive" ] ; then 34 | # Self destruct! 35 | unset -f deactivate 36 | fi 37 | } 38 | 39 | # unset irrelevant variables 40 | deactivate nondestructive 41 | 42 | VIRTUAL_ENV="/Users/paul/Bitpay/bitpay-python" 43 | export VIRTUAL_ENV 44 | 45 | _OLD_VIRTUAL_PATH="$PATH" 46 | PATH="$VIRTUAL_ENV/bin:$PATH" 47 | export PATH 48 | 49 | # unset PYTHONHOME if set 50 | # this will fail if PYTHONHOME is set to the empty string (which is bad anyway) 51 | # could use `if (set -u; : $PYTHONHOME) ;` in bash 52 | if [ -n "$PYTHONHOME" ] ; then 53 | _OLD_VIRTUAL_PYTHONHOME="$PYTHONHOME" 54 | unset PYTHONHOME 55 | fi 56 | 57 | if [ -z "$VIRTUAL_ENV_DISABLE_PROMPT" ] ; then 58 | _OLD_VIRTUAL_PS1="$PS1" 59 | if [ "x" != x ] ; then 60 | PS1="$PS1" 61 | else 62 | if [ "`basename \"$VIRTUAL_ENV\"`" = "__" ] ; then 63 | # special case for Aspen magic directories 64 | # see http://www.zetadev.com/software/aspen/ 65 | PS1="[`basename \`dirname \"$VIRTUAL_ENV\"\``] $PS1" 66 | else 67 | PS1="(`basename \"$VIRTUAL_ENV\"`)$PS1" 68 | fi 69 | fi 70 | export PS1 71 | fi 72 | 73 | alias pydoc="python -m pydoc" 74 | 75 | # This should detect bash and zsh, which have a hash command that must 76 | # be called to get it to forget past commands. Without forgetting 77 | # past commands the $PATH changes we made may not be respected 78 | if [ -n "$BASH" -o -n "$ZSH_VERSION" ] ; then 79 | hash -r 2>/dev/null 80 | fi 81 | -------------------------------------------------------------------------------- /bin/activate.csh: -------------------------------------------------------------------------------- 1 | # This file must be used with "source bin/activate.csh" *from csh*. 2 | # You cannot run it directly. 3 | # Created by Davide Di Blasi . 4 | 5 | alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; test "\!:*" != "nondestructive" && unalias deactivate && unalias pydoc' 6 | 7 | # Unset irrelevant variables. 8 | deactivate nondestructive 9 | 10 | setenv VIRTUAL_ENV "/Users/paul/Bitpay/bitpay-python" 11 | 12 | set _OLD_VIRTUAL_PATH="$PATH" 13 | setenv PATH "$VIRTUAL_ENV/bin:$PATH" 14 | 15 | 16 | 17 | if ("" != "") then 18 | set env_name = "" 19 | else 20 | if (`basename "$VIRTUAL_ENV"` == "__") then 21 | # special case for Aspen magic directories 22 | # see http://www.zetadev.com/software/aspen/ 23 | set env_name = `basename \`dirname "$VIRTUAL_ENV"\`` 24 | else 25 | set env_name = `basename "$VIRTUAL_ENV"` 26 | endif 27 | endif 28 | 29 | # Could be in a non-interactive environment, 30 | # in which case, $prompt is undefined and we wouldn't 31 | # care about the prompt anyway. 32 | if ( $?prompt ) then 33 | set _OLD_VIRTUAL_PROMPT="$prompt" 34 | set prompt = "[$env_name] $prompt" 35 | endif 36 | 37 | unset env_name 38 | 39 | alias pydoc python -m pydoc 40 | 41 | rehash 42 | 43 | -------------------------------------------------------------------------------- /bin/activate.fish: -------------------------------------------------------------------------------- 1 | # This file must be used with "source bin/activate.fish" *from fish* (http://fishshell.com) 2 | # you cannot run it directly 3 | 4 | function deactivate -d "Exit virtualenv and return to normal shell environment" 5 | # reset old environment variables 6 | if test -n "$_OLD_VIRTUAL_PATH" 7 | set -gx PATH $_OLD_VIRTUAL_PATH 8 | set -e _OLD_VIRTUAL_PATH 9 | end 10 | if test -n "$_OLD_VIRTUAL_PYTHONHOME" 11 | set -gx PYTHONHOME $_OLD_VIRTUAL_PYTHONHOME 12 | set -e _OLD_VIRTUAL_PYTHONHOME 13 | end 14 | 15 | if test -n "$_OLD_FISH_PROMPT_OVERRIDE" 16 | # set an empty local fish_function_path, so fish_prompt doesn't automatically reload 17 | set -l fish_function_path 18 | # erase the virtualenv's fish_prompt function, and restore the original 19 | functions -e fish_prompt 20 | functions -c _old_fish_prompt fish_prompt 21 | functions -e _old_fish_prompt 22 | set -e _OLD_FISH_PROMPT_OVERRIDE 23 | end 24 | 25 | set -e VIRTUAL_ENV 26 | if test "$argv[1]" != "nondestructive" 27 | # Self destruct! 28 | functions -e deactivate 29 | end 30 | end 31 | 32 | # unset irrelevant variables 33 | deactivate nondestructive 34 | 35 | set -gx VIRTUAL_ENV "/Users/paul/Bitpay/bitpay-python" 36 | 37 | set -gx _OLD_VIRTUAL_PATH $PATH 38 | set -gx PATH "$VIRTUAL_ENV/bin" $PATH 39 | 40 | # unset PYTHONHOME if set 41 | if set -q PYTHONHOME 42 | set -gx _OLD_VIRTUAL_PYTHONHOME $PYTHONHOME 43 | set -e PYTHONHOME 44 | end 45 | 46 | if test -z "$VIRTUAL_ENV_DISABLE_PROMPT" 47 | # fish uses a function instead of an env var to generate the prompt. 48 | 49 | # copy the current fish_prompt function as the function _old_fish_prompt 50 | functions -c fish_prompt _old_fish_prompt 51 | 52 | # with the original prompt function copied, we can override with our own. 53 | function fish_prompt 54 | # Prompt override? 55 | if test -n "" 56 | printf "%s%s" "" (set_color normal) 57 | _old_fish_prompt 58 | return 59 | end 60 | # ...Otherwise, prepend env 61 | set -l _checkbase (basename "$VIRTUAL_ENV") 62 | if test $_checkbase = "__" 63 | # special case for Aspen magic directories 64 | # see http://www.zetadev.com/software/aspen/ 65 | printf "%s[%s]%s " (set_color -b blue white) (basename (dirname "$VIRTUAL_ENV")) (set_color normal) 66 | _old_fish_prompt 67 | else 68 | printf "%s(%s)%s" (set_color -b blue white) (basename "$VIRTUAL_ENV") (set_color normal) 69 | _old_fish_prompt 70 | end 71 | end 72 | 73 | set -gx _OLD_FISH_PROMPT_OVERRIDE "$VIRTUAL_ENV" 74 | end 75 | -------------------------------------------------------------------------------- /bin/activate_this.py: -------------------------------------------------------------------------------- 1 | """By using execfile(this_file, dict(__file__=this_file)) you will 2 | activate this virtualenv environment. 3 | 4 | This can be used when you must use an existing Python interpreter, not 5 | the virtualenv bin/python 6 | """ 7 | 8 | try: 9 | __file__ 10 | except NameError: 11 | raise AssertionError( 12 | "You must run this like execfile('path/to/activate_this.py', dict(__file__='path/to/activate_this.py'))") 13 | import sys 14 | import os 15 | 16 | old_os_path = os.environ.get('PATH', '') 17 | os.environ['PATH'] = os.path.dirname(os.path.abspath(__file__)) + os.pathsep + old_os_path 18 | base = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 19 | if sys.platform == 'win32': 20 | site_packages = os.path.join(base, 'Lib', 'site-packages') 21 | else: 22 | site_packages = os.path.join(base, 'lib', 'python%s' % sys.version[:3], 'site-packages') 23 | prev_sys_path = list(sys.path) 24 | import site 25 | site.addsitedir(site_packages) 26 | sys.real_prefix = sys.prefix 27 | sys.prefix = base 28 | # Move the added items to the front of the path: 29 | new_sys_path = [] 30 | for item in list(sys.path): 31 | if item not in prev_sys_path: 32 | new_sys_path.append(item) 33 | sys.path.remove(item) 34 | sys.path[:0] = new_sys_path 35 | -------------------------------------------------------------------------------- /bin/easy_install: -------------------------------------------------------------------------------- 1 | #!/Users/paul/Bitpay/bitpay-python/bin/python3.4 2 | 3 | # -*- coding: utf-8 -*- 4 | import re 5 | import sys 6 | 7 | from setuptools.command.easy_install import main 8 | 9 | if __name__ == '__main__': 10 | sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) 11 | sys.exit(main()) 12 | -------------------------------------------------------------------------------- /bin/easy_install-3.4: -------------------------------------------------------------------------------- 1 | #!/Users/paul/Bitpay/bitpay-python/bin/python3.4 2 | 3 | # -*- coding: utf-8 -*- 4 | import re 5 | import sys 6 | 7 | from setuptools.command.easy_install import main 8 | 9 | if __name__ == '__main__': 10 | sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) 11 | sys.exit(main()) 12 | -------------------------------------------------------------------------------- /bin/pip: -------------------------------------------------------------------------------- 1 | #!/Users/paul/Bitpay/bitpay-python/bin/python3.4 2 | 3 | # -*- coding: utf-8 -*- 4 | import re 5 | import sys 6 | 7 | from pip import main 8 | 9 | if __name__ == '__main__': 10 | sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) 11 | sys.exit(main()) 12 | -------------------------------------------------------------------------------- /bin/pip3: -------------------------------------------------------------------------------- 1 | #!/Users/paul/Bitpay/bitpay-python/bin/python3.4 2 | 3 | # -*- coding: utf-8 -*- 4 | import re 5 | import sys 6 | 7 | from pip import main 8 | 9 | if __name__ == '__main__': 10 | sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) 11 | sys.exit(main()) 12 | -------------------------------------------------------------------------------- /bin/pip3.4: -------------------------------------------------------------------------------- 1 | #!/Users/paul/Bitpay/bitpay-python/bin/python3.4 2 | 3 | # -*- coding: utf-8 -*- 4 | import re 5 | import sys 6 | 7 | from pip import main 8 | 9 | if __name__ == '__main__': 10 | sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) 11 | sys.exit(main()) 12 | -------------------------------------------------------------------------------- /bin/python: -------------------------------------------------------------------------------- 1 | python3.4 -------------------------------------------------------------------------------- /bin/python3: -------------------------------------------------------------------------------- 1 | python3.4 -------------------------------------------------------------------------------- /bin/python3.4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitpay/bitpay-python/ff3045a7812344c9602fb25d354248ee74b5bccc/bin/python3.4 -------------------------------------------------------------------------------- /bitpay/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitpay/bitpay-python/ff3045a7812344c9602fb25d354248ee74b5bccc/bitpay/__init__.py -------------------------------------------------------------------------------- /bitpay/client.py: -------------------------------------------------------------------------------- 1 | from bitpay.exceptions import * 2 | from bitpay import key_utils 3 | import requests 4 | import json 5 | import re 6 | 7 | class Client: 8 | def __init__(self, api_uri="https://bitpay.com", insecure=False, pem=key_utils.generate_pem(), tokens={}): 9 | self.uri = api_uri 10 | self.verify = not(insecure) 11 | self.pem = pem 12 | self.client_id = key_utils.get_sin_from_pem(pem) 13 | self.tokens = tokens 14 | self.user_agent = 'bitpay-python' 15 | 16 | def pair_pos_client(self, code): 17 | if re.match("^\w{7,7}$", code) is None: 18 | raise BitPayArgumentError("pairing code is not legal") 19 | payload = {'id': self.client_id, 'pairingCode': code} 20 | response = self.unsigned_request('/tokens', payload) 21 | if response.ok: 22 | self.tokens = self.token_from_response(response.json()) 23 | return self.tokens 24 | self.response_error(response) 25 | 26 | def create_token(self, facade): 27 | payload = {'id': self.client_id, 'facade': facade} 28 | response = self.unsigned_request('/tokens', payload) 29 | if response.ok: 30 | self.tokens = self.token_from_response(response.json()) 31 | return response.json()['data'][0]['pairingCode'] 32 | self.response_error(response) 33 | 34 | def create_invoice(self, params): 35 | self.verify_invoice_params(params['price'], params['currency']) 36 | payload = json.dumps(params) 37 | uri = self.uri + "/invoices" 38 | xidentity = key_utils.get_compressed_public_key_from_pem(self.pem) 39 | xsignature = key_utils.sign(uri + payload, self.pem) 40 | headers = {"content-type": "application/json", 'X-Identity': xidentity, 'X-Signature': xsignature, 'X-accept-version': '2.0.0'} 41 | try: 42 | response = requests.post(uri, data=payload, headers=headers, verify=self.verify) 43 | except Exception as pro: 44 | raise BitPayConnectionError(pro.args) 45 | if response.ok: 46 | return response.json()['data'] 47 | self.response_error(response) 48 | 49 | def get_invoice(self, invoice_id, token=None): 50 | if token: 51 | uri = self.uri + "/invoices/" + invoice_id + "?token=" + token 52 | xidentity = key_utils.get_compressed_public_key_from_pem(self.pem) 53 | xsignature = key_utils.sign(uri, self.pem) 54 | headers = {"content-type": "application/json", 'X-Identity': xidentity, 'X-Signature': xsignature, 'X-accept-version': '2.0.0'} 55 | else: 56 | uri = self.uri + "/invoices/" + invoice_id 57 | headers = {"content-type": "application/json"} 58 | 59 | try: 60 | response = requests.get(uri, headers=headers, verify=self.verify) 61 | except Exception as pro: 62 | raise BitPayConnectionError(pro.args) 63 | if response.ok: 64 | return response.json()['data'] 65 | self.response_error(response) 66 | 67 | def verify_tokens(self): 68 | """ 69 | Deprecated, will be made private in 2.4 70 | """ 71 | xidentity = key_utils.get_compressed_public_key_from_pem(self.pem) 72 | url = self.uri + "/tokens" 73 | xsignature = key_utils.sign(self.uri + "/tokens", self.pem) 74 | headers = {"content-type": "application/json", 'X-Identity': xidentity, 'X-Signature': xsignature, 'X-accept-version': '2.0.0'} 75 | response = requests.get(self.uri + "/tokens", headers=headers, verify=self.verify) 76 | if response.ok: 77 | allTokens = response.json()['data'] 78 | selfKeys = self.tokens.keys() 79 | matchedTokens = [token for token in allTokens for key in selfKeys if token.get(key) == self.tokens.get(key)] 80 | if not matchedTokens: 81 | return False 82 | return True 83 | 84 | def token_from_response(self, responseJson): 85 | """ 86 | Deprecated, will be made private in 2.4 87 | """ 88 | token = responseJson['data'][0]['token'] 89 | facade = responseJson['data'][0]['facade'] 90 | return {facade: token} 91 | raise BitPayBitPayError('%(code)d: %(message)s' % {'code': response.status_code, 'message': response.json()['error']}) 92 | 93 | def verify_invoice_params(self, price, currency): 94 | """ 95 | Deprecated, will be made private in 2.4 96 | """ 97 | if re.match("^[A-Z]{3,3}$", currency) is None: 98 | raise BitPayArgumentError("Currency is invalid.") 99 | try: 100 | float(price) 101 | except: 102 | raise BitPayArgumentError("Price must be formatted as a float") 103 | def response_error(self, response): 104 | raise BitPayBitPayError('%(code)d: %(message)s' % {'code': response.status_code, 'message': response.json()['error']}) 105 | 106 | def unsigned_request(self, path, payload=None): 107 | """ 108 | generic bitpay usigned wrapper 109 | passing a payload will do a POST, otherwise a GET 110 | """ 111 | headers = {"content-type": "application/json", "X-accept-version": "2.0.0"} 112 | try: 113 | if payload: 114 | response = requests.post(self.uri + path, verify=self.verify, data=json.dumps(payload), headers=headers) 115 | else: 116 | response = requests.get(self.uri + path, verify=self.verify, headers=headers) 117 | except Exception as pro: 118 | raise BitPayConnectionError('Connection refused') 119 | return response 120 | 121 | def unsigned_get_request(self, path, payload=None): 122 | """ 123 | Deprecated, will be removed in 2.4 124 | """ 125 | return self.unsigned_request('/tokens', payload) 126 | -------------------------------------------------------------------------------- /bitpay/exceptions.py: -------------------------------------------------------------------------------- 1 | class BitPayArgumentError(Exception): pass 2 | class BitPayBitPayError(Exception): pass 3 | class BitPayConnectionError(Exception): pass 4 | -------------------------------------------------------------------------------- /bitpay/key_utils.py: -------------------------------------------------------------------------------- 1 | from ecdsa import SigningKey, SECP256k1, VerifyingKey 2 | from ecdsa import util as ecdsaUtil 3 | import binascii 4 | import hashlib 5 | 6 | def generate_pem(): 7 | sk = SigningKey.generate(curve=SECP256k1) 8 | pem = sk.to_pem() 9 | pem = pem.decode("utf-8") 10 | return pem 11 | 12 | def get_sin_from_pem(pem): 13 | public_key = get_compressed_public_key_from_pem(pem) 14 | version = get_version_from_compressed_key(public_key) 15 | checksum = get_checksum_from_version(version) 16 | return base58encode(version + checksum) 17 | 18 | def get_compressed_public_key_from_pem(pem): 19 | vks = SigningKey.from_pem(pem).get_verifying_key().to_string() 20 | bts = binascii.hexlify(vks) 21 | compressed = compress_key(bts) 22 | return compressed 23 | 24 | def sign(message, pem): 25 | message = message.encode() 26 | sk = SigningKey.from_pem(pem) 27 | signed = sk.sign(message, hashfunc=hashlib.sha256, sigencode=ecdsaUtil.sigencode_der) 28 | return binascii.hexlify(signed).decode() 29 | 30 | def base58encode(hexastring): 31 | chars = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' 32 | int_val = int(hexastring, 16) 33 | encoded = encode58("", int_val, chars) 34 | return encoded 35 | 36 | def encode58(string, int_val, chars): 37 | if int_val == 0: 38 | return string 39 | else: 40 | new_val, rem = divmod(int_val, 58) 41 | new_string = (chars[rem]) + string 42 | return encode58(new_string, new_val, chars) 43 | 44 | def get_checksum_from_version(version): 45 | return sha_digest(sha_digest(version))[0:8] 46 | 47 | def get_version_from_compressed_key(key): 48 | sh2 = sha_digest(key) 49 | rphash = hashlib.new('ripemd160') 50 | rphash.update(binascii.unhexlify(sh2)) 51 | rp1 = rphash.hexdigest() 52 | return '0F02' + rp1 53 | 54 | def sha_digest(hexastring): 55 | return hashlib.sha256(binascii.unhexlify(hexastring)).hexdigest() 56 | 57 | def compress_key(bts): 58 | intval = int(bts, 16) 59 | prefix = find_prefix(intval) 60 | return prefix + bts[0:64].decode("utf-8") 61 | 62 | def find_prefix(intval): 63 | if(intval % 2 == 0): 64 | prefix = '02' 65 | else: 66 | prefix = '03' 67 | return prefix 68 | 69 | -------------------------------------------------------------------------------- /examples/test_merchant_facade.py: -------------------------------------------------------------------------------- 1 | from bitpay.exceptions import * 2 | import bitpay.key_utils as bku 3 | from bitpay.client import * 4 | import pprint 5 | import requests 6 | import json 7 | import os.path 8 | import sys 9 | 10 | #API_HOST = "https://bitpay.com" #for production, live bitcoin 11 | API_HOST = "https://test.bitpay.com" #for testing, testnet bitcoin 12 | KEY_FILE = "tmp/key.priv" 13 | TOKEN_FILE = "tmp/token.priv" 14 | 15 | # check if there is a preexisting key file 16 | if os.path.isfile(KEY_FILE): 17 | f = open(KEY_FILE, 'r') 18 | key = f.read() 19 | f.close() 20 | print("Creating a bitpay client using existing private key from disk.") 21 | else: 22 | key = bku.generate_pem() 23 | f = open(KEY_FILE, 'w') 24 | f.write(key) 25 | f.close() 26 | 27 | client = Client(API_HOST, False, key) 28 | 29 | def fetch_token(facade): 30 | if os.path.isfile(TOKEN_FILE + facade): 31 | f = open(TOKEN_FILE + facade, 'r') 32 | token = f.read() 33 | f.close() 34 | print("Reading " + facade + " token from disk.") 35 | #global client 36 | #client = Client(API_HOST, False, key, {facade: token}) 37 | client.tokens[facade] = token 38 | else: 39 | pairingCode = client.create_token(facade) 40 | print("Creating " + facade + " token.") 41 | print("Please go to: %s/dashboard/merchant/api-tokens then enter \"%s\" then click the \"Find\" button, then click \"Approve\"" % (API_HOST, pairingCode)) 42 | if int(sys.version[0]) == 3: 43 | input("When you've complete the above, hit enter to continue...") 44 | else: 45 | raw_input("When you've complete the above, hit enter to continue...") 46 | print("token is: %s" % client.tokens[facade]) 47 | f = open(TOKEN_FILE + facade, 'w') 48 | f.write(client.tokens[facade]) 49 | f.close() 50 | 51 | def get_from_bitpay_api(client, uri, token): 52 | payload = "?token=%s" % token 53 | xidentity = bku.get_compressed_public_key_from_pem(client.pem) 54 | xsignature = bku.sign(uri + payload, client.pem) 55 | headers = {"content-type": "application/json", 56 | "X-Identity": xidentity, 57 | "X-Signature": xsignature, "X-accept-version": "2.0.0"} 58 | try: 59 | pp.pprint(headers) 60 | print(uri + payload) 61 | response = requests.get(uri + payload, headers=headers, verify=client.verify) 62 | except Exception as pro: 63 | raise BitPayConnectionError(pro.args) 64 | if response.ok: 65 | return response.json()['data'] 66 | client.response_error(response) 67 | 68 | """ 69 | POST to any resource 70 | Make sure to include the proper token in the params 71 | """ 72 | def post_to_bitpay_api(client, uri, resource, params): 73 | payload = json.dumps(params) 74 | uri = uri + "/" + resource 75 | xidentity = key_utils.get_compressed_public_key_from_pem(client.pem) 76 | xsignature = key_utils.sign(uri + payload, client.pem) 77 | headers = {"content-type": "application/json", 78 | "X-Identity": xidentity,"X-Signature": xsignature, 79 | "X-accept-version": "2.0.0"} 80 | try: 81 | response = requests.post(uri, data=payload, headers=headers, 82 | verify=client.verify) 83 | except Exception as pro: 84 | raise BitPayConnectionError(pro.args) 85 | if response.ok: 86 | return response.json()['data'] 87 | client.response_error(response) 88 | 89 | fetch_token("merchant") 90 | 91 | #Now we assume that the pairing code that we generated along with the crypto keys is paired with your merchant account 92 | # 93 | print("We will create an invoice using the merchant facade") 94 | 95 | invoice = client.create_invoice({"price": 50.00, "currency": "USD", "token": client.tokens['merchant']}) 96 | 97 | pp = pprint.PrettyPrinter(indent=4) 98 | pp.pprint(invoice) 99 | 100 | print("hopefully the above looks OK?") 101 | 102 | print("continuing if we can...") 103 | 104 | 105 | invoiceId = invoice['id'] 106 | print("**") 107 | print("Now fetching an invoice with invoiceId " + invoiceId) 108 | print("**") 109 | token = client.tokens['merchant'] 110 | invoice = get_from_bitpay_api(client, client.uri + "/invoices/" + invoiceId, token) 111 | pp.pprint(invoice) 112 | 113 | -------------------------------------------------------------------------------- /examples/test_payouts.py: -------------------------------------------------------------------------------- 1 | from bitpay.exceptions import * 2 | import bitpay.key_utils as bku 3 | from bitpay.client import * 4 | import pprint 5 | import requests 6 | import json 7 | import os.path 8 | import sys 9 | 10 | #API_HOST = "https://bitpay.com" #for production, live bitcoin 11 | API_HOST = "https://test.bitpay.com" #for testing, testnet bitcoin 12 | KEY_FILE = "tmp/key.priv" 13 | TOKEN_FILE = "tmp/token.priv" 14 | pp = pprint.PrettyPrinter(indent=4) 15 | 16 | # check if there is a preexisting key file 17 | if os.path.isfile(KEY_FILE): 18 | f = open(KEY_FILE, 'r') 19 | key = f.read() 20 | f.close() 21 | print("Creating a bitpay client using existing private key from disk.") 22 | else: 23 | key = bku.generate_pem() 24 | f = open(KEY_FILE, 'w') 25 | f.write(key) 26 | f.close() 27 | 28 | client = Client(API_HOST, False, key) 29 | 30 | def fetch_token(facade): 31 | if os.path.isfile(TOKEN_FILE + facade): 32 | f = open(TOKEN_FILE + facade, 'r') 33 | token = f.read() 34 | f.close() 35 | print("Reading " + facade + " token from disk.") 36 | client.tokens[facade] = token 37 | else: 38 | pairingCode = client.create_token(facade) 39 | print("Creating " + facade + " token.") 40 | print("Please go to: %s/dashboard/merchant/api-tokens then enter \"%s\" then click the \"Find\" button, then click \"Approve\"" % (API_HOST, pairingCode)) 41 | if int(sys.version[0]) == 3: 42 | input("When you've complete the above, hit enter to continue...") 43 | else: 44 | raw_input("When you've complete the above, hit enter to continue...") 45 | print("token is: %s" % client.tokens[facade]) 46 | f = open(TOKEN_FILE + facade, 'w') 47 | f.write(client.tokens[facade]) 48 | f.close() 49 | 50 | def get_from_bitpay_api(client, uri, token): 51 | payload = "?token=%s" % token 52 | xidentity = bku.get_compressed_public_key_from_pem(client.pem) 53 | xsignature = bku.sign(uri + payload, client.pem) 54 | headers = {"content-type": "application/json", 55 | "X-Identity": xidentity, 56 | "X-Signature": xsignature, "X-accept-version": "2.0.0"} 57 | try: 58 | pp.pprint(headers) 59 | print(uri + payload) 60 | response = requests.get(uri + payload, headers=headers, verify=client.verify) 61 | except Exception as pro: 62 | raise BitPayConnectionError(pro.args) 63 | if response.ok: 64 | return response.json()['data'] 65 | client.response_error(response) 66 | 67 | """ 68 | POST to any resource 69 | Make sure to include the proper token in the params 70 | """ 71 | def post_to_bitpay_api(client, uri, resource, params): 72 | payload = json.dumps(params) 73 | uri = uri + "/" + resource 74 | xidentity = key_utils.get_compressed_public_key_from_pem(client.pem) 75 | xsignature = key_utils.sign(uri + payload, client.pem) 76 | headers = {"content-type": "application/json", 77 | "X-Identity": xidentity,"X-Signature": xsignature, 78 | "X-accept-version": "2.0.0"} 79 | try: 80 | response = requests.post(uri, data=payload, headers=headers, 81 | verify=client.verify) 82 | except Exception as pro: 83 | raise BitPayConnectionError(pro.args) 84 | if response.ok: 85 | return response.json()['data'] 86 | client.response_error(response) 87 | 88 | fetch_token("payroll") 89 | 90 | #create a payroll token 91 | token = client.tokens['payroll'] 92 | print("Creating a payout batch now") 93 | print("token = " + token) 94 | # posting a payout batch 95 | params = {"token":token, "notificationURL":"https://hookb.in/3OBkOPk23ouKeKj2M2WQ", "effectiveDate":"2019-05-21", "amount":"10","currency":"USD","instructions":[ {"label":"Test1","address":"mx2Wv5j8SrPnxAQtNB3uf8mii1Vc5UDKsZ","amount":"7"},{"label":"Test2","address":"mx2Wv5j8SrPnxAQtNB3uf8mii1Vc5UDKsZ","amount":"3"}]} 96 | payoutBatch = post_to_bitpay_api(client, "https://test.bitpay.com", "payouts", params) 97 | pp.pprint(payoutBatch) 98 | 99 | -------------------------------------------------------------------------------- /features/creating_invoices.feature: -------------------------------------------------------------------------------- 1 | Feature: creating an invoice 2 | The user won't get any money 3 | If they can't 4 | Create Invoices 5 | 6 | Background: 7 | Given the user is authenticated with BitPay 8 | 9 | Scenario Outline: The request is correct 10 | When the user creates an invoice for with float input 11 | Then they should recieve an invoice in response for 12 | Examples: 13 | | price | currency | 14 | | 5.23 | USD | 15 | | 10.21 | EUR | 16 | | 0.231 | BTC | 17 | 18 | Scenario Outline: The request is correct 19 | When the user creates an invoice for with integer input 20 | Then they should recieve an invoice in response for 21 | Examples: 22 | | price | currency | 23 | | 10 | USD | 24 | 25 | Scenario Outline: The request is correct 26 | When the user creates an invoice for with string input 27 | Then they should recieve an invoice in response for 28 | Examples: 29 | | price | currency | 30 | | 5.23 | USD | 31 | | 10.21 | EUR | 32 | | 10 | USD | 33 | 34 | Scenario Outline: The invoice contains illegal characters 35 | When the user creates an invoice for with string input 36 | Then they will receive a BitPayArgumentError matching 37 | Examples: 38 | | price | currency | message | 39 | | 5,023 | USD | Price must be formatted as a float | 40 | | 3.21 | EaUR | Currency is invalid. | 41 | | "" | USD | Price must be formatted as a float | 42 | | Ten | USD | Price must be formatted as a float | 43 | | 10 | "" | Currency is invalid. | 44 | 45 | -------------------------------------------------------------------------------- /features/pairing.feature: -------------------------------------------------------------------------------- 1 | Feature: pairing with bitpay 2 | In order to access bitpay 3 | It is required that the library 4 | Is able to pair successfully 5 | 6 | Scenario: the client has a correct pairing code 7 | Given the user waits 1 seconds 8 | And the user pairs with BitPay with a valid pairing code 9 | Then the user is paired with BitPay 10 | 11 | Scenario: the client initiates pairing 12 | Given the user waits 1 seconds 13 | And the user requests a client-side pairing 14 | Then they will receive a claim code 15 | 16 | Scenario Outline: the client has a bad pairing code 17 | Given the user waits 1 seconds 18 | Given the user fails to pair with a semantically code 19 | Then they will receive a matching 20 | Examples: 21 | | valid | code | error | message | 22 | | valid | a1b2c3d | BitPayBitPayError | 500: Unable to create token | 23 | | invalid | a1b2c3d4 | BitPayArgumentError | pairing code is not legal | 24 | 25 | Scenario: the client has a bad port configuration to a closed port 26 | When the user fails to pair with BitPay because of an incorrect port 27 | Then they will receive a BitPayConnectionError matching Connection refused 28 | 29 | -------------------------------------------------------------------------------- /features/retrieving_invoices.feature: -------------------------------------------------------------------------------- 1 | Feature: retrieving an invoice 2 | The user may want to retrieve invoices 3 | So that they can view them 4 | 5 | Scenario: The request is correct 6 | Given that a user knows an invoice id 7 | Then they can retrieve that invoice 8 | -------------------------------------------------------------------------------- /features/steps/pair_steps.py: -------------------------------------------------------------------------------- 1 | from splinter import Browser 2 | import time 3 | import os 4 | import six 5 | import json 6 | import re 7 | from bitpay.client import Client 8 | from bitpay import key_utils 9 | 10 | ROOT_ADDRESS = os.environ['RCROOTADDRESS'] 11 | USER_NAME = os.environ['RCTESTUSER'] 12 | PASSWORD = os.environ['RCTESTPASSWORD'] 13 | PEM = '-----BEGIN EC PRIVATE KEY-----\nMHQCAQEEICg7E4NN53YkaWuAwpoqjfAofjzKI7Jq1f532dX+0O6QoAcGBSuBBAAK\noUQDQgAEjZcNa6Kdz6GQwXcUD9iJ+t1tJZCx7hpqBuJV2/IrQBfue8jh8H7Q/4vX\nfAArmNMaGotTpjdnymWlMfszzXJhlw==\n-----END EC PRIVATE KEY-----\n' 14 | client = Client() 15 | invoice = None 16 | exception = None 17 | 18 | @given(u'the user pairs with BitPay with a valid pairing code') 19 | def step_impl(context): 20 | claim_code = get_claim_code_from_server() 21 | global client 22 | client = Client(api_uri=ROOT_ADDRESS, insecure=True, pem=PEM) 23 | try: 24 | client.pair_pos_client(claim_code) 25 | except Exception as error: 26 | if error.args[0] == "500: Unable to create token because of too many requests.": 27 | time.sleep(60) 28 | client.pair_pos_client(claim_code) 29 | assert client.tokens['pos'] 30 | 31 | @given(u'the user waits {wait:d} seconds') 32 | def step_impl(context, wait): 33 | time.sleep(wait) 34 | 35 | @then(u'the user is paired with BitPay') 36 | def step_impl(context): 37 | assert client.verify_tokens() 38 | 39 | @given(u'the user fails to pair with a semantically {valid} code {code}') 40 | def step_impl(context, code, valid): 41 | try: 42 | client.pair_pos_client(code) 43 | except Exception as error: 44 | global exception 45 | exception = error 46 | if exception.args[0] == "500: Unable to create token because of too many requests.": 47 | time.sleep(60) 48 | try: 49 | client.pair_pos_client(code) 50 | except Exception as error: 51 | global exception 52 | exception = error 53 | 54 | @when(u'the user fails to pair with BitPay because of an incorrect port') 55 | def step_impl(context): 56 | badAddress = ROOT_ADDRESS.split(":") 57 | badAddress = badAddress[0] + ":" + badAddress[1] + ":999" 58 | newclient = Client(api_uri=badAddress, insecure=True) 59 | try: 60 | newclient.pair_pos_client("1a2C3d4") 61 | raise "That should totally not have worked" 62 | except Exception as error: 63 | global exception 64 | exception = error 65 | 66 | @given(u'the user requests a client-side pairing') 67 | def step_impl(context): 68 | global pairing_code 69 | client = Client(api_uri=ROOT_ADDRESS, insecure=True, pem=PEM) 70 | pairing_code = client.create_token("merchant") 71 | 72 | 73 | @then(u'they will receive a claim code') 74 | def step_impl(context): 75 | assert re.match("^\w{7,7}$", pairing_code) != None 76 | 77 | @then(u'they will receive a {error} matching {message}') 78 | def step_impl(context, error, message): 79 | assert exception.__class__.__name__ == error and exception.args[0] == message, "expected %s to match %s" % (exception.args[0], message) 80 | 81 | @given(u'the user is authenticated with BitPay') 82 | def step_impl(context): 83 | global client 84 | client = client_from_stored_values() 85 | assert client.verify_tokens() 86 | 87 | @when(u'the user creates an invoice for {amount:f} {currency} with float input') 88 | def step_impl(context, amount, currency): 89 | create_invoice(amount, currency) 90 | 91 | @when(u'the user creates an invoice for {amount:d} {currency} with integer input') 92 | def step_impl(context, amount, currency): 93 | create_invoice(amount, currency) 94 | 95 | @when(u'the user creates an invoice for {amount} {currency} with string input') 96 | def step_impl(context, amount, currency): 97 | if amount == '""': 98 | amount = "" 99 | if currency == '""': 100 | currency == "" 101 | create_invoice(amount, currency) 102 | 103 | @then(u'they should recieve an invoice in response for {amount:g} {currency}') 104 | def step_impl(context, amount, currency): 105 | global invoice 106 | assert invoice['price'] == amount and invoice['currency'] == currency 107 | 108 | def create_invoice(amount, currency): 109 | global client 110 | global invoice 111 | try: 112 | token = client.tokens['pos'] 113 | invoice = client.create_invoice({"price": amount, "currency": currency, "token": token }) 114 | except Exception as error: 115 | global exception 116 | print(error.__class__.__name__) 117 | print(error.args[0]) 118 | exception = error 119 | 120 | @given(u'that a user knows an invoice id') 121 | def step_impl(context): 122 | global client 123 | global invoice 124 | client = client_from_stored_values() 125 | invoice = client.create_invoice({"price": 10, "currency": "USD", "token": client.tokens['pos'] }) 126 | 127 | @then(u'they can retrieve that invoice') 128 | def step_impl(context): 129 | global client 130 | global invoice 131 | amount = invoice['price'] 132 | invoice_id = invoice['id'] 133 | retrieved_invoice = client.get_invoice(invoice_id) 134 | assert amount == retrieved_invoice['price'] 135 | 136 | def client_from_stored_values(): 137 | for f in ["local.pem", "tokens.json"]: 138 | try: 139 | open("temp/" + f) 140 | exists = True 141 | except: 142 | exists = False 143 | break 144 | if exists: 145 | f = open("temp/local.pem", 'r') 146 | pem = f.read() 147 | f = open("temp/tokens.json", 'r') 148 | token = f.read() 149 | token = json.loads(token) 150 | client = Client(api_uri=ROOT_ADDRESS, insecure=True, pem=pem, tokens=token) 151 | else: 152 | claim_code = get_claim_code_from_server() 153 | pem = key_utils.generate_pem() 154 | client = Client(api_uri=ROOT_ADDRESS, insecure=True, pem=pem) 155 | token = json.dumps(client.pair_pos_client(claim_code)) 156 | if not os.path.exists("temp"): 157 | os.makedirs("temp") 158 | f = open("temp/local.pem", 'w') 159 | f.write(pem) 160 | f = open("temp/tokens.json", 'w') 161 | f.write(token) 162 | return client 163 | 164 | def get_claim_code_from_server(): 165 | browser = Browser('phantomjs', service_args=['--ignore-ssl-errors=true']) 166 | browser.visit(ROOT_ADDRESS + "/merchant-login") 167 | browser.fill_form({"email": USER_NAME, "password": PASSWORD}) 168 | browser.find_by_id("loginButton")[0].click() 169 | time.sleep(5) 170 | browser.visit(ROOT_ADDRESS + "/api-tokens") 171 | browser.find_by_css(".token-access-new-button").find_by_css(".btn").find_by_css(".icon-plus")[0].click() 172 | browser.find_by_id("token-new-form").find_by_css(".btn")[0].click() 173 | return browser.find_by_css(".token-claimcode")[0].html 174 | 175 | 176 | -------------------------------------------------------------------------------- /include/python3.4m: -------------------------------------------------------------------------------- 1 | /usr/local/opt/pyenv/versions/3.4.2/include/python3.4m -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | certifi==2019.3.9 2 | chardet==3.0.4 3 | coverage==4.5.3 4 | coveralls==1.7.0 5 | docopt==0.6.2 6 | ecdsa==0.13.3 7 | httmock==1.3.0 8 | idna==2.8 9 | nose==1.3.7 10 | parse==1.12.0 11 | pymongo==3.8.0 12 | requests==2.21.0 13 | selenium==3.141.0 14 | splinter==0.10.0 15 | urllib3==1.25.2 16 | virtualenv==16.6.0 17 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # chardet's setup.py 2 | import setuptools 3 | setuptools.setup( 4 | name="bitpay", 5 | packages=["bitpay"], 6 | version="2.5.1906.1", 7 | description="Accept bitcoin with BitPay", 8 | author="Antonio Buedo", 9 | author_email="sales-engineering@bitpay.com", 10 | url="https://github.com/bitpay/bitpay-python", 11 | download_url="https://github.com/bitpay/bitpay-python/tarball/v2.5.1906", 12 | keywords=["bitcoin", "payments", "crypto", "cash", "ethereum", "online payments"], 13 | python_requires=">=2.7.16", 14 | install_requires=["requests", "ecdsa"], 15 | classifiers=[ 16 | "Programming Language :: Python :: 2.7", 17 | 'Programming Language :: Python :: 3.4', 18 | 'Programming Language :: Python :: 3.5', 19 | 'Programming Language :: Python :: 3.6', 20 | 'Programming Language :: Python :: 3.7', 21 | "Development Status :: 5 - Production/Stable", 22 | 'Intended Audience :: Developers', 23 | 'Natural Language :: English', 24 | "Environment :: Web Environment", 25 | "Intended Audience :: Developers", 26 | "License :: OSI Approved :: MIT License", 27 | "Operating System :: OS Independent", 28 | "Topic :: Software Development :: Libraries :: Python Modules", 29 | "Topic :: Office/Business :: Financial" 30 | ], 31 | long_description="""\ 32 | Python Library for integrating with BitPay 33 | 34 | This library is a simple way to integrate your application with 35 | BitPay for taking Bitcoin payments. It exposes three basic 36 | functions, authenticating with BitPay, creating invoices, 37 | and retrieving invoices. It is not meant as a replacement for 38 | the entire BitPay API. However, the key_utils module contains 39 | all of the tools you need to use the BitPay API for other 40 | purposes. 41 | 42 | This version requires Python 2.7 or later. 43 | """, 44 | long_description_content_type='text/markdown' 45 | ) 46 | -------------------------------------------------------------------------------- /tasks/set_constants.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export RCROOTADDRESS=$1 4 | echo $RCROOTADDRESS 5 | export RCTESTUSER=$2 6 | echo $RCTESTUSER 7 | export RCTESTPASSWORD=$3 8 | echo $RCTESTPASSWORD 9 | -------------------------------------------------------------------------------- /test/client_test.py: -------------------------------------------------------------------------------- 1 | from bitpay.exceptions import * 2 | from bitpay.client import Client 3 | from httmock import HTTMock 4 | import unittest 5 | import sys 6 | 7 | class TestClient(unittest.TestCase): 8 | def test_pair_code_check(self): 9 | """tests whether the pairing code is syntatically correct""" 10 | new_client = Client(api_uri="https://test.bitpay.com") 11 | if int(sys.version[0]) == 3: 12 | with self.assertRaisesRegex(BitPayArgumentError, "pairing code is not legal"): 13 | new_client.pair_pos_client("abcd") 14 | else: 15 | with self.assertRaisesRegexp(BitPayArgumentError, "pairing code is not legal"): 16 | new_client.pair_pos_client("abcd") 17 | 18 | def test_passes_errors_when_pairing(self): 19 | """web errors should be gracefully passed to the client""" 20 | new_client = Client() 21 | def a_request(url, request): 22 | return {'status_code': 403, 'content': b'{"error": "this is a 403 error"}'} 23 | with HTTMock(a_request): 24 | if int(sys.version[0]) == 3: 25 | with self.assertRaisesRegex(BitPayBitPayError, "403: this is a 403 error"): 26 | new_client.pair_pos_client("a1B2c3d") 27 | else: 28 | with self.assertRaisesRegexp(BitPayBitPayError, "403: this is a 403 error"): 29 | new_client.pair_pos_client("a1B2c3d") 30 | 31 | def test_passes_errors_when_creating_invoice(self): 32 | """web errors should be gracefully passed to the client""" 33 | new_client = Client() 34 | def a_request(url, request): 35 | return {'status_code': 403, 'content': b'{"error": "this is a 403 error"}'} 36 | with HTTMock(a_request): 37 | if int(sys.version[0]) == 3: 38 | with self.assertRaisesRegex(BitPayBitPayError, "403: this is a 403 error"): 39 | new_client.create_invoice({"price": 20, "currency": "USD"}) 40 | else: 41 | with self.assertRaisesRegexp(BitPayBitPayError, "403: this is a 403 error"): 42 | new_client.create_invoice({"price": 20, "currency": "USD"}) 43 | 44 | def test_unsigned_request_rates(self): 45 | """tests whether the generic wrapper returns properly 46 | when asked for rates 47 | """ 48 | new_client = Client() 49 | request = new_client.unsigned_request('/rates/EUR') 50 | self.assertIn('rate', request.json()['data']) 51 | -------------------------------------------------------------------------------- /test/key_utils_test.py: -------------------------------------------------------------------------------- 1 | from bitpay import key_utils as utils 2 | import re 3 | import unittest 4 | from ecdsa import SigningKey 5 | from ecdsa import util as ecdsaUtil 6 | import binascii 7 | import hashlib 8 | 9 | class TestKeyUtils(unittest.TestCase): 10 | 11 | def test_generate_pem(self): 12 | pem = utils.generate_pem() 13 | match = re.match(r"-----BEGIN EC PRIVATE KEY-----", pem) 14 | self.assertIsNotNone(match) 15 | 16 | def test_sin_from_pem(self): 17 | pem = '-----BEGIN EC PRIVATE KEY-----\nMHQCAQEEICg7E4NN53YkaWuAwpoqjfAofjzKI7Jq1f532dX+0O6QoAcGBSuBBAAK\noUQDQgAEjZcNa6Kdz6GQwXcUD9iJ+t1tJZCx7hpqBuJV2/IrQBfue8jh8H7Q/4vX\nfAArmNMaGotTpjdnymWlMfszzXJhlw==\n-----END EC PRIVATE KEY-----\n' 18 | assert utils.get_sin_from_pem(pem) == 'TeyN4LPrXiG5t2yuSamKqP3ynVk3F52iHrX' 19 | 20 | def test_sign(self): 21 | pem = '-----BEGIN EC PRIVATE KEY-----\nMHQCAQEEICg7E4NN53YkaWuAwpoqjfAofjzKI7Jq1f532dX+0O6QoAcGBSuBBAAK\noUQDQgAEjZcNa6Kdz6GQwXcUD9iJ+t1tJZCx7hpqBuJV2/IrQBfue8jh8H7Q/4vX\nfAArmNMaGotTpjdnymWlMfszzXJhlw==\n-----END EC PRIVATE KEY-----\n' 22 | signed = utils.sign("message", pem) 23 | sk = SigningKey.from_pem(pem) 24 | vk = sk.get_verifying_key() 25 | print(signed) 26 | signed = binascii.unhexlify(signed) 27 | vk.verify(signed, "message".encode(), hashfunc=hashlib.sha256, sigdecode=ecdsaUtil.sigdecode_der) 28 | 29 | --------------------------------------------------------------------------------