Files
PineSript/Buyer/BitgetAuthClient.cs
2026-04-26 19:00:54 +09:00

143 lines
5.6 KiB
C#

using System.Security.Cryptography;
using System.Text;
using Serilog;
namespace Buyer
{
/// <summary>
/// Bitget 인증이 필요한 REST API 호출 전용 HTTP 클라이언트.
///
/// 서명 방식:
/// HMAC-SHA256(timestamp + METHOD + requestPath + body) → Base64
///
/// 필수 헤더:
/// ACCESS-KEY, ACCESS-SIGN, ACCESS-TIMESTAMP, ACCESS-PASSPHRASE
/// </summary>
public class BitgetAuthClient : IDisposable
{
private readonly HttpClient _http;
private readonly string _accessKey;
private readonly string _secretKey;
private readonly string _passphrase;
public BitgetAuthClient(ApiKeyInfo key,
string baseUrl = BitgetEndpoints.BaseUrl,
int timeoutSec = 10)
: this(key.AccessKey, key.SecretKey, key.PassphraseKey, baseUrl, timeoutSec)
{ }
public BitgetAuthClient(
string accessKey, string secretKey, string passphrase,
string baseUrl = BitgetEndpoints.BaseUrl,
int timeoutSec = 10)
{
_accessKey = accessKey;
_secretKey = secretKey;
_passphrase = passphrase;
_http = new HttpClient
{
BaseAddress = new Uri(baseUrl),
Timeout = TimeSpan.FromSeconds(timeoutSec)
};
_http.DefaultRequestHeaders.Add("User-Agent", "Buyer/1.0");
_http.DefaultRequestHeaders.Add("locale", "en-US");
}
// ════════════════════════════════════════════════════════════════
// 공개 메서드
// ════════════════════════════════════════════════════════════════
/// <summary>
/// 인증 헤더를 포함한 GET 요청.
/// requestPath는 쿼리스트링 포함 경로 (예: /api/v2/mix/position/all-position?productType=USDT-FUTURES)
/// </summary>
public async Task<string> GetAsync(string requestPath,
CancellationToken ct = default)
{
string timestamp = Timestamp();
string sign = Sign(timestamp, "GET", requestPath, "");
var req = new HttpRequestMessage(HttpMethod.Get, requestPath);
AttachAuthHeaders(req, timestamp, sign);
Log.Debug("[Auth] GET {Path}", requestPath);
return await SendAsync(req, ct);
}
/// <summary>인증 헤더를 포함한 POST 요청.</summary>
public async Task<string> PostAsync(string requestPath, string jsonBody,
CancellationToken ct = default)
{
string timestamp = Timestamp();
string sign = Sign(timestamp, "POST", requestPath, jsonBody);
var req = new HttpRequestMessage(HttpMethod.Post, requestPath)
{
Content = new StringContent(jsonBody, Encoding.UTF8, "application/json")
};
AttachAuthHeaders(req, timestamp, sign);
Log.Debug("[Auth] POST {Path} body={Body}", requestPath, jsonBody);
return await SendAsync(req, ct);
}
// ════════════════════════════════════════════════════════════════
// 내부 헬퍼
// ════════════════════════════════════════════════════════════════
private async Task<string> SendAsync(HttpRequestMessage req,
CancellationToken ct)
{
HttpResponseMessage response;
string body;
try
{
response = await _http.SendAsync(req, ct);
body = await response.Content.ReadAsStringAsync(ct);
}
catch (Exception ex)
{
Log.Error(ex, "[Auth] HTTP 요청 실패: {Method} {Uri}",
req.Method, req.RequestUri);
throw;
}
if (!response.IsSuccessStatusCode)
{
Log.Error("[Auth] HTTP {Status}: {Body}",
(int)response.StatusCode, body);
throw new HttpRequestException(
$"HTTP {(int)response.StatusCode}: {body}");
}
return body;
}
private void AttachAuthHeaders(HttpRequestMessage req,
string timestamp, string sign)
{
req.Headers.Add("ACCESS-KEY", _accessKey);
req.Headers.Add("ACCESS-SIGN", sign);
req.Headers.Add("ACCESS-TIMESTAMP", timestamp);
req.Headers.Add("ACCESS-PASSPHRASE", _passphrase);
}
/// <summary>HMAC-SHA256 → Base64 서명</summary>
private string Sign(string timestamp, string method,
string requestPath, string body)
{
string message = timestamp + method.ToUpper() + requestPath + body;
byte[] key = Encoding.UTF8.GetBytes(_secretKey);
byte[] data = Encoding.UTF8.GetBytes(message);
using var hmac = new HMACSHA256(key);
return Convert.ToBase64String(hmac.ComputeHash(data));
}
private static string Timestamp()
=> DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString();
public void Dispose() => _http.Dispose();
}
}