22

I'm trying to use Python to access the trading API at poloniex.com, a cryptocurrency exchange. To do this I must follow this prescription:

All calls to the trading API are sent via HTTP POST to https://poloniex.com/tradingApi and must contain the following headers:

Key - Your API key.
Sign - The query's POST data signed by your key's "secret" according to the HMAC-SHA512 method.

Additionally, all queries must include a "nonce" POST parameter. The nonce parameter is an integer which must always be greater than the previous nonce used.

Here is what I have so far. My current issue is that I do not know how to compile the POST url so that it can be signed without sending the incomplete request first. This obviously doesn't work.

import requests
import hmac
import hashlib
import time

headers = { 'nonce': '',
            'Key' : 'myKey',
            'Sign': '',}
payload = { 'command': 'returnCompleteBalances',
            'account': 'all'}
secret = 'mySecret'

headers['nonce'] = int(time.time())
response = requests.post( 'https://poloniex.com/tradingApi', params= payload, headers= headers )
headers['Sign'] = hmac.new( secret, response.url, hashlib.sha512)
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
Werhli
  • 251
  • 1
  • 2
  • 4

1 Answers1

31

Create a prepared request; you can add headers to that after the body has been created:

import requests
import hmac
import hashlib


request = requests.Request(
    'POST', 'https://poloniex.com/tradingApi',
    data=payload, headers=headers)
prepped = request.prepare()
signature = hmac.new(secret, prepped.body, digestmod=hashlib.sha512)
prepped.headers['Sign'] = signature.hexdigest()

with requests.Session() as session:
    response = session.send(prepped)

I changed your params argument to data; for a POST request it is customary to send the parameters in the body, not the URL.

For the nonce, I'd use a itertools.count() object, seeded from the current time so restarts don't affect it. According to the Poloniex API documentation (which you quoted in your question), the nonce is part of the POST body, not the headers, so put it in the payload dictionary:

from itertools import count
import time

# store as a global variable
NONCE_COUNTER = count(int(time.time() * 1000))

# then every time you create a request
payload['nonce'] = next(NONCE_COUNTER)

Using int(time.time()) would re-use the same number if you created more than one request per second. The example code provided by Poloniex uses int(time.time()*1000) to make it possible to create a request every microsecond instead, but using your own monotonically increasing counter (seeded from time.time()) is far more robust.

You can also encapsulate the digest signing process in a custom authentication object; such an object is passed in the prepared request as the last step in preparation:

import hmac
import hashlib

class BodyDigestSignature(object):
    def __init__(self, secret, header='Sign', algorithm=hashlib.sha512):
        self.secret = secret
        self.header = header
        self.algorithm = algorithm

    def __call__(self, request):
        body = request.body
        if not isinstance(body, bytes):   # Python 3
            body = body.encode('latin1')  # standard encoding for HTTP
        signature = hmac.new(self.secret, body, digestmod=self.algorithm)
        request.headers[self.header] = signature.hexdigest()
        return request

Use this with your requests calls:

response = requests.post(
    'https://poloniex.com/tradingApi',
    data=payload, headers=headers, auth=BodyDigestSignature(secret))

The argument passed in is the secret used in the HMAC digest; you can also pass in a different header name.

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
  • That was so fast, Thanks very much! – Werhli Feb 07 '17 at 23:39
  • @MartijnPieters when I run this I get an error saying: 'Request' object has no attribute 'body'. for this line: signature = hmac.new(secret, request.body, digestmod=hashlib.sha512) – Mustard Tiger Nov 11 '17 at 04:49
  • When I run the code now I constantly get : "error":"invalid command", similar to the issue described here: https://kaijento.github.io/2017/05/05/poloniex-api-invalid-command/ – Mustard Tiger Nov 11 '17 at 15:42
  • @abcla: I see that the OP sent parameters in the URL; that should probably be in the post body instead. I've adjusted the answer to reflect that. – Martijn Pieters Nov 11 '17 at 15:46
  • 1
    @abcla: another potential issue: the nonce should go in the POST body, not the headers, at least according to the [current documentation](https://poloniex.com/support/api/). – Martijn Pieters Nov 11 '17 at 15:47
  • Hi thank you for your help, with the new edits it now works fine. – Mustard Tiger Nov 11 '17 at 15:51
  • I'm having trouble trying to add the nonce into the body in the auth __call__. Any hints? – pjz Oct 05 '18 at 20:29
  • @pjz: that would require far more information on exactly what you are trying to do. Perhaps you can post a question? – Martijn Pieters Oct 05 '18 at 21:23
  • I thought it was plenty clear, but done: https://stackoverflow.com/questions/52705158/how-do-i-sign-the-body-of-a-requests-request-in-an-auth-objects-call-method – pjz Oct 08 '18 at 15:06
  • @pjz: thanks; what was not clear, for example, is that you are talking about a `application/x-www-form-urlencoded` or `multipart/form-data` POST body. That's not a given, many APIs use JSON for example. – Martijn Pieters Oct 08 '18 at 15:11
  • @pjz: ah, perhaps this confusion stems from the API documentation involved, where they make no mention whatsoever of what kind of payload the POST body should use. Only from the PHP sample code using `http_build_query` does it become clear that they mean you to POST using `application/x-www-form-urlencoded` encoding. That kind of implicit assumption in API documentation can be frustrating (most *RESTful* APIs use JSON these days, *and* a nonce is not commonly stuck in the body). – Martijn Pieters Oct 08 '18 at 16:18