How to Implement Authentication with the NIMLAB Crypto Exchange API in PHP?

I’m trying to implement authentication to connect with the NIMLAB crypto exchange, following their official documentation: NIMLAB API Docs.

However, I keep running into issues with the authentication and signature generation. Unfortunately, the solutions I’ve tried so far are not working.

The NIMLAB support team provided a Python example, which they claim works perfectly:

import json
import hashlib
import asyncio
import aiohttp
import logging

from aiohttp.client_exceptions import ContentTypeError, ClientConnectorError
from urllib.parse import urlencode, quote

class NIMLABAPI:
    def __init__(self, apiUrl, apiKey, apiSecret):
        if not apiUrl:
            raise ValueError('apiUrl is required in class ExchangerAPI')
        if not apiKey:
            raise ValueError('apiKey is required in class ExchangerAPI')
        if not apiSecret:
            raise ValueError('apiSecret is required in class ExchangerAPI')

        self.config = {
            "apiUrl": apiUrl,
            "apikey": apiKey,
            "secret": apiSecret
        }

        self.session = aiohttp.ClientSession()

    def generate_hash(self, get_params, post_params):

        for key in get_params:
            if isinstance(get_params[key], list):
                get_params[key] = [str(value) for value in get_params[key]]
            else:
                get_params[key] = str(get_params[key])

        params = {**get_params, **post_params}
        checksum_params = hashlib.sha256(json.dumps(params, ensure_ascii=False, separators=(',', ':')).encode()).hexdigest()
        HASH = hashlib.sha256((checksum_params + self.config['secret']).encode()).hexdigest()
        return HASH

    async def call(self, method, param=None, rewriteConfig=None):
        if param is None:
            param = {}
        get = param.get('get', {})
        post = param.get('post', {})
        post_out =  json.dumps(post, ensure_ascii=False)

        if not isinstance(get, dict):
            return {"errorCode": 20, "message": 'Error invalid "get" params must be object'}
        if not isinstance(post, dict):
            return {"errorCode": 21, "message": 'Error invalid "post" params must be object'}

        configService = {**self.config, **(rewriteConfig or {})}
        method_parts = method.split(':')
        typeMethod = 'POST'

        if len(method_parts) == 2:
            typeMethod, method = method_parts[0].upper(), method_parts[1]
        else:
            method = method_parts[0]

        if typeMethod == 'GET':
            get['time'] = int(1000 * asyncio.get_event_loop().time())

        url_params = "&".join(f"{key}={value}" if not isinstance(value, list) else "&".join(f"{key}[]={item}" for item in value) for key, value in get.items())

        url = f"{configService['apiUrl']}{method}?{url_params}" if url_params else f"{configService['apiUrl']}{method}"

        HASH = self.generate_hash(get, post)
        print(f"URL: {url}")
        async with self.session.request(typeMethod, url, headers={
            'cache-control': 'no-cache',
            'apikey': configService['apikey'],
            'hash': HASH,
            'Content-Type': 'application/json'
        }, data=post_out) as response:
            while True:
                try:
                    if response.status in [502, 504, 409]:
                        logging.warning(f"Error {response.status}: {response.reason}. Retrying in a while...")
                        await asyncio.sleep(15)
                        continue

                    if response.status != 200:
                        cf_mitigated = response.headers.get('cf-mitigated')
                        if cf_mitigated:
                            ray_id = response.headers.get('cf-ray', 'unknown')
                            raise Exception(f'CloudFlare mitigation required "{cf_mitigated}". RayID: {ray_id}. '
                                            'Ask support to whitelist your IP.')

                        reason = response.reason or 'Unknown error'
                        raise Exception(f'Error API: {response.status} - {reason}')

                    if 'application/json' not in response.headers.get("content-type", ""):
                        text = await response.text()
                        raise Exception(f'Error: Response not JSON: {text}')

                    text = await response.text()
                    response_data = json.loads(text)
                    if not response_data.get('success') or not response_data.get('data'):
                        error_msg = response_data.get('error', 'Unknown error')
                        raise Exception(f'Error API: {error_msg}')

                    return json.dumps(response_data.get('data', {}))

                except aiohttp.ClientError as error:
                    logging.exception("API call error", exc_info=error)

    async def close(self):
        await self.session.close()

I need to adapt this logic to PHP. I’ve already tried using hash_hmac and various approaches to create the signature, but nothing seems to work.

Can anyone help me convert this code to PHP or guide me on how to properly implement authentication with signature generation?

API DOC: https://nimlab.eu/service/api-docs/#/

Thank you in advance!

Yeah, I forgot to include the code:

<?php

class NIMLABAPI {
    private $apiUrl;
    private $apiKey;
    private $apiSecret;

    public function __construct($apiUrl, $apiKey, $apiSecret) {
        if (empty($apiUrl)) {
            throw new InvalidArgumentException('apiUrl is required');
        }
        if (empty($apiKey)) {
            throw new InvalidArgumentException('apiKey is required');
        }
        if (empty($apiSecret)) {
            throw new InvalidArgumentException('apiSecret is required');
        }

        $this->apiUrl = $apiUrl;
        $this->apiKey = $apiKey;
        $this->apiSecret = $apiSecret;
    }

    private function generateHash($getParams, $postParams) {
        foreach ($getParams as $key => &$value) {
            if (is_array($value)) {
                $value = array_map('strval', $value);
            } else {
                $value = strval($value);
            }
        }

        $params = array_merge($getParams, $postParams);
        $checksumParams = hash('sha256', json_encode($params, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
        $hash = hash('sha256', $checksumParams . $this->apiSecret);
        return $hash;
    }

    public function call($method, $params = [], $rewriteConfig = []) {
        $get = $params['get'] ?? [];
        $post = $params['post'] ?? [];

        if (!is_array($get)) {
            return ["errorCode" => 20, "message" => 'Error: "get" params must be an array'];
        }
        if (!is_array($post)) {
            return ["errorCode" => 21, "message" => 'Error: "post" params must be an array'];
        }

        $configService = array_merge([
            'apiUrl' => $this->apiUrl,
            'apikey' => $this->apiKey,
            'secret' => $this->apiSecret
        ], $rewriteConfig);

        $methodParts = explode(':', $method);
        $typeMethod = count($methodParts) === 2 ? strtoupper($methodParts[0]) : 'POST';
        $method = count($methodParts) === 2 ? $methodParts[1] : $methodParts[0];

        if ($typeMethod === 'GET') {
            $get['time'] = intval(microtime(true) * 1000);
        }

        $urlParams = http_build_query($get);
        $url = $configService['apiUrl'] . $method . ($urlParams ? '?' . $urlParams : '');

        $hash = $this->generateHash($get, $post);

        $headers = [
            'cache-control: no-cache',
            'apikey: ' . $configService['apikey'],
            'hash: ' . $hash,
            'Content-Type: application/json'
        ];

        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $typeMethod);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
        if ($typeMethod === 'POST') {
            curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($post, JSON_UNESCAPED_UNICODE));
        }

        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $contentType = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);

        if ($httpCode !== 200) {
            $error = "Error: HTTP $httpCode";
            if (strpos($contentType, 'application/json') !== false) {
                $responseData = json_decode($response, true);
                $error = $responseData['error'] ?? $error;
            }
            curl_close($ch);
            throw new Exception($error);
        }

        curl_close($ch);
        $responseData = json_decode($response, true);

        if (empty($responseData['success']) || empty($responseData['data'])) {
            throw new Exception('API Error: ' . ($responseData['error'] ?? 'Unknown error'));
        }

        return $responseData['data'];
    }
}

?>