diff --git a/CMakeLists.txt b/CMakeLists.txt index 72ffbc00..2e37774b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -72,7 +72,10 @@ target_sources(reporting PRIVATE handlers/Ledger.cpp handlers/LedgerEntry.cpp handlers/AccountChannels.cpp - handlers/AccountLines.cpp) + handlers/AccountLines.cpp + handlers/AccountCurrencies.cpp + handlers/AccountOffers.cpp + handlers/AccountObjects.cpp) message(${Boost_LIBRARIES}) diff --git a/handlers/AccountChannels.cpp b/handlers/AccountChannels.cpp index 05cf0cbf..262f4d68 100644 --- a/handlers/AccountChannels.cpp +++ b/handlers/AccountChannels.cpp @@ -36,7 +36,7 @@ addChannel(boost::json::array& jsonLines, ripple::SLE const& line) if (auto const& v = line[~ripple::sfDestinationTag]) jDst["destination_tag"] = *v; - jsonLines.push_back(Json::objectValue); + jsonLines.push_back(jDst); } boost::json::object @@ -95,46 +95,72 @@ doAccountChannels( } } - auto const rootIndex = ripple::keylet::ownerDir(accountID); - auto currentIndex = rootIndex; - - std::vector keys; - - for (;;) + std::uint32_t limit = 200; + if (request.contains("limit")) { - auto ownedNode = - backend.fetchLedgerObject(currentIndex.key, *ledgerSequence); + if(!request.at("limit").is_int64()) + { + response["error"] = "limit must be integer"; + return response; + } - ripple::SerialIter it{ownedNode->data(), ownedNode->size()}; - ripple::SLE dir{it, currentIndex.key}; - for (auto const& key : dir.getFieldV256(ripple::sfIndexes)) - keys.push_back(key); - - auto const uNodeNext = dir.getFieldU64(ripple::sfIndexNext); - if (uNodeNext == 0) - break; - - currentIndex = ripple::keylet::page(rootIndex, uNodeNext); + limit = request.at("limit").as_int64(); + if (limit <= 0) + { + response["error"] = "limit must be positive"; + return response; + } } - auto objects = backend.fetchLedgerObjects(keys, *ledgerSequence); + ripple::uint256 cursor = beast::zero; + if (request.contains("cursor")) + { + if(!request.at("cursor").is_string()) + { + response["error"] = "limit must be string"; + return response; + } + + auto bytes = ripple::strUnHex(request.at("cursor").as_string().c_str()); + if (bytes and bytes->size() == 32) + { + response["error"] = "invalid cursor"; + return response; + } + + cursor = ripple::uint256::fromVoid(bytes->data()); + } response["channels"] = boost::json::value(boost::json::array_kind); boost::json::array& jsonChannels = response.at("channels").as_array(); - for (auto i = 0; i < objects.size(); ++i) - { - ripple::SerialIter it{objects[i].data(), objects[i].size()}; - ripple::SLE sle{it, keys[i]}; - + auto const addToResponse = [&](ripple::SLE const& sle) { if (sle.getType() == ripple::ltPAYCHAN && sle.getAccountID(ripple::sfAccount) == accountID && (!destAccount || *destAccount == sle.getAccountID(ripple::sfDestination))) { + if (limit-- == 0) + { + return false; + } + addChannel(jsonChannels, sle); } - } + + return true; + }; + + auto nextCursor = + traverseOwnedNodes( + backend, + accountID, + *ledgerSequence, + cursor, + addToResponse); + + if (nextCursor) + response["next_cursor"] = ripple::strHex(*nextCursor); return response; } diff --git a/handlers/AccountCurrencies.cpp b/handlers/AccountCurrencies.cpp new file mode 100644 index 00000000..98e6bd76 --- /dev/null +++ b/handlers/AccountCurrencies.cpp @@ -0,0 +1,95 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +boost::json::object +doAccountCurrencies( + boost::json::object const& request, + BackendInterface const& backend) +{ + boost::json::object response; + + auto ledgerSequence = ledgerSequenceFromRequest(request, backend); + if (!ledgerSequence) + { + response["error"] = "Empty database"; + return response; + } + + if(!request.contains("account")) + { + response["error"] = "Must contain account"; + return response; + } + + if(!request.at("account").is_string()) + { + response["error"] = "Account must be a string"; + return response; + } + + ripple::AccountID accountID; + auto parsed = ripple::parseBase58( + request.at("account").as_string().c_str()); + + if (!parsed) + { + response["error"] = "Invalid account"; + return response; + } + + accountID = *parsed; + + + std::set send, receive; + auto const addToResponse = [&](ripple::SLE const& sle) { + if (sle.getType() == ripple::ltRIPPLE_STATE) + { + ripple::STAmount const& balance = + sle.getFieldAmount(ripple::sfBalance); + + auto lowLimit = sle.getFieldAmount(ripple::sfLowLimit); + auto highLimit = sle.getFieldAmount(ripple::sfHighLimit); + bool viewLowest = (lowLimit.getIssuer() == accountID); + auto lineLimit = viewLowest ? lowLimit : highLimit; + auto lineLimitPeer = !viewLowest ? lowLimit : highLimit; + + if (balance < lineLimit) + receive.insert(ripple::to_string(balance.getCurrency())); + if ((-balance) < lineLimitPeer) + send.insert(ripple::to_string(balance.getCurrency())); + } + + return true; + }; + + traverseOwnedNodes( + backend, + accountID, + *ledgerSequence, + beast::zero, + addToResponse); + + response["send_currencies"] = boost::json::value(boost::json::array_kind); + boost::json::array& jsonSend = response.at("send_currencies").as_array(); + + for (auto const& currency : send) + jsonSend.push_back(currency.c_str()); + + response["receive_currencies"] = boost::json::value(boost::json::array_kind); + boost::json::array& jsonReceive = response.at("receive_currencies").as_array(); + + for (auto const& currency : receive) + jsonReceive.push_back(currency.c_str()); + + return response; +} diff --git a/handlers/AccountLines.cpp b/handlers/AccountLines.cpp index 3bcbbf6a..3d59ae16 100644 --- a/handlers/AccountLines.cpp +++ b/handlers/AccountLines.cpp @@ -131,43 +131,70 @@ doAccountLines( } } - auto const rootIndex = ripple::keylet::ownerDir(accountID); - auto currentIndex = rootIndex; - - std::vector keys; - - for (;;) + std::uint32_t limit = 200; + if (request.contains("limit")) { - auto ownedNode = - backend.fetchLedgerObject(currentIndex.key, *ledgerSequence); + if(!request.at("limit").is_int64()) + { + response["error"] = "limit must be integer"; + return response; + } - ripple::SerialIter it{ownedNode->data(), ownedNode->size()}; - ripple::SLE dir{it, currentIndex.key}; - for (auto const& key : dir.getFieldV256(ripple::sfIndexes)) - keys.push_back(key); - - auto const uNodeNext = dir.getFieldU64(ripple::sfIndexNext); - if (uNodeNext == 0) - break; - - currentIndex = ripple::keylet::page(rootIndex, uNodeNext); + limit = request.at("limit").as_int64(); + if (limit <= 0) + { + response["error"] = "limit must be positive"; + return response; + } } - auto objects = backend.fetchLedgerObjects(keys, *ledgerSequence); + ripple::uint256 cursor = beast::zero; + if (request.contains("cursor")) + { + if(!request.at("cursor").is_string()) + { + response["error"] = "limit must be string"; + return response; + } + + auto bytes = ripple::strUnHex(request.at("cursor").as_string().c_str()); + if (bytes and bytes->size() != 32) + { + response["error"] = "invalid cursor"; + return response; + } + + cursor = ripple::uint256::fromVoid(bytes->data()); + } + response["lines"] = boost::json::value(boost::json::array_kind); boost::json::array& jsonLines = response.at("lines").as_array(); - for (auto i = 0; i < objects.size(); ++i) - { - ripple::SerialIter it{objects[i].data(), objects[i].size()}; - ripple::SLE sle(it, keys[i]); - + auto const addToResponse = [&](ripple::SLE const& sle) { if (sle.getType() == ripple::ltRIPPLE_STATE) { + if (limit-- == 0) + { + return false; + } + addLine(jsonLines, sle, accountID, peerAccount); } - } + + return true; + }; + + auto nextCursor = + traverseOwnedNodes( + backend, + accountID, + *ledgerSequence, + cursor, + addToResponse); + + if (nextCursor) + response["next_cursor"] = ripple::strHex(*nextCursor); return response; } \ No newline at end of file diff --git a/handlers/AccountObjects.cpp b/handlers/AccountObjects.cpp new file mode 100644 index 00000000..01a3e802 --- /dev/null +++ b/handlers/AccountObjects.cpp @@ -0,0 +1,125 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +std::unordered_map types { + {"state", ripple::ltRIPPLE_STATE}, + {"ticket", ripple::ltTICKET}, + {"signer_list", ripple::ltSIGNER_LIST}, + {"payment_channel", ripple::ltPAYCHAN}, + {"offer", ripple::ltOFFER}, + {"escrow", ripple::ltESCROW}, + {"deposit_preauth", ripple::ltDEPOSIT_PREAUTH}, + {"check", ripple::ltCHECK}, +}; + +boost::json::object +doAccountObjects( + boost::json::object const& request, + BackendInterface const& backend) +{ + boost::json::object response; + + auto ledgerSequence = ledgerSequenceFromRequest(request, backend); + if (!ledgerSequence) + { + response["error"] = "Empty database"; + return response; + } + + if(!request.contains("account")) + { + response["error"] = "Must contain account"; + return response; + } + + if(!request.at("account").is_string()) + { + response["error"] = "Account must be a string"; + return response; + } + + ripple::AccountID accountID; + auto parsed = ripple::parseBase58( + request.at("account").as_string().c_str()); + + if (!parsed) + { + response["error"] = "Invalid account"; + return response; + } + + accountID = *parsed; + + ripple::uint256 cursor = beast::zero; + if (request.contains("cursor")) + { + if(!request.at("cursor").is_string()) + { + response["error"] = "limit must be string"; + return response; + } + + auto bytes = ripple::strUnHex(request.at("cursor").as_string().c_str()); + if (bytes and bytes->size() != 32) + { + response["error"] = "invalid cursor"; + return response; + } + + cursor = ripple::uint256::fromVoid(bytes->data()); + } + + std::optional objectType = {}; + if (request.contains("type")) + { + if(!request.at("type").is_string()) + { + response["error"] = "type must be string"; + return response; + } + + std::string typeAsString = request.at("type").as_string().c_str(); + if(types.find(typeAsString) == types.end()) + { + response["error"] = "invalid object type"; + return response; + } + + objectType = types[typeAsString]; + } + + response["objects"] = boost::json::value(boost::json::array_kind); + boost::json::array& jsonObjects = response.at("objects").as_array(); + + auto const addToResponse = [&](ripple::SLE const& sle) { + if (!objectType || objectType == sle.getType()) + { + jsonObjects.push_back(getJson(sle)); + } + + return true; + }; + + auto nextCursor = + traverseOwnedNodes( + backend, + accountID, + *ledgerSequence, + cursor, + addToResponse); + + if (nextCursor) + response["next_cursor"] = ripple::strHex(*nextCursor); + + return response; +} \ No newline at end of file diff --git a/handlers/AccountOffers.cpp b/handlers/AccountOffers.cpp new file mode 100644 index 00000000..d57f0296 --- /dev/null +++ b/handlers/AccountOffers.cpp @@ -0,0 +1,164 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +void +addOffer(boost::json::array& offersJson, ripple::SLE const& offer) +{ + auto quality = getQuality(offer.getFieldH256(ripple::sfBookDirectory)); + ripple::STAmount rate = ripple::amountFromQuality(quality); + + ripple::STAmount takerPays = offer.getFieldAmount(ripple::sfTakerPays); + ripple::STAmount takerGets = offer.getFieldAmount(ripple::sfTakerGets); + + boost::json::object obj; + + if (!takerPays.native()) + { + obj["taker_pays"] = boost::json::value(boost::json::object_kind); + boost::json::object& takerPaysJson = obj.at("taker_pays").as_object(); + + takerPaysJson["value"] = takerPays.getText(); + takerPaysJson["currency"] = ripple::to_string(takerPays.getCurrency()); + takerPaysJson["issuer"] = ripple::to_string(takerPays.getIssuer()); + } + else + { + obj["taker_pays"] = takerPays.getText(); + } + + if (!takerGets.native()) + { + obj["taker_gets"] = boost::json::value(boost::json::object_kind); + boost::json::object& takerGetsJson = obj.at("taker_gets").as_object(); + + takerGetsJson["value"] = takerGets.getText(); + takerGetsJson["currency"] = ripple::to_string(takerGets.getCurrency()); + takerGetsJson["issuer"] = ripple::to_string(takerGets.getIssuer()); + } + else + { + obj["taker_gets"] = takerGets.getText(); + } + + obj["seq"] = offer.getFieldU32(ripple::sfSequence); + obj["flags"] = offer.getFieldU32(ripple::sfFlags); + obj["quality"] = rate.getText(); + if (offer.isFieldPresent(ripple::sfExpiration)) + obj["expiration"] = offer.getFieldU32(ripple::sfExpiration); + + offersJson.push_back(obj); +}; +boost::json::object +doAccountOffers( + boost::json::object const& request, + BackendInterface const& backend) +{ + boost::json::object response; + + auto ledgerSequence = ledgerSequenceFromRequest(request, backend); + if (!ledgerSequence) + { + response["error"] = "Empty database"; + return response; + } + + if(!request.contains("account")) + { + response["error"] = "Must contain account"; + return response; + } + + if(!request.at("account").is_string()) + { + response["error"] = "Account must be a string"; + return response; + } + + ripple::AccountID accountID; + auto parsed = ripple::parseBase58( + request.at("account").as_string().c_str()); + + if (!parsed) + { + response["error"] = "Invalid account"; + return response; + } + + accountID = *parsed; + + std::uint32_t limit = 200; + if (request.contains("limit")) + { + if(!request.at("limit").is_int64()) + { + response["error"] = "limit must be integer"; + return response; + } + + limit = request.at("limit").as_int64(); + if (limit <= 0) + { + response["error"] = "limit must be positive"; + return response; + } + } + + ripple::uint256 cursor = beast::zero; + if (request.contains("cursor")) + { + if(!request.at("cursor").is_string()) + { + response["error"] = "limit must be string"; + return response; + } + + auto bytes = ripple::strUnHex(request.at("cursor").as_string().c_str()); + if (bytes and bytes->size() != 32) + { + response["error"] = "invalid cursor"; + return response; + } + + cursor = ripple::uint256::fromVoid(bytes->data()); + } + + response["offers"] = boost::json::value(boost::json::array_kind); + boost::json::array& jsonLines = response.at("offers").as_array(); + + auto const addToResponse = [&](ripple::SLE const& sle) { + if (sle.getType() == ripple::ltOFFER) + { + if (limit-- == 0) + { + return false; + } + + addOffer(jsonLines, sle); + } + + return true; + }; + + auto nextCursor = + traverseOwnedNodes( + backend, + accountID, + *ledgerSequence, + cursor, + addToResponse); + + if (nextCursor) + response["next_cursor"] = ripple::strHex(*nextCursor); + + return response; +} \ No newline at end of file diff --git a/handlers/RPCHelpers.cpp b/handlers/RPCHelpers.cpp index 77e0f997..7010ccf9 100644 --- a/handlers/RPCHelpers.cpp +++ b/handlers/RPCHelpers.cpp @@ -80,3 +80,59 @@ ledgerSequenceFromRequest( return request.at("ledger_index").as_int64(); } } + +std::optional +traverseOwnedNodes( + BackendInterface const& backend, + ripple::AccountID const& accountID, + std::uint32_t sequence, + ripple::uint256 const& cursor, + std::function atOwnedNode) +{ + auto const rootIndex = ripple::keylet::ownerDir(accountID); + auto currentIndex = rootIndex; + + std::vector keys; + std::optional nextCursor = {}; + + for (;;) + { + auto ownedNode = + backend.fetchLedgerObject(currentIndex.key, sequence); + + if (!ownedNode) + { + throw std::runtime_error("Could not find owned node"); + } + + ripple::SerialIter it{ownedNode->data(), ownedNode->size()}; + ripple::SLE dir{it, currentIndex.key}; + + for (auto const& key : dir.getFieldV256(ripple::sfIndexes)) + { + if (key >= cursor) + keys.push_back(key); + } + + auto const uNodeNext = dir.getFieldU64(ripple::sfIndexNext); + if (uNodeNext == 0) + break; + + currentIndex = ripple::keylet::page(rootIndex, uNodeNext); + } + + auto objects = backend.fetchLedgerObjects(keys, sequence); + + for (auto i = 0; i < objects.size(); ++i) + { + ripple::SerialIter it{objects[i].data(), objects[i].size()}; + ripple::SLE sle(it, keys[i]); + if (!atOwnedNode(sle)) + { + nextCursor = keys[i+1]; + break; + } + } + + return nextCursor; +} diff --git a/handlers/RPCHelpers.h b/handlers/RPCHelpers.h index f07bf3ef..319faf6c 100644 --- a/handlers/RPCHelpers.h +++ b/handlers/RPCHelpers.h @@ -25,4 +25,12 @@ ledgerSequenceFromRequest( boost::json::object const& request, BackendInterface const& backend); +std::optional +traverseOwnedNodes( + BackendInterface const& backend, + ripple::AccountID const& accountID, + std::uint32_t sequence, + ripple::uint256 const& cursor, + std::function atOwnedNode); + #endif diff --git a/websocket_server_async.cpp b/websocket_server_async.cpp index 1617d81b..1d6e69b7 100644 --- a/websocket_server_async.cpp +++ b/websocket_server_async.cpp @@ -45,7 +45,10 @@ enum RPCCommand { ledger_range, ledger_entry, account_channels, - account_lines + account_lines, + account_currencies, + account_offers, + account_objects }; std::unordered_map commandMap{ {"tx", tx}, @@ -57,7 +60,10 @@ std::unordered_map commandMap{ {"ledger_data", ledger_data}, {"book_offers", book_offers}, {"account_channels", account_channels}, - {"account_lines", account_lines}}; + {"account_lines", account_lines}, + {"account_currencies", account_currencies}, + {"account_offers", account_offers}, + {"account_objects", account_objects}}; boost::json::object doAccountInfo( @@ -95,6 +101,18 @@ boost::json::object doAccountLines( boost::json::object const& request, BackendInterface const& backend); +boost::json::object +doAccountCurrencies( + boost::json::object const& request, + BackendInterface const& backend); +boost::json::object +doAccountOffers( + boost::json::object const& request, + BackendInterface const& backend); +boost::json::object +doAccountObjects( + boost::json::object const& request, + BackendInterface const& backend); boost::json::object buildResponse( @@ -136,6 +154,15 @@ buildResponse( case account_lines: return doAccountLines(request, backend); break; + case account_currencies: + return doAccountCurrencies(request, backend); + break; + case account_offers: + return doAccountOffers(request, backend); + break; + case account_objects: + return doAccountObjects(request, backend); + break; default: BOOST_LOG_TRIVIAL(error) << "Unknown command: " << command; }