Issues (11)

Severity
1
#! /usr/bin/env python
2
"""API Wrapper for Bitcoin.de Trading API."""
3
4
import requests
5
import time
6
import json
7
import hmac
8
import hashlib
9
import logging
10
import codecs
11
import decimal
12
import inspect
13
import urllib
14
15
from urllib.parse import urlencode
16
17
logging.basicConfig()
18
log = logging.getLogger(__name__)
19
requests_log = logging.getLogger("requests.packages.urllib3")
20
requests_log.propagate = True
21
22
__version__ = '4.1'
23
24
class ParameterBuilder(object):
25
    '''To verify given parameters for API.'''
26
    def __init__(self, avail_params, given_params, uri):
27
        self.verify_keys_and_values(avail_params, given_params)
28
        self.params = given_params
29
        self.create_url(uri)
30
31
    def verify_keys_and_values(self, avail_params, given_params):
32
        for k, v in given_params.items():
33
            if k not in avail_params:
34
                list_string = ', '.join(avail_params)
35
                raise KeyError("{} is not any of {}".format(k, list_string))
36
            if k == 'trading_pair':
37
                self.error_on_invalid_value(v, self.TRADING_PAIRS)
38
            elif k == 'type':
39
                self.error_on_invalid_value(v, self.TRADE_TYPES)
40
            elif k == 'currency':
41
                self.error_on_invalid_value(v, self.CURRENCIES)
42
            elif k == 'seat_of_bank':
43
                self.error_on_invalid_value(v, self.BANK_SEATS)
44
            elif k in ['min_trust_level', 'trust_level']:
45
                self.error_on_invalid_value(v, self.TRUST_LEVELS)
46
            elif k == 'payment_option':
47
                self.error_on_invalid_value(v, self.PAYMENT_OPTIONS)
48
            elif k == 'state':
49
                caller = inspect.stack()[2][3]
50
                if caller in ["showMyOrders", "showMyOrderDetails"]:
51
                    self.error_on_invalid_value(v, self.ORDER_STATES)
52
                elif caller in ["showMyTrades", "showMyTradesDetails"]:
53
                    self.error_on_invalid_value(v, self.TRADE_STATES)
54
55
    def error_on_invalid_value(self, value, list):
56
        if value not in list:
57
            list_string = ', '.join(str(x) for x in list)
58
            raise ValueError("{} is not any of {}".format(value, list_string))
59
60
    def create_url(self, uri):
61
        if self.params:
62
            self.encoded_string = urlencode(sorted(self.params.items()))
63
            self.url = uri + '?' + self.encoded_string
64
        else:
65
            self.encoded_string = ''
66
            self.url = uri
67
68
69
    TRADING_PAIRS = ['btceur', 'bcheur', 'etheur', 'btgeur', 'bsveur', 'ltceur',
70
                     'iotabtc', 'dashbtc', 'gntbtc', 'ltcbtc', 'xrpeur', 'usdteur', 'btcusdt', 'ethusdt']
71
    ORDER_TYPES = ['buy', 'sell']
72
    CURRENCIES = ['btc', 'bch', 'eth', 'btg', 'bsv', 'ltc',
73
                  'iota', 'dash', 'gnt', 'xrp', 'usdt']
74
    BANK_SEATS = ['AT', 'BE', 'BG', 'CH', 'CY', 'CZ',
75
                  'DE', 'DK', 'EE', 'ES', 'FI', 'FR',
76
                  'GB', 'GR', 'HR', 'HU', 'IE', 'IS',
77
                  'IT', 'LI', 'LT', 'LU', 'LV', 'MT',
78
                  'MQ', 'NL', 'NO', 'PL', 'PT', 'RO',
79
                  'SE', 'SI', 'SK']
80
    TRUST_LEVELS = ['bronze', 'silver', 'gold', 'platin']
81
    TRADE_STATES = [-1, 0, 1]
82
    ORDER_STATES = [-2, -1, 0]
83
    PAYMENT_OPTIONS = [1, 2, 3]
84
    TRADE_TYPES = ['all', 'buy', 'sell', 'inpayment',
85
                   'payout', 'affiliate', 'welcome_btc',
86
                   'buy_yubikey', 'buy_goldshop',
87
                   'buy_diamondshop', 'kickback',
88
                   'outgoing_fee_voluntary']
89
90
def HandleRequestsException(e):
91
    """Handle Exception from request."""
92
    log.warning(e)
93
94
95
def HandleAPIErrors(r):
96
    """To handle Errors from BTCDE API."""
97
    valid_status_codes = [200, 201, 204]
98
    if r.status_code not in valid_status_codes:
99
        content = r.json()
100
        errors = content.get('errors')
101
        log.warning('API Error Code: {}'.format(str(errors[0]['code'])))
102
        log.warning('API Error Message: {}'.format(errors[0]['message']))
103
        log.warning('API Error URL: {}'.format(r.url))
104
        return False
105
    else:
106
        return True
107
108
class Connection(object):
109
    """To provide connection credentials to the trading API"""
110
    def __init__(self, api_key, api_secret, ssl_verify=False):
111
        self.api_key = api_key
112
        self.api_secret = api_secret
113
        # set initial self.nonce
114
        self.nonce = int(time.time() * 1000000)
115
        # Bitcoin.de API URI
116
        self.apihost = 'https://api.bitcoin.de'
117
        self.apiversion = 'v4'
118
        self.apibase = f'{self.apihost}/{self.apiversion}/'
119
        self.ssl_verify = ssl_verify # avoid warnings for ssl-cert
120
121
    def build_hmac_sign(self, md5string, method, url):
122
        hmac_data = '#'.join([method, url, self.api_key, str(self.nonce), md5string])
123
        hmac_signed = hmac.new(bytearray(self.api_secret.encode()), msg=hmac_data.encode(), digestmod=hashlib.sha256).hexdigest()
124
        return hmac_signed
125
126
    def set_header(self, url, method, encoded_string):
127
        # raise self.nonce before using
128
        self.nonce = int(time.time() * 1000000)
129
        if method == 'POST':
130
            md5_encoded_query_string = hashlib.md5(encoded_string.encode()).hexdigest()
131
        else:
132
            md5_encoded_query_string = hashlib.md5(b'').hexdigest()
133
        hmac_signed = self.build_hmac_sign(md5_encoded_query_string,
134
                                           method, url)
135
        # set header
136
        header = {'content-type':
137
                  'application/x-www-form-urlencoded; charset=utf-8',
138
                  'X-API-KEY': self.api_key,
139
                  'X-API-NONCE': str(self.nonce),
140
                  'X-API-SIGNATURE': hmac_signed }
141
        return header
142
143
    def send_request(self, url, method, header, encoded_string):
144
        if method == 'GET':
145
            r = requests.get(url, headers=(header),
146
                             stream=True, verify=self.ssl_verify)
147
        elif method == 'POST':
148
            r = requests.post(url, headers=(header), data=encoded_string,
149
                              stream=True, verify=self.ssl_verify)
150
        elif method == 'DELETE':
151
            r = requests.delete(url, headers=(header),
152
                                stream=True, verify=self.ssl_verify)
153
        return r
0 ignored issues
show
The variable r does not seem to be defined for all execution paths.
Loading history...
154
155
    def APIConnect(self, method, params):
156
        """Transform Parameters to URL"""
157
        header = self.set_header(params.url, method,
158
                                 params.encoded_string)
159
        log.debug('Set Header: {}'.format(header))
160
        try:
161
            r = self.send_request(params.url, method, header,
162
                                  params.encoded_string)
163
            # Handle API Errors
164
            if HandleAPIErrors(r):
165
                # get results
166
                result = r.json(parse_float=decimal.Decimal)
167
            else:
168
                result = {}
169
        except requests.exceptions.RequestException as e:
170
            HandleRequestsException(e)
171
            result = {}
172
        return result
173
174
    def addToAddressPool(self, currency, address, **args):
175
        """Add address to pool"""
176
        uri = f'{self.apibase}{currency}/address'
177
        params = {'address': address}
178
        params.update(args)
179
        avail_params = ['address', 'amount_usages', 'comment']
180
        p = ParameterBuilder(avail_params, params, uri)
181
        return self.APIConnect('POST', p)
182
183
    def removeFromAddressPool(self, currency, address):
184
        """Remove address from pool"""
185
        uri = f'{self.apibase}{currency}/address/{address}'
186
        params = {'currency': currency, 'address': address}
187
        avail_params = ['currency', 'address']
188
        p = ParameterBuilder({}, {}, uri)
189
        p.verify_keys_and_values(avail_params, params)
190
        return self.APIConnect('DELETE', p)
191
192
    def listAddressPool(self, currency, **args):
193
        """List address pool"""
194
        uri = f'{self.apibase}{currency}/address'
195
        params = args
196
        avail_params = ['usable', 'comment', 'page']
197
        p = ParameterBuilder(avail_params, params, uri)
198
        return self.APIConnect('GET', p)
199
200
    def showOrderbook(self, order_type, trading_pair, **args):
201
        """Search Orderbook for offers."""
202
        uri = f'{self.apibase}{trading_pair}/orderbook'
203
        params = {'type': order_type}
204
        params.update(args)
205
        avail_params = ['type', 'trading_pair', 'amount_currency_to_trade', 'price',
206
                        'order_requirements_fullfilled',
207
                        'only_kyc_full', 'only_express_orders', 'payment_option',
208
                        'sepa_option', 'only_same_bankgroup', 'only_same_bic',
209
                        'seat_of_bank', 'page_size']
210
        p = ParameterBuilder(avail_params, params, uri)
211
        return self.APIConnect('GET', p)
212
213
    def showOrderDetails(self, trading_pair, order_id):
214
        """Show details for an offer."""
215
        uri = f'{self.apibase}{trading_pair}/orders/public/details/{order_id}'
216
        params = {'trading_pair': trading_pair, 'order_id': order_id}
217
        avail_params = ['trading_pair', 'order_id']
218
        p = ParameterBuilder({}, {}, uri)
219
        p.verify_keys_and_values(avail_params, params)
220
        return self.APIConnect('GET', p)
221
222
    def createOrder(self, order_type, trading_pair, max_amount_currency_to_trade, price, **args):
223
        """Create a new Order."""
224
        uri = f'{self.apibase}{trading_pair}/orders'
225
        # Build parameters
226
        params = {'type': order_type,
227
                  'max_amount_currency_to_trade': max_amount_currency_to_trade,
228
                  'price': price}
229
        params.update(args)
230
        avail_params = ['type', 'max_amount_currency_to_trade', 'price',
231
                        'min_amount_currency_to_trade', 'end_datetime',
232
                        'new_order_for_remaining_amount', 'trading_pair',
233
                        'min_trust_level', 'only_kyc_full', 'payment_option',
234
                        'sepa_option', 'seat_of_bank']
235
        p = ParameterBuilder(avail_params, params, uri)
236
        p.verify_keys_and_values(avail_params, {'trading_pair': trading_pair})
237
        return self.APIConnect('POST', p)
238
239
    def deleteOrder(self, order_id, trading_pair):
240
        """Delete an Order."""
241
        # Build parameters
242
        uri = f'{self.apibase}{trading_pair}/orders/{order_id}'
243
        avail_params = ['order_id', 'trading_pair']
244
        params = { 'order_id': order_id, 'trading_pair': trading_pair}
245
        p = ParameterBuilder({}, {}, uri)
246
        p.verify_keys_and_values(avail_params, params)
247
        return self.APIConnect('DELETE', p)
248
249
    def showMyOrders(self, **args):
250
        """Query and Filter own Orders."""
251
        # Build parameters
252
        params = args
253
        avail_params = ['type', 'trading_pair', 'state',
254
                        'date_start', 'date_end', 'page']
255
        if params.get("trading_pair"):
256
            uri = f'{self.apibase}{params["trading_pair"]}/orders'
257
            del params["trading_pair"]
258
        else:
259
            uri = f'{self.apibase}orders'
260
        p = ParameterBuilder(avail_params, params, uri)
261
        return self.APIConnect('GET', p)
262
263
    def showMyOrderDetails(self, trading_pair, order_id):
264
        """Details to an own Order."""
265
        uri = f'{self.apibase}{trading_pair}/orders/{order_id}'
266
        p = ParameterBuilder({}, {}, uri)
267
        return self.APIConnect('GET', p)
268
269
    def executeTrade(self, trading_pair, order_id, order_type, amount, payment_option=2):
270
        """Buy/Sell on a specific Order."""
271
        uri = f'{self.apibase}{trading_pair}/trades/{order_id}'
272
        params = { 'type': order_type,
273
                   'amount_currency_to_trade': amount,
274
                   'payment_option': payment_option
275
                 } 
276
        avail_params = ['type', 'amount_currency_to_trade', 'payment_option']
277
        p = ParameterBuilder(avail_params, params, uri)
278
        return self.APIConnect('POST', p)
279
280
    def showMyTrades(self, **args):
281
        """Query and Filter on past Trades."""
282
        # Build parameters
283
        params = args
284
        avail_params = ['type', 'trading_pair', 'state',
285
                        'only_trades_with_action_for_payment_or_transfer_required',
286
                        'payment_method', 'date_start', 'date_end', 'page']
287
        if params.get("trading_pair"):
288
            uri = f'{self.apibase}{params["trading_pair"]}/trades'
289
            del params["trading_pair"]
290
        else:
291
            uri = f'{self.apibase}trades'
292
        p = ParameterBuilder(avail_params, params, uri)
293
        return self.APIConnect('GET', p)
294
295
    def showMyTradeDetails(self, trading_pair, trade_id):
296
        """Details to a specific Trade."""
297
        params = {'trading_pair': trading_pair, 'trade_id': trade_id}
298
        avail_params = [ 'trading_pair', 'trade_id' ]
299
        uri = f'{self.apibase}{trading_pair}/trades/{trade_id}'
300
        p = ParameterBuilder({}, {}, uri)
301
        p.verify_keys_and_values(avail_params, params)
302
        return self.APIConnect('GET', p)
303
304
    def markCoinsAsTransferred(self, trading_pair, trade_id, amount_currency_to_trade_after_fee):
305
        """Mark trade as transferred."""
306
        params = {'amount_currency_to_trade_after_fee': amount_currency_to_trade_after_fee,
307
                  'trading_pair': trading_pair, 'trade_id': trade_id}
308
309
        avail_params = [ 'trading_pair', 'trade_id', 'amount_currency_to_trade_after_fee' ]
310
        uri = f'{self.apibase}{trading_pair}/trades/{trade_id}/mark_coins_as_transferred'
311
        p = ParameterBuilder(avail_params,
312
            {'amount_currency_to_trade_after_fee': amount_currency_to_trade_after_fee}, uri)
313
        p.verify_keys_and_values(avail_params, params)
314
        return self.APIConnect('POST', p)
315
316
    def markTradeAsPaid(self, trading_pair, trade_id, volume_currency_to_pay_after_fee):
317
        """Mark traded as paid."""
318
        params = {'volume_currency_to_pay_after_fee': volume_currency_to_pay_after_fee,
319
                  'trading_pair': trading_pair, 'trade_id': trade_id}
320
321
        avail_params = [ 'trading_pair', 'trade_id', 'volume_currency_to_pay_after_fee' ]
322
        uri = f'{self.apibase}{trading_pair}/trades/{trade_id}/mark_trade_as_paid'
323
        p = ParameterBuilder(avail_params,
324
            {'volume_currency_to_pay_after_fee': volume_currency_to_pay_after_fee}, uri)
325
        p.verify_keys_and_values(avail_params, params)
326
        return self.APIConnect('POST', p)
327
328
    def markCoinsAsReceived(self, trading_pair, trade_id, amount_currency_to_trade_after_fee, rating):
329
        """Mark coins as received."""
330
        params = {'amount_currency_to_trade_after_fee': amount_currency_to_trade_after_fee,
331
                  'trading_pair': trading_pair, 'trade_id': trade_id, 'rating': rating}
332
        params_post = {'amount_currency_to_trade_after_fee': amount_currency_to_trade_after_fee,
333
                       'rating': rating}
334
        avail_params = [ 'trading_pair', 'trade_id', 'amount_currency_to_trade_after_fee', 'rating' ]
335
        uri = f'{self.apibase}{trading_pair}/trades/{trade_id}/mark_coins_as_received'
336
        p = ParameterBuilder(avail_params, params_post, uri)
337
        p.verify_keys_and_values(avail_params, params)
338
        return self.APIConnect('POST', p)
339
340
    def markTradeAsPaymentReceived(self, trading_pair, trade_id,
341
                                   volume_currency_to_pay_after_fee, rating,
342
                                   is_paid_from_correct_bank_account):
343
        """Mark coins as received."""
344
        params = {'volume_currency_to_pay_after_fee': volume_currency_to_pay_after_fee,
345
                  'trading_pair': trading_pair, 'trade_id': trade_id, 'rating': rating}
346
        params_post = {'volume_currency_to_pay_after_fee': volume_currency_to_pay_after_fee,
347
                       'rating': rating,
348
                       'is_paid_from_correct_bank_account': is_paid_from_correct_bank_account}
349
        avail_params = [ 'trading_pair', 'trade_id', 'volume_currency_to_pay_after_fee',
350
                         'rating', 'is_paid_from_correct_bank_account' ]
351
        uri = f'{self.apibase}{trading_pair}/trades/{trade_id}/mark_trade_as_payment_received'
352
        p = ParameterBuilder(avail_params, params_post, uri)
353
        p.verify_keys_and_values(avail_params, params)
354
        return self.APIConnect('POST', p)
355
356
    def addTradeRating(self, trading_pair, trade_id, rating):
357
        """Mark coins as received."""
358
        params = {'trading_pair': trading_pair, 'trade_id': trade_id, 'rating': rating}
359
        params_post = {'rating': rating}
360
        avail_params = [ 'trading_pair', 'trade_id', 'rating' ]
361
        uri = f'{self.apibase}{trading_pair}/trades/{trade_id}/add_trade_rating'
362
        p = ParameterBuilder(avail_params, params_post, uri)
363
        p.verify_keys_and_values(avail_params, params)
364
        return self.APIConnect('POST', p)
365
366
    def showAccountInfo(self):
367
        """Query on Account Infos."""
368
        uri = f'{self.apibase}account'
369
        p = ParameterBuilder({}, {}, uri)
370
        return self.APIConnect('GET', p)
371
372
    def showOrderbookCompact(self, trading_pair):
373
        """Bids and Asks in compact format."""
374
        params = {'trading_pair': trading_pair}
375
        avail_params = ['trading_pair']
376
        uri = f'{self.apibase}{trading_pair}/orderbook/compact'
377
        # Build parameters
378
        p = ParameterBuilder({}, {}, uri)
379
        p.verify_keys_and_values(avail_params, params)
380
        return self.APIConnect('GET', p)
381
382
    def showPublicTradeHistory(self, trading_pair, **args):
383
        """All successful trades of the last 24 hours."""
384
        params = { 'trading_pair': trading_pair }
385
        params.update(args)
386
        avail_params = ['trading_pair', 'since_tid']
387
        uri = f'{self.apibase}{trading_pair}/trades/history'
388
        if params.get('since_tid'):
389
            del params["trading_pair"]
390
            p = ParameterBuilder(avail_params, params, uri)
391
        else:
392
            p = ParameterBuilder({}, {}, uri)
393
        p.verify_keys_and_values(avail_params, params)
394
        return self.APIConnect('GET', p)
395
396
    def showRates(self, trading_pair):
397
        """Query of the average rate last 3 and 12 hours."""
398
        uri = f'{self.apibase}{trading_pair}/rates'
399
        params = {'trading_pair': trading_pair}
400
        avail_params = ['trading_pair']
401
        # Build parameters
402
        p = ParameterBuilder({}, {}, uri)
403
        p.verify_keys_and_values(avail_params, params)
404
        return self.APIConnect('GET', p)
405
406
    def showAccountLedger(self, currency, **args):
407
        """Query on Account statement."""
408
        params = {'currency': currency}
409
        params.update(args)
410
        uri = f'{self.apibase}{currency}/account/ledger'
411
        avail_params = ['currency', 'type',
412
                        'datetime_start', 'datetime_end', 'page']
413
        p = ParameterBuilder(avail_params, params, uri)
414
        del params['currency']
415
        p = ParameterBuilder(avail_params, params, uri)
416
        return self.APIConnect('GET', p)
417
418
    def showPermissions(self):
419
        """Show permissions that are allowed for used API key"""
420
        uri = f'{self.apibase}permissions'
421
        p = ParameterBuilder({}, {}, uri)
422
        return self.APIConnect('GET', p)