Retry backoff: 10s, 30s, 60s */ private const RETRY_BACKOFF_MS = [10_000, 30_000, 60_000]; public function __construct( private string $apiKey, private string $apiSecret, ) {} /** * Get account information including balances, omitting zero-balance assets. * * @return array{balances: array} */ public function getAccount(): array { return $this->signedRequest('/api/v3/account', ['omitZeroBalances' => 'true']); } /** * Get all ticker prices for trading pairs. * * @return array */ public function getTickerPrices(): array { $response = $this->publicClient()->get('/api/v3/ticker/price'); $response->throw(); return $response->json(); } /** * Get daily account snapshots (up to 30 per request, max 180 days history). * * @return array{snapshotVos: array, totalAssetOfBtc: string}}>} */ public function getAccountSnapshots(int $startTime, int $endTime, int $limit = 30): array { return $this->signedRequest('/sapi/v1/accountSnapshot', [ 'type' => 'SPOT', 'startTime' => $startTime, 'endTime' => $endTime, 'limit' => $limit, ]); } /** * Get deposit history with optional time range and pagination. * Default time range is 90 days. Max window is 90 days per request. * * @return array */ public function getDepositHistory(?int $startTime = null, ?int $endTime = null, int $offset = 0, int $limit = 1000): array { $params = ['offset' => $offset, 'limit' => $limit]; if ($startTime !== null) { $params['startTime'] = $startTime; } if ($endTime !== null) { $params['endTime'] = $endTime; } return $this->signedRequest('/sapi/v1/capital/deposit/hisrec', $params); } /** * Get withdrawal history with optional time range and pagination. * Default time range is 90 days. Max window is 90 days per request. * * @return array */ public function getWithdrawHistory(?int $startTime = null, ?int $endTime = null, int $offset = 0, int $limit = 1000): array { $params = ['offset' => $offset, 'limit' => $limit]; if ($startTime !== null) { $params['startTime'] = $startTime; } if ($endTime !== null) { $params['endTime'] = $endTime; } return $this->signedRequest('/sapi/v1/capital/withdraw/history', $params); } /** * Execute a signed request with fresh timestamp on each retry attempt. */ private function signedRequest(string $path, array $params = []): array { return retry( self::RETRY_BACKOFF_MS, function () use ($path, $params) { $params['timestamp'] = (int) (microtime(true) * 1000); $queryString = http_build_query($params); $signature = hash_hmac('sha256', $queryString, $this->apiSecret); $response = $this->authenticatedClient() ->get("{$path}?{$queryString}&signature={$signature}"); $response->throw(); return $response->json(); }, when: fn (Exception $e) => $e instanceof RequestException && $e->response->status() === 429, ); } private function authenticatedClient(): PendingRequest { return Http::baseUrl(self::BASE_URL) ->withHeaders(['X-MBX-APIKEY' => $this->apiKey]) ->acceptJson() ->throw(function ($response, $exception) { Log::error('Binance API error', [ 'status' => $response->status(), 'body' => $response->json(), ]); }); } private function publicClient(): PendingRequest { return Http::baseUrl(self::BASE_URL) ->acceptJson() ->retry( self::RETRY_BACKOFF_MS, when: fn (\Throwable $e) => $e instanceof RequestException && $e->response->status() === 429, ); } }