DEV Community

Cover image for Craft a Kraken API client with PHP8 (1/2)
Nicolas Bonnici
Nicolas Bonnici

Posted on • Edited on

Craft a Kraken API client with PHP8 (1/2)

Update: This is the first part of this post, you can find the second part right here.

Today we gonna build from scratch a simple but flexible client to use the Kraken API, which is at writing time the fourth biggest exchange in volume. The final result will be a Composer package that you'll can easily use as dependency in any PHP project using Composer as dependencies manager.

This client will support any Kraken API endpoint, public or private, and also open WebSocket streams to perform real time actions or monitoring.

This first part will describe the REST usage of the Kraken API, then the second will be about WebSocket.

Accessing public endpoints

Like most exchange APIs, Kraken allow us to retrieve market data without authentication. Let's say we need to access all exchange trading pairs, we simply need to make a GET request to this endpoint: https://api.kraken.com/0/public/AssetPairs then parse response, nothing fancy.

Accessing private endpoints

When dealing with private endpoints, the Kraken API need both a specific header containing your API client key, but also require you to sign your request under a API-SIGN header built from your payload, the endpoint URI, your API secret Base64 encoded and a nonce, which is a unsigned 64-bit integer.

Be careful with this nonce parameter, you cannot make API calls using smaller nonce so don't use a random big number or your client will be trapped be sure to follow the current Unix timestamp for instance, more info here on the Kraken API documentation.

The other point of attention, is the fact that unlike numerous other exchange API, Kraken do not offer a public test environment, where you safe to connect integration unit tests for instance. With this API on some critical endpoint, such like interact with order book, you'll need to pass an extra validate parameter.

Image description

The code

Image description

Talk is cheap, show me the code.

-- Linus Torvald, Linux creator

No need to over engineering everything, make some abstraction layers for the request or response, make tons of custom methods for each endpoint. We'll put the minimal amount of code to cover the maximum API use cases.

<?php
namespace NicolasBonnici\PhpKrakenApiClient;

use NicolasBonnici\PhpKrakenApiClient\Exception\KrakenAPIException;

class KrakenAPIClient
{
    public const USER_AGENT = __NAMESPACE__;
    public const DEFAULT_API_ROOT = 'https://api.kraken.com';
    public const DEFAULT_API_VERSION = '0';
    public const AUTHENTICATED_ENDPOINTS_URI_PATH = 'private/';

    public function __construct(
        private ?string $key = null,
        private ?string $secret = null,
        private string $apiRoot = self::DEFAULT_API_ROOT,
        private string $version = self::DEFAULT_API_VERSION,
        private bool $sslCheck = true,
        private bool|\CurlHandle $curl = false,
    ) {
        if (!function_exists('curl_init')) {
            throw new KrakenAPIException('No curl extension available.');
        }

        $this->loadClient();
    }

    public function __destruct()
    {
        if (function_exists('curl_close') && $this->curl instanceof \CurlHandle) {
            curl_close($this->curl);
        }
    }

    /**
     * @throws KrakenAPIException
     */
    public function query(
        string $endpoint,
        array $request = [],
        array $headers = []
    ): array {
        $authenticated = str_starts_with($endpoint, self::AUTHENTICATED_ENDPOINTS_URI_PATH);

        $uri = sprintf('/%s/%s', $this->version, $endpoint);

        if (true === $authenticated) {
            $headers = $this->signRequest($uri, $request, $headers);
        } else {
            $this->buildRequest($request);
        }

        curl_setopt($this->curl, CURLOPT_URL, $this->apiRoot . $uri);
        curl_setopt($this->curl, CURLOPT_HTTPHEADER, $headers);

        $response = curl_exec($this->curl);
        if ($response === false) {
            throw new KrakenAPIException(sprintf('CURL error: "%s"', curl_error($this->curl)));
        }

        $response = json_decode($response, true);
        if (!is_array($response)) {
            throw new KrakenAPIException(sprintf('JSON decode error: "%s"', json_last_error_msg()));
        }

        if (false === isset($response['result'])) {
            throw new KrakenAPIException(
                $response['error'] ? implode('. ', $response['error']) : 'Unknown error occur'
            );
        }

        return $response['result'];
    }

    private function loadClient(): void
    {
        if ($this->curl === false) {
            $this->curl = curl_init();
        }

        curl_setopt_array($this->curl, [
            CURLOPT_SSL_VERIFYPEER => $this->sslCheck,
            CURLOPT_SSL_VERIFYHOST => 2,
            CURLOPT_USERAGENT => self::USER_AGENT,
            CURLOPT_POST => true,
            CURLOPT_RETURNTRANSFER => true
        ]);
    }

    private function signRequest(string $uri, array $request, array $headers): array
    {
        if (!$this->key || !$this->secret) {
            throw new KrakenAPIException('No API credentials, please provide both key and secret.');
        }

        if (!isset($request['nonce'])) {
            $request['nonce'] = $this->generateNonce();
        }

        $sign = hash_hmac(
            'sha512',
            $uri . hash('sha256', $request['nonce'] . $this->buildRequest($request), true),
            base64_decode($this->secret),
            true
        );

        return array_merge($headers, [
            'API-Key: ' . $this->key,
            'API-Sign: ' . base64_encode($sign)
        ]);
    }

    private function buildRequest(array $request = []): string
    {
        $httpQuery = http_build_query($request, '', '&');
        curl_setopt($this->curl, CURLOPT_POSTFIELDS, $httpQuery);

        return $httpQuery;
    }

    private function generateNonce(): string
    {
        // generate a 64 bit nonce using a timestamp at microsecond resolution
        $nonce = explode(' ', microtime());
        return $nonce[1] . str_pad(substr($nonce[0], 2, 6), 6, '0');
    }

    public function setKey(?string $key): self
    {
        $this->key = $key;

        return $this;
    }

    public function setSecret(?string $secret): self
    {
        $this->secret = $secret;

        return $this;
    }

    public function setApiRoot(string $apiRoot): self
    {
        $this->apiRoot = $apiRoot;

        return $this;
    }

    public function setVersion(string $version): self
    {
        $this->version = $version;

        return $this;
    }

    public function setSslCheck(bool $sslCheck): self
    {
        $this->sslCheck = $sslCheck;

        return $this;
    }
}

Enter fullscreen mode Exit fullscreen mode

Almost all API use cases are covered with this simple class and his query method. The Curl extension is the only dependency used, with approximately 152 lines of code at this level.

Thank to the Kraken API architecture, it's very easy for the query method to detect if we need to authenticate and sign request for a private endpoint.

Let's break this code in chunks

Constructor

The __construct method allow us to directly retrieve a freshly instantiated API client, all classe attributes are optional to request data from a public Kraken API endpoint.

For private calls that need to be authenticated, just pass your API client key and secret. Let's say you already had a specific curl instance in your project with his own custom configuration, you can override the one created from this client.

The query method

Signature is self explaining here, note that the first $endpoint parameter is relative to the API root, the value you pass is the same you'll find in API documentation, without the first trailing slash, example public/AssetPairs.

Image description

No need to sign your calls, configure fancy headers etc this method will do all the magic for you when needed. All you need to do is pass the needed $endpoint first parameter and if needed, data onto the second $request parameter.

Under the hood, the query method parse the requested endpoint to determine if authentication is needed. In that the request will be signed with API client credentials, all the needed header and parameters will be added. Otherwise we just parse and build the POST request, if any.

We could use and implement an abstraction layer on top of curl for the request and also the response, but for this project let's keep things simple stupid as suggested in the KISS principle.

Request signature

The generateNonce() method, rather than simply using a Unix timestamp in seconds return an integer. This interger is casted as a string variable but don't worry the HTTP request will be sent finally as a string by Curl later anyway.

While the signRequest() method will set the required nonce parameter if missing, the API-KEY and the API-SIGN headers containing respectively the API client key and a signature built with a SHA 512 hash from the requested endpoint, nonce and request data as a SHA 256 hash.

Accessors (only setters)

To allow more flexibility at usage, adding setters like setKey() or setSecret() methods can be very useful when dealing with many API clients on behalf your users to tweak the client settings between calls if needed.

How to use it

Install

First install the nicolasbonnici/php-kraken-api-client Composer package on versions around 2.1 branch onto your project.

composer require nicolasbonnici/php-kraken-api-client:2.1
Enter fullscreen mode Exit fullscreen mode

Public endpoints

Now retrieve all available pairs from public/AssetPairs public endpoint.

$client = new KrakenAPIClient();
$pairs = $client->query('public/AssetPairs', ['pair' => 'BTCUSDT']);
var_dump($pairs);

Enter fullscreen mode Exit fullscreen mode

Output

array(467) {
  ["1INCHEUR"]=>
  array(18) {
    ["altname"]=>
    string(8) "1INCHEUR"
    ["wsname"]=>
    string(9) "1INCH/EUR"
    ["aclass_base"]=>
    string(8) "currency"
    ["base"]=>
    string(5) "1INCH"
    ["aclass_quote"]=>
    string(8) "currency"
    ["quote"]=>
    string(4) "ZEUR"
    ["lot"]=>
    string(4) "unit"
    ["pair_decimals"]=>
    int(3)
    ["lot_decimals"]=>
    int(8)
    ["lot_multiplier"]=>
    int(1)
    ["leverage_buy"]=>
    array(0) {
    }
    ["leverage_sell"]=>
    array(0) {
    }
    ["fees"]=>
    array(9) {
      [0]=>
      array(2) {
        [0]=>
        int(0)
        [1]=>
        float(0)
      }
      ...
    }
    ["fee_volume_currency"]=>
    string(4) "ZUSD"
    ["margin_call"]=>
    int(80)
    ["margin_stop"]=>
    int(40)
    ["ordermin"]=>
    string(1) "1"
  }
...
}
Enter fullscreen mode Exit fullscreen mode

Private endpoints

Retrieve user's account balance from private endpoint.

$client = new KrakenAPIClient('YOUR API KEY', 'YOUR API SECRET');
$balances = $client->query('private/Balance');
var_dump($balances);
Enter fullscreen mode Exit fullscreen mode

Output

array(467) {
  ["ZEUR"]=>string(7) "47913.8"
  ...
}
Enter fullscreen mode Exit fullscreen mode

According to official API documentation, this endpoint "Retrieve all cash balances, net of pending withdrawals", note that this endpoint will return an empty response if you got no asset on your portfolio in Kraken exchange.

Next

In the next part of this article, we'll learn about how to interact in real time with the Kraken API using WebSocket, then bundle the final code as a Composer package.

Feel free to contribute on the project located here on Gitlab and also comments and/or reactions are still welcome.

Thank you for reading, and be sure to subscribe for the next part if you enjoy.

Top comments (0)