436 lines
14 KiB
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);
|
|
}
|