diff --git a/CMakeLists.txt b/CMakeLists.txt index 28b74856..72ffbc00 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -70,7 +70,9 @@ target_sources(reporting PRIVATE handlers/BookOffers.cpp handlers/LedgerRange.cpp handlers/Ledger.cpp - handlers/LedgerEntry.cpp) + handlers/LedgerEntry.cpp + handlers/AccountChannels.cpp + handlers/AccountLines.cpp) message(${Boost_LIBRARIES}) diff --git a/handlers/AccountChannels.cpp b/handlers/AccountChannels.cpp new file mode 100644 index 00000000..05cf0cbf --- /dev/null +++ b/handlers/AccountChannels.cpp @@ -0,0 +1,140 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +void +addChannel(boost::json::array& jsonLines, ripple::SLE const& line) +{ + boost::json::object jDst; + jDst["channel_id"] = ripple::to_string(line.key()); + jDst["account"] = ripple::to_string(line.getAccountID(ripple::sfAccount)); + jDst["destination_account"] = ripple::to_string(line.getAccountID(ripple::sfDestination)); + jDst["amount"] = line[ripple::sfAmount].getText(); + jDst["balance"] = line[ripple::sfBalance].getText(); + if (publicKeyType(line[ripple::sfPublicKey])) + { + ripple::PublicKey const pk(line[ripple::sfPublicKey]); + jDst["public_key"] = toBase58(ripple::TokenType::AccountPublic, pk); + jDst["public_key_hex"] = strHex(pk); + } + jDst["settle_delay"] = line[ripple::sfSettleDelay]; + if (auto const& v = line[~ripple::sfExpiration]) + jDst["expiration"] = *v; + if (auto const& v = line[~ripple::sfCancelAfter]) + jDst["cancel_after"] = *v; + if (auto const& v = line[~ripple::sfSourceTag]) + jDst["source_tag"] = *v; + if (auto const& v = line[~ripple::sfDestinationTag]) + jDst["destination_tag"] = *v; + + jsonLines.push_back(Json::objectValue); +} + +boost::json::object +doAccountChannels( + 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; + + boost::optional destAccount; + if (request.contains("destination_account")) + { + if (!request.at("destination_account").is_string()) + { + response["error"] = "destination_account should be a string"; + return response; + } + + destAccount = ripple::parseBase58( + request.at("destination_account").as_string().c_str()); + if (!destAccount) + { + response["error"] = "Invalid destination account"; + return response; + } + } + + auto const rootIndex = ripple::keylet::ownerDir(accountID); + auto currentIndex = rootIndex; + + std::vector keys; + + for (;;) + { + auto ownedNode = + backend.fetchLedgerObject(currentIndex.key, *ledgerSequence); + + 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); + } + + auto objects = backend.fetchLedgerObjects(keys, *ledgerSequence); + + 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]}; + + if (sle.getType() == ripple::ltPAYCHAN && + sle.getAccountID(ripple::sfAccount) == accountID && + (!destAccount || + *destAccount == sle.getAccountID(ripple::sfDestination))) + { + addChannel(jsonChannels, sle); + } + } + + return response; +} diff --git a/handlers/AccountLines.cpp b/handlers/AccountLines.cpp new file mode 100644 index 00000000..3bcbbf6a --- /dev/null +++ b/handlers/AccountLines.cpp @@ -0,0 +1,173 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +void +addLine( + boost::json::array& jsonLines, + ripple::SLE const& line, + ripple::AccountID const& account, + boost::optional const& peerAccount) +{ + auto flags = line.getFieldU32(ripple::sfFlags); + auto lowLimit = line.getFieldAmount(ripple::sfLowLimit); + auto highLimit = line.getFieldAmount(ripple::sfHighLimit); + auto lowID = lowLimit.getIssuer(); + auto highID = highLimit.getIssuer(); + auto lowQualityIn = line.getFieldU32(ripple::sfLowQualityIn); + auto lowQualityOut = line.getFieldU32(ripple::sfLowQualityOut); + auto highQualityIn = line.getFieldU32(ripple::sfHighQualityIn); + auto highQualityOut = line.getFieldU32(ripple::sfHighQualityOut); + auto balance = line.getFieldAmount(ripple::sfBalance); + + bool viewLowest = (lowID == account); + auto lineLimit = viewLowest ? lowLimit : highLimit; + auto lineLimitPeer = !viewLowest ? lowLimit : highLimit; + auto lineAccountIDPeer = !viewLowest ? lowID : highID; + auto lineQualityIn = viewLowest ? lowQualityIn : highQualityIn; + auto lineQualityOut = viewLowest ? lowQualityOut : highQualityOut; + + if (peerAccount and peerAccount != lineAccountIDPeer) + return; + + bool lineAuth = flags & (viewLowest ? ripple::lsfLowAuth : ripple::lsfHighAuth); + bool lineAuthPeer = flags & (!viewLowest ? ripple::lsfLowAuth : ripple::lsfHighAuth); + bool lineNoRipple = flags & (viewLowest ? ripple::lsfLowNoRipple : ripple::lsfHighNoRipple); + bool lineDefaultRipple = flags & ripple::lsfDefaultRipple; + bool lineNoRipplePeer = flags & (!viewLowest ? ripple::lsfLowNoRipple : ripple::lsfHighNoRipple); + bool lineFreeze = flags & (viewLowest ? ripple::lsfLowFreeze : ripple::lsfHighFreeze); + bool lineFreezePeer = flags & (!viewLowest ? ripple::lsfLowFreeze : ripple::lsfHighFreeze); + + ripple::STAmount const& saBalance(balance); + ripple::STAmount const& saLimit(lineLimit); + ripple::STAmount const& saLimitPeer(lineLimitPeer); + + boost::json::object jPeer; + jPeer["account"] = ripple::to_string(lineAccountIDPeer); + jPeer["balance"] = saBalance.getText(); + jPeer["currency"] = ripple::to_string(saBalance.issue().currency); + jPeer["limit"] = saLimit.getText(); + jPeer["limit_peer"] = saLimitPeer.getText(); + jPeer["quality_in"] = lineQualityIn; + jPeer["quality_out"] = lineQualityOut; + if (lineAuth) + jPeer["authorized"] = true; + if (lineAuthPeer) + jPeer["peer_authorized"] = true; + if (lineNoRipple || !lineDefaultRipple) + jPeer["no_ripple"] = lineNoRipple; + if (lineNoRipple || !lineDefaultRipple) + jPeer["no_ripple_peer"] = lineNoRipplePeer; + if (lineFreeze) + jPeer["freeze"] = true; + if (lineFreezePeer) + jPeer["freeze_peer"] = true; + + jsonLines.push_back(jPeer); +} + +boost::json::object +doAccountLines( + 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; + + boost::optional peerAccount; + if (request.contains("peer")) + { + if (!request.at("peer").is_string()) + { + response["error"] = "peer should be a string"; + return response; + } + + peerAccount = ripple::parseBase58( + request.at("peer").as_string().c_str()); + if (!peerAccount) + { + response["error"] = "Invalid peer account"; + return response; + } + } + + auto const rootIndex = ripple::keylet::ownerDir(accountID); + auto currentIndex = rootIndex; + + std::vector keys; + + for (;;) + { + auto ownedNode = + backend.fetchLedgerObject(currentIndex.key, *ledgerSequence); + + 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); + } + + auto objects = backend.fetchLedgerObjects(keys, *ledgerSequence); + + 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]); + + if (sle.getType() == ripple::ltRIPPLE_STATE) + { + addLine(jsonLines, sle, accountID, peerAccount); + } + } + + return response; +} \ No newline at end of file diff --git a/handlers/BookOffers.cpp b/handlers/BookOffers.cpp index d0e37ddb..72847f82 100644 --- a/handlers/BookOffers.cpp +++ b/handlers/BookOffers.cpp @@ -11,38 +11,6 @@ #include #include -std::optional -ledgerSequenceFromRequest( - boost::json::object const& request, - std::shared_ptr const& pool) -{ - std::stringstream sql; - sql << "SELECT ledger_seq FROM ledgers WHERE "; - - if (request.contains("ledger_index")) - { - sql << "ledger_seq = " - << std::to_string(request.at("ledger_index").as_int64()); - } - else if (request.contains("ledger_hash")) - { - sql << "ledger_hash = \\\\x" << request.at("ledger_hash").as_string(); - } - else - { - sql.str(""); - sql << "SELECT max_ledger()"; - } - - sql << ";"; - - auto index = PgQuery(pool)(sql.str().c_str()); - if (!index || index.isNull()) - return {}; - - return std::optional{index.asInt()}; -} - boost::json::object doBookOffers( boost::json::object const& request, diff --git a/websocket_server_async.cpp b/websocket_server_async.cpp index 1b4d65e3..1617d81b 100644 --- a/websocket_server_async.cpp +++ b/websocket_server_async.cpp @@ -43,7 +43,9 @@ enum RPCCommand { ledger_data, book_offers, ledger_range, - ledger_entry + ledger_entry, + account_channels, + account_lines }; std::unordered_map commandMap{ {"tx", tx}, @@ -53,7 +55,9 @@ std::unordered_map commandMap{ {"ledger_entry", ledger_entry}, {"account_info", account_info}, {"ledger_data", ledger_data}, - {"book_offers", book_offers}}; + {"book_offers", book_offers}, + {"account_channels", account_channels}, + {"account_lines", account_lines}}; boost::json::object doAccountInfo( @@ -83,6 +87,14 @@ boost::json::object doLedgerRange( boost::json::object const& request, BackendInterface const& backend); +boost::json::object +doAccountChannels( + boost::json::object const& request, + BackendInterface const& backend); +boost::json::object +doAccountLines( + boost::json::object const& request, + BackendInterface const& backend); boost::json::object buildResponse( @@ -118,6 +130,12 @@ buildResponse( case book_offers: return doBookOffers(request, backend); break; + case account_channels: + return doAccountChannels(request, backend); + break; + case account_lines: + return doAccountLines(request, backend); + break; default: BOOST_LOG_TRIVIAL(error) << "Unknown command: " << command; }