triangular_arbitrage_bot/src/rest_client.c

436 lines
14 KiB
C

#include "rest_client.h"
#include "http_client.h"
#include "log.h"
#include "cJSON.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <errno.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <openssl/ssl.h>
#include <openssl/err.h>
#define HOST "api.kucoin.com"
#define PORT 443
struct rest_conn_s {
char api_key[64];
char api_secret[128];
char api_passphrase[64];
/* Keepalive TLS connection */
SSL_CTX *ctx;
SSL *ssl;
int fd;
};
/* Forward declaration of the HMAC helper from http_client.c */
extern int hmac_sha256_base64(const char *key, const char *data,
char *out, size_t out_len);
/* ── Connection management ── */
rest_conn_t *rest_conn_new(void) {
rest_conn_t *rc = calloc(1, sizeof(*rc));
if (!rc) return NULL;
rc->fd = -1;
return rc;
}
void rest_conn_set_auth(rest_conn_t *rc,
const char *api_key,
const char *api_secret,
const char *api_passphrase) {
if (api_key) strncpy(rc->api_key, api_key, sizeof(rc->api_key) - 1);
if (api_secret) strncpy(rc->api_secret, api_secret, sizeof(rc->api_secret) - 1);
if (api_passphrase) strncpy(rc->api_passphrase, api_passphrase, sizeof(rc->api_passphrase) - 1);
}
static int tcp_connect(const char *host, int port) {
char port_str[8];
snprintf(port_str, sizeof(port_str), "%d", port);
struct addrinfo hints = {0}, *res = NULL;
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
int ret = getaddrinfo(host, port_str, &hints, &res);
if (ret != 0 || !res) return -1;
int fd = -1;
for (struct addrinfo *rp = res; rp; rp = rp->ai_next) {
fd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
if (fd < 0) continue;
struct timeval tv = { .tv_sec = 10, .tv_usec = 0 };
setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
if (connect(fd, rp->ai_addr, rp->ai_addrlen) == 0) break;
close(fd);
fd = -1;
}
freeaddrinfo(res);
return fd;
}
static int ensure_connected(rest_conn_t *rc) {
if (rc->ssl && rc->fd >= 0) {
/* Quick check: is the connection still alive? */
char buf;
if (recv(rc->fd, &buf, 1, MSG_PEEK | MSG_DONTWAIT) == 0) {
goto reconnect;
}
return 0; /* Connection alive */
}
reconnect:
/* Close stale connection */
if (rc->ssl) { SSL_free(rc->ssl); rc->ssl = NULL; }
if (rc->fd >= 0) { close(rc->fd); rc->fd = -1; }
if (!rc->ctx) {
rc->ctx = SSL_CTX_new(TLS_client_method());
if (!rc->ctx) return -1;
}
int fd = tcp_connect(HOST, PORT);
if (fd < 0) return -1;
SSL *ssl = SSL_new(rc->ctx);
if (!ssl) { close(fd); return -1; }
SSL_set_fd(ssl, fd);
SSL_set_tlsext_host_name(ssl, HOST);
if (SSL_connect(ssl) <= 0) {
SSL_free(ssl); close(fd);
return -1;
}
rc->fd = fd;
rc->ssl = ssl;
return 0;
}
/* ── Low-level signed request ── */
static bool do_signed_request(rest_conn_t *rc,
const char *method,
const char *path,
const char *body,
char *out_response, size_t out_sz) {
if (ensure_connected(rc) < 0) return false;
struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);
char timestamp[32];
snprintf(timestamp, sizeof(timestamp), "%lld",
(long long)(ts.tv_sec * 1000LL + ts.tv_nsec / 1000000LL));
/* Build signing string: timestamp + method + path + body */
char sign_input[1536];
int body_len = body ? (int)strlen(body) : 0;
snprintf(sign_input, sizeof(sign_input), "%s%s%s%s",
timestamp, method, path, body ? body : "");
char sign_b64[256] = {0};
if (hmac_sha256_base64(rc->api_secret, sign_input,
sign_b64, sizeof(sign_b64)) < 0) {
return false;
}
/* Build password-encrypted passphrase */
char pass_b64[256] = {0};
if (hmac_sha256_base64(rc->api_secret, rc->api_passphrase,
pass_b64, sizeof(pass_b64)) < 0) {
return false;
}
/* Build the HTTP request */
char req[4096];
if (body && body_len > 0) {
snprintf(req, sizeof(req),
"%s %s HTTP/1.1\r\n"
"Host: " HOST "\r\n"
"Accept: application/json\r\n"
"User-Agent: fused-engine/1.0\r\n"
"Connection: keep-alive\r\n"
"Content-Type: application/json\r\n"
"Content-Length: %d\r\n"
"KC-API-KEY: %s\r\n"
"KC-API-SIGN: %s\r\n"
"KC-API-TIMESTAMP: %s\r\n"
"KC-API-PASSPHRASE: %s\r\n"
"KC-API-SIGN-TYPE: 2\r\n"
"KC-API-KEY-VERSION: 3\r\n"
"\r\n%s",
method, path, body_len,
rc->api_key, sign_b64, timestamp, pass_b64, body);
} else {
snprintf(req, sizeof(req),
"%s %s HTTP/1.1\r\n"
"Host: " HOST "\r\n"
"Accept: application/json\r\n"
"User-Agent: fused-engine/1.0\r\n"
"Connection: keep-alive\r\n"
"KC-API-KEY: %s\r\n"
"KC-API-SIGN: %s\r\n"
"KC-API-TIMESTAMP: %s\r\n"
"KC-API-PASSPHRASE: %s\r\n"
"KC-API-SIGN-TYPE: 2\r\n"
"KC-API-KEY-VERSION: 3\r\n"
"\r\n",
method, path,
rc->api_key, sign_b64, timestamp, pass_b64);
}
int req_len = (int)strlen(req);
/* Send */
if (SSL_write(rc->ssl, req, req_len) <= 0) {
log_write("[REST] SSL_write failed, will reconnect\n");
/* Force reconnect on next call */
SSL_free(rc->ssl); rc->ssl = NULL;
close(rc->fd); rc->fd = -1;
return false;
}
/* Read response — handle both Content-Length and chunked encoding */
char raw[65536];
int total = 0;
int need = -1; /* total bytes needed (-1 = not yet known) */
bool chunked = false;
while (total < (int)sizeof(raw) - 1) {
int n = SSL_read(rc->ssl, raw + total, (int)sizeof(raw) - 1 - total);
if (n <= 0) {
int err = SSL_get_error(rc->ssl, n);
if (err == SSL_ERROR_WANT_READ || err == SSL_ERROR_WANT_WRITE) continue;
break;
}
total += n;
if (need < 0) {
char *end_hdrs = strstr(raw, "\r\n\r\n");
if (end_hdrs) {
int hdr_len = (int)(end_hdrs + 4 - raw);
char *cl = strcasestr(raw, "Content-Length:");
if (cl && cl < end_hdrs) {
long len = atol(cl + 15);
if (len > 0) need = hdr_len + (int)len;
}
chunked = (strcasestr(raw, "transfer-encoding") != NULL);
if (!chunked && need < 0) {
/* No Content-Length and not chunked — read all (close). */
need = -2;
}
if (!chunked && need > 0 && total >= need) {
break; /* got the full Content-Length body */
}
if (chunked) {
/* For chunked, we decode below and stop at 0\r\n\r\n */
if (total >= 5 && memcmp(raw + total - 5, "0\r\n\r\n", 5) == 0) break;
}
}
} else if (need > 0 && total >= need) {
break;
} else if (chunked) {
if (total >= 5 && memcmp(raw + total - 5, "0\r\n\r\n", 5) == 0) break;
}
}
raw[total] = '\0';
/* Find header/body boundary */
char *headers_end = strstr(raw, "\r\n\r\n");
if (!headers_end) {
strncpy(out_response, raw, out_sz - 1);
out_response[out_sz - 1] = '\0';
return total > 0;
}
int header_len = (int)(headers_end - raw) + 4;
int body_start = header_len;
int body_end = total;
if (chunked) {
char *src = raw + body_start;
char *dst = raw;
int wrote = 0;
while (*src) {
char *end = NULL;
long cl = strtol(src, &end, 16);
if (end == src || cl <= 0) break;
while (*end && (*end == '\r' || *end == '\n')) end++;
if (cl > 0 && wrote + cl + 1 < (int)sizeof(raw)) {
memmove(dst + wrote, end, cl);
wrote += (int)cl;
src = end + cl;
while (*src && (*src == '\r' || *src == '\n')) src++;
} else {
break;
}
}
raw[wrote] = '\0';
body_end = wrote;
} else {
memmove(raw, raw + body_start, body_end - body_start);
body_end -= body_start;
raw[body_end] = '\0';
}
strncpy(out_response, raw, out_sz - 1);
out_response[out_sz - 1] = '\0';
return true;
}
/* ── Order placement ── */
bool rest_order_place(rest_conn_t *rc,
const char *symbol, const char *side,
double funds, double size,
const char *client_oid,
char *out_order_id, size_t out_sz,
char *out_error, size_t err_sz) {
char body[512];
if (funds > 0) {
snprintf(body, sizeof(body),
"{\"clientOid\":\"%s\",\"symbol\":\"%s\",\"type\":\"market\""
",\"side\":\"%s\",\"funds\":\"%.10f\"}",
client_oid, symbol, side, funds);
} else {
snprintf(body, sizeof(body),
"{\"clientOid\":\"%s\",\"symbol\":\"%s\",\"type\":\"market\""
",\"side\":\"%s\",\"size\":\"%.10f\"}",
client_oid, symbol, side, size);
}
char resp[65536] = {0};
if (!do_signed_request(rc, "POST", "/api/v1/hf/orders", body, resp, sizeof(resp))) {
snprintf(out_error, err_sz, "transport_error");
return false;
}
/* Parse JSON response */
cJSON *root = cJSON_Parse(resp);
if (!root) {
snprintf(out_error, err_sz, "parse_error: %.100s", resp);
return false;
}
cJSON *code = cJSON_GetObjectItem(root, "code");
cJSON *data = cJSON_GetObjectItem(root, "data");
if (code && cJSON_IsString(code) && strcmp(code->valuestring, "200000") == 0 && data) {
cJSON *oid = cJSON_GetObjectItem(data, "orderId");
if (oid && cJSON_IsString(oid)) {
strncpy(out_order_id, oid->valuestring, out_sz - 1);
out_order_id[out_sz - 1] = '\0';
cJSON_Delete(root);
return true;
}
}
/* Error */
cJSON *msg = cJSON_GetObjectItem(root, "msg");
if (msg && cJSON_IsString(msg)) {
snprintf(out_error, err_sz, "%s: %s",
code && cJSON_IsString(code) ? code->valuestring : "ERR",
msg->valuestring);
} else {
snprintf(out_error, err_sz, "unknown_error: %.100s", resp);
}
cJSON_Delete(root);
return false;
}
bool rest_order_test(rest_conn_t *rc,
const char *symbol, const char *side,
double funds, double size,
char *out_error, size_t err_sz) {
/* Build a unique clientOid for the test order */
struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);
char body[512];
unsigned rnd = (unsigned)(ts.tv_nsec ^ (uintptr_t)&body);
char cid[48];
snprintf(cid, sizeof(cid), "pt-%08x", rnd);
if (funds > 0) {
snprintf(body, sizeof(body),
"{\"clientOid\":\"%s\",\"symbol\":\"%s\",\"type\":\"market\",\"side\":\"%s\",\"funds\":\"%.8g\"}",
cid, symbol, side, funds);
} else {
snprintf(body, sizeof(body),
"{\"clientOid\":\"%s\",\"symbol\":\"%s\",\"type\":\"market\",\"side\":\"%s\",\"size\":\"%.8g\"}",
cid, symbol, side, size);
}
char resp[65536] = {0};
if (!do_signed_request(rc, "POST", "/api/v1/hf/orders/test", body, resp, sizeof(resp))) {
snprintf(out_error, err_sz, "transport_error");
return false;
}
cJSON *root = cJSON_Parse(resp);
if (!root) {
snprintf(out_error, err_sz, "parse_error: %.100s", resp);
return false;
}
cJSON *code = cJSON_GetObjectItem(root, "code");
if (code && cJSON_IsString(code) && strcmp(code->valuestring, "200000") == 0) {
cJSON_Delete(root);
return true;
}
cJSON *msg = cJSON_GetObjectItem(root, "msg");
if (msg && cJSON_IsString(msg)) {
snprintf(out_error, err_sz, "%s: %s",
code && cJSON_IsString(code) ? code->valuestring : "ERR",
msg->valuestring);
} else {
snprintf(out_error, err_sz, "unknown_error: %.100s", resp);
}
cJSON_Delete(root);
return false;
}
bool rest_get_balance(rest_conn_t *rc, const char *currency, double *out) {
char path[128];
char resp[65536] = {0};
snprintf(path, sizeof(path), "/api/v1/accounts?currency=%s", currency);
if (!do_signed_request(rc, "GET", path, NULL, resp, sizeof(resp))) {
return false;
}
cJSON *root = cJSON_Parse(resp);
if (!root) return false;
cJSON *data_arr = cJSON_GetObjectItem(root, "data");
if (!data_arr || !cJSON_IsArray(data_arr) || cJSON_GetArraySize(data_arr) == 0) {
cJSON_Delete(root);
return false;
}
cJSON *item = cJSON_GetArrayItem(data_arr, 0);
cJSON *avail = cJSON_GetObjectItem(item, "available");
if (avail && cJSON_IsString(avail)) {
*out = atof(avail->valuestring);
cJSON_Delete(root);
return true;
}
cJSON_Delete(root);
return false;
}
void rest_conn_free(rest_conn_t *rc) {
if (rc->ssl) SSL_free(rc->ssl);
if (rc->fd >= 0) close(rc->fd);
if (rc->ctx) SSL_CTX_free(rc->ctx);
free(rc);
}