LTP API V2 is the upgraded version of LTP API V1 by allowing access of trading endpoints.
Attention: For users who created their LTP API before 2023-Oct-19th, you can upgrade to LTP API V2 or still utilise LTP API V1 for DMA features. If you wish to begin using RapidX trading, you must first inform your account manager to obtain permission. Afterward, you will need to use the example code of LTP API V2 for authentication.
With LTP API V2, users can access real-time market statistics and place orders on their preferred venues. LTP provides a WebSocket API for real-time market statistics and a REST API that caters to our users' trading requirements. To authenticate and begin your trading journey, please refer to the provided code example as follows.
If you don't have LTP API or your LTP API didn't have RapidX Trading permission yet, please contact your account manager for more information.
LTP REST API V2
API domain: https://api.liquiditytech.com
Please note that this API document only covers features related to the LTP API. For information on the Exchange API, please refer to the documentation provided by the relevant exchanges.
Third party class libraries involved
// JDK11 or later
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.26</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>3.1.3</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.15</version>
</dependency>
<dependency>
<groupId>org.java-websocket</groupId>
<artifactId>Java-WebSocket</artifactId>
<version>1.5.7</version>
</dependency>
</dependencies>
requests
json
hashlib
hmac
base64
websocket
time
ssl
Code example
Tip:Using java httpclient requires jdk11 or later jdk version.
import com.alibaba.fastjson.JSON;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.time.Instant;
import java.util.Formatter;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
public class RapidXDemo {
private static final String API_KEY = "your api key";
private static final String SECRET_KEY = "your secret key";
private static final String HOST = "https://api.liquiditytech.com";
private static final HttpClient client = HttpClient.newHttpClient();
public static void main(String[] args) {
// Place order, POST example
String placeOrderResult = placeOrder("OKX_SPOT_ETH_USDT", "BUY", "LIMIT", "0.0001", "3001");
System.out.println(placeOrderResult);
// // Query order, GET example
// String queryOrderResult = queryOrder("");
// System.out.println(queryOrderResult);
//
// // Cancel order, DELETE example
// String cancelOrderResult = cancelOrder("");
// System.out.println(cancelOrderResult);
}
/**
* Place order, POST example
*
* @Param sym Trading pair unique identifier, example:BINANCE_SPOT_BTC_USDT, BINANCE_PERP_BTC_USDT
* @Param side BUY or SELL
* @Param orderType Order type(default LIMIT, support type enums:LIMIT,MARKET)
* @Param orderQty Order quantity(Mandatory, unless spot market buy ) note: trading unit of OKX is the number of contracts/ trading unit of Binance is the number of coin
* @Param limitPrice Limit order price(Mandatory for limit order)
* @see <a href="https://apidocliquidity.readme.io/reference/place-order">Place order</a>
*/
private static String placeOrder(String sym, String side, String orderType, String orderQty, String limitPrice) {
try {
String url = HOST + "/api/v1/trading/order";
SortedMap<String, Object> paramMap = new TreeMap<>();
paramMap.put("sym", sym);
paramMap.put("side", side);
paramMap.put("orderType", orderType);
paramMap.put("orderQty", orderQty);
paramMap.put("limitPrice", limitPrice);
String nonce = gmtNow();
String sign = getSign(paramMap, SECRET_KEY, nonce);
String jsonBody = JSON.toJSONString(paramMap);
HttpRequest request = HttpRequest.newBuilder()
.timeout(Duration.ofSeconds(5))
.header("Content-Type", "application/json")
.header("nonce", nonce)
.header("signature", sign)
.header("X-MBX-APIKEY", API_KEY)
.POST(HttpRequest.BodyPublishers.ofString(jsonBody))
.uri(URI.create(url))
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
return response.body();
} catch (Exception exception) {
System.out.println("placeOrder error:" + exception.getMessage());
}
return null;
}
/**
* Query order, GET example
*
* @Param orderId
* @see <a href="https://apidocliquidity.readme.io/reference/query-order">Query order</a>
*/
private static String queryOrder(String orderId) {
try {
String url = HOST + "/api/v1/trading/order";
SortedMap<String, Object> paramMap = new TreeMap<>();
paramMap.put("orderId", orderId);
String nonce = gmtNow();
String sign = getSign(paramMap, SECRET_KEY, nonce);
String payload = getPayload(paramMap);
if (!payload.isEmpty()) {
url += "?" + payload;
}
HttpRequest request = HttpRequest.newBuilder()
.timeout(Duration.ofSeconds(5))
.header("Content-Type", "application/json")
.header("nonce", nonce)
.header("signature", sign)
.header("X-MBX-APIKEY", API_KEY)
.GET()
.uri(URI.create(url))
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
return response.body();
} catch (Exception exception) {
System.out.println("queryOrder error:" + exception.getMessage());
}
return null;
}
/**
* Cancel order, DELETE example
*
* @see <a href="https://apidocliquidity.readme.io/reference/cancel-order">Cancel order</a>
*/
private static String cancelOrder(String orderId) {
try {
String url = HOST + "/api/v1/trading/order";
SortedMap<String, Object> paramMap = new TreeMap<>();
paramMap.put("orderId", orderId);
String nonce = gmtNow();
String sign = getSign(paramMap, SECRET_KEY, nonce);
String jsonBody = JSON.toJSONString(paramMap);
HttpRequest request = HttpRequest.newBuilder()
.timeout(Duration.ofSeconds(5))
.header("Content-Type", "application/json")
.header("nonce", nonce)
.header("signature", sign)
.header("X-MBX-APIKEY", API_KEY)
.method("DELETE", HttpRequest.BodyPublishers.ofString(jsonBody))
.uri(URI.create(url))
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
return response.body();
} catch (Exception exception) {
System.out.println("cancelOrder error:" + exception.getMessage());
}
return null;
}
/**
* Get the current time, in seconds
*/
private static String gmtNow() {
return String.valueOf(Instant.now().getEpochSecond());
}
/**
* Generate signature
*/
private static String getSign(Map<String, Object> param, String secretKey, String nonce) {
String payload = getPayloadForSign(param);
payload += "&" + nonce;
try {
String MAC_NAME = "HmacSHA256";
String ENCODING = "UTF-8";
byte[] data = secretKey.getBytes(ENCODING);
SecretKey secretKeySpec = new SecretKeySpec(data, MAC_NAME);
Mac mac = Mac.getInstance(MAC_NAME);
mac.init(secretKeySpec);
byte[] text = payload.getBytes(ENCODING);
byte[] textFinal = mac.doFinal(text);
Formatter formatter = new Formatter();
for (byte b : textFinal) {
formatter.format("%02x", b);
}
return formatter.toString();
} catch (Exception e) {
throw new RuntimeException("Error generating sign", e);
}
}
/**
* Generate payload
*/
private static String getPayload(Map<String, Object> paramMap) {
StringBuilder queryString = new StringBuilder();
try {
for (Map.Entry<String, Object> entry : paramMap.entrySet()) {
if (queryString.length() > 0) {
queryString.append("&");
}
queryString.append(URLEncoder.encode(entry.getKey(), "UTF-8"))
.append("=")
.append(URLEncoder.encode(entry.getValue().toString(), "UTF-8"));
}
} catch (Exception e) {
throw new RuntimeException(e);
}
return queryString.toString();
}
/**
* Generate payload for sign
*/
private static String getPayloadForSign(Map<String, Object> paramMap) {
StringBuilder queryString = new StringBuilder();
try {
for (Map.Entry<String, Object> entry : paramMap.entrySet()) {
if (queryString.length() > 0) {
queryString.append("&");
}
// force contact, without url decode
queryString.append(entry.getKey()).append("=").append(entry.getValue());
}
} catch (Exception e) {
throw new RuntimeException(e);
}
return queryString.toString();
}
}
#!/usr/bin/python
# -*encoding: utf-8 -*-
import json
import requests
import hmac
import time
import hashlib
# Method
GET = "GET"
POST = "POST"
PUT = "PUT"
DELETE = 'DELETE'
def get_header(api_key: str, secret: str, params: dict = None):
if params is None:
params = dict()
nonce = get_timestamp()
message = "&".join([f"{arg}={params[arg]}" for arg in sorted(params.keys())]) + "&" + str(nonce)
sign = hmac.new(secret.encode("utf-8"), message.encode("utf-8"), digestmod=hashlib.sha256).hexdigest()
headers = {
'Content-Type': 'application/json',
'User-Agent': 'PythonClient/1.0.0',
'X-MBX-APIKEY': api_key,
'signature': sign,
'nonce': str(nonce)
}
return headers
def get_timestamp():
return int(time.time())
class Client(object):
def __init__(self, api_url, api_key, secret_key):
self.API_KEY = api_key
self.API_SECRET_KEY = secret_key
self.domain = api_url
def _request(self, method, request_path, params, timeout):
headers = get_header(self.API_KEY, self.API_SECRET_KEY, params)
params = json.dumps(params) if method != 'GET' else params
response = None
url = self.domain + request_path
if method == GET:
response = requests.get(url, params=params, headers=headers)
elif method == POST:
response = requests.post(url, data=params, headers=headers)
elif method == PUT:
response = requests.put(url, data=params, headers=headers)
elif method == DELETE:
response = requests.delete(url, data=params, headers=headers)
return response.json()
def _request_without_params(self, method, request_path):
return self._request(method, request_path, {})
def _request_with_params(self, method, request_path, params):
return self._request(method, request_path, params, timeout=100)
class ClientAPI(Client):
def transfer(self, from_trade_account_id=0, to_trade_account_id=0, from_account_type='', to_account_type='', currency='',
network='', amount=0, rapid_transfer="false", client_order_id=''):
params = {
"clientOrderId": client_order_id,
"fromAccountType": from_account_type,
"fromTradeAccountId": from_trade_account_id,
"toAccountType": to_account_type,
"toTradeAccountId": to_trade_account_id,
"currency": currency,
# "network": network,
"rapidTransfer": rapid_transfer,
"amount": amount,
}
return self._request_with_params(POST, '/api/v1/transfer/apply', params)
def get_transfer_record(self, transfer_id=0, client_order_id=''):
params = {
"clientOrderId": client_order_id,
"transferId": transfer_id,
}
return self._request_with_params(GET, '/api/v1/transfer/get', params)
def place_order(self, client_order_id: str, sym: str, side: str, position_side: str, ord_type: str,
time_in_force: str, order_qty: str, limit_price: str, quote_order_qty: str, reduce_only: str):
params = {
'clientOrderId': client_order_id,
'sym': sym,
'side': side,
'positionSide': position_side,
'ordType': ord_type,
'timeInForce': time_in_force,
'orderQty': order_qty,
'limitPrice': limit_price,
'quoteOrderQty': quote_order_qty,
'reduceOnly': reduce_only
}
return self._request_with_params(POST, '/api/v1/trading/order', params)
def get_order(self, order_id: str, client_order_id: str):
params = {
'orderId': order_id,
'clientOrderId': client_order_id
}
return self._request_with_params(GET, '/api/v1/trading/order', params)
def replace_order(self, order_id, replace_qty: str, replace_price: str):
params = {
'orderId': order_id,
'replaceQty': replace_qty,
'replacePrice': replace_price
}
return self._request_with_params(PUT, '/api/v1/trading/order', params)
def cancel_order(self, order_id: str, client_order_id: str):
params = {
'orderId': order_id,
'clientOrderId': client_order_id
}
return self._request_with_params(DELETE, '/api/v1/trading/order', params)
if __name__ == '__main__':
API_KEY = ""
SECRET_KEY = ""
client = ClientAPI('https://api.liquiditytech.com', API_KEY, SECRET_KEY)
# transfer
transfer_resp = client.transfer(from_trade_account_id=123456, to_trade_account_id=123456,
from_account_type='BINANCE', to_account_type='OKX', currency='USDT', network='BSC',
amount=10, rapid_transfer="false", client_order_id='')
transfer_id: str = transfer_resp["data"]["transferId"]
# transfer detail
print(client.get_transfer_record(transfer_id, client_order_id=''))
# place order
order_resp = client.place_order(client_order_id='', sym='BINANCE_PERP_BTC_USDT', side='BUY', position_side='LONG',
ord_type='LIMIT',time_in_force='GTC', order_qty='0.003', limit_price='90000',
quote_order_qty='', reduce_only='')
order_id: str=order_resp['data']['orderId']
# get order
print(client.get_order(order_id, client_order_id=''))
# replace order
print(client.replace_order(order_id, '0.003', '90001'))
# cancel order
print(client.cancel_order(order_id, ''))
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.commons.codec.binary.Hex;
import org.java_websocket.client.WebSocketClient;
import org.java_websocket.handshake.ServerHandshake;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class WebSocketClientDemo {
private static final String API_KEY = "your api key";
private static final String SECRET_KEY = "your secret key";
private static final String WS_URL = "wss://wss.liquiditytech.com/v1/private";
public static void main(String[] args) throws Exception {
WebSocketClient client = new WebSocketClient(new URI(WS_URL)) {
@Override
public void onOpen(ServerHandshake handshakedata) {
System.out.println("open");
}
@Override
public void onMessage(String message) {
System.out.println("receive message " + message);
}
@Override
public void onClose(int code, String reason, boolean remote) {
System.out.println("close");
}
@Override
public void onError(Exception ex) {
System.out.println("error");
}
};
client.connectBlocking();
// send ping every 10 seconds
Executors.newScheduledThreadPool(1).scheduleAtFixedRate(() -> {
client.send("ping");
}, 10, 10, TimeUnit.SECONDS);
// login
String timestamp = String.valueOf(Instant.now().getEpochSecond());
String message = timestamp + "GET" + "/users/self/verify";
String signature = getSignature(message);
UserRequestArgs userRequestArgs = UserRequestArgs.builder()
.apiKey(API_KEY)
.timestamp(timestamp)
.sign(signature)
.build();
UserRequest userRequest = UserRequest.builder()
.action("login")
.args(userRequestArgs)
.build();
ObjectMapper objectMapper = new ObjectMapper();
String jsonString = objectMapper.writeValueAsString(userRequest);
client.send(jsonString);
}
private static String getSignature(String message) throws Exception {
byte[] keyBytes = SECRET_KEY.getBytes(StandardCharsets.UTF_8);
byte[] messageBytes = message.getBytes();
Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKey = new SecretKeySpec(keyBytes, "HmacSHA256");
sha256_HMAC.init(secretKey);
return Hex.encodeHexString(sha256_HMAC.doFinal(messageBytes));
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class UserRequestArgs {
private String apiKey;
private String timestamp;
private String sign;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class UserRequest {
private String action;
private UserRequestArgs args;
}
}
import json, hashlib, hmac, time, ssl, requests
import websocket
import _thread as thread
ws_url = "wss://wss.liquiditytech.com/v1/private"
apiKey = "xxx"
SECRET_KEY = "xxx"
timestamp = str(int(time.time()))
method = 'GET'
requestPath = '/users/self/verify'
message = timestamp + method + requestPath
# sign
key = bytes(SECRET_KEY, 'utf-8')
data = bytes(message, 'utf-8')
signature = hmac.new(key, data, hashlib.sha256).hexdigest()
def on_message(ws, message):
# print(time.time_ns())
print("message", message)
# Define a callback function to handle errors
def on_error(ws, error):
print(error)
# Define a callback function to handle the WebSocket connection closing
def on_close(ws):
print("Connection closed")
# Define a callback function to handle the WebSocket connection opening
def on_open(ws):
def process():
while True:
data = {
"action": "login",
"args":
{
"apiKey": apiKey,
"timestamp": timestamp,
"sign": signature
}
}
ws.send(json.dumps(data))
time.sleep(0.2)
while True:
time.sleep(28)
ws.send("ping")
ws.close()
print("Websocket closed")
thread.start_new_thread(process, ())
# Create a WebSocket connection with the defined callbacks
ws = websocket.WebSocketApp(ws_url,
on_message=on_message,
on_error=on_error,
on_close=on_close
)
ws.on_open = on_open
# Start the WebSocket connection
ws.run_forever(sslopt={"cert_reqs": ssl.CERT_NONE})
C++ Example
#include <iostream>
#include <string>
#include <map>
#include <chrono>
#include <iomanip>
#include <sstream>
#include <curl/curl.h>
#include <nlohmann/json.hpp>
#include <openssl/hmac.h>
using json = nlohmann::json;
const std::string HOST = "https://api.liquiditytech.com";
const std::string API_KEY = "your api key";
const std::string SECRET_KEY = "your secret key";
// Helper function to get the current GMT timestamp
std::string gmtNow() {
auto now = std::chrono::system_clock::now();
auto now_c = std::chrono::system_clock::to_time_t(now);
std::ostringstream oss;
oss << std::chrono::duration_cast<std::chrono::seconds>(now.time_since_epoch()).count();
return oss.str();
}
// Helper function to generate HMAC SHA256 signature
std::string hmacSHA256(const std::string& data, const std::string& key) {
unsigned char* result;
static const EVP_MD* md = EVP_sha256();
unsigned int len = 32;
result = (unsigned char*)malloc(len);
HMAC(md, key.c_str(), key.length(), (unsigned char*)data.c_str(), data.length(), result, &len);
std::ostringstream oss;
for (unsigned int i = 0; i < len; i++) {
oss << std::hex << std::setw(2) << std::setfill('0') << (int)result[i];
}
free(result);
return oss.str();
}
// Helper function to convert a map to a query string
std::string getPayload(const std::map<std::string, std::string>& paramMap) {
std::ostringstream oss;
bool first = true;
for (const auto& [key, value] : paramMap) {
if (!first) {
oss << "&";
}
first = false;
oss << key << "=" << value;
}
return oss.str();
}
// Function to perform an HTTP request
std::string httpRequest(const std::string& url, const std::string& method, const std::string& payload, const std::map<std::string, std::string>& headers) {
CURL* curl;
CURLcode res;
std::string response_string;
std::string header_string;
curl_global_init(CURL_GLOBAL_ALL);
curl = curl_easy_init();
if(curl) {
struct curl_slist *chunk = nullptr;
for (const auto& [key, value] : headers) {
chunk = curl_slist_append(chunk, (key + ": " + value).c_str());
}
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, chunk);
if (method == "POST" || method == "DELETE") {
curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, method.c_str());
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, payload.c_str());
} else if (method == "GET") {
curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L);
}
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, +[](void* contents, size_t size, size_t nmemb, std::string* userp) -> size_t {
size_t total_size = size * nmemb;
userp->append((char*)contents, total_size);
return total_size;
});
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response_string);
res = curl_easy_perform(curl);
if(res != CURLE_OK) {
std::cerr << "curl_easy_perform() failed: " << curl_easy_strerror(res) << std::endl;
}
curl_easy_cleanup(curl);
curl_slist_free_all(chunk);
}
curl_global_cleanup();
return response_string;
}
void placeOrder() {
try {
std::string url = HOST + "/api/v1/trading/order";
std::map<std::string, std::string> paramMap = {
{"sym", "BINANCE_SPOT_SOL_USDT"},
{"side", "BUY"},
{"orderType", "LIMIT"},
{"orderQty", "0.1"},
{"limitPrice", "110"}
};
std::string nonce = gmtNow();
std::string payload = getPayload(paramMap);
payload += "&" + nonce;
std::string sign = hmacSHA256(payload, SECRET_KEY);
std::cout << "payload: " << payload << " sign: " << sign << std::endl;
json j(paramMap);
std::string jsonBody = j.dump();
std::cout << "jsonBody: " << jsonBody << std::endl;
std::map<std::string, std::string> headers = {
{"Content-Type", "application/json"},
{"nonce", nonce},
{"signature", sign},
{"X-MBX-APIKEY", API_KEY}
};
std::string response = httpRequest(url, "POST", jsonBody, headers);
std::cout << "placeOrder response body: " << response << std::endl;
} catch (const std::exception& e) {
std::cerr << "placeOrder error: " << e.what() << std::endl;
}
}
void queryOrder() {
try {
std::string orderId = "orderId"; // replace with your orderId
std::map<std::string, std::string> paramMap = {
{"orderId", orderId}
};
std::string nonce = gmtNow();
std::string payload = getPayload(paramMap);
payload += "&" + nonce;
std::string sign = hmacSHA256(payload, SECRET_KEY);
std::string url = HOST + "/api/v1/trading/order";
if (!payload.empty()) {
url += "?" + payload;
}
std::map<std::string, std::string> headers = {
{"Content-Type", "application/json"},
{"nonce", nonce},
{"signature", sign},
{"X-MBX-APIKEY", API_KEY}
};
std::string response = httpRequest(url, "GET", "", headers);
std::cout << "queryOrder response body: " << response << std::endl;
} catch (const std::exception& e) {
std::cerr << "queryOrder error: " << e.what() << std::endl;
}
}
void cancelOrder() {
try {
std::string url = HOST + "/api/v1/trading/order";
std::string orderId = "your orderId"; // replace with your orderId
std::map<std::string, std::string> paramMap = {
{"orderId", orderId}
};
std::string nonce = gmtNow();
std::string payload = getPayload(paramMap);
payload += "&" + nonce;
std::string sign = hmacSHA256(payload, SECRET_KEY);
json j(paramMap);
std::string jsonBody = j.dump();
std::map<std::string, std::string> headers = {
{"Content-Type", "application/json"},
{"nonce", nonce},
{"signature", sign},
{"X-MBX-APIKEY", API_KEY}
};
std::string response = httpRequest(url, "DELETE", jsonBody, headers);
std::cout << "cancelOrder response body: " << response << std::endl;
} catch (const std::exception& e) {
std::cerr << "cancelOrder error: " << e.what() << std::endl;
}
}
int main() {
placeOrder();
queryOrder();
cancelOrder();
return 0;
}
cmake_minimum_required(VERSION 3.30)
project(rapidx_sdk_c)
set(CMAKE_CXX_STANDARD 14)
include(FetchContent)
FetchContent_Declare(
nlohmann_json
GIT_REPOSITORY https://github.com/nlohmann/json.git
GIT_TAG v3.11.2
)
FetchContent_MakeAvailable(nlohmann_json)
find_package(OpenSSL REQUIRED)
find_package(CURL REQUIRED)
add_executable(rapidx_sdk_c main.cpp)
target_link_libraries(rapidx_sdk_c PRIVATE nlohmann_json::nlohmann_json)
target_link_libraries(rapidx_sdk_c PRIVATE OpenSSL::SSL OpenSSL::Crypto)
target_link_libraries(rapidx_sdk_c PRIVATE CURL::libcurl)
#
## libcurl
* Ubuntu: sudo apt install libcurl4-openssl-dev
* macOS: brew install curl
## openssl
* Ubuntu: sudo apt install libssl-dev
* macOS: brew install openssl
# build step
* rm -rf build
* mkdir build
* cd build
* cmake ..
* make