diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 0217970..feff4d8 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -5,6 +5,7 @@ add_executable(example-get-account-info getAccountInfo.cpp) add_executable(example-account-subscribe accountSubscribe.cpp) add_executable(example-send-transaction sendTransaction.cpp) add_executable(example-place-order placeOrder.cpp) +add_executable(example-orderbook-subscribe orderbookSubscribe.cpp) add_executable(example-positions positions.cpp) # link @@ -12,6 +13,7 @@ target_link_libraries(example-get-account-info ${CONAN_LIBS} sol) target_link_libraries(example-account-subscribe ${CONAN_LIBS} sol) target_link_libraries(example-send-transaction ${CONAN_LIBS} sol) target_link_libraries(example-place-order ${CONAN_LIBS} sol) +target_link_libraries(example-orderbook-subscribe ${CONAN_LIBS} sol) target_link_libraries(example-positions ${CONAN_LIBS} sol) target_compile_definitions(example-place-order PUBLIC FIXTURES_DIR="${CMAKE_SOURCE_DIR}/tests/fixtures") diff --git a/examples/orderbookSubscribe.cpp b/examples/orderbookSubscribe.cpp new file mode 100644 index 0000000..6420824 --- /dev/null +++ b/examples/orderbookSubscribe.cpp @@ -0,0 +1,105 @@ +#include +#include + +#include +#include +#include +#include + +#include "mango_v3.hpp" +#include "solana.hpp" +#include "subscriptions/accountSubscriber.hpp" +#include "subscriptions/orderbook.hpp" + +class updateLogger { + public: + updateLogger( + mango_v3::subscription::orderbook& orderbook, + mango_v3::subscription::accountSubscriber& trades, + const mango_v3::NativeToUi& nativeToUi) + : orderbook(orderbook), trades(trades), nativeToUi(nativeToUi) { + orderbook.registerUpdateCallback(std::bind(&updateLogger::logUpdate, this)); + trades.registerUpdateCallback(std::bind(&updateLogger::logUpdate, this)); + orderbook.registerCloseCallback(std::bind(&updateLogger::abort, this)); + trades.registerCloseCallback(std::bind(&updateLogger::abort, this)); + } + + void start() { + orderbook.subscribe(); + trades.subscribe(); + } + + void logUpdate() { + std::scoped_lock lock(logMtx); + auto level1Snapshot = orderbook.getLevel1(); + if (level1Snapshot) { + if (level1Snapshot->valid()) { + spdlog::info("============Update============"); + auto lastTrade = trades.getAccount()->getLastTrade(); + if (lastTrade) { + spdlog::info("Last trade: price {:.2f}, quantity {:.2f}", + nativeToUi.getPrice(lastTrade->price), + nativeToUi.getQuantity(lastTrade->quantity)); + } else { + spdlog::info("No trade since the subscription started"); + } + spdlog::info("Bid-Ask ({:.2f}) {:.2f}-{:.2f} ({:.2f})", + nativeToUi.getQuantity(level1Snapshot->highestBidSize), + nativeToUi.getPrice(level1Snapshot->highestBid), + nativeToUi.getPrice(level1Snapshot->lowestAsk), + nativeToUi.getQuantity(level1Snapshot->lowestAskSize)); + spdlog::info("MidPrice: {:.2f}", + nativeToUi.getPrice(level1Snapshot->midPoint)); + spdlog::info("Spread: {:.2f} bps", level1Snapshot->spreadBps); + + constexpr auto depth = 2; + spdlog::info("Market depth -{}%: {}", depth, + orderbook.getDepth(-depth)); + spdlog::info("Market depth +{}%: {}", depth, orderbook.getDepth(depth)); + } + } + } + + void abort() { + spdlog::error("websocket subscription error"); + throw std::runtime_error("websocket subscription error"); + } + + private: + mango_v3::subscription::orderbook& orderbook; + mango_v3::subscription::accountSubscriber& trades; + const mango_v3::NativeToUi& nativeToUi; + std::mutex logMtx; +}; + +int main() { + using namespace mango_v3; + const auto& config = MAINNET; + const solana::rpc::Connection solCon; + const auto group = solCon.getAccountInfo(config.group); + + const auto symbolIt = + std::find(config.symbols.begin(), config.symbols.end(), "SOL"); + const auto marketIndex = symbolIt - config.symbols.begin(); + assert(config.symbols[marketIndex] == "SOL"); + + const auto perpMarketPk = group.perpMarkets[marketIndex].perpMarket; + auto market = solCon.getAccountInfo(perpMarketPk.toBase58()); + assert(market.mangoGroup.toBase58() == config.group); + + subscription::accountSubscriber trades(market.eventQueue.toBase58()); + subscription::orderbook book(market); + + mango_v3::NativeToUi nativeToUi(market.quoteLotSize, market.baseLotSize, + group.tokens[QUOTE_INDEX].decimals, + group.tokens[marketIndex].decimals); + updateLogger logger(book, trades, nativeToUi); + logger.start(); + + using namespace std::literals::chrono_literals; + while (true) { + std::this_thread::sleep_for(100s); + } + + return 0; +} diff --git a/include/mango_v3.hpp b/include/mango_v3.hpp index 36f9407..ed21c1b 100644 --- a/include/mango_v3.hpp +++ b/include/mango_v3.hpp @@ -1,6 +1,10 @@ #pragma once +#include #include +#include +#include +#include #include #include "fixedp.h" @@ -18,6 +22,8 @@ const int INFO_LEN = 32; const int QUOTE_INDEX = 15; const int EVENT_SIZE = 200; const int EVENT_QUEUE_SIZE = 256; +const int BOOK_NODE_SIZE = 88; +const int BOOK_SIZE = 1024; const int MAXIMUM_NUMBER_OF_BLOCKS_FOR_TRANSACTION = 152; struct Config { @@ -127,6 +133,7 @@ struct MangoCache { PerpMarketCache perp_market_cache[MAX_PAIRS]; }; +// todo: change to scoped enum class enum Side : uint8_t { Buy, Sell }; struct PerpAccountInfo { @@ -199,6 +206,7 @@ struct EventQueueHeader { uint64_t seqNum; }; +// todo: change to scoped enum class enum EventType : uint8_t { Fill, Out, Liquidate }; struct AnyEvent { @@ -259,6 +267,270 @@ struct EventQueue { AnyEvent items[EVENT_QUEUE_SIZE]; }; +// todo: change to scoped enum class +enum NodeType : uint32_t { + Uninitialized = 0, + InnerNode, + LeafNode, + FreeNode, + LastFreeNode +}; + +struct AnyNode { + NodeType tag; + uint8_t padding[BOOK_NODE_SIZE - 4]; +}; + +struct InnerNode { + NodeType tag; + uint32_t prefixLen; + __uint128_t key; + uint32_t children[2]; + uint8_t padding[BOOK_NODE_SIZE - 32]; +}; + +struct LeafNode { + NodeType tag; + uint8_t ownerSlot; + uint8_t orderType; + uint8_t version; + uint8_t timeInForce; + __uint128_t key; + solana::PublicKey owner; + uint64_t quantity; + uint64_t clientOrderId; + uint64_t bestInitial; + uint64_t timestamp; +}; + +struct FreeNode { + NodeType tag; + uint32_t next; + uint8_t padding[BOOK_NODE_SIZE - 8]; +}; + +struct L1Orderbook { + uint64_t highestBid = 0; + uint64_t highestBidSize = 0; + uint64_t lowestAsk = 0; + uint64_t lowestAskSize = 0; + double midPoint = 0.0; + double spreadBps = 0.0; + + bool valid() const { + return ((highestBid && lowestAsk) && (lowestAsk > highestBid)) ? true + : false; + } +}; + +class NativeToUi { + public: + NativeToUi(int64_t quoteLotSize, int64_t baseLotSize, uint8_t quoteDecimals, + uint8_t baseDecimals) { + baseLotsToUiConvertor = baseLotSize / std::pow(10, baseDecimals); + priceLotsToUiConvertor = std::pow(10, (baseDecimals - quoteDecimals)) * + quoteLotSize / baseLotSize; + } + + double getPrice(int64_t price) const { + return price * priceLotsToUiConvertor; + } + + double getQuantity(int64_t quantity) const { + return quantity * baseLotsToUiConvertor; + } + + private: + double baseLotsToUiConvertor; + double priceLotsToUiConvertor; +}; + +class BookSide { + public: + using order_t = struct LeafNode; + using orders_t = std::vector; + + BookSide(Side side, uint8_t maxBookDelay = 255) + : side(side), maxBookDelay(maxBookDelay) {} + + bool update(const std::string& decoded) { + if (decoded.size() != sizeof(BookSideRaw)) { + throw std::runtime_error("invalid response length " + + std::to_string(decoded.size()) + " expected " + + std::to_string(sizeof(BookSideRaw))); + } + std::scoped_lock lock(updateMtx); + memcpy(&(*raw), decoded.data(), sizeof(BookSideRaw)); + auto iter = BookSide::BookSideRaw::iterator(side, *raw); + orders_t newOrders; + + const auto now = getMaxTimestamp(); + + while (iter.stack.size() > 0) { + if ((*iter).tag == NodeType::LeafNode) { + const auto leafNode = + reinterpret_cast(&(*iter)); + const auto isValid = + !leafNode->timeInForce || + ((leafNode->timestamp + leafNode->timeInForce) > now); + if (isValid) { + newOrders.push_back(*leafNode); + } + } + ++iter; + } + + if (!newOrders.empty()) { + orders = std::make_shared(std::move(newOrders)); + return true; + } else { + return false; + } + } + + std::shared_ptr getBestOrder() const { + std::scoped_lock lock(updateMtx); + return orders->empty() ? nullptr + : std::make_shared(orders->front()); + } + + uint64_t getVolume(uint64_t price) const { + std::scoped_lock lock(updateMtx); + if (side == Side::Buy) { + return getVolume>(price); + } else { + return getVolume>(price); + } + } + + private: + long long getMaxTimestamp() { + const auto now = std::chrono::system_clock::now(); + const auto nowUnix = + std::chrono::duration_cast(now.time_since_epoch()) + .count(); + + auto maxTimestamp = nowUnix - maxBookDelay; + auto iter = BookSide::BookSideRaw::iterator(side, *raw); + while (iter.stack.size() > 0) { + if ((*iter).tag == NodeType::LeafNode) { + const auto leafNode = + reinterpret_cast(&(*iter)); + if (leafNode->timestamp > maxTimestamp) { + maxTimestamp = leafNode->timestamp; + } + } + ++iter; + } + return maxTimestamp; + } + + template + uint64_t getVolume(uint64_t price) const { + Op operation; + uint64_t volume = 0; + auto tmpOrders = *orders; + for (auto&& order : tmpOrders) { + auto orderPrice = (uint64_t)(order.key >> 64); + if (operation(orderPrice, price)) { + volume += order.quantity; + } else { + break; + } + } + return volume; + } + + struct BookSideRaw { + MetaData metaData; + uint64_t bumpIndex; + uint64_t freeListLen; + uint32_t freeListHead; + uint32_t rootNode; + uint64_t leafCount; + AnyNode nodes[BOOK_SIZE]; + + struct iterator { + Side side; + const BookSideRaw& bookSide; + std::stack stack; + uint32_t left, right; + + iterator(Side side, const BookSideRaw& bookSide) + : side(side), bookSide(bookSide) { + stack.push(bookSide.rootNode); + left = side == Side::Buy ? 1 : 0; + right = side == Side::Buy ? 0 : 1; + } + + bool operator==(const iterator& other) const { + return &bookSide == &other.bookSide && stack.top() == other.stack.top(); + } + + iterator& operator++() { + if (stack.size() > 0) { + const auto& elem = **this; + stack.pop(); + + if (elem.tag == NodeType::InnerNode) { + const auto innerNode = + reinterpret_cast(&elem); + stack.push(innerNode->children[right]); + stack.push(innerNode->children[left]); + } + } + return *this; + } + + const AnyNode& operator*() const { return bookSide.nodes[stack.top()]; } + }; + }; + + const Side side; + uint8_t maxBookDelay; + std::shared_ptr raw = std::make_shared(); + std::shared_ptr orders = std::make_shared(); + mutable std::mutex updateMtx; +}; + +class Trades { + public: + auto getLastTrade() const { return lastTrade; } + + bool update(const std::string& decoded) { + std::scoped_lock lock(updateMtx); + const auto events = reinterpret_cast(decoded.data()); + const auto seqNumDiff = events->header.seqNum - lastSeqNum; + const auto lastSlot = + (events->header.head + events->header.count) % EVENT_QUEUE_SIZE; + + bool gotLatest = false; + if (events->header.seqNum > lastSeqNum) { + for (int offset = seqNumDiff; offset > 0; --offset) { + const auto slot = + (lastSlot - offset + EVENT_QUEUE_SIZE) % EVENT_QUEUE_SIZE; + const auto& event = events->items[slot]; + + if (event.eventType == EventType::Fill) { + const auto& fill = (FillEvent&)event; + lastTrade = std::make_shared(fill); + gotLatest = true; + } + // no break; let's iterate to the last fill to get the latest fill + // order + } + } + + lastSeqNum = events->header.seqNum; + return gotLatest; + } + + private: + uint64_t lastSeqNum = INT_MAX; + std::shared_ptr lastTrade; + std::mutex updateMtx; +}; + #pragma pack(pop) // instructions are even tighter packed, every byte counts @@ -344,4 +616,4 @@ solana::Instruction cancelAllPerpOrdersInstruction( #pragma pack(pop) -} // namespace mango_v3 \ No newline at end of file +} // namespace mango_v3 diff --git a/include/solana.hpp b/include/solana.hpp index 3d61609..7e35d75 100644 --- a/include/solana.hpp +++ b/include/solana.hpp @@ -268,7 +268,8 @@ class Connection { /// json getAccountInfoRequest(const std::string &account, const std::string &encoding = "base64", - const size_t offset = 0, const size_t length = 0); + const size_t offset = 0, + const size_t length = 0) const; json getMultipleAccountsRequest(const std::vector &accounts, const std::string &encoding = "base64", const size_t offset = 0, @@ -295,7 +296,8 @@ class Connection { template inline T getAccountInfo(const std::string &account, const std::string &encoding = "base64", - const size_t offset = 0, const size_t length = 0) { + const size_t offset = 0, + const size_t length = 0) const { const json req = getAccountInfoRequest(account, encoding, offset, length); cpr::Response r = cpr::Post(cpr::Url{rpc_url_}, cpr::Body{req.dump()}, @@ -372,4 +374,4 @@ inline json accountSubscribeRequest(const std::string &account, } } // namespace subscription } // namespace rpc -} // namespace solana \ No newline at end of file +} // namespace solana diff --git a/include/subscriptions/accountSubscriber.hpp b/include/subscriptions/accountSubscriber.hpp new file mode 100644 index 0000000..3987705 --- /dev/null +++ b/include/subscriptions/accountSubscriber.hpp @@ -0,0 +1,67 @@ +#pragma once + +#include +#include +#include + +#include "wssSubscriber.hpp" + +namespace mango_v3 { +namespace subscription { + +template +class accountSubscriber { + public: + template + accountSubscriber(const std::string &name, Args... args) + : wssConnection(name), account(std::make_shared(args...)) {} + + template + void registerUpdateCallback(func &&callback) { + notifyCb = std::forward(callback); + } + + template + void registerCloseCallback(func &&callback) { + closeCb = std::forward(callback); + ; + } + + std::shared_ptr getAccount() { return account; } + + void subscribe() { + wssConnection.registerOnMessageCallback( + std::bind(&accountSubscriber::onMessage, this, std::placeholders::_1)); + wssConnection.registerOnCloseCallback( + std::bind(&accountSubscriber::onClose, this)); + wssConnection.start(); + } + + private: + void onMessage(const nlohmann::json &msg) { + const auto itResult = msg.find("result"); + if (itResult != msg.end()) { + return; + } + + const std::string encoded = msg["params"]["result"]["value"]["data"][0]; + const std::string decoded = solana::b64decode(encoded); + if (account->update(decoded)) { + if (notifyCb) { + notifyCb(); + } + } + } + void onClose() { + if (closeCb) { + closeCb(); + } + } + + std::shared_ptr account; + wssSubscriber wssConnection; + std::function notifyCb; + std::function closeCb; +}; +} // namespace subscription +} // namespace mango_v3 diff --git a/include/subscriptions/orderbook.hpp b/include/subscriptions/orderbook.hpp new file mode 100644 index 0000000..c7b191d --- /dev/null +++ b/include/subscriptions/orderbook.hpp @@ -0,0 +1,74 @@ +#pragma once +#include + +#include + +#include "mango_v3.hpp" + +namespace mango_v3 { +namespace subscription { +class orderbook { + public: + orderbook(const PerpMarket& perpMarket) + : bids(perpMarket.bids.toBase58(), Buy), + asks(perpMarket.asks.toBase58(), Sell) { + bids.registerUpdateCallback(std::bind(&orderbook::updateCallback, this)); + asks.registerUpdateCallback(std::bind(&orderbook::updateCallback, this)); + } + + template + void registerUpdateCallback(func&& callback) { + onUpdateCb = std::forward(callback); + } + + template + void registerCloseCallback(func&& callback) { + bids.registerCloseCallback(callback); + asks.registerCloseCallback(std::forward(callback)); + } + + void subscribe() { + bids.subscribe(); + asks.subscribe(); + } + + void updateCallback() { + L1Orderbook newL1; + auto bestBid = bids.getAccount()->getBestOrder(); + auto bestAsk = asks.getAccount()->getBestOrder(); + if (bestBid && bestAsk) { + newL1.highestBid = (uint64_t)(bestBid->key >> 64); + newL1.highestBidSize = bestBid->quantity; + newL1.lowestAsk = (uint64_t)(bestAsk->key >> 64); + newL1.lowestAskSize = bestAsk->quantity; + + if (newL1.valid()) { + newL1.midPoint = ((double)newL1.lowestAsk + newL1.highestBid) / 2; + newL1.spreadBps = + ((newL1.lowestAsk - newL1.highestBid) * 10000) / newL1.midPoint; + level1 = std::make_shared(std::move(newL1)); + onUpdateCb(); + } + } + } + + auto getLevel1() const { return level1; } + + auto getDepth(int8_t percent) { + uint64_t depth = 0; + if (level1) { + auto price = (level1->midPoint * (100 + percent)) / 100; + depth = (percent > 0) ? asks.getAccount()->getVolume(price) + : bids.getAccount()->getVolume(price); + } + return depth; + } + + private: + std::shared_ptr level1; + std::function onUpdateCb; + subscription::accountSubscriber bids; + subscription::accountSubscriber asks; +}; +} // namespace subscription +} // namespace mango_v3 diff --git a/include/subscriptions/wssSubscriber.hpp b/include/subscriptions/wssSubscriber.hpp new file mode 100644 index 0000000..67e1349 --- /dev/null +++ b/include/subscriptions/wssSubscriber.hpp @@ -0,0 +1,115 @@ +#pragma once + +#include + +#include +#include +#include +#include + +#include "mango_v3.hpp" + +typedef websocketpp::client ws_client; +typedef websocketpp::config::asio_client::message_type::ptr ws_message_ptr; +typedef std::shared_ptr context_ptr; + +namespace mango_v3 { +namespace subscription { + +using json = nlohmann::json; + +class wssSubscriber { + public: + wssSubscriber(const std::string& account) : account(account) {} + + ~wssSubscriber() { + if (runThread.joinable()) { + websocketpp::lib::error_code ec; + c.stop(); + runThread.join(); + } + } + + void registerOnMessageCallback( + std::function callback) { + onMessageCb = callback; + } + + void registerOnCloseCallback(std::function callback) { + onCloseCb = callback; + } + + void start() { + c.clear_access_channels(websocketpp::log::alevel::all); + c.init_asio(); + c.set_tls_init_handler( + websocketpp::lib::bind(&wssSubscriber::on_tls_init, this)); + c.set_open_handler(websocketpp::lib::bind( + &wssSubscriber::on_open, this, websocketpp::lib::placeholders::_1)); + c.set_message_handler(websocketpp::lib::bind( + &wssSubscriber::on_message, this, websocketpp::lib::placeholders::_1, + websocketpp::lib::placeholders::_2)); + c.set_close_handler(websocketpp::lib::bind( + &wssSubscriber::on_close, this, websocketpp::lib::placeholders::_1)); + + try { + websocketpp::lib::error_code ec; + ws_client::connection_ptr con = c.get_connection(MAINNET.endpoint, ec); + if (ec) { + spdlog::error("could not create connection because: {}", ec.message()); + } + c.connect(con); + runThread = std::thread(&ws_client::run, &c); + } catch (websocketpp::exception const& e) { + spdlog::error("{}", e.what()); + } + } + + private: + context_ptr on_tls_init() { + context_ptr ctx = std::make_shared( + boost::asio::ssl::context::sslv23); + + try { + ctx->set_options(boost::asio::ssl::context::default_workarounds | + boost::asio::ssl::context::no_sslv2 | + boost::asio::ssl::context::no_sslv3 | + boost::asio::ssl::context::single_dh_use); + } catch (std::exception& e) { + spdlog::error("Error in context pointer: {}", e.what()); + } + return ctx; + } + + void on_open(websocketpp::connection_hdl hdl) { + websocketpp::lib::error_code ec; + + c.send(hdl, + solana::rpc::subscription::accountSubscribeRequest(account).dump(), + websocketpp::frame::opcode::value::text, ec); + if (ec) { + spdlog::error("subscribe failed because: {}", ec.message()); + } else { + spdlog::info("subscribed to account {}", account); + } + } + + void on_close(websocketpp::connection_hdl hdl) { + if (onCloseCb) { + onCloseCb(); + } + } + + void on_message(websocketpp::connection_hdl hdl, ws_message_ptr msg) { + const json parsedMsg = json::parse(msg->get_payload()); + onMessageCb(parsedMsg); + } + + ws_client c; + const std::string account; + std::thread runThread; + std::function onMessageCb; + std::function onCloseCb; +}; +} // namespace subscription +} // namespace mango_v3 diff --git a/lib/solana.cpp b/lib/solana.cpp index 21e5781..fbb3daf 100644 --- a/lib/solana.cpp +++ b/lib/solana.cpp @@ -20,7 +20,7 @@ Connection::Connection(const std::string &rpc_url, json Connection::getAccountInfoRequest(const std::string &account, const std::string &encoding, const size_t offset, - const size_t length) { + const size_t length) const { json params = {account}; json options = {{"encoding", encoding}}; if (offset && length) { @@ -153,4 +153,4 @@ std::string Connection::signAndSendTransaction( } } // namespace rpc -} // namespace solana \ No newline at end of file +} // namespace solana diff --git a/tests/main.cpp b/tests/main.cpp index d0dd338..b92b250 100644 --- a/tests/main.cpp +++ b/tests/main.cpp @@ -630,4 +630,59 @@ TEST_CASE("account9") { auto leverage = mangoAccount.getLeverage(mangoGroup, mangoCache); CHECK_EQ(leverage.to_double(), 3.919442838288937); CHECK_FALSE(mangoAccount.isLiquidatable(mangoGroup, mangoCache)); -} \ No newline at end of file +} + +TEST_CASE("NativeToUi Conversions") { + using namespace mango_v3; + const auto& config = MAINNET; + const solana::rpc::Connection solCon; + const auto group = solCon.getAccountInfo(config.group); + + for (auto&& symbol : config.symbols) { + const auto marketIndex = (&symbol - &config.symbols[0]); + const auto perpMarketPk = group.perpMarkets[marketIndex].perpMarket; + /* todo: currently getAccountInfo throws invalid response length exception + * for following tokens, so filtering them*/ + if (symbol == "USDT" or symbol == "COPE" or symbol == "BNB" or + symbol == "USDC" or symbol == "") { + continue; + } + auto market = solCon.getAccountInfo(perpMarketPk.toBase58()); + + mango_v3::NativeToUi nativeToUi(market.quoteLotSize, market.baseLotSize, + group.tokens[QUOTE_INDEX].decimals, + group.tokens[marketIndex].decimals); + + if (symbol == "MNGO") { + CHECK_EQ(nativeToUi.getPrice(10000), 1.0); + CHECK_EQ(nativeToUi.getQuantity(10000), 10000); + } else if (symbol == "BTC") { + CHECK_EQ(nativeToUi.getPrice(10000), 1000); + CHECK_EQ(nativeToUi.getQuantity(10000), 1); + } else if (symbol == "ETH") { + CHECK_EQ(nativeToUi.getPrice(10000), 1000); + CHECK_EQ(nativeToUi.getQuantity(10000), 10); + } else if (symbol == "SOL") { + CHECK_EQ(nativeToUi.getPrice(10000), 100); + CHECK_EQ(nativeToUi.getQuantity(10000), 100); + } else if (symbol == "SRM") { + CHECK_EQ(nativeToUi.getPrice(10000), 10); + CHECK_EQ(nativeToUi.getQuantity(10000), 1000); + } else if (symbol == "RAY") { + CHECK_EQ(nativeToUi.getPrice(10000), 10); + CHECK_EQ(nativeToUi.getQuantity(10000), 1000); + } else if (symbol == "FTT") { + CHECK_EQ(nativeToUi.getPrice(10000), 10); + CHECK_EQ(nativeToUi.getQuantity(10000), 1000); + } else if (symbol == "MSOL") { + CHECK_EQ(nativeToUi.getPrice(10000), 1); + CHECK_EQ(nativeToUi.getQuantity(10000), 10000); + } else if (symbol == "AVAX") { + CHECK_EQ(nativeToUi.getPrice(10000), 1000); + CHECK_EQ(nativeToUi.getQuantity(10000), 10); + } else if (symbol == "LUNA") { + CHECK_EQ(nativeToUi.getPrice(10000), 100); + CHECK_EQ(nativeToUi.getQuantity(10000), 100); + } + } +}