future-public: add set leverage request

This commit is contained in:
mahdi msr 2025-09-28 15:22:55 +03:30
parent bea493935b
commit 0996c5fc27
9 changed files with 377 additions and 25 deletions

1
.gitignore vendored
View File

@ -30,3 +30,4 @@ phpstan.neon
testbench.yaml testbench.yaml
/docs /docs
/coverage /coverage
.env

View File

@ -1,7 +1,9 @@
<?php <?php
// config for Msr/LaravelBitunixApi
return [ return [
'future_base_uri' => 'https://fapi.bitunix.com/', 'future_base_uri' => 'https://fapi.bitunix.com/',
'api_key' => env('BITUNIX_API_KEY'),
'api_secret' => env('BITUNIX_API_SECRET'),
'language' => 'en-US',
]; ];

View File

@ -0,0 +1,61 @@
<?php
/**
* 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';
use Msr\LaravelBitunixApi\Requests\ChangeLeverageRequestContract;
// Example configuration (in real usage, these would be in your .env file)
config([
'bitunix-api.future_base_uri' => '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
*/

View File

@ -4,9 +4,11 @@ namespace Msr\LaravelBitunixApi;
use GuzzleHttp\Client; use GuzzleHttp\Client;
use Msr\LaravelBitunixApi\Requests\FutureKLineRequestContract; use Msr\LaravelBitunixApi\Requests\FutureKLineRequestContract;
use Msr\LaravelBitunixApi\Requests\ChangeLeverageRequestContract;
use Msr\LaravelBitunixApi\Requests\Header;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
class LaravelBitunixApi implements FutureKLineRequestContract class LaravelBitunixApi implements FutureKLineRequestContract, ChangeLeverageRequestContract
{ {
private Client $publicFutureClient; 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 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', [ $response = $this->publicFutureClient->get('kline', [
@ -32,4 +45,19 @@ class LaravelBitunixApi implements FutureKLineRequestContract
return $response; 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;
}
} }

View File

@ -3,6 +3,7 @@
namespace Msr\LaravelBitunixApi; namespace Msr\LaravelBitunixApi;
use Msr\LaravelBitunixApi\Commands\LaravelBitunixApiCommand; use Msr\LaravelBitunixApi\Commands\LaravelBitunixApiCommand;
use Msr\LaravelBitunixApi\Requests\ChangeLeverageRequestContract;
use Msr\LaravelBitunixApi\Requests\FutureKLineRequestContract; use Msr\LaravelBitunixApi\Requests\FutureKLineRequestContract;
use Spatie\LaravelPackageTools\Package; use Spatie\LaravelPackageTools\Package;
use Spatie\LaravelPackageTools\PackageServiceProvider; use Spatie\LaravelPackageTools\PackageServiceProvider;
@ -29,5 +30,6 @@ class LaravelBitunixApiServiceProvider extends PackageServiceProvider
parent::packageRegistered(); parent::packageRegistered();
$this->app->bind(FutureKLineRequestContract::class, LaravelBitunixApi::class); $this->app->bind(FutureKLineRequestContract::class, LaravelBitunixApi::class);
$this->app->bind(ChangeLeverageRequestContract::class, LaravelBitunixApi::class);
} }
} }

View File

@ -0,0 +1,19 @@
<?php
namespace Msr\LaravelBitunixApi\Requests;
use Psr\Http\Message\ResponseInterface;
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
*/
public function changeLeverage(string $symbol, string $marginCoin, int $leverage): ResponseInterface;
}

View File

@ -4,47 +4,126 @@ namespace Msr\LaravelBitunixApi\Requests;
class Header class Header
{ {
public static function sortQueryParameters(array $parameters): ?array /**
* Sort query parameters in ascending ASCII order by Key
*
* @param array $parameters
* @return array
*/
public static function sortQueryParameters(array $parameters): array
{ {
$sortableParameters = $parameters; if (empty($parameters)) {
if (!count($sortableParameters)) {
return []; return [];
} }
ksort($sortableParameters, SORT_LOCALE_STRING); ksort($parameters, SORT_STRING);
return $sortableParameters; return $parameters;
} }
public static function digestQueryParameters(array $parameters): ?string /**
* 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
{ {
if (!count($parameters)) { if (empty($parameters)) {
return null; return '';
} }
$sortedParameters = self::sortQueryParameters($parameters); $sortedParameters = self::sortQueryParameters($parameters);
return implode('', array_map( $result = '';
fn($k, $v) => $k . $v,
array_keys($sortedParameters), foreach ($sortedParameters as $key => $value) {
array_values($sortedParameters) $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'); $apiKey = config('bitunix-api.api_key');
$apiSecret = config('bitunix-api.api_secret'); $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; // Step 1: Sort query parameters in ascending ASCII order
$hash = hash('sha256', $digestedHeader); $queryParamsString = self::digestQueryParameters($queryParams);
$sign = $hash . $apiSecret;
return hash('sha256', $sign); // 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',
];
} }
} }

View File

@ -0,0 +1,25 @@
<?php
use Msr\LaravelBitunixApi\Requests\ChangeLeverageRequestContract;
use GuzzleHttp\Client;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Psr7\Response;
it('can change leverage successfully', function () {
$api = app(ChangeLeverageRequestContract::class);
// This test will make a real API call if credentials are valid
// For testing purposes, we'll just verify the method exists and can be called
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))
->not->toThrow(Exception::class)
->and(fn() => $api->changeLeverage('BTCUSDT', 'USDT', 0))
->not->toThrow(Exception::class);
});

135
tests/HeaderTest.php Normal file
View File

@ -0,0 +1,135 @@
<?php
use Msr\LaravelBitunixApi\Requests\Header;
beforeEach(function () {
config([
'bitunix-api.api_key' => '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);
});