├── .gitignore ├── MANIFEST ├── README.md ├── coinbase ├── CoinbaseAPIKeyAuthentication.py ├── CoinbaseAuthentication.py ├── CoinbaseOAuth.py ├── CoinbaseOAuthAuthentication.py ├── CoinbaseRPC.py ├── __init__.py └── error.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | bin/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # Installer logs 26 | pip-log.txt 27 | pip-delete-this-directory.txt 28 | 29 | # Unit test / coverage reports 30 | htmlcov/ 31 | .tox/ 32 | .coverage 33 | .cache 34 | nosetests.xml 35 | coverage.xml 36 | 37 | # Translations 38 | *.mo 39 | 40 | # Mr Developer 41 | .mr.developer.cfg 42 | .project 43 | .pydevproject 44 | 45 | # Rope 46 | .ropeproject 47 | 48 | # Django stuff: 49 | *.log 50 | *.pot 51 | 52 | # Sphinx documentation 53 | docs/_build/ 54 | 55 | -------------------------------------------------------------------------------- /MANIFEST: -------------------------------------------------------------------------------- 1 | # file GENERATED by distutils, do NOT edit 2 | setup.py 3 | coinbase/CoinbaseAPIKeyAuthentication.py 4 | coinbase/CoinbaseAuthentication.py 5 | coinbase/CoinbaseOAuth.py 6 | coinbase/CoinbaseOAuthAuthentication.py 7 | coinbase/CoinbaseRPC.py 8 | coinbase/__init__.py 9 | coinbase/error.py 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resy/coinbase_python3/a22a9897e0e690e8c575538d4c39745f58d151f9/README.md -------------------------------------------------------------------------------- /coinbase/CoinbaseAPIKeyAuthentication.py: -------------------------------------------------------------------------------- 1 | # ----- Author ---------------------------------------------------------------- 2 | 3 | __author__ = 'Michael Montero ' 4 | 5 | # ----- Imports --------------------------------------------------------------- 6 | 7 | from .CoinbaseAuthentication import CoinbaseAuthentication 8 | 9 | # ----- Public Classes -------------------------------------------------------- 10 | 11 | class CoinbaseAPIKeyAuthentication(CoinbaseAuthentication): 12 | ''' 13 | Implements the authentication mechanism that uses the API key and secret. 14 | ''' 15 | 16 | def __init__(self, key, secret): 17 | self.api_key = key 18 | self.api_secret = secret 19 | 20 | 21 | def get_data(self): 22 | return { 23 | 'api_key': self.api_key, 24 | 'api_secret': self.api_secret 25 | } 26 | -------------------------------------------------------------------------------- /coinbase/CoinbaseAuthentication.py: -------------------------------------------------------------------------------- 1 | # ----- Author ---------------------------------------------------------------- 2 | 3 | __author__ = 'Michael Montero ' 4 | 5 | # ----- Public Classes -------------------------------------------------------- 6 | 7 | class CoinbaseAuthentication(object): 8 | ''' 9 | Base class for API authentication mechanisms. 10 | ''' 11 | 12 | def get_data(): 13 | raise NotImplementedError 14 | -------------------------------------------------------------------------------- /coinbase/CoinbaseOAuth.py: -------------------------------------------------------------------------------- 1 | # ----- Author ---------------------------------------------------------------- 2 | 3 | __author__ = 'Michael Montero ' 4 | 5 | # ----- Imports --------------------------------------------------------------- 6 | 7 | from .error import CoinbaseAPIException 8 | 9 | import json 10 | import requests 11 | import urllib.parse 12 | 13 | # ----- Public Classes -------------------------------------------------------- 14 | 15 | class CoinbaseOAuth(object): 16 | ''' 17 | Handles client OAuth functionality like authorizing a user's account to be 18 | accessed. 19 | ''' 20 | 21 | def __init__(self, client_id, client_secret, redirect_uri): 22 | self.__client_id = client_id 23 | self.__client_secret = client_secret 24 | self.__redirect_uri = redirect_uri 25 | 26 | 27 | def create_authorize_url(self, scope=tuple()): 28 | params = { 29 | 'response_type': 'code', 30 | 'client_id': self.__client_id, 31 | 'client_secret': self.__client_secret, 32 | 'redirect_uri': self.__redirect_uri, 33 | } 34 | 35 | for i in range(len(scope)): 36 | scope[i] = urllib.parse.quote_plus(scope[i]) 37 | 38 | params = urllib.parse.urlencode(params) + 'scope=' + '+'.join(scope) 39 | 40 | return 'https://coinbase.com/oauth/authorize?' + params 41 | 42 | 43 | def get_tokens(self, code, grant_type='authorization_code'): 44 | params = { 45 | 'grant_type': grant_type, 46 | 'redirect_uri': self.__redirect_uri, 47 | 'client_id': self.__client_id, 48 | 'client_secret': self.__client_secret 49 | } 50 | 51 | if grant_type == 'refresh_token': 52 | params['refresh_token'] = code 53 | else: 54 | params['code'] = code 55 | 56 | request = \ 57 | requests.post( 58 | 'https://coinbase.com/oauth/token', 59 | data=params) 60 | 61 | if request.status_code != 200: 62 | raise CoinbaseAPIException( 63 | 'Could not get tokens - code', 64 | request.status_code, 65 | request.content.decode()) 66 | 67 | return json.loads(request.content.decode()) 68 | 69 | 70 | def refresh_tokens(self, refresh_token): 71 | return self.get_tokens(refresh_token, 'refresh_token') 72 | -------------------------------------------------------------------------------- /coinbase/CoinbaseOAuthAuthentication.py: -------------------------------------------------------------------------------- 1 | # ----- Author ---------------------------------------------------------------- 2 | 3 | __author__ = 'Michael Montero ' 4 | 5 | # ----- Imports --------------------------------------------------------------- 6 | 7 | from .CoinbaseAuthentication import CoinbaseAuthentication 8 | 9 | # ----- Public Classes -------------------------------------------------------- 10 | 11 | class CoinbaseOAuthAuthentication(CoinbaseAuthentication): 12 | ''' 13 | Implements the authentication mechanism that uses an access token and a 14 | refresh token. 15 | ''' 16 | 17 | def __init__(self, access_token, refresh_token): 18 | self.access_token = access_token 19 | self.refresh_token = refresh_token 20 | 21 | 22 | def get_data(self): 23 | return { 24 | 'access_token': self.access_token, 25 | 'refresh_token': self.refresh_token 26 | } 27 | -------------------------------------------------------------------------------- /coinbase/CoinbaseRPC.py: -------------------------------------------------------------------------------- 1 | # ----- Author ---------------------------------------------------------------- 2 | 3 | __author__ = 'Michael Montero ' 4 | 5 | # ----- Imports --------------------------------------------------------------- 6 | 7 | from .CoinbaseAPIKeyAuthentication import CoinbaseAPIKeyAuthentication 8 | from .CoinbaseOAuthAuthentication import CoinbaseOAuthAuthentication 9 | from .error import CoinbaseAPIException 10 | 11 | import hashlib 12 | import hmac 13 | import json 14 | import requests 15 | import time 16 | import urllib.parse 17 | 18 | # ----- Public Classes -------------------------------------------------------- 19 | 20 | class CoinbaseRPC(object): 21 | ''' 22 | Abstracts functionality for executing remote procedure calls. 23 | ''' 24 | 25 | COINBASE_API = 'https://coinbase.com/api/v1' 26 | 27 | def __init__(self, authentication, nonce=None): 28 | self.__authentication = authentication 29 | self.__nonce = None 30 | 31 | 32 | def request(self, method, url, params=None): 33 | url = self.COINBASE_API + url 34 | 35 | method = method.lower() 36 | if method == 'get' or method == 'delete': 37 | if params is not None: 38 | url += '?' + urllib.parse.urlencode(params) 39 | else: 40 | params = json.dumps(params) 41 | 42 | headers = { 43 | 'Content-Type': 'application/json', 44 | 'User-Agent': 'CoinbasePython3/v1' 45 | } 46 | 47 | auth = self.__authentication.get_data() 48 | 49 | if isinstance(self.__authentication, CoinbaseOAuthAuthentication): 50 | headers['Authorization'] = 'Bearer ' + auth['access_token'] 51 | elif isinstance(self.__authentication, CoinbaseAPIKeyAuthentication): 52 | if self.__nonce is None: 53 | self.__nonce = int(time.time() * 1e6) 54 | message = str(self.__nonce) + url 55 | 56 | if method == 'post' or method == 'put': 57 | if params is not None: 58 | message += params 59 | 60 | signature = \ 61 | hmac.new( 62 | auth['api_secret'].encode(), 63 | message.encode(), 64 | hashlib.sha256) \ 65 | .hexdigest() 66 | 67 | headers['ACCESS_KEY'] = auth['api_key'] 68 | headers['ACCESS_SIGNATURE'] = signature 69 | headers['ACCESS_NONCE'] = self.__nonce 70 | headers['Accept'] = 'application/json' 71 | else: 72 | raise CoinbaseAPIException('Invalid authentication mechanism') 73 | 74 | if method == 'get': 75 | request = requests.get(url, headers=headers) 76 | elif method == 'delete': 77 | request = requests.delete(url, headers=headers) 78 | elif method == 'post': 79 | request = requests.post(url, data=params, headers=headers) 80 | elif method == 'put': 81 | request = requests.put(url, data=params, headers=headers) 82 | 83 | if request.status_code != 200: 84 | raise CoinbaseAPIException( 85 | 'Status Code ' + str(request.status_code), 86 | request.status_code, 87 | request.content.decode()) 88 | 89 | content = json.loads(request.content.decode()) 90 | if content is None or len(content) == 0: 91 | raise CoinbaseAPIException( 92 | 'Invalid response body', 93 | request.status_code, 94 | request.content.decode()) 95 | 96 | if 'error' in content: 97 | raise CoinbaseAPIException( 98 | content['error'], 99 | request.status_code, 100 | request.content.decode()) 101 | 102 | if 'errors' in content: 103 | raise CoinbaseAPIException( 104 | ', '.join(content['errors']), 105 | request.status_code, 106 | request.content.decode()) 107 | 108 | return content 109 | -------------------------------------------------------------------------------- /coinbase/__init__.py: -------------------------------------------------------------------------------- 1 | # ----- Author ---------------------------------------------------------------- 2 | 3 | __author__ = 'Michael Montero ' 4 | 5 | # ----- Imports --------------------------------------------------------------- 6 | 7 | from .CoinbaseAPIKeyAuthentication import CoinbaseAPIKeyAuthentication 8 | from .CoinbaseOAuth import CoinbaseOAuth 9 | from .CoinbaseOAuthAuthentication import CoinbaseOAuthAuthentication 10 | from .CoinbaseRPC import CoinbaseRPC 11 | from .error import CoinbaseAPIException 12 | from .error import CoinbaseException 13 | 14 | # ----- Public Classes -------------------------------------------------------- 15 | 16 | class Coinbase(object): 17 | ''' 18 | Manages all data and interactions with the Coinbase API. 19 | ''' 20 | 21 | @staticmethod 22 | def with_api_key(key, secret, nonce=None): 23 | return Coinbase( 24 | CoinbaseAPIKeyAuthentication( 25 | key, secret), 26 | nonce) 27 | 28 | 29 | @staticmethod 30 | def with_oauth(access_token, refresh_token): 31 | return Coinbase( 32 | CoinbaseOAuthAuthentication( 33 | access_token, refresh_token)) 34 | 35 | 36 | def __init__(self, authentication, nonce=None): 37 | self.__authentication = authentication 38 | self.__rpc = CoinbaseRPC(self.__authentication, nonce) 39 | 40 | 41 | def buy(self, amount, agree_btc_amount_varies=False): 42 | params = { 43 | 'qty': amount, 44 | 'agree_btc_amount_varies': agree_btc_amount_varies 45 | } 46 | 47 | return self.post('/buys', params) 48 | 49 | 50 | def cancel_request(self, id): 51 | return self.delete('/transactions/' + id + '/cancel_request') 52 | 53 | 54 | def complete_request(self, id): 55 | return self.put('/transactions/' + id + '/complete_request') 56 | 57 | 58 | def create_button(self, name, price, currency, custom=None, options=None): 59 | params = { 60 | 'name': name, 61 | 'price_string': str(price), 62 | 'price_currency_iso': currency 63 | } 64 | 65 | if custom is not None: 66 | params['custom'] = custom 67 | 68 | if options is not None: 69 | for key, value in options.items(): 70 | params[key] = value 71 | 72 | return self.create_button_with_options(params) 73 | 74 | 75 | def create_button_with_options(self, options=None): 76 | response = self.post('/buttons', {'button': options}) 77 | 78 | if response['success'] == False: 79 | return response 80 | 81 | return { 82 | 'button': response['button'], 83 | 'embed_html': '
', 87 | 'success': True 88 | } 89 | 90 | 91 | def create_order_from_button(self, button_code): 92 | return self.post('/buttons/' + button_code + '/create_order') 93 | 94 | 95 | def create_user(self, email, password): 96 | params = { 97 | 'user': { 98 | 'email': email, 99 | 'password': password 100 | } 101 | } 102 | 103 | return self.post('/users', params) 104 | 105 | 106 | def delete(self, path, params=None): 107 | return self.__rpc.request('DELETE', path, params) 108 | 109 | 110 | def generate_receive_address(self, callback=None, label=None): 111 | params = {} 112 | if callback is not None: 113 | params['callback'] = callback 114 | 115 | if label is not None: 116 | params['label'] = label 117 | 118 | return self.post('/account/generate_receive_address', params)['address'] 119 | 120 | 121 | def get(self, path, params=None): 122 | return self.__rpc.request('GET', path, params) 123 | 124 | 125 | def get_all_addresses(self, query=None, page=0, limit=None): 126 | params = {} 127 | if query is not None: 128 | params['query'] = query 129 | 130 | if limit is not None: 131 | params['limit'] = limit 132 | 133 | return self.__get_paginated_resource( 134 | '/addresses', 'addresses', 'address', page, params) 135 | 136 | 137 | def get_balance(self): 138 | return self.get('/account/balance')['amount'] 139 | 140 | 141 | def get_buy_price(self, qty=1): 142 | return self.get('/prices/buy', {'qty': qty})['amount'] 143 | 144 | 145 | def get_contacts(self, query=None, page=0, limit=None): 146 | params = { 147 | 'page': page 148 | } 149 | 150 | if query is not None: 151 | params['query'] = query 152 | 153 | if limit is not None: 154 | params['limit'] = limit 155 | 156 | result = self.get('/contacts', params) 157 | 158 | contacts = [] 159 | for contact in results: 160 | if len(contact['email'].strip()) > 0: 161 | contacts.append(contact['email'].strip()) 162 | 163 | return { 164 | 'total_count': result['total_count'], 165 | 'num_pages': result['num_pages'], 166 | 'current_page': result['current_page'], 167 | 'contacts': contacts 168 | } 169 | 170 | 171 | def get_currencies(self): 172 | result = self.get('/currencies') 173 | 174 | currencies = [] 175 | for currency in result: 176 | currencies.append({ 177 | 'name': currency[0], 178 | 'iso': currency[1] 179 | }) 180 | 181 | return currencies 182 | 183 | 184 | def get_exchange_rate(self, from_=None, to=None): 185 | result = self.get('/currencies/exchange_rates') 186 | 187 | if from_ is not None and to is not None: 188 | return result[from_ + '_to_' + to] 189 | else: 190 | return result 191 | 192 | 193 | def get_order(self, id): 194 | return self.get('/orders/' + id) 195 | 196 | 197 | def get_orders(self, page=0): 198 | return self.__get_paginated_resource('/orders', 'orders', 'order', page) 199 | 200 | 201 | def __get_paginated_resource(self, 202 | resource, 203 | list_element, 204 | unwrap_element, 205 | page=0, 206 | params=None): 207 | if params is None: 208 | params = {} 209 | 210 | params['page'] = page 211 | 212 | result = self.get(resource, params) 213 | elements = [] 214 | for element in result[list_element]: 215 | elements.append(element[unwrap_element]) 216 | 217 | return { 218 | 'total_count': result['total_count'], 219 | 'num_pages': result['num_pages'], 220 | 'current_page': result['current_page'], 221 | list_element: elements 222 | } 223 | 224 | 225 | def get_receive_address(self): 226 | return self.get('/account/receive_address')['address'] 227 | 228 | 229 | def get_sell_price(self, qty=1): 230 | return self.get('/prices/sell', {'qty': qty})['amount'] 231 | 232 | 233 | def get_transaction(self, id): 234 | return self.get('/transactions/' + id) 235 | 236 | 237 | def get_transactions(self, page=0): 238 | return self.__get_paginated_resource( 239 | '/transactions', 'transactions', 'transaction', page) 240 | 241 | 242 | def get_transfers(self, page=0): 243 | return self.__get_paginated_resource( 244 | '/transfers', 'transfers', 'transfer', page) 245 | 246 | 247 | def get_user(self): 248 | return self.get('/users')['users'][0]['user'] 249 | 250 | 251 | def order(self, name, type_, price, price_currency_iso='USD'): 252 | params = { 253 | 'button': { 254 | 'name': name, 255 | 'type': type_, 256 | 'price_string': str(price), 257 | 'price_currency_iso': price_currency_iso 258 | } 259 | } 260 | 261 | return self.post('/orders', params) 262 | 263 | 264 | def post(self, path, params=None): 265 | return self.__rpc.request('POST', path, params) 266 | 267 | 268 | def put(self, path, params=None): 269 | return self.__rpc.request('PUT', path, params) 270 | 271 | 272 | def refund(self, transaction_id, refund_iso_code='BTC'): 273 | return self.post('/orders/' + str(transaction_id) + '/refund', 274 | {'order': {'refund_iso_code': refund_iso_code}}) 275 | 276 | 277 | def request_money(self, from_, amount, notes, amount_currency): 278 | params = { 279 | 'transaction': { 280 | 'from': from_ 281 | } 282 | } 283 | 284 | if amount_currency is not None: 285 | params['transaction']['amount_string'] = str(amount) 286 | params['transaction']['amount_currency_iso'] = amount_currency 287 | else: 288 | params['transaction']['amount'] = str(amount) 289 | 290 | if notes is not None: 291 | params['transaction']['notes'] = notes 292 | 293 | return self.post('/transactions/request_money', params) 294 | 295 | 296 | def resend_request(self, id): 297 | return self.put('/transactions/' + id + '/resend_request') 298 | 299 | 300 | def sell(self, amount): 301 | return self.post('/sells', {'qty': amount}) 302 | 303 | 304 | def send_money(self, 305 | to, 306 | amount, 307 | notes=None, 308 | user_fee=None, 309 | amount_currency=None): 310 | params = { 311 | 'transaction': { 312 | 'to': to 313 | } 314 | } 315 | 316 | if amount_currency is not None: 317 | params['transaction']['amount_string'] = str(amount) 318 | params['transaction']['amount_currency_iso'] = amount_currency 319 | else: 320 | params['transaction']['amount'] = str(amount) 321 | 322 | if notes is not None: 323 | params['transaction']['notes'] = notes 324 | 325 | if user_fee is not None: 326 | params['transaction']['user_fee'] = user_fee 327 | 328 | return self.post('/transactions/send_money', params) 329 | -------------------------------------------------------------------------------- /coinbase/error.py: -------------------------------------------------------------------------------- 1 | # ----- Author ---------------------------------------------------------------- 2 | 3 | __author__ = 'Michael Montero ' 4 | 5 | # ----- Public Classes -------------------------------------------------------- 6 | 7 | class CoinbaseException(Exception): 8 | 9 | def __init__(self, message, http_code=None, response=None): 10 | self.message = message 11 | self.http_code = http_code 12 | self.response = response 13 | 14 | 15 | def get_http_code(self): 16 | return self.http_code 17 | 18 | 19 | def get_message(self): 20 | return self.message 21 | 22 | 23 | def get_response(self): 24 | return self.response 25 | 26 | 27 | class CoinbaseAPIException(CoinbaseException): 28 | pass 29 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # ----- Imports --------------------------------------------------------------- 2 | 3 | from distutils.core import setup 4 | 5 | # ----- Instructions ---------------------------------------------------------- 6 | 7 | setup( 8 | name = 'coinbase', 9 | version = '0.3.0', 10 | packages = ['coinbase'], 11 | url = 'https://github.com/resy/coinbase_python3', 12 | license = 'MIT', 13 | author = 'Michael Montero', 14 | author_email = 'mike@resy.com', 15 | description = 'Python3 Coinbase API', 16 | classifiers = [ 17 | 'Development Status :: 4 - Beta', 18 | 'Environment :: Console', 19 | 'Environment :: Web Environment', 20 | 'Intended Audience :: Developers', 21 | 'License :: OSI Approved :: MIT License', 22 | 'Operating System :: OS Independent', 23 | 'Programming Language :: Python :: 3', 24 | 'Topic :: Software Development :: Libraries :: Python Modules', 25 | ] 26 | ) 27 | --------------------------------------------------------------------------------