Skip to content

Commit 5b79908

Browse files
authored
Add C++ pythd websocket example (#183)
* Add C++ pythd websocket example * Rename get_product_list to get_product_list_and_subscribe * Add docstrings to pythd_websocket::status_t and pythd_websocket::symbol_t * Don't send updates if they are stale * Reset subscriptions after reconnect * Decrease update_staleness_threshold_secs_ * Log websocket error code * Access class fields and methods through this * Add comment explaining qt dependency * Update dependencies for bullseye * Scale price and conf by exponent * Don't build example in CI/CD
1 parent aea724a commit 5b79908

File tree

3 files changed

+336
-2
lines changed

3 files changed

+336
-2
lines changed

CMakeLists.txt

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ include_directories( program/c/src/ )
1616

1717
# gcc compiler/linker flags
1818
add_compile_options( -ggdb -Wall -Wextra -Wsign-conversion -Werror -Wno-deprecated-declarations -m64 )
19-
set( CMAKE_CXX_FLAGS -std=c++11 )
19+
set( CMAKE_CXX_STANDARD 14)
20+
set( CMAKE_CXX_FLAGS -std=c++14 )
2021
set( CMAKE_C_FLAGS -std=c99 )
2122
set( CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -pthread")
2223

@@ -101,6 +102,14 @@ add_executable( test_net pctest/test_net.cpp )
101102
target_link_libraries( test_net ${PC_DEP} )
102103
add_executable( test_publish pctest/test_publish.cpp )
103104
target_link_libraries( test_publish ${PC_DEP} )
105+
106+
# This doesn't build on the bullseye base image, due to a packaging bug
107+
# in the newer version of libqt5websockets5-dev for Debian. The below build instructions
108+
# are left in for completeness, and work with libqt5websockets5-dev=5.11.3-5 (buster).
109+
# find_package(Qt5 COMPONENTS Core Network WebSockets)
110+
# add_executable( test_publish_websocket pctest/test_publish_websocket.cpp )
111+
# target_link_libraries( test_publish_websocket Qt5::Core Qt5::Network Qt5::WebSockets /usr/local/lib/libjcon.so)
112+
104113
add_executable( test_qset pctest/test_qset.cpp )
105114
target_link_libraries( test_qset ${PC_DEP} )
106115
add_executable( test_twap pctest/test_twap.cpp )

docker/Dockerfile

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,15 @@ RUN apt-get install -qq \
2020
python3-websockets \
2121
sudo \
2222
zlib1g \
23-
zlib1g-dev
23+
zlib1g-dev \
24+
qtbase5-dev \
25+
qtchooser \
26+
qt5-qmake \
27+
qtbase5-dev-tools \
28+
libqt5websockets5-dev
29+
30+
# Install jcon-cpp library
31+
RUN git clone https://github.com/joncol/jcon-cpp.git /jcon-cpp && cd /jcon-cpp && git checkout 2235654e39c7af505d7158bf996e47e37a23d6e3 && mkdir build && cd build && cmake .. && make -j4 && make install
2432

2533
# Grant sudo access to pyth user
2634
RUN echo "pyth ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers

pctest/test_publish_websocket.cpp

Lines changed: 317 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,317 @@
1+
#include <stdlib.h>
2+
#include <unistd.h>
3+
#include <iostream>
4+
#include <ctime>
5+
#include <iostream>
6+
#include <memory>
7+
#include <list>
8+
#include <ctime>
9+
#include <iostream>
10+
#include <math.h>
11+
12+
#include <jcon/json_rpc_tcp_client.h>
13+
#include <jcon/json_rpc_websocket_client.h>
14+
15+
#include <QCoreApplication>
16+
#include <QDebug>
17+
#include <QTimer>
18+
#include <QUrl>
19+
#include <QtGlobal>
20+
21+
/*
22+
This file demonstrates how to use publish price updates to the Pyth Network using
23+
the pythd websocket API described at https://docs.pyth.network/publish-data/pyth-client-websocket-api.
24+
25+
High-level flow:
26+
- Call get_product_list_and_subscribe to fetch the product metadata, enabling us to associate the price account public
27+
keys with the symbols we want to publish for.
28+
- For each price account public key, call subscribe_price_sched to subscribe to the price update schedule
29+
for that price account. This will return a subscription ID.
30+
- When we receive a notify_price_sched message, we should:
31+
- Look up the price account associated with the subscription ID.
32+
- If we have any new price information for that price account, send an update_price message to submit this
33+
to the network.
34+
35+
This example uses the jcon-cpp library, which depends on qt, but you can use any JSON-RPC protocol that
36+
suits your stack.
37+
*/
38+
39+
// This class is responsible for the communication with pythd over the websocket protocol
40+
// described at https://docs.pyth.network/publish-data/pyth-client-websocket-api
41+
//
42+
// It abstracts away any solana-specific details, such as public keys.
43+
class pythd_websocket
44+
{
45+
public:
46+
// The status of a given symbol. Either "trading" or "halted".
47+
typedef std::string status_t;
48+
// The symbol of the product. For example "Crypto.BTC/USD" or "Equity.US.GOOG/USD".
49+
typedef std::string symbol_t;
50+
51+
pythd_websocket( QObject* parent, std::string pythd_websocket_endpoint );
52+
53+
// Submit a new price update for the given symbol, which will be sent to
54+
// pythd next time a notify_price_sched message for that symbol is received.
55+
void add_price_update( symbol_t symbol, int64_t price, uint64_t conf, status_t status );
56+
57+
private:
58+
59+
typedef struct {
60+
int64_t price;
61+
uint64_t conf;
62+
std::string status;
63+
time_t timestamp;
64+
} update_t;
65+
66+
typedef struct {
67+
int64_t exponent;
68+
} account_metadata_t;
69+
70+
typedef int64_t subscription_id_t;
71+
typedef std::string account_pubkey_t;
72+
73+
// Mapping of account public keys to account metadata
74+
std::map<account_pubkey_t, account_metadata_t> account_to_metadata_;
75+
// Mapping of product symbols to price account public keys.
76+
std::map<symbol_t, account_pubkey_t> symbol_to_account_;
77+
// Mapping of pythd subscription identifiers to price account public keys.
78+
std::map<subscription_id_t, account_pubkey_t> subscription_to_account_;
79+
// Mapping of price account public key to pythd subscription identifiers
80+
std::map<account_pubkey_t, subscription_id_t> account_to_subscription_;
81+
// Mapping of price account public keys to the updates we will send to pythd
82+
// next time we receive a notify_price_sched message with a subscription identifier
83+
// corresponding to the price account public key.
84+
std::map<account_pubkey_t, update_t> pending_updates_;
85+
86+
// Websocket client to handle connection to pythd
87+
std::string pythd_websocket_endpoint_;
88+
jcon::JsonRpcWebSocketClient *rpc_client_;
89+
void connect( );
90+
91+
// The duration between the price update being submitted and notify_price_sched being received
92+
// after which the update will be dropped.
93+
long update_staleness_threshold_secs_ = 3;
94+
95+
// The pythd websocket API calls
96+
97+
// Fetch the product list, and update the internal mapping of symbols to accounts.
98+
void get_product_list_and_subscribe( );
99+
100+
// Send an update_price websocket message for the given price account.
101+
void update_price( account_pubkey_t account, int price, uint conf, status_t status );
102+
103+
// Subscribe to the price update schedule for the given price account.
104+
void subscribe_price_sched( account_pubkey_t account );
105+
106+
// Handler for notify_price_sched messages, indicating that any pending updates
107+
// for the price account associated with the given subscription id should be sent.
108+
void on_notify_price_sched( subscription_id_t subscription_id );
109+
110+
};
111+
112+
pythd_websocket::pythd_websocket( QObject* parent, std::string pythd_websocket_endpoint )
113+
{
114+
this->pythd_websocket_endpoint_ = pythd_websocket_endpoint;
115+
this->rpc_client_ = new jcon::JsonRpcWebSocketClient(parent);
116+
117+
// Set up the handler for notify_price_sched
118+
this->rpc_client_->enableReceiveNotification(true);
119+
QObject::connect(rpc_client_, &jcon::JsonRpcClient::notificationReceived, parent, [this](const QString& key, const QVariant& value){
120+
if (key == "notify_price_sched") {
121+
this->on_notify_price_sched( value.toMap()["subscription"].toInt() );
122+
}
123+
});
124+
125+
// Continually check the connection and reconnect if dropped
126+
QTimer *timer = new QTimer( parent );
127+
QObject::connect(timer, &QTimer::timeout, parent, [this](){
128+
if ( !this->rpc_client_->isConnected() ) {
129+
// Reset the subscription state
130+
this->subscription_to_account_.clear();
131+
this->account_to_subscription_.clear();
132+
133+
// Reconnect
134+
this->connect();
135+
this->get_product_list_and_subscribe();
136+
}
137+
});
138+
timer->start(1000);
139+
}
140+
141+
void pythd_websocket::connect()
142+
{
143+
this->rpc_client_->connectToServer(QUrl(QString::fromStdString(pythd_websocket_endpoint_)));
144+
}
145+
146+
void pythd_websocket::get_product_list_and_subscribe( )
147+
{
148+
auto req = this->rpc_client_->callAsync("get_product_list");
149+
150+
req->connect(req.get(), &jcon::JsonRpcRequest::result, [this](const QVariant& result){
151+
// Loop over all the products
152+
auto products = result.toList();
153+
for (int i = 0; i < products.length(); i++) {
154+
155+
auto product = products[i].toMap();
156+
157+
// Extract the symbol, price account and exponent
158+
auto attr_dict = product["attr_dict"].toMap();
159+
symbol_t symbol = attr_dict["symbol"].toString().toStdString();
160+
auto price_account = product["price"].toList()[0].toMap();
161+
account_pubkey_t account = price_account["account"].toString().toStdString();
162+
int64_t exponent = price_account["price_exponent"].toInt();
163+
164+
// If this is a new symbol, associate the symbol with the account
165+
if (this->symbol_to_account_.find(account) == this->symbol_to_account_.end() || this->symbol_to_account_[symbol] != account) {
166+
this->symbol_to_account_[symbol] = account;
167+
}
168+
169+
// Update the account metadata
170+
this->account_to_metadata_[account] = account_metadata_t{
171+
exponent = exponent,
172+
};
173+
174+
// If we don't already have a subscription for this account, subscribe to it
175+
if (account_to_subscription_.find(account) == account_to_subscription_.end()) {
176+
this->subscribe_price_sched(account);
177+
}
178+
}
179+
});
180+
181+
req->connect(req.get(), &jcon::JsonRpcRequest::error, [](int code, const QString& message) {
182+
std::cout << "error sending get_product_list (" << code << ") " << message.toStdString() << std::endl;
183+
});
184+
}
185+
186+
void pythd_websocket::subscribe_price_sched( account_pubkey_t account )
187+
{
188+
auto req = this->rpc_client_->callAsyncNamedParams("subscribe_price_sched",
189+
QVariantMap{
190+
{"account", QString::fromStdString(account)},
191+
});
192+
193+
req->connect(req.get(), &jcon::JsonRpcRequest::error, [](int code, const QString& message) {
194+
std::cout << "error sending subscribe_price_sched (" << code << ") " << message.toStdString() << std::endl;
195+
});
196+
197+
req->connect(req.get(), &jcon::JsonRpcRequest::result, [this, account](const QVariant& result){
198+
auto subscription_id = result.toMap()["subscription"].toInt();
199+
subscription_to_account_[subscription_id] = account;
200+
account_to_subscription_[account] = subscription_id;
201+
std::cout << "received subscription id " << subscription_id << " for account " << account << std::endl;
202+
});
203+
}
204+
205+
void pythd_websocket::update_price( account_pubkey_t account, int price, uint conf, status_t status )
206+
{
207+
// Scale the price and confidence by the exponent
208+
int64_t exponent = (-1) * this->account_to_metadata_[account].exponent;
209+
double scaled_price = price * pow(10, exponent);
210+
double scaled_conf = conf * pow(10, exponent);
211+
212+
auto req = this->rpc_client_->callAsyncNamedParams("update_price",
213+
QVariantMap{
214+
{"account", QString::fromStdString(account)},
215+
{"price", scaled_price},
216+
{"conf", scaled_conf},
217+
{"status", QString::fromStdString(status)}
218+
});
219+
220+
req->connect(req.get(), &jcon::JsonRpcRequest::error, [account](int code, const QString& message) {
221+
std::cout << "error sending update_price (" << code << ") " << message.toStdString() << std::endl;
222+
});
223+
}
224+
225+
void pythd_websocket::on_notify_price_sched( subscription_id_t subscription_id )
226+
{
227+
// Fetch the account associated with the subscription
228+
if (this->subscription_to_account_.find(subscription_id) == this->subscription_to_account_.end()) {
229+
return;
230+
}
231+
account_pubkey_t account = subscription_to_account_[subscription_id];
232+
233+
// Fetch any price update we have for this account
234+
if (this->pending_updates_.find(account) == this->pending_updates_.end()) {
235+
return;
236+
}
237+
update_t update = pending_updates_[account];
238+
this->pending_updates_.erase(account);
239+
240+
// Check that the update is not stale
241+
if ( (std::time(nullptr) - update.timestamp) > update_staleness_threshold_secs_) {
242+
return;
243+
}
244+
245+
// Send the price update
246+
update_price( account, update.price, update.conf, update.status );
247+
}
248+
249+
void pythd_websocket::add_price_update( symbol_t symbol, int64_t price, uint64_t conf, status_t status ) {
250+
if (this->symbol_to_account_.find(symbol) == this->symbol_to_account_.end()) {
251+
return;
252+
}
253+
account_pubkey_t account = symbol_to_account_[symbol];
254+
255+
pending_updates_[account] = update_t{
256+
price: price,
257+
conf: conf,
258+
status: status,
259+
timestamp: std::time(nullptr),
260+
};
261+
}
262+
263+
class test_publish;
264+
265+
// The test_publish class is responsible for computing the next price and confidence values
266+
// for the symbols and submitting these to its pythd_websocket publisher.
267+
class test_publish
268+
{
269+
public:
270+
test_publish( QObject* parent, std::vector<std::string> symbols_to_publish, std::string pythd_websocket_endpoint );
271+
272+
private:
273+
pythd_websocket *pythd_websocket_;
274+
std::vector<std::string> symbols_to_publish_;
275+
276+
void update_symbols();
277+
};
278+
279+
test_publish::test_publish(
280+
QObject* parent,
281+
std::vector<std::string> symbols_to_publish,
282+
std::string pythd_websocket_endpoint )
283+
{
284+
pythd_websocket_ = new pythd_websocket( parent, pythd_websocket_endpoint );
285+
this->symbols_to_publish_.insert(this->symbols_to_publish_.end(), symbols_to_publish.begin(), symbols_to_publish.end());
286+
287+
// Continually generate new values for the symbols
288+
QTimer *timer = new QTimer( parent );
289+
QObject::connect(timer, &QTimer::timeout, parent, [this](){
290+
this->update_symbols();
291+
});
292+
timer->start(1000);
293+
}
294+
295+
void test_publish::update_symbols() {
296+
297+
// Send a random price update for each symbol
298+
for (size_t i = 0; i < symbols_to_publish_.size(); i++) {
299+
int64_t next_price = std::rand();
300+
uint64_t next_conf = (uint64_t) std::rand();
301+
302+
this->pythd_websocket_->add_price_update(
303+
this->symbols_to_publish_[i], next_price, next_conf, "trading" );
304+
}
305+
306+
}
307+
308+
int main( int argc, char* argv[] )
309+
{
310+
std::string pythd_websocket_endpoint = "ws://127.0.0.1:8910";
311+
312+
QCoreApplication app(argc, argv);
313+
std::vector<std::string> symbols_to_publish = { "Crypto.BTC/USD", "Crypto.ETH/USD" };
314+
new test_publish( &app, symbols_to_publish, pythd_websocket_endpoint );
315+
316+
app.exec();
317+
}

0 commit comments

Comments
 (0)