commit
ccebbf8f70
|
|
@ -30,3 +30,4 @@ phpstan.neon
|
|||
testbench.yaml
|
||||
/docs
|
||||
/coverage
|
||||
.env
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
<?php
|
||||
|
||||
// config for Msr/LaravelBitunixApi
|
||||
return [
|
||||
|
||||
'future_base_uri' => 'https://fapi.bitunix.com/',
|
||||
'api_key' => env('BITUNIX_API_KEY'),
|
||||
'api_secret' => env('BITUNIX_API_SECRET'),
|
||||
'language' => 'en-US',
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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
|
||||
*/
|
||||
|
|
@ -2,4 +2,62 @@
|
|||
|
||||
namespace Msr\LaravelBitunixApi;
|
||||
|
||||
class LaravelBitunixApi {}
|
||||
use GuzzleHttp\Client;
|
||||
use Msr\LaravelBitunixApi\Requests\ChangeLeverageRequestContract;
|
||||
use Msr\LaravelBitunixApi\Requests\FutureKLineRequestContract;
|
||||
use Msr\LaravelBitunixApi\Requests\Header;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
class LaravelBitunixApi implements ChangeLeverageRequestContract, FutureKLineRequestContract
|
||||
{
|
||||
private Client $publicFutureClient;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->publicFutureClient = new Client([
|
||||
'base_uri' => config('bitunix-api.future_base_uri').'/api/v1/futures/market/',
|
||||
]);
|
||||
}
|
||||
|
||||
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', [
|
||||
'query' => [
|
||||
'symbol' => $symbol,
|
||||
'interval' => $interval,
|
||||
'limit' => $limit,
|
||||
'startTime' => $startTime,
|
||||
'endTime' => $endTime,
|
||||
'type' => $type,
|
||||
],
|
||||
]);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,9 +2,11 @@
|
|||
|
||||
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;
|
||||
use Msr\LaravelBitunixApi\Commands\LaravelBitunixApiCommand;
|
||||
|
||||
class LaravelBitunixApiServiceProvider extends PackageServiceProvider
|
||||
{
|
||||
|
|
@ -22,4 +24,12 @@ 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);
|
||||
$this->app->bind(ChangeLeverageRequestContract::class, LaravelBitunixApi::class);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
<?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
|
||||
*/
|
||||
public function changeLeverage(string $symbol, string $marginCoin, int $leverage): ResponseInterface;
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
namespace Msr\LaravelBitunixApi\Requests;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
interface FutureKLineRequestContract
|
||||
{
|
||||
/**
|
||||
* @return ResponseInterface
|
||||
*
|
||||
* interval could be: 1m 5m 15m 30m 1h 2h 4h 6h 8h 12h 1d 3d 1w 1M
|
||||
* limit: max is 200
|
||||
* startTime : milliseconds format
|
||||
* endTime : milliseconds format
|
||||
* 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;
|
||||
}
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
<?php
|
||||
|
||||
namespace Msr\LaravelBitunixApi\Requests;
|
||||
|
||||
class Header
|
||||
{
|
||||
/**
|
||||
* Sort query parameters in ascending ASCII order by Key
|
||||
*/
|
||||
public static function sortQueryParameters(array $parameters): array
|
||||
{
|
||||
if (empty($parameters)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
ksort($parameters, SORT_STRING);
|
||||
|
||||
return $parameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert sorted parameters to string format
|
||||
* Example: ["id" => "1", "uid" => "200"] becomes "id1uid200"
|
||||
*/
|
||||
public static function digestQueryParameters(array $parameters): string
|
||||
{
|
||||
if (empty($parameters)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$sortedParameters = self::sortQueryParameters($parameters);
|
||||
$result = '';
|
||||
|
||||
foreach ($sortedParameters as $key => $value) {
|
||||
$result .= $key.$value;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a random 32-bit nonce string
|
||||
*/
|
||||
public static function generateNonce(): string
|
||||
{
|
||||
return bin2hex(random_bytes(16)); // 32 characters
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate current timestamp in milliseconds
|
||||
*/
|
||||
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)
|
||||
*/
|
||||
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;
|
||||
$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
|
||||
*/
|
||||
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',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
<?php
|
||||
|
||||
use Msr\LaravelBitunixApi\Requests\Header;
|
||||
|
||||
it('sort query parameters', function () {
|
||||
|
||||
$emptyQuery = Header::sortQueryParameters([]);
|
||||
expect($emptyQuery)->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();
|
||||
});
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
|
||||
use Msr\LaravelBitunixApi\Requests\ChangeLeverageRequestContract;
|
||||
|
||||
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);
|
||||
});
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
<?php
|
||||
|
||||
it('can test', function () {
|
||||
expect(true)->toBeTrue();
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
|
||||
use Msr\LaravelBitunixApi\Requests\FutureKLineRequestContract;
|
||||
|
||||
it('check kline request response code', function () {
|
||||
|
||||
$bootedClass = app(FutureKLineRequestContract::class);
|
||||
$response = $bootedClass->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']);
|
||||
});
|
||||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue