#include "rest_client.h" #include "http_client.h" #include "log.h" #include "cJSON.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #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); }