Skip to content

Commit d06089c

Browse files
committed
feat: add logging module and upgrade slick-net to 2.0.0
- Upgrade slick-net dependency from 1.2.4 to 2.0.0 - Add coinbase::logging namespace exposing log handler configuration - Fix pagination bug preserving query parameters in list_accounts - Update slick-net header includes from .h to .hpp extension - Add additional Product struct fields (quote_name, base_increment, etc.) - Bump version to 0.2.0
1 parent f4ebf80 commit d06089c

13 files changed

Lines changed: 133 additions & 97 deletions

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8-
## [0.1.3] - 2026-02-09
8+
## [0.2.0] - 2026-02-19
99

1010
### Added
1111
- Comprehensive unit tests for CoinbaseAwaitableRestClient coroutine-based API

CMakeLists.txt

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ cmake_minimum_required(VERSION 3.20)
33
set(CMAKE_CXX_STANDARD 20)
44

55
project(coinbase-advanced-cpp
6-
VERSION 0.1.3
6+
VERSION 0.2.0
77
LANGUAGES CXX)
88

99
if (CMAKE_BUILD_TYPE MATCHES Debug)
@@ -18,20 +18,18 @@ find_package(nlohmann_json CONFIG REQUIRED)
1818
find_package(OpenSSL CONFIG REQUIRED)
1919
find_package(jwt-cpp CONFIG REQUIRED)
2020

21-
find_package(slick-net 1.2.4 CONFIG QUIET)
21+
find_package(slick-net 2.0.0 CONFIG QUIET)
2222
if (NOT slick-net_FOUND)
2323
message(STATUS "fetching slick-net...")
2424
include(FetchContent)
25-
set(BUILD_SLICK_NET_EXAMPLES OFF CACHE BOOL "" FORCE)
26-
set(BUILD_SLICK_NET_TESTS OFF CACHE BOOL "" FORCE)
2725
FetchContent_Declare(
2826
slick-net
2927
GIT_REPOSITORY https://github.com/SlickQuant/slick-net.git
30-
GIT_TAG v1.2.4
28+
GIT_TAG v2.0.0
3129
)
3230
FetchContent_MakeAvailable(slick-net)
3331
else()
34-
message(STATUS "slick-net: ${slick-net_VERSION}")
32+
message(STATUS "Found slick-net: ${slick-net_VERSION}")
3533
endif()
3634

3735
add_library(coinbase-advanced-cpp STATIC
@@ -40,6 +38,7 @@ add_library(coinbase-advanced-cpp STATIC
4038
src/rest_awaitable.cpp
4139
src/websocket.cpp
4240
src/utils.cpp
41+
src/logging.cpp
4342
)
4443
add_library(slick::coinbase-advanced-cpp ALIAS coinbase-advanced-cpp)
4544
target_include_directories(coinbase-advanced-cpp PUBLIC

include/coinbase/logging.hpp

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
#pragma once
2+
3+
#include <slick/net/logging.hpp>
4+
5+
namespace coinbase::logging {
6+
7+
using LogLevel = slick::net::LogLevel;
8+
using LogHandler = slick::net::LogHandler;
9+
10+
void set_log_handler(LogHandler handler);
11+
void clear_log_handler() noexcept;
12+
13+
} // namespace coinbase::logging

include/coinbase/order.hpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -555,7 +555,7 @@ inline void from_json(const json &j, Order &o) {
555555
}
556556
}
557557
catch (const std::exception &e) {
558-
LOG_INFO(j.dump());
558+
LOG_INFO(j.dump().c_str());
559559
LOG_ERROR(e.what());
560560
}
561561
}

include/coinbase/product.hpp

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,15 +297,43 @@ struct Product {
297297
inline void from_json(const json& j, Product& p) {
298298
VARIABLE_FROM_JSON(j, p, product_id);
299299
VARIABLE_FROM_JSON(j, p, base_name);
300+
VARIABLE_FROM_JSON(j, p, base_display_symbol);
300301
VARIABLE_FROM_JSON(j, p, quote_name);
302+
VARIABLE_FROM_JSON(j, p, quote_display_symbol);
301303
VARIABLE_FROM_JSON(j, p, status);
304+
VARIABLE_FROM_JSON(j, p, quote_currency_id);
305+
VARIABLE_FROM_JSON(j, p, base_currency_id);
306+
VARIABLE_FROM_JSON(j, p, alias);
307+
VARIABLE_FROM_JSON(j, p, display_name);
308+
VARIABLE_FROM_JSON(j, p, product_venue);
309+
DOUBLE_FROM_JSON(j, p, price);
310+
DOUBLE_FROM_JSON(j, p, price_percentage_change_24h);
311+
DOUBLE_FROM_JSON(j, p, price_increment);
312+
DOUBLE_FROM_JSON(j, p, volume_24h);
313+
DOUBLE_FROM_JSON(j, p, volume_percentage_change_24h);
302314
DOUBLE_FROM_JSON(j, p, base_increment);
303315
DOUBLE_FROM_JSON(j, p, base_min_size);
304316
DOUBLE_FROM_JSON(j, p, base_max_size);
305317
DOUBLE_FROM_JSON(j, p, quote_increment);
306318
DOUBLE_FROM_JSON(j, p, quote_min_size);
307319
DOUBLE_FROM_JSON(j, p, quote_max_size);
320+
DOUBLE_FROM_JSON(j, p, mid_market_price);
321+
DOUBLE_FROM_JSON(j, p, approximate_quote_24h_volume);
322+
DOUBLE_FROM_JSON(j, p, market_cap);
323+
INT_FROM_JSON(j, p, new_at);
308324
ENUM_FROM_JSON(j, p, product_type);
325+
BOOL_FROM_JSON(j, p, watched);
326+
BOOL_FROM_JSON(j, p, is_disabled);
327+
BOOL_FROM_JSON_VALUE(j, p, is_new, "new");
328+
BOOL_FROM_JSON(j, p, cancel_only);
329+
BOOL_FROM_JSON(j, p, limit_only);
330+
BOOL_FROM_JSON(j, p, post_only);
331+
BOOL_FROM_JSON(j, p, trading_disabled);
332+
BOOL_FROM_JSON(j, p, auction_mode);
333+
BOOL_FROM_JSON(j, p, view_only);
334+
STRUCT_FROM_JSON(j, p, fcm_trading_session_details);
335+
STRUCT_FROM_JSON(j, p, future_product_details);
336+
STRUCT_FROM_JSON(j, p, equity_product_details);
309337
}
310338

311339
enum class ExpiringContractStatus : uint8_t {

include/coinbase/utils.hpp

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
#pragma once
66

7-
#include <slick/logger.hpp>
87
#include <string>
98
#include <stdlib.h>
109
#include <chrono>
@@ -14,6 +13,7 @@
1413
#include <cstdio>
1514
#include <nlohmann/json.hpp>
1615
#include <coinbase/side.hpp>
16+
#include <slick/net/logging.hpp>
1717

1818
using json = nlohmann::json;
1919

@@ -114,8 +114,9 @@ inline std::string to_string(double value, Side side, double min_increment) {
114114
#define NANOSECONDS_FROM_JSON(j, o, field) o.field = nanoseconds_from_json(j, #field)
115115
#define DOUBLE_FROM_JSON(j, o, field) o.field = double_from_json(j, #field)
116116
#define INT_FROM_JSON(j, o, field) o.field = int_from_json(j, #field)
117-
#define VARIABLE_FROM_JSON(j, o, field) if (j.contains(#field)) try { j.at(#field).get_to(o.field); } catch(const std::exception &e) { LOG_INFO(#field " {}", j.dump()); LOG_ERROR(e.what()); }
117+
#define VARIABLE_FROM_JSON(j, o, field) if (j.contains(#field) && !j[#field].is_null()) try { j.at(#field).get_to(o.field); } catch(const std::exception &e) { LOG_INFO(#field " {}", j.dump()); LOG_ERROR(e.what()); }
118118
#define BOOL_FROM_JSON(j, o, field) if (j.contains(#field) && !j[#field].is_null() && j[#field].is_string()) { if (j[#field] == "true") o.field = true; else if (j[#field] == "false") o.field = false; } else try { j.at(#field).get_to(o.field); } catch(const std::exception &e) { LOG_INFO(#field " {}", j.dump()); LOG_ERROR(e.what()); }
119+
#define BOOL_FROM_JSON_VALUE(j, o, field, j_name) if (j.contains(j_name) && !j[j_name].is_null() && j[j_name].is_string()) { if (j[j_name] == "true") o.field = true; else if (j[j_name] == "false") o.field = false; } else try { j.at(j_name).get_to(o.field); } catch(const std::exception &e) { LOG_INFO(j_name " {}", j.dump()); LOG_ERROR(e.what()); }
119120
#define STRUCT_FROM_JSON(j, o, field) if (j.contains(#field) && !j[#field].is_null()) from_json(j[#field], o.field)
120121
#define ENUM_FROM_JSON(j, o, field) if (j.contains(#field)) try { o.field = to_##field(j.at(#field).get<std::string_view>()); } catch(const std::exception &e) { LOG_INFO(#field " {}", j.dump()); LOG_ERROR(e.what()); }
121122
} // end namespace coinbase

include/coinbase/websocket.hpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
#include <thread>
1515
#include <unordered_map>
1616
#include <chrono>
17-
#include <slick/net/websocket.h>
17+
#include <slick/net/websocket.hpp>
1818
#include <nlohmann/json.hpp>
1919
#include <coinbase/market_data.hpp>
2020
#include <coinbase/order.hpp>

src/logging.cpp

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#include <coinbase/logging.hpp>
2+
#include <slick/net/logging.hpp>
3+
4+
namespace coinbase::logging {
5+
6+
void set_log_handler(LogHandler handler) {
7+
slick::net::set_log_handler(std::move(handler));
8+
}
9+
10+
void clear_log_handler() noexcept {
11+
slick::net::clear_log_handler();
12+
}
13+
14+
} // namespace coinbase::logging

src/rest.cpp

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
#include <coinbase/auth.hpp>
77
#include <coinbase/utils.hpp>
88
#include <nlohmann/json.hpp>
9-
#include <slick/net/http.h>
9+
#include <slick/net/http.hpp>
1010
#include <format>
1111
#include <numeric>
1212
#include <algorithm>
@@ -76,7 +76,7 @@ std::vector<Account> CoinbaseRestClient::list_accounts(const AccountQueryParams
7676
auto j = json::parse(res.result_text);
7777
std::vector<Account> accounts = j["accounts"];
7878
while (j.contains("has_next") && j["has_next"].get<bool>() && !j["cursor"].get<std::string_view>().empty()) {
79-
AccountQueryParams new_params;
79+
AccountQueryParams new_params = params;
8080
new_params.cursor = j["cursor"].get<std::string_view>();
8181
res = Http::get(std::format("{}/api/v3/brokerage/accounts{}", base_url_, new_params()), {
8282
{"Authorization", "Bearer " + coinbase::generate_coinbase_jwt(std::format("GET {}/api/v3/brokerage/accounts", domain_).c_str())}
@@ -190,7 +190,7 @@ std::vector<Order> CoinbaseRestClient::list_orders(const OrderQueryParams &query
190190
auto j = json::parse(res.result_text);
191191
std::vector<Order> orders = j["orders"];
192192
while (j.contains("has_next") && j["has_next"].get<bool>() && !j["cursor"].get<std::string_view>().empty()) {
193-
OrderQueryParams new_query;
193+
OrderQueryParams new_query = query;
194194
new_query.cursor = j["cursor"].get<std::string_view>();
195195
res = Http::get(std::format("{}/api/v3/brokerage/orders/historical/batch{}", base_url_, new_query()), {
196196
{"Authorization", "Bearer " + coinbase::generate_coinbase_jwt(std::format("GET {}/api/v3/brokerage/orders/historical/batch", domain_).c_str())}
@@ -221,7 +221,7 @@ Order CoinbaseRestClient::get_order(std::string_view order_id) const {
221221
});
222222
if (res.is_ok()) {
223223
auto j = json::parse(res.result_text);
224-
LOG_TRACE(j.dump());
224+
LOG_TRACE(j.dump().c_str());
225225
return j["order"].get<Order>();
226226
}
227227
LOG_ERROR("Failed to get order {}. error: {}", order_id, res.result_text);
@@ -241,7 +241,7 @@ std::vector<Fill> CoinbaseRestClient::list_fills(const FillQueryParams &params)
241241
auto j = json::parse(res.result_text);
242242
std::vector<Fill> fills = j["fills"];
243243
while(j.contains("cursor") && !j["cursor"].get<std::string_view>().empty()) {
244-
FillQueryParams new_query;
244+
FillQueryParams new_query = params;
245245
new_query.cursor = j["cursor"].get<std::string_view>();
246246
res = Http::get(std::format("{}/api/v3/brokerage/orders/historical/fills{}", base_url_, new_query()), {
247247
{"Authorization", "Bearer " + coinbase::generate_coinbase_jwt(std::format("GET {}/api/v3/brokerage/orders/historical/fills", domain_).c_str())}
@@ -285,7 +285,7 @@ std::vector<PriceBook> CoinbaseRestClient::get_best_bid_ask(const std::vector<st
285285
});
286286
if (res.is_ok()) {
287287
auto j = json::parse(res.result_text);
288-
LOG_TRACE(j.dump());
288+
LOG_TRACE(j.dump().c_str());
289289
return j["pricebooks"];
290290
}
291291
LOG_ERROR("get_best_bid_ask failed. error: {}", res.result_text);
@@ -333,7 +333,7 @@ MarketTrades CoinbaseRestClient::get_market_trades(std::string_view product_id,
333333
std::vector<Candle> CoinbaseRestClient::get_product_candles(std::string_view product_id, const ProductCandlesQueryParams &params) const
334334
{
335335
try {
336-
LOG_TRACE(params());
336+
LOG_TRACE(params().c_str());
337337
auto res = Http::get(std::format("{}/api/v3/brokerage/products/{}/candles{}", base_url_, product_id, params()), {
338338
{"Authorization", "Bearer " + coinbase::generate_coinbase_jwt(std::format("GET {}/api/v3/brokerage/products/{}/candles", domain_, product_id).c_str())}
339339
});
@@ -404,7 +404,7 @@ CreateOrderResponse CoinbaseRestClient::create_order(
404404
else {
405405
rsp.error_response.message = std::format("TimeInForce {} invalid for market order", to_string(time_in_force));
406406
rsp.success = false;
407-
LOG_ERROR(rsp.error_response.message);
407+
LOG_ERROR(rsp.error_response.message.c_str());
408408
return rsp;
409409
}
410410

@@ -496,7 +496,7 @@ CreateOrderResponse CoinbaseRestClient::create_order(
496496
else {
497497
rsp.error_response.message = std::format("TimeInForce {} invalid for market order", to_string(time_in_force));
498498
rsp.success = false;
499-
LOG_ERROR(rsp.error_response.message);
499+
LOG_ERROR(rsp.error_response.message.c_str());
500500
return rsp;
501501
}
502502

@@ -558,7 +558,7 @@ CreateOrderResponse CoinbaseRestClient::create_order(
558558
else {
559559
rsp.error_response.message = std::format("TimeInForce {} invalid for market order", to_string(time_in_force));
560560
rsp.success = false;
561-
LOG_ERROR(rsp.error_response.message);
561+
LOG_ERROR(rsp.error_response.message.c_str());
562562
return rsp;
563563
}
564564
break;
@@ -624,7 +624,7 @@ CreateOrderResponse CoinbaseRestClient::create_order(
624624
}
625625
default: {
626626
rsp.error_response.message = std::format("OrderType {} is not supported. client_order_id: {}", to_string(order_type), client_order_id);
627-
LOG_ERROR(rsp.error_response.message);
627+
LOG_ERROR(rsp.error_response.message.c_str());
628628
rsp.success = false;
629629
return rsp;
630630
}
@@ -650,15 +650,15 @@ CreateOrderResponse CoinbaseRestClient::create_order(
650650
});
651651
if (res.is_ok()) {
652652
auto j = json::parse(res.result_text);
653-
LOG_TRACE(j.dump());
653+
LOG_TRACE(j.dump().c_str());
654654
return j;
655655
}
656656
rsp.error_response.message = std::format("Failed to create order. client_order_id: {} error: {}", client_order_id, res.result_text);
657-
LOG_ERROR(rsp.error_response.message);
657+
LOG_ERROR(rsp.error_response.message.c_str());
658658
}
659659
catch (const std::exception &e) {
660660
rsp.error_response.message = std::format("Failed to create order. client_order_id: {} error: {}", client_order_id, e.what());
661-
LOG_ERROR(rsp.error_response.message);
661+
LOG_ERROR(rsp.error_response.message.c_str());
662662
}
663663
rsp.success = false;
664664
return rsp;
@@ -703,7 +703,7 @@ ModifyOrderResponse CoinbaseRestClient::modify_order(
703703
});
704704
if (res.is_ok()) {
705705
auto j = json::parse(res.result_text);
706-
LOG_TRACE(j.dump());
706+
LOG_TRACE(j.dump().c_str());
707707
return j;
708708
}
709709
LOG_ERROR("modify_order failed. order_id: {}, error: {}", order_id, res.result_text);
@@ -729,7 +729,7 @@ std::vector<CancelOrderResponse> CoinbaseRestClient::cancel_orders(const std::ve
729729
});
730730
if (res.is_ok()) {
731731
auto j = json::parse(res.result_text);
732-
LOG_TRACE(j.dump());
732+
LOG_TRACE(j.dump().c_str());
733733
return j["results"];
734734
}
735735
LOG_ERROR("cancel_orders failed. error: {}", res.result_text);

src/websocket.cpp

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -234,9 +234,9 @@ void WebSocketClient::subscribe(const std::vector<std::string> &product_ids, con
234234
pending_user_socket_close_.store(0, std::memory_order_release);
235235

236236
// subscribe heartbeat to keep user channel alive
237-
auto heartbeat_sub = json{{"type", "subscribe"}, {"channel", "heartbeat"}};
237+
auto heartbeat_sub = json{{"type", "subscribe"}, {"channel", "heartbeats"}};
238238
heartbeat_sub["jwt"] = generate_coinbase_jwt(user_data_url_.c_str());
239-
auto subscribe_str = subscribe_json.dump();
239+
auto subscribe_str = heartbeat_sub.dump();
240240
user_data_websocket_->send(subscribe_str.c_str(), subscribe_str.size());
241241
}
242242
websocket = user_data_websocket_;
@@ -464,7 +464,7 @@ void DataHandler::processMarketData(WebSocketClient *ws_client, const char* data
464464
}
465465
else if (channel == "subscriptions") {
466466
}
467-
else if (channel == "heartbeat") {
467+
else if (channel == "heartbeats") {
468468
processHeartbeat(ws_client, j);
469469
}
470470
else {
@@ -486,7 +486,7 @@ void DataHandler::processUserData(WebSocketClient *ws_client, const char* data,
486486
}
487487
else if (channel == "subscriptions") {
488488
}
489-
else if (channel == "heartbeat") {
489+
else if (channel == "heartbeats") {
490490
processHeartbeat(ws_client, j);
491491
}
492492
else if (channel == "futures_balance_summary") {

0 commit comments

Comments
 (0)