From 1c4346805a2bf38a3b5774fe3d603223d0bf2c4c Mon Sep 17 00:00:00 2001 From: mahdi msr Date: Fri, 26 Sep 2025 14:54:16 +0330 Subject: [PATCH 1/6] signature: generate dependencies for signature header --- src/Requests/Header.php | 37 +++++++++++++++++++++++++++++++ tests/AuthHeaderTest.php | 48 ++++++++++++++++++++++++++++++++++++++++ tests/ExampleTest.php | 5 ----- 3 files changed, 85 insertions(+), 5 deletions(-) create mode 100644 src/Requests/Header.php create mode 100644 tests/AuthHeaderTest.php delete mode 100644 tests/ExampleTest.php diff --git a/src/Requests/Header.php b/src/Requests/Header.php new file mode 100644 index 0000000..45c3caa --- /dev/null +++ b/src/Requests/Header.php @@ -0,0 +1,37 @@ + $k . $v, + array_keys($sortedParameters), + array_values($sortedParameters) + )); + } + + public static function generateNonce(): ?string + { + return md5(uniqid(mt_rand(), true)); + } +} diff --git a/tests/AuthHeaderTest.php b/tests/AuthHeaderTest.php new file mode 100644 index 0000000..3f39327 --- /dev/null +++ b/tests/AuthHeaderTest.php @@ -0,0 +1,48 @@ +toBeEmpty(); + + $params = [ + 'z_index' => 'z value', + 'a_index' => 'a value', + 'b_index' => 'b value', + ]; + $sortedParams = Header::sortQueryParameters($params); + expect($sortedParams) + ->toMatchArray([ + 'a_index' => 'a value', + 'b_index' => 'b value', + 'z_index' => 'z value', + ]); +}); + +it('get sorted query params as string value', function () { + + $digestedParam = Header::digestQueryParameters([]); + expect($digestedParam)->toBeNull(); + + $params = [ + 'z_index' => 'z_value', + 'a_index' => 'a_value', + 'b_index' => 'b_value', + ]; + $sortedParams = Header::digestQueryParameters($params); + expect($sortedParams)->toEqual('a_indexa_valueb_indexb_valuez_indexz_value'); +}); + +it('get some random string 32 bit', function () { + + $firstRandom = Header::generateNonce(); + $secondRandom = Header::generateNonce(); + + expect($firstRandom) + ->toBeString() + ->toHaveLength(32) + ->not()->toEqual($secondRandom) + ->not()->toBeNull(); +}); diff --git a/tests/ExampleTest.php b/tests/ExampleTest.php deleted file mode 100644 index 5d36321..0000000 --- a/tests/ExampleTest.php +++ /dev/null @@ -1,5 +0,0 @@ -toBeTrue(); -}); From d57006f6c3faf06b74fcf038f2ac42c522906f68 Mon Sep 17 00:00:00 2001 From: mahdi msr Date: Fri, 26 Sep 2025 15:53:16 +0330 Subject: [PATCH 2/6] signature: add generate sign value method --- src/Requests/Header.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/Requests/Header.php b/src/Requests/Header.php index 45c3caa..8a4201b 100644 --- a/src/Requests/Header.php +++ b/src/Requests/Header.php @@ -34,4 +34,17 @@ class Header { return md5(uniqid(mt_rand(), true)); } + + public static function generateSignValue(array $queryParams = [], string $body = '', string $randomNonce = ''): string + { + $apiKey = config('bitunix-api.api_key'); + $apiSecret = config('bitunix-api.api_secret'); + $timeStamp = (string)round(microtime(true) * 1000); + $digestedQueryParam = self::digestQueryParameters($queryParams); + + $digestedHeader = $randomNonce . $timeStamp . $apiKey . $digestedQueryParam . $body; + $hash = hash('sha256', $digestedHeader); + $sign = $hash . $apiSecret; + return hash('sha256', $sign); + } } From 0503da734915d0f62112fe32ee1f57fee0105f81 Mon Sep 17 00:00:00 2001 From: mahdi msr Date: Fri, 26 Sep 2025 16:22:44 +0330 Subject: [PATCH 3/6] future-public: add kline request --- config/bitunix-api.php | 1 + src/LaravelBitunixApi.php | 32 ++++++++++++++++++++- src/LaravelBitunixApiServiceProvider.php | 8 ++++++ src/Requests/FutureKLineRequestContract.php | 30 +++++++++++++++++++ tests/KLineTest.php | 13 +++++++++ 5 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 src/Requests/FutureKLineRequestContract.php create mode 100644 tests/KLineTest.php diff --git a/config/bitunix-api.php b/config/bitunix-api.php index b9ffe0e..b0bbf2a 100644 --- a/config/bitunix-api.php +++ b/config/bitunix-api.php @@ -3,4 +3,5 @@ // config for Msr/LaravelBitunixApi return [ + 'future_base_uri' => 'https://fapi.bitunix.com/', ]; diff --git a/src/LaravelBitunixApi.php b/src/LaravelBitunixApi.php index 823fc07..90e4075 100644 --- a/src/LaravelBitunixApi.php +++ b/src/LaravelBitunixApi.php @@ -2,4 +2,34 @@ namespace Msr\LaravelBitunixApi; -class LaravelBitunixApi {} +use GuzzleHttp\Client; +use Msr\LaravelBitunixApi\Requests\FutureKLineRequestContract; +use Psr\Http\Message\ResponseInterface; + +class LaravelBitunixApi implements FutureKLineRequestContract +{ + private Client $publicFutureClient; + + public function __construct() + { + $this->publicFutureClient = new Client([ + 'base_uri' => config('bitunix-api.future_base_uri') . '/api/v1/futures/market/', + ]); + } + + public function getFutureKline(string $symbol, string $interval, int $limit = 100, ?int $startTime = null, ?int $endTime = null, string $type = 'LAST_PRICE'): ResponseInterface + { + $response = $this->publicFutureClient->get('kline', [ + 'query' => [ + 'symbol' => $symbol, + 'interval' => $interval, + 'limit' => $limit, + 'startTime' => $startTime, + 'endTime' => $endTime, + 'type' => $type, + ] + ]); + + return $response; + } +} diff --git a/src/LaravelBitunixApiServiceProvider.php b/src/LaravelBitunixApiServiceProvider.php index c30e13b..b8aa134 100644 --- a/src/LaravelBitunixApiServiceProvider.php +++ b/src/LaravelBitunixApiServiceProvider.php @@ -2,6 +2,7 @@ namespace Msr\LaravelBitunixApi; +use Msr\LaravelBitunixApi\Requests\FutureKLineRequestContract; use Spatie\LaravelPackageTools\Package; use Spatie\LaravelPackageTools\PackageServiceProvider; use Msr\LaravelBitunixApi\Commands\LaravelBitunixApiCommand; @@ -22,4 +23,11 @@ class LaravelBitunixApiServiceProvider extends PackageServiceProvider ->hasMigration('create_laravel_bitunix_api_table') ->hasCommand(LaravelBitunixApiCommand::class); } + + public function packageRegistered(): void + { + parent::packageRegistered(); + + $this->app->bind(FutureKLineRequestContract::class, LaravelBitunixApi::class); + } } diff --git a/src/Requests/FutureKLineRequestContract.php b/src/Requests/FutureKLineRequestContract.php new file mode 100644 index 0000000..2e36784 --- /dev/null +++ b/src/Requests/FutureKLineRequestContract.php @@ -0,0 +1,30 @@ +getFutureKline('BTCUSDT', '1h', 100, now()->subHours(6)->milliseconds, now()->milliseconds); + expect($response->getStatusCode()) + ->toBe(200) + ->and(json_decode($response->getBody()->getContents())) + ->toHaveKeys(['code', 'data', 'msg']); +}); From 464f8fec4145fea6714c4234f0bcbc75b84100e0 Mon Sep 17 00:00:00 2001 From: mahdimsr <32928013+mahdimsr@users.noreply.github.com> Date: Fri, 26 Sep 2025 12:53:15 +0000 Subject: [PATCH 4/6] Fix styling --- src/LaravelBitunixApi.php | 4 ++-- src/LaravelBitunixApiServiceProvider.php | 2 +- src/Requests/FutureKLineRequestContract.php | 16 +++++----------- tests/TestCase.php | 2 +- 4 files changed, 9 insertions(+), 15 deletions(-) diff --git a/src/LaravelBitunixApi.php b/src/LaravelBitunixApi.php index 90e4075..5b73040 100644 --- a/src/LaravelBitunixApi.php +++ b/src/LaravelBitunixApi.php @@ -13,7 +13,7 @@ class LaravelBitunixApi implements FutureKLineRequestContract public function __construct() { $this->publicFutureClient = new Client([ - 'base_uri' => config('bitunix-api.future_base_uri') . '/api/v1/futures/market/', + 'base_uri' => config('bitunix-api.future_base_uri').'/api/v1/futures/market/', ]); } @@ -27,7 +27,7 @@ class LaravelBitunixApi implements FutureKLineRequestContract 'startTime' => $startTime, 'endTime' => $endTime, 'type' => $type, - ] + ], ]); return $response; diff --git a/src/LaravelBitunixApiServiceProvider.php b/src/LaravelBitunixApiServiceProvider.php index b8aa134..a984a54 100644 --- a/src/LaravelBitunixApiServiceProvider.php +++ b/src/LaravelBitunixApiServiceProvider.php @@ -2,10 +2,10 @@ namespace Msr\LaravelBitunixApi; +use Msr\LaravelBitunixApi\Commands\LaravelBitunixApiCommand; use Msr\LaravelBitunixApi\Requests\FutureKLineRequestContract; use Spatie\LaravelPackageTools\Package; use Spatie\LaravelPackageTools\PackageServiceProvider; -use Msr\LaravelBitunixApi\Commands\LaravelBitunixApiCommand; class LaravelBitunixApiServiceProvider extends PackageServiceProvider { diff --git a/src/Requests/FutureKLineRequestContract.php b/src/Requests/FutureKLineRequestContract.php index 2e36784..d0654cc 100644 --- a/src/Requests/FutureKLineRequestContract.php +++ b/src/Requests/FutureKLineRequestContract.php @@ -7,12 +7,6 @@ use Psr\Http\Message\ResponseInterface; interface FutureKLineRequestContract { /** - * @param string $symbol - * @param string $interval - * @param int $limit - * @param int|null $startTime - * @param int|null $endTime - * @param string $type * @return ResponseInterface * * interval could be: 1m 5m 15m 30m 1h 2h 4h 6h 8h 12h 1d 3d 1w 1M @@ -22,9 +16,9 @@ interface FutureKLineRequestContract * type could be: LAST_PRICE, MARK_PRICE */ public function getFutureKline(string $symbol, - string $interval, - int $limit = 100, - ?int $startTime = null, - ?int $endTime = null, - string $type = 'LAST_PRICE'): ResponseInterface; + string $interval, + int $limit = 100, + ?int $startTime = null, + ?int $endTime = null, + string $type = 'LAST_PRICE'): ResponseInterface; } diff --git a/tests/TestCase.php b/tests/TestCase.php index 6e766a1..11a3486 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -3,8 +3,8 @@ namespace Msr\LaravelBitunixApi\Tests; use Illuminate\Database\Eloquent\Factories\Factory; -use Orchestra\Testbench\TestCase as Orchestra; use Msr\LaravelBitunixApi\LaravelBitunixApiServiceProvider; +use Orchestra\Testbench\TestCase as Orchestra; class TestCase extends Orchestra { From 0996c5fc271f139d3d3613c03d40b5228009da3b Mon Sep 17 00:00:00 2001 From: mahdi msr Date: Sun, 28 Sep 2025 15:22:55 +0330 Subject: [PATCH 5/6] future-public: add set leverage request --- .gitignore | 1 + config/bitunix-api.php | 4 +- examples/ChangeLeverageExample.php | 61 ++++++++ src/LaravelBitunixApi.php | 30 +++- src/LaravelBitunixApiServiceProvider.php | 2 + .../ChangeLeverageRequestContract.php | 19 +++ src/Requests/Header.php | 125 +++++++++++++--- tests/ChangeLeverageTest.php | 25 ++++ tests/HeaderTest.php | 135 ++++++++++++++++++ 9 files changed, 377 insertions(+), 25 deletions(-) create mode 100644 examples/ChangeLeverageExample.php create mode 100644 src/Requests/ChangeLeverageRequestContract.php create mode 100644 tests/ChangeLeverageTest.php create mode 100644 tests/HeaderTest.php diff --git a/.gitignore b/.gitignore index b60507f..4b1d8c2 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,4 @@ phpstan.neon testbench.yaml /docs /coverage +.env diff --git a/config/bitunix-api.php b/config/bitunix-api.php index b0bbf2a..056ba9b 100644 --- a/config/bitunix-api.php +++ b/config/bitunix-api.php @@ -1,7 +1,9 @@ 'https://fapi.bitunix.com/', + 'api_key' => env('BITUNIX_API_KEY'), + 'api_secret' => env('BITUNIX_API_SECRET'), + 'language' => 'en-US', ]; diff --git a/examples/ChangeLeverageExample.php b/examples/ChangeLeverageExample.php new file mode 100644 index 0000000..4129e60 --- /dev/null +++ b/examples/ChangeLeverageExample.php @@ -0,0 +1,61 @@ + 'https://fapi.bitunix.com/', + 'bitunix-api.api_key' => 'your-api-key-here', + 'bitunix-api.api_secret' => 'your-api-secret-here', + 'bitunix-api.language' => 'en-US', +]); + +try { + // Get the API instance + $api = app(ChangeLeverageRequestContract::class); + + // Change leverage for BTCUSDT pair + $symbol = 'BTCUSDT'; + $marginCoin = 'USDT'; + $leverage = 12; + + echo "Changing leverage for {$symbol} to {$leverage}x...\n"; + + $response = $api->changeLeverage($symbol, $marginCoin, $leverage); + + // Check response status + if ($response->getStatusCode() === 200) { + $data = json_decode($response->getBody()->getContents(), true); + + if ($data['code'] === 0) { + echo "✅ Leverage changed successfully!\n"; + echo "Symbol: " . $data['data'][0]['symbol'] . "\n"; + echo "Margin Coin: " . $data['data'][0]['marginCoin'] . "\n"; + echo "New Leverage: " . $data['data'][0]['leverage'] . "\n"; + } else { + echo "❌ API Error: " . $data['msg'] . "\n"; + } + } else { + echo "❌ HTTP Error: " . $response->getStatusCode() . "\n"; + } + +} catch (Exception $e) { + echo "❌ Exception: " . $e->getMessage() . "\n"; +} + +/** + * Environment Variables Required: + * + * BITUNIX_API_KEY=your-api-key + * BITUNIX_API_SECRET=your-api-secret + * BITUNIX_LANGUAGE=en-US + */ diff --git a/src/LaravelBitunixApi.php b/src/LaravelBitunixApi.php index 5b73040..4b5b2b9 100644 --- a/src/LaravelBitunixApi.php +++ b/src/LaravelBitunixApi.php @@ -4,9 +4,11 @@ namespace Msr\LaravelBitunixApi; use GuzzleHttp\Client; use Msr\LaravelBitunixApi\Requests\FutureKLineRequestContract; +use Msr\LaravelBitunixApi\Requests\ChangeLeverageRequestContract; +use Msr\LaravelBitunixApi\Requests\Header; use Psr\Http\Message\ResponseInterface; -class LaravelBitunixApi implements FutureKLineRequestContract +class LaravelBitunixApi implements FutureKLineRequestContract, ChangeLeverageRequestContract { private Client $publicFutureClient; @@ -17,6 +19,17 @@ class LaravelBitunixApi implements FutureKLineRequestContract ]); } + protected function getPrivateFutureClient(array $queryParams = [], array $body = []): Client + { + $bodyString = json_encode($body); + $headers = Header::generateHeaders($queryParams, $bodyString); + + return new Client([ + 'base_uri' => config('bitunix-api.future_base_uri').'/api/v1/futures/', + 'headers' => $headers, + ]); + } + public function getFutureKline(string $symbol, string $interval, int $limit = 100, ?int $startTime = null, ?int $endTime = null, string $type = 'LAST_PRICE'): ResponseInterface { $response = $this->publicFutureClient->get('kline', [ @@ -32,4 +45,19 @@ class LaravelBitunixApi implements FutureKLineRequestContract return $response; } + + public function changeLeverage(string $symbol, string $marginCoin, int $leverage): ResponseInterface + { + $body = [ + 'symbol' => $symbol, + 'marginCoin' => $marginCoin, + 'leverage' => $leverage, + ]; + + $response = $this->getPrivateFutureClient([], $body)->post('account/change_leverage', [ + 'json' => $body, + ]); + + return $response; + } } diff --git a/src/LaravelBitunixApiServiceProvider.php b/src/LaravelBitunixApiServiceProvider.php index a984a54..589a68b 100644 --- a/src/LaravelBitunixApiServiceProvider.php +++ b/src/LaravelBitunixApiServiceProvider.php @@ -3,6 +3,7 @@ namespace Msr\LaravelBitunixApi; use Msr\LaravelBitunixApi\Commands\LaravelBitunixApiCommand; +use Msr\LaravelBitunixApi\Requests\ChangeLeverageRequestContract; use Msr\LaravelBitunixApi\Requests\FutureKLineRequestContract; use Spatie\LaravelPackageTools\Package; use Spatie\LaravelPackageTools\PackageServiceProvider; @@ -29,5 +30,6 @@ class LaravelBitunixApiServiceProvider extends PackageServiceProvider parent::packageRegistered(); $this->app->bind(FutureKLineRequestContract::class, LaravelBitunixApi::class); + $this->app->bind(ChangeLeverageRequestContract::class, LaravelBitunixApi::class); } } diff --git a/src/Requests/ChangeLeverageRequestContract.php b/src/Requests/ChangeLeverageRequestContract.php new file mode 100644 index 0000000..83e91d1 --- /dev/null +++ b/src/Requests/ChangeLeverageRequestContract.php @@ -0,0 +1,19 @@ + "1", "uid" => "200"] becomes "id1uid200" + * + * @param array $parameters + * @return string + */ + public static function digestQueryParameters(array $parameters): string { - if (!count($parameters)) { - return null; + if (empty($parameters)) { + return ''; } $sortedParameters = self::sortQueryParameters($parameters); - return implode('', array_map( - fn($k, $v) => $k . $v, - array_keys($sortedParameters), - array_values($sortedParameters) - )); + $result = ''; + + foreach ($sortedParameters as $key => $value) { + $result .= $key . $value; + } + + return $result; } - public static function generateNonce(): ?string + /** + * Generate a random 32-bit nonce string + * + * @return string + */ + public static function generateNonce(): string { - return md5(uniqid(mt_rand(), true)); + return bin2hex(random_bytes(16)); // 32 characters } - public static function generateSignValue(array $queryParams = [], string $body = '', string $randomNonce = ''): string + /** + * Generate current timestamp in milliseconds + * + * @return string + */ + public static function generateTimestamp(): string + { + return (string) round(microtime(true) * 1000); + } + + /** + * Generate signature according to Bitunix API documentation + * + * Steps: + * 1. Sort all queryParams in ascending ASCII order by Key + * 2. Remove all spaces from body string + * 3. Create digest: SHA256(nonce + timestamp + api-key + queryParams + body) + * 4. Create sign: SHA256(digest + secretKey) + * + * @param array $queryParams + * @param string $body + * @param string $nonce + * @param string $timestamp + * @return string + */ + public static function generateSignValue(array $queryParams = [], string $body = '', string $nonce = '', string $timestamp = ''): string { $apiKey = config('bitunix-api.api_key'); $apiSecret = config('bitunix-api.api_secret'); - $timeStamp = (string)round(microtime(true) * 1000); - $digestedQueryParam = self::digestQueryParameters($queryParams); + + if (empty($apiKey) || empty($apiSecret)) { + throw new \InvalidArgumentException('API key and secret must be configured'); + } - $digestedHeader = $randomNonce . $timeStamp . $apiKey . $digestedQueryParam . $body; - $hash = hash('sha256', $digestedHeader); - $sign = $hash . $apiSecret; - return hash('sha256', $sign); + // Step 1: Sort query parameters in ascending ASCII order + $queryParamsString = self::digestQueryParameters($queryParams); + + // Step 2: Remove all spaces from body (already done if JSON encoded properly) + $bodyString = trim($body); + + // Step 3: Create digest: SHA256(nonce + timestamp + api-key + queryParams + body) + $digestInput = $nonce . $timestamp . $apiKey . $queryParamsString . $bodyString; + $digest = hash('sha256', $digestInput); + + // Step 4: Create sign: SHA256(digest + secretKey) + $signInput = $digest . $apiSecret; + $sign = hash('sha256', $signInput); + + return $sign; + } + + /** + * Generate complete headers for authenticated requests + * + * @param array $queryParams + * @param string $body + * @return array + */ + public static function generateHeaders(array $queryParams = [], string $body = ''): array + { + $nonce = self::generateNonce(); + $timestamp = self::generateTimestamp(); + $sign = self::generateSignValue($queryParams, $body, $nonce, $timestamp); + + return [ + 'api-key' => config('bitunix-api.api_key'), + 'sign' => $sign, + 'nonce' => $nonce, + 'timestamp' => $timestamp, + 'language' => config('bitunix-api.language', 'en-US'), + 'Content-Type' => 'application/json', + ]; } } diff --git a/tests/ChangeLeverageTest.php b/tests/ChangeLeverageTest.php new file mode 100644 index 0000000..ced3d13 --- /dev/null +++ b/tests/ChangeLeverageTest.php @@ -0,0 +1,25 @@ + $api->changeLeverage('BTCUSDT', 'USDT', 12)) + ->not->toThrow(Exception::class); +}); + +it('validates required parameters for change leverage', function () { + $api = app(ChangeLeverageRequestContract::class); + + expect(fn() => $api->changeLeverage('BTCUSDT', 'USDT', 10)) + ->not->toThrow(Exception::class) + ->and(fn() => $api->changeLeverage('BTCUSDT', 'USDT', 0)) + ->not->toThrow(Exception::class); +}); + diff --git a/tests/HeaderTest.php b/tests/HeaderTest.php new file mode 100644 index 0000000..f680c3b --- /dev/null +++ b/tests/HeaderTest.php @@ -0,0 +1,135 @@ + 'test-api-key', + 'bitunix-api.api_secret' => 'test-secret-key', + 'bitunix-api.language' => 'en-US', + ]); +}); + +it('can sort query parameters in ascending ASCII order', function () { + $parameters = [ + 'uid' => '200', + 'id' => '1', + 'name' => 'test' + ]; + + $sorted = Header::sortQueryParameters($parameters); + + expect(array_keys($sorted))->toBe(['id', 'name', 'uid']); + expect($sorted)->toBe([ + 'id' => '1', + 'name' => 'test', + 'uid' => '200' + ]); +}); + +it('can digest query parameters to string format', function () { + $parameters = [ + 'id' => '1', + 'uid' => '200' + ]; + + $result = Header::digestQueryParameters($parameters); + + expect($result)->toBe('id1uid200'); +}); + +it('can generate a valid nonce', function () { + $nonce = Header::generateNonce(); + + expect($nonce) + ->toBeString() + ->toHaveLength(32) + ->toMatch('/^[a-f0-9]+$/'); +}); + +it('can generate timestamp in milliseconds', function () { + $timestamp = Header::generateTimestamp(); + + expect($timestamp) + ->toBeString() + ->toMatch('/^\d{13}$/'); // 13 digits for milliseconds +}); + +it('can generate signature according to Bitunix documentation', function () { + $nonce = '123456'; + $timestamp = '20241120123045'; + $queryParams = ['id' => '1', 'uid' => '200']; + $body = '{"uid":"2899","arr":[{"id":1,"name":"maple"},{"id":2,"name":"lily"}]}'; + + $sign = Header::generateSignValue($queryParams, $body, $nonce, $timestamp); + + expect($sign) + ->toBeString() + ->toHaveLength(64) // SHA256 hex length + ->toMatch('/^[a-f0-9]+$/'); +}); + +it('can generate complete headers for authenticated requests', function () { + $queryParams = ['symbol' => 'BTCUSDT']; + $body = '{"symbol":"BTCUSDT","marginCoin":"USDT","leverage":12}'; + + $headers = Header::generateHeaders($queryParams, $body); + + expect($headers) + ->toHaveKeys(['api-key', 'sign', 'nonce', 'timestamp', 'language', 'Content-Type']) + ->and($headers['api-key'])->toBe('test-api-key') + ->and($headers['sign'])->toBeString() + ->and($headers['nonce'])->toBeString() + ->and($headers['timestamp'])->toBeString() + ->and($headers['language'])->toBe('en-US') + ->and($headers['Content-Type'])->toBe('application/json'); +}); + +it('throws exception when API credentials are missing', function () { + config([ + 'bitunix-api.api_key' => '', + 'bitunix-api.api_secret' => '', + ]); + + expect(fn() => Header::generateSignValue([], '', 'nonce', 'timestamp')) + ->toThrow(InvalidArgumentException::class, 'API key and secret must be configured'); +}); + +it('handles empty query parameters correctly', function () { + $result = Header::digestQueryParameters([]); + + expect($result)->toBe(''); +}); + +it('handles empty body correctly', function () { + $nonce = '123456'; + $timestamp = '20241120123045'; + + $sign = Header::generateSignValue([], '', $nonce, $timestamp); + + expect($sign) + ->toBeString() + ->toHaveLength(64); +}); + +it('generates consistent signature for same inputs', function () { + $nonce = '123456'; + $timestamp = '20241120123045'; + $queryParams = ['id' => '1']; + $body = '{"test":"value"}'; + + $sign1 = Header::generateSignValue($queryParams, $body, $nonce, $timestamp); + $sign2 = Header::generateSignValue($queryParams, $body, $nonce, $timestamp); + + expect($sign1)->toBe($sign2); +}); + +it('generates different signatures for different inputs', function () { + $nonce = '123456'; + $timestamp = '20241120123045'; + + $sign1 = Header::generateSignValue(['id' => '1'], '{"test":"value1"}', $nonce, $timestamp); + $sign2 = Header::generateSignValue(['id' => '2'], '{"test":"value2"}', $nonce, $timestamp); + + expect($sign1)->not->toBe($sign2); +}); From 49c1633f52c1eef54a85da7472becb7f4dd54b94 Mon Sep 17 00:00:00 2001 From: mahdimsr <32928013+mahdimsr@users.noreply.github.com> Date: Sun, 28 Sep 2025 11:53:47 +0000 Subject: [PATCH 6/6] Fix styling --- examples/ChangeLeverageExample.php | 30 ++++++------- src/LaravelBitunixApi.php | 4 +- .../ChangeLeverageRequestContract.php | 8 ++-- src/Requests/Header.php | 45 ++++++------------- tests/ChangeLeverageTest.php | 11 ++--- tests/HeaderTest.php | 44 +++++++++--------- 6 files changed, 58 insertions(+), 84 deletions(-) diff --git a/examples/ChangeLeverageExample.php b/examples/ChangeLeverageExample.php index 4129e60..cc1a38d 100644 --- a/examples/ChangeLeverageExample.php +++ b/examples/ChangeLeverageExample.php @@ -2,12 +2,12 @@ /** * Example usage of Change Leverage functionality - * + * * This example demonstrates how to use the LaravelBitunixApi package * to change leverage for a trading pair on Bitunix exchange. */ -require_once __DIR__ . '/../vendor/autoload.php'; +require_once __DIR__.'/../vendor/autoload.php'; use Msr\LaravelBitunixApi\Requests\ChangeLeverageRequestContract; @@ -22,39 +22,39 @@ config([ try { // Get the API instance $api = app(ChangeLeverageRequestContract::class); - + // Change leverage for BTCUSDT pair $symbol = 'BTCUSDT'; $marginCoin = 'USDT'; $leverage = 12; - + echo "Changing leverage for {$symbol} to {$leverage}x...\n"; - + $response = $api->changeLeverage($symbol, $marginCoin, $leverage); - + // Check response status if ($response->getStatusCode() === 200) { $data = json_decode($response->getBody()->getContents(), true); - + if ($data['code'] === 0) { echo "✅ Leverage changed successfully!\n"; - echo "Symbol: " . $data['data'][0]['symbol'] . "\n"; - echo "Margin Coin: " . $data['data'][0]['marginCoin'] . "\n"; - echo "New Leverage: " . $data['data'][0]['leverage'] . "\n"; + echo 'Symbol: '.$data['data'][0]['symbol']."\n"; + echo 'Margin Coin: '.$data['data'][0]['marginCoin']."\n"; + echo 'New Leverage: '.$data['data'][0]['leverage']."\n"; } else { - echo "❌ API Error: " . $data['msg'] . "\n"; + echo '❌ API Error: '.$data['msg']."\n"; } } else { - echo "❌ HTTP Error: " . $response->getStatusCode() . "\n"; + echo '❌ HTTP Error: '.$response->getStatusCode()."\n"; } - + } catch (Exception $e) { - echo "❌ Exception: " . $e->getMessage() . "\n"; + echo '❌ Exception: '.$e->getMessage()."\n"; } /** * Environment Variables Required: - * + * * BITUNIX_API_KEY=your-api-key * BITUNIX_API_SECRET=your-api-secret * BITUNIX_LANGUAGE=en-US diff --git a/src/LaravelBitunixApi.php b/src/LaravelBitunixApi.php index 4b5b2b9..eb2fe77 100644 --- a/src/LaravelBitunixApi.php +++ b/src/LaravelBitunixApi.php @@ -3,12 +3,12 @@ namespace Msr\LaravelBitunixApi; use GuzzleHttp\Client; -use Msr\LaravelBitunixApi\Requests\FutureKLineRequestContract; use Msr\LaravelBitunixApi\Requests\ChangeLeverageRequestContract; +use Msr\LaravelBitunixApi\Requests\FutureKLineRequestContract; use Msr\LaravelBitunixApi\Requests\Header; use Psr\Http\Message\ResponseInterface; -class LaravelBitunixApi implements FutureKLineRequestContract, ChangeLeverageRequestContract +class LaravelBitunixApi implements ChangeLeverageRequestContract, FutureKLineRequestContract { private Client $publicFutureClient; diff --git a/src/Requests/ChangeLeverageRequestContract.php b/src/Requests/ChangeLeverageRequestContract.php index 83e91d1..034b3ab 100644 --- a/src/Requests/ChangeLeverageRequestContract.php +++ b/src/Requests/ChangeLeverageRequestContract.php @@ -9,11 +9,9 @@ interface ChangeLeverageRequestContract /** * Change leverage for a trading pair * - * @param string $symbol Trading pair (e.g., 'BTCUSDT') - * @param string $marginCoin Margin coin (e.g., 'USDT') - * @param int $leverage Leverage value - * @return ResponseInterface + * @param string $symbol Trading pair (e.g., 'BTCUSDT') + * @param string $marginCoin Margin coin (e.g., 'USDT') + * @param int $leverage Leverage value */ public function changeLeverage(string $symbol, string $marginCoin, int $leverage): ResponseInterface; } - diff --git a/src/Requests/Header.php b/src/Requests/Header.php index b953c0f..2192635 100644 --- a/src/Requests/Header.php +++ b/src/Requests/Header.php @@ -6,9 +6,6 @@ class Header { /** * Sort query parameters in ascending ASCII order by Key - * - * @param array $parameters - * @return array */ public static function sortQueryParameters(array $parameters): array { @@ -17,15 +14,13 @@ class Header } ksort($parameters, SORT_STRING); + return $parameters; } /** * Convert sorted parameters to string format * Example: ["id" => "1", "uid" => "200"] becomes "id1uid200" - * - * @param array $parameters - * @return string */ public static function digestQueryParameters(array $parameters): string { @@ -35,18 +30,16 @@ class Header $sortedParameters = self::sortQueryParameters($parameters); $result = ''; - + foreach ($sortedParameters as $key => $value) { - $result .= $key . $value; + $result .= $key.$value; } - + return $result; } /** * Generate a random 32-bit nonce string - * - * @return string */ public static function generateNonce(): string { @@ -55,8 +48,6 @@ class Header /** * Generate current timestamp in milliseconds - * - * @return string */ public static function generateTimestamp(): string { @@ -65,58 +56,48 @@ class Header /** * Generate signature according to Bitunix API documentation - * + * * Steps: * 1. Sort all queryParams in ascending ASCII order by Key * 2. Remove all spaces from body string * 3. Create digest: SHA256(nonce + timestamp + api-key + queryParams + body) * 4. Create sign: SHA256(digest + secretKey) - * - * @param array $queryParams - * @param string $body - * @param string $nonce - * @param string $timestamp - * @return string */ public static function generateSignValue(array $queryParams = [], string $body = '', string $nonce = '', string $timestamp = ''): string { $apiKey = config('bitunix-api.api_key'); $apiSecret = config('bitunix-api.api_secret'); - + if (empty($apiKey) || empty($apiSecret)) { throw new \InvalidArgumentException('API key and secret must be configured'); } // Step 1: Sort query parameters in ascending ASCII order $queryParamsString = self::digestQueryParameters($queryParams); - + // Step 2: Remove all spaces from body (already done if JSON encoded properly) $bodyString = trim($body); - + // Step 3: Create digest: SHA256(nonce + timestamp + api-key + queryParams + body) - $digestInput = $nonce . $timestamp . $apiKey . $queryParamsString . $bodyString; + $digestInput = $nonce.$timestamp.$apiKey.$queryParamsString.$bodyString; $digest = hash('sha256', $digestInput); - + // Step 4: Create sign: SHA256(digest + secretKey) - $signInput = $digest . $apiSecret; + $signInput = $digest.$apiSecret; $sign = hash('sha256', $signInput); - + return $sign; } /** * Generate complete headers for authenticated requests - * - * @param array $queryParams - * @param string $body - * @return array */ public static function generateHeaders(array $queryParams = [], string $body = ''): array { $nonce = self::generateNonce(); $timestamp = self::generateTimestamp(); $sign = self::generateSignValue($queryParams, $body, $nonce, $timestamp); - + return [ 'api-key' => config('bitunix-api.api_key'), 'sign' => $sign, diff --git a/tests/ChangeLeverageTest.php b/tests/ChangeLeverageTest.php index ced3d13..4db10c8 100644 --- a/tests/ChangeLeverageTest.php +++ b/tests/ChangeLeverageTest.php @@ -1,25 +1,20 @@ $api->changeLeverage('BTCUSDT', 'USDT', 12)) + expect(fn () => $api->changeLeverage('BTCUSDT', 'USDT', 12)) ->not->toThrow(Exception::class); }); it('validates required parameters for change leverage', function () { $api = app(ChangeLeverageRequestContract::class); - expect(fn() => $api->changeLeverage('BTCUSDT', 'USDT', 10)) + expect(fn () => $api->changeLeverage('BTCUSDT', 'USDT', 10)) ->not->toThrow(Exception::class) - ->and(fn() => $api->changeLeverage('BTCUSDT', 'USDT', 0)) + ->and(fn () => $api->changeLeverage('BTCUSDT', 'USDT', 0)) ->not->toThrow(Exception::class); }); - diff --git a/tests/HeaderTest.php b/tests/HeaderTest.php index f680c3b..161798f 100644 --- a/tests/HeaderTest.php +++ b/tests/HeaderTest.php @@ -14,33 +14,33 @@ it('can sort query parameters in ascending ASCII order', function () { $parameters = [ 'uid' => '200', 'id' => '1', - 'name' => 'test' + 'name' => 'test', ]; - + $sorted = Header::sortQueryParameters($parameters); - + expect(array_keys($sorted))->toBe(['id', 'name', 'uid']); expect($sorted)->toBe([ 'id' => '1', 'name' => 'test', - 'uid' => '200' + 'uid' => '200', ]); }); it('can digest query parameters to string format', function () { $parameters = [ 'id' => '1', - 'uid' => '200' + 'uid' => '200', ]; - + $result = Header::digestQueryParameters($parameters); - + expect($result)->toBe('id1uid200'); }); it('can generate a valid nonce', function () { $nonce = Header::generateNonce(); - + expect($nonce) ->toBeString() ->toHaveLength(32) @@ -49,7 +49,7 @@ it('can generate a valid nonce', function () { it('can generate timestamp in milliseconds', function () { $timestamp = Header::generateTimestamp(); - + expect($timestamp) ->toBeString() ->toMatch('/^\d{13}$/'); // 13 digits for milliseconds @@ -60,9 +60,9 @@ it('can generate signature according to Bitunix documentation', function () { $timestamp = '20241120123045'; $queryParams = ['id' => '1', 'uid' => '200']; $body = '{"uid":"2899","arr":[{"id":1,"name":"maple"},{"id":2,"name":"lily"}]}'; - + $sign = Header::generateSignValue($queryParams, $body, $nonce, $timestamp); - + expect($sign) ->toBeString() ->toHaveLength(64) // SHA256 hex length @@ -72,9 +72,9 @@ it('can generate signature according to Bitunix documentation', function () { it('can generate complete headers for authenticated requests', function () { $queryParams = ['symbol' => 'BTCUSDT']; $body = '{"symbol":"BTCUSDT","marginCoin":"USDT","leverage":12}'; - + $headers = Header::generateHeaders($queryParams, $body); - + expect($headers) ->toHaveKeys(['api-key', 'sign', 'nonce', 'timestamp', 'language', 'Content-Type']) ->and($headers['api-key'])->toBe('test-api-key') @@ -90,23 +90,23 @@ it('throws exception when API credentials are missing', function () { 'bitunix-api.api_key' => '', 'bitunix-api.api_secret' => '', ]); - - expect(fn() => Header::generateSignValue([], '', 'nonce', 'timestamp')) + + expect(fn () => Header::generateSignValue([], '', 'nonce', 'timestamp')) ->toThrow(InvalidArgumentException::class, 'API key and secret must be configured'); }); it('handles empty query parameters correctly', function () { $result = Header::digestQueryParameters([]); - + expect($result)->toBe(''); }); it('handles empty body correctly', function () { $nonce = '123456'; $timestamp = '20241120123045'; - + $sign = Header::generateSignValue([], '', $nonce, $timestamp); - + expect($sign) ->toBeString() ->toHaveLength(64); @@ -117,19 +117,19 @@ it('generates consistent signature for same inputs', function () { $timestamp = '20241120123045'; $queryParams = ['id' => '1']; $body = '{"test":"value"}'; - + $sign1 = Header::generateSignValue($queryParams, $body, $nonce, $timestamp); $sign2 = Header::generateSignValue($queryParams, $body, $nonce, $timestamp); - + expect($sign1)->toBe($sign2); }); it('generates different signatures for different inputs', function () { $nonce = '123456'; $timestamp = '20241120123045'; - + $sign1 = Header::generateSignValue(['id' => '1'], '{"test":"value1"}', $nonce, $timestamp); $sign2 = Header::generateSignValue(['id' => '2'], '{"test":"value2"}', $nonce, $timestamp); - + expect($sign1)->not->toBe($sign2); });