From 37abec14016b534d462fc47a945377ea8698aa1f Mon Sep 17 00:00:00 2001 From: Nathan Nichols Date: Tue, 22 Jun 2021 13:09:40 -0500 Subject: [PATCH] Achieve parity with rippled API --- CMakeLists.txt | 77 ++-- src/backend/BackendInterface.cpp | 34 ++ src/backend/BackendInterface.h | 7 +- src/backend/CassandraBackend.cpp | 25 +- src/backend/CassandraBackend.h | 33 +- src/backend/PostgresBackend.cpp | 14 + src/backend/PostgresBackend.h | 3 + src/backend/README.md | 118 +++++- src/etl/ETLSource.h | 2 +- src/etl/ReportingETL.cpp | 45 +-- src/etl/ReportingETL.h | 8 +- src/handlers/AccountChannels.cpp | 160 -------- src/handlers/AccountObjects.cpp | 119 ------ src/handlers/AccountTx.cpp | 137 ------- src/handlers/BookOffers.cpp | 302 --------------- src/handlers/Context.cpp | 71 ++++ src/handlers/Context.h | 85 +++++ src/handlers/Handlers.cpp | 90 +++++ src/handlers/Handlers.h | 29 ++ src/handlers/Ledger.cpp | 89 ----- src/handlers/LedgerData.cpp | 185 --------- src/handlers/LedgerEntry.cpp | 55 --- src/handlers/RPCHelpers.cpp | 349 ++++++++++++----- src/handlers/RPCHelpers.h | 51 ++- src/handlers/ServerInfo.cpp | 53 --- src/handlers/Status.cpp | 62 +++ src/handlers/Status.h | 72 ++++ src/handlers/methods/Account.h | 51 +++ src/handlers/methods/Channel.h | 38 ++ src/handlers/methods/Exchange.h | 36 ++ src/handlers/methods/Ledger.h | 45 +++ src/handlers/methods/Subscribe.h | 39 ++ src/handlers/methods/Transaction.h | 39 ++ src/handlers/methods/impl/AccountChannels.cpp | 141 +++++++ .../{ => methods/impl}/AccountCurrencies.cpp | 83 ++-- .../{ => methods/impl}/AccountInfo.cpp | 83 ++-- .../{ => methods/impl}/AccountLines.cpp | 147 +++---- src/handlers/methods/impl/AccountObjects.cpp | 123 ++++++ .../{ => methods/impl}/AccountOffers.cpp | 115 +++--- src/handlers/methods/impl/AccountTx.cpp | 182 +++++++++ src/handlers/methods/impl/BookOffers.cpp | 325 ++++++++++++++++ .../{ => methods/impl}/ChannelAuthorize.cpp | 68 ++-- .../{ => methods/impl}/ChannelVerify.cpp | 81 ++-- src/handlers/methods/impl/Ledger.cpp | 123 ++++++ src/handlers/methods/impl/LedgerData.cpp | 170 +++++++++ src/handlers/methods/impl/LedgerEntry.cpp | 360 ++++++++++++++++++ .../{ => methods/impl}/LedgerRange.cpp | 19 +- src/handlers/{ => methods/impl}/Subscribe.cpp | 195 ++++------ src/handlers/{ => methods/impl}/Tx.cpp | 62 +-- src/main.cpp | 36 +- src/server/Handlers.cpp | 136 ------- src/server/Handlers.h | 154 -------- src/{server => webserver}/DOSGuard.h | 0 src/{server => webserver}/HttpBase.h | 141 ++++--- src/{server => webserver}/HttpSession.h | 5 +- src/webserver/Listener.h | 352 +++++++++++++++++ src/{server => webserver}/PlainWsSession.h | 12 +- src/{server => webserver}/README.md | 0 src/{server => webserver}/SslHttpSession.h | 7 +- src/{server => webserver}/SslWsSession.h | 4 +- .../SubscriptionManager.cpp | 43 ++- .../SubscriptionManager.h | 5 + src/{server => webserver}/WsBase.h | 146 +++++-- src/{server => webserver}/listener.h | 17 +- 64 files changed, 3643 insertions(+), 2215 deletions(-) delete mode 100644 src/handlers/AccountChannels.cpp delete mode 100644 src/handlers/AccountObjects.cpp delete mode 100644 src/handlers/AccountTx.cpp delete mode 100644 src/handlers/BookOffers.cpp create mode 100644 src/handlers/Context.cpp create mode 100644 src/handlers/Context.h create mode 100644 src/handlers/Handlers.cpp create mode 100644 src/handlers/Handlers.h delete mode 100644 src/handlers/Ledger.cpp delete mode 100644 src/handlers/LedgerData.cpp delete mode 100644 src/handlers/LedgerEntry.cpp delete mode 100644 src/handlers/ServerInfo.cpp create mode 100644 src/handlers/Status.cpp create mode 100644 src/handlers/Status.h create mode 100644 src/handlers/methods/Account.h create mode 100644 src/handlers/methods/Channel.h create mode 100644 src/handlers/methods/Exchange.h create mode 100644 src/handlers/methods/Ledger.h create mode 100644 src/handlers/methods/Subscribe.h create mode 100644 src/handlers/methods/Transaction.h create mode 100644 src/handlers/methods/impl/AccountChannels.cpp rename src/handlers/{ => methods/impl}/AccountCurrencies.cpp (55%) rename src/handlers/{ => methods/impl}/AccountInfo.cpp (68%) rename src/handlers/{ => methods/impl}/AccountLines.cpp (54%) create mode 100644 src/handlers/methods/impl/AccountObjects.cpp rename src/handlers/{ => methods/impl}/AccountOffers.cpp (60%) create mode 100644 src/handlers/methods/impl/AccountTx.cpp create mode 100644 src/handlers/methods/impl/BookOffers.cpp rename src/handlers/{ => methods/impl}/ChannelAuthorize.cpp (68%) rename src/handlers/{ => methods/impl}/ChannelVerify.cpp (66%) create mode 100644 src/handlers/methods/impl/Ledger.cpp create mode 100644 src/handlers/methods/impl/LedgerData.cpp create mode 100644 src/handlers/methods/impl/LedgerEntry.cpp rename src/handlers/{ => methods/impl}/LedgerRange.cpp (50%) rename src/handlers/{ => methods/impl}/Subscribe.cpp (55%) rename src/handlers/{ => methods/impl}/Tx.cpp (59%) delete mode 100644 src/server/Handlers.cpp delete mode 100644 src/server/Handlers.h rename src/{server => webserver}/DOSGuard.h (100%) rename src/{server => webserver}/HttpBase.h (77%) rename src/{server => webserver}/HttpSession.h (98%) create mode 100644 src/webserver/Listener.h rename src/{server => webserver}/PlainWsSession.h (94%) rename src/{server => webserver}/README.md (100%) rename src/{server => webserver}/SslHttpSession.h (97%) rename src/{server => webserver}/SslWsSession.h (98%) rename src/{server => webserver}/SubscriptionManager.cpp (75%) rename src/{server => webserver}/SubscriptionManager.h (96%) rename src/{server => webserver}/WsBase.h (57%) rename src/{server => webserver}/listener.h (97%) diff --git a/CMakeLists.txt b/CMakeLists.txt index a23b6b4d..4ea2cfc2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -36,29 +36,22 @@ add_executable (clio_server add_executable (clio_tests unittests/main.cpp ) -add_library(clio src/backend/BackendInterface.h) +add_library(clio) include_directories(src) list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/deps") include(ExternalProject) message(${CMAKE_CURRENT_BINARY_DIR}) message(${CMAKE_MODULE_PATH}) -#include(rippled/Builds/CMake/RippledCore.cmake) add_subdirectory(rippled) target_link_libraries(clio PUBLIC xrpl_core grpc_pbufs) add_dependencies(clio xrpl_core) add_dependencies(clio grpc_pbufs) get_target_property(grpc_includes grpc_pbufs INCLUDE_DIRECTORIES) -#get_target_property(xrpl_core_includes xrpl_core INCLUDE_DIRECTORIES) -# get_target_property(proto_includes protobuf_src INCLUDE_DIRECTORIES) -message("hi") message("${grpc_includes}") message("${proto_includes}") -# ExternalProject_Get_Property(protobuf_src SOURCE_DIR) message("${SOURCE_DIR}") INCLUDE_DIRECTORIES(${grpc_includes}) -#INCLUDE_DIRECTORIES(${xrpl_core_includes}) INCLUDE_DIRECTORIES(${SOURCE_DIR}/src) -# ExternalProject_Get_Property(grpc_src SOURCE_DIR) INCLUDE_DIRECTORIES(${SOURCE_DIR}/include) get_target_property(xrpl_core_includes xrpl_core INCLUDE_DIRECTORIES) message("${xrpl_core_includes}") @@ -68,34 +61,46 @@ include(Postgres) target_sources(clio PRIVATE - src/backend/CassandraBackend.cpp - src/backend/PostgresBackend.cpp - src/backend/BackendIndexer.cpp - src/backend/BackendInterface.cpp - src/backend/Pg.cpp - src/backend/DBHelpers.cpp - src/etl/ETLSource.cpp - src/etl/ReportingETL.cpp - src/server/Handlers.cpp - src/server/SubscriptionManager.cpp - src/handlers/AccountInfo.cpp - src/handlers/Tx.cpp - src/handlers/RPCHelpers.cpp - src/handlers/AccountTx.cpp - src/handlers/LedgerData.cpp - src/handlers/BookOffers.cpp - src/handlers/LedgerRange.cpp - src/handlers/Ledger.cpp - src/handlers/LedgerEntry.cpp - src/handlers/AccountChannels.cpp - src/handlers/AccountLines.cpp - src/handlers/AccountCurrencies.cpp - src/handlers/AccountOffers.cpp - src/handlers/AccountObjects.cpp - src/handlers/ChannelAuthorize.cpp - src/handlers/ChannelVerify.cpp - src/handlers/Subscribe.cpp - src/handlers/ServerInfo.cpp) +## Backend + src/backend/CassandraBackend.cpp + src/backend/PostgresBackend.cpp + src/backend/BackendIndexer.cpp + src/backend/BackendInterface.cpp + src/backend/Pg.cpp + src/backend/DBHelpers.cpp +## Reporting + src/etl/ETLSource.cpp + src/etl/ReportingETL.cpp +## Server + src/webserver/SubscriptionManager.cpp +## Handlers + src/handlers/Status.cpp + src/handlers/RPCHelpers.cpp + src/handlers/Handlers.cpp + src/handlers/Context.cpp +## Methods + # Account + src/handlers/methods/impl/AccountChannels.cpp + src/handlers/methods/impl/AccountCurrencies.cpp + src/handlers/methods/impl/AccountInfo.cpp + src/handlers/methods/impl/AccountLines.cpp + src/handlers/methods/impl/AccountOffers.cpp + src/handlers/methods/impl/AccountObjects.cpp + # Ledger + src/handlers/methods/impl/Ledger.cpp + src/handlers/methods/impl/LedgerData.cpp + src/handlers/methods/impl/LedgerEntry.cpp + src/handlers/methods/impl/LedgerRange.cpp + # Transaction + src/handlers/methods/impl/Tx.cpp + src/handlers/methods/impl/AccountTx.cpp + # Dex + src/handlers/methods/impl/BookOffers.cpp + # Payment Channel + src/handlers/methods/impl/ChannelAuthorize.cpp + src/handlers/methods/impl/ChannelVerify.cpp + # Subscribe + src/handlers/methods/impl/Subscribe.cpp) message(${Boost_LIBRARIES}) diff --git a/src/backend/BackendInterface.cpp b/src/backend/BackendInterface.cpp index 29cd2610..f7bc29eb 100644 --- a/src/backend/BackendInterface.cpp +++ b/src/backend/BackendInterface.cpp @@ -224,6 +224,7 @@ BackendInterface::fetchLedgerPage( page.objects.end(), partial.objects.begin(), partial.objects.end()); page.cursor = partial.cursor; } while (page.objects.size() < limit && page.cursor); + if (incomplete) { auto rng = fetchLedgerRange(); @@ -332,4 +333,37 @@ BackendInterface::checkFlagLedgers() const } } } + +std::optional +BackendInterface::fetchFees(std::uint32_t seq) const +{ + ripple::Fees fees; + + auto key = ripple::keylet::fees().key; + auto bytes = fetchLedgerObject(key, seq); + + if (!bytes) + { + BOOST_LOG_TRIVIAL(error) << __func__ << " - could not find fees"; + return {}; + } + + ripple::SerialIter it(bytes->data(), bytes->size()); + ripple::SLE sle{it, key}; + + if (sle.getFieldIndex(ripple::sfBaseFee) != -1) + fees.base = sle.getFieldU64(ripple::sfBaseFee); + + if (sle.getFieldIndex(ripple::sfReferenceFeeUnits) != -1) + fees.units = sle.getFieldU32(ripple::sfReferenceFeeUnits); + + if (sle.getFieldIndex(ripple::sfReserveBase) != -1) + fees.reserve = sle.getFieldU32(ripple::sfReserveBase); + + if (sle.getFieldIndex(ripple::sfReserveIncrement) != -1) + fees.increment = sle.getFieldU32(ripple::sfReserveIncrement); + + return fees; +} + } // namespace Backend diff --git a/src/backend/BackendInterface.h b/src/backend/BackendInterface.h index f528f38d..edf0f217 100644 --- a/src/backend/BackendInterface.h +++ b/src/backend/BackendInterface.h @@ -91,18 +91,23 @@ public: virtual std::optional fetchLedgerBySequence(uint32_t sequence) const = 0; + virtual std::optional + fetchLedgerByHash(ripple::uint256 const& hash) const = 0; + virtual std::optional fetchLatestLedgerSequence() const = 0; virtual std::optional fetchLedgerRange() const = 0; + std::optional + fetchFees(std::uint32_t seq) const; + // Doesn't throw DatabaseTimeout. Should be used with care. std::optional fetchLedgerRangeNoThrow() const; // *** transaction methods - virtual std::optional fetchTransaction(ripple::uint256 const& hash) const = 0; diff --git a/src/backend/CassandraBackend.cpp b/src/backend/CassandraBackend.cpp index c24197eb..05e7e463 100644 --- a/src/backend/CassandraBackend.cpp +++ b/src/backend/CassandraBackend.cpp @@ -286,6 +286,7 @@ struct ReadDiffCallbackData CassandraBackend const& backend; uint32_t sequence; std::vector& result; + std::mutex& mtx; std::condition_variable& cv; std::atomic_uint32_t& numFinished; @@ -295,12 +296,14 @@ struct ReadDiffCallbackData CassandraBackend const& backend, uint32_t sequence, std::vector& result, + std::mutex& mtx, std::condition_variable& cv, std::atomic_uint32_t& numFinished, size_t batchSize) : backend(backend) , sequence(sequence) , result(result) + , mtx(mtx) , cv(cv) , numFinished(numFinished) , batchSize(batchSize) @@ -355,6 +358,7 @@ CassandraBackend::fetchLedgerDiffs(std::vector const& sequences) const *this, sequences[i], results[sequences[i]], + mtx, cv, numFinished, sequences.size())); @@ -414,7 +418,10 @@ CassandraBackend::doFetchLedgerPage( CassandraStatement statement{selectKeys_}; statement.bindInt(index->keyIndex); if (cursor) - statement.bindBytes(*cursor); + { + auto thisCursor = *cursor; + statement.bindBytes(++thisCursor); + } else { ripple::uint256 zero; @@ -426,12 +433,13 @@ CassandraBackend::doFetchLedgerPage( { BOOST_LOG_TRIVIAL(debug) << __func__ << " - got keys - size = " << result.numRows(); - std::vector keys; + std::vector keys; do { keys.push_back(result.getUInt256()); } while (result.nextRow()); + if (keys.size() && keys.size() >= limit) { page.cursor = keys.back(); @@ -454,12 +462,17 @@ CassandraBackend::doFetchLedgerPage( page.objects.push_back({std::move(key), std::move(obj)}); } } + if (!cursor && (!keys.size() || !keys[0].isZero())) + { page.warning = "Data may be incomplete"; + } + return page; } if (!cursor) return {{}, {}, "Data may be incomplete"}; + return {}; } std::vector @@ -479,7 +492,7 @@ CassandraBackend::fetchLedgerObjects( for (std::size_t i = 0; i < keys.size(); ++i) { cbs.push_back(std::make_shared( - *this, keys[i], sequence, results[i], cv, numFinished, numKeys)); + *this, keys[i], sequence, results[i], mtx, cv, numFinished, numKeys)); readObject(*cbs[i]); } assert(results.size() == cbs.size()); @@ -1574,6 +1587,12 @@ CassandraBackend::open(bool readOnly) if (!insertLedgerHash_.prepareStatement(query, session_.get())) continue; + query.str(""); + query << "SELECT sequence FROM " << tablePrefix << "ledger_hashes " + << "WHERE hash = ? LIMIT 1"; + if (!selectLedgerByHash_.prepareStatement(query, session_.get())) + continue; + query.str(""); query << " update " << tablePrefix << "ledger_range" << " set sequence = ? where is_latest = ? if sequence in " diff --git a/src/backend/CassandraBackend.h b/src/backend/CassandraBackend.h index 4f958530..66f24e7c 100644 --- a/src/backend/CassandraBackend.h +++ b/src/backend/CassandraBackend.h @@ -575,6 +575,8 @@ public: { BOOST_LOG_TRIVIAL(trace) << "finished a request"; size_t batchSize = requestParams_.batchSize; + + std::unique_lock lk(requestParams_.mtx); if (++(requestParams_.numFinished) == batchSize) requestParams_.cv.notify_all(); } @@ -657,6 +659,7 @@ private: CassandraPreparedStatement deleteLedgerRange_; CassandraPreparedStatement updateLedgerHeader_; CassandraPreparedStatement selectLedgerBySeq_; + CassandraPreparedStatement selectLedgerByHash_; CassandraPreparedStatement selectLatestLedger_; CassandraPreparedStatement selectLedgerRange_; CassandraPreparedStatement selectLedgerDiff_; @@ -906,9 +909,7 @@ public: { BOOST_LOG_TRIVIAL(trace) << __func__; CassandraStatement statement{selectLatestLedger_}; - std::cout << "READ" << std::endl; CassandraResult result = executeSyncRead(statement); - std::cout << "ITS THE READ" << std::endl; if (!result.hasResult()) { BOOST_LOG_TRIVIAL(error) @@ -934,6 +935,26 @@ public: std::vector header = result.getBytes(); return deserializeHeader(ripple::makeSlice(header)); } + + std::optional + fetchLedgerByHash(ripple::uint256 const& hash) const override + { + CassandraStatement statement{selectLedgerByHash_}; + + statement.bindBytes(hash); + + CassandraResult result = executeSyncRead(statement); + if (!result.hasResult()) + { + BOOST_LOG_TRIVIAL(debug) << __func__ << " - no rows returned"; + return {}; + } + + std::uint32_t sequence = result.getInt64(); + + return fetchLedgerBySequence(sequence); + } + std::optional fetchLedgerRange() const override; @@ -1033,6 +1054,7 @@ public: CassandraBackend const& backend; ripple::uint256 const& hash; TransactionAndMetadata& result; + std::mutex& mtx; std::condition_variable& cv; std::atomic_uint32_t& numFinished; @@ -1042,12 +1064,14 @@ public: CassandraBackend const& backend, ripple::uint256 const& hash, TransactionAndMetadata& result, + std::mutex& mtx, std::condition_variable& cv, std::atomic_uint32_t& numFinished, size_t batchSize) : backend(backend) , hash(hash) , result(result) + , mtx(mtx) , cv(cv) , numFinished(numFinished) , batchSize(batchSize) @@ -1072,7 +1096,7 @@ public: for (std::size_t i = 0; i < hashes.size(); ++i) { cbs.push_back(std::make_shared( - *this, hashes[i], results[i], cv, numFinished, numHashes)); + *this, hashes[i], results[i], mtx, cv, numFinished, numHashes)); read(*cbs[i]); } assert(results.size() == cbs.size()); @@ -1106,6 +1130,7 @@ public: ripple::uint256 const& key; uint32_t sequence; Blob& result; + std::mutex& mtx; std::condition_variable& cv; std::atomic_uint32_t& numFinished; @@ -1116,6 +1141,7 @@ public: ripple::uint256 const& key, uint32_t sequence, Blob& result, + std::mutex& mtx, std::condition_variable& cv, std::atomic_uint32_t& numFinished, size_t batchSize) @@ -1123,6 +1149,7 @@ public: , key(key) , sequence(sequence) , result(result) + , mtx(mtx) , cv(cv) , numFinished(numFinished) , batchSize(batchSize) diff --git a/src/backend/PostgresBackend.cpp b/src/backend/PostgresBackend.cpp index aab7471a..f4ba7c28 100644 --- a/src/backend/PostgresBackend.cpp +++ b/src/backend/PostgresBackend.cpp @@ -199,6 +199,20 @@ PostgresBackend::fetchLedgerBySequence(uint32_t sequence) const return {}; } +std::optional +PostgresBackend::fetchLedgerByHash(ripple::uint256 const& hash) const +{ + PgQuery pgQuery(pgPool_); + pgQuery("SET statement_timeout TO 10000"); + std::stringstream sql; + sql << "SELECT * FROM ledgers WHERE ledger_hash = " + << ripple::to_string(hash); + auto res = pgQuery(sql.str().data()); + if (checkResult(res, 10)) + return parseLedgerInfo(res); + return {}; +} + std::optional PostgresBackend::fetchLedgerRange() const { diff --git a/src/backend/PostgresBackend.h b/src/backend/PostgresBackend.h index ea7c9a72..01269cfc 100644 --- a/src/backend/PostgresBackend.h +++ b/src/backend/PostgresBackend.h @@ -27,6 +27,9 @@ public: std::optional fetchLedgerBySequence(uint32_t sequence) const override; + std::optional + fetchLedgerByHash(ripple::uint256 const& hash) const override; + std::optional fetchLedgerRange() const override; diff --git a/src/backend/README.md b/src/backend/README.md index 8b897aba..745cbb63 100644 --- a/src/backend/README.md +++ b/src/backend/README.md @@ -1,26 +1,108 @@ -The backend is clio's view into the database. The database could be either PostgreSQL or Cassandra. -Multiple clio servers can share access to the same database. +Reporting mode is a special operating mode of rippled, designed to handle RPCs +for validated data. A server running in reporting mode does not connect to the +p2p network, but rather extracts validated data from a node that is connected +to the p2p network. To run rippled in reporting mode, you must also run a +separate rippled node in p2p mode, to use as an ETL source. Multiple reporting +nodes can share access to the same network accessible databases (Postgres and +Cassandra); at any given time, only one reporting node will be performing ETL +and writing to the databases, while the others simply read from the databases. +A server running in reporting mode will forward any requests that require access +to the p2p network to a p2p node. -`BackendInterface`, and it's derived classes, store very little state. The read methods go directly to the database, -and generally don't access any internal data structures. Nearly all of the methods are const. +# Reporting ETL +A single reporting node has one or more ETL sources, specified in the config +file. A reporting node will subscribe to the "ledgers" stream of each of the ETL +sources. This stream sends a message whenever a new ledger is validated. Upon +receiving a message on the stream, reporting will then fetch the data associated +with the newly validated ledger from one of the ETL sources. The fetch is +performed via a gRPC request ("GetLedger"). This request returns the ledger +header, transactions+metadata blobs, and every ledger object +added/modified/deleted as part of this ledger. ETL then writes all of this data +to the databases, and moves on to the next ledger. ETL does not apply +transactions, but rather extracts the already computed results of those +transactions (all of the added/modified/deleted SHAMap leaf nodes of the state +tree). The new SHAMap inner nodes are computed by the ETL writer; this computation mainly +involves manipulating child pointers and recomputing hashes, logic which is +buried inside of SHAMap. -The data model used by clio is called the flat map data model. The flat map data model does not store any -SHAMap inner nodes, and instead only stores the raw ledger objects contained in the leaf node. Ledger objects -are stored in the database with a compound key of `(object_id, ledger_sequence)`, where `ledger_sequence` is the -ledger in which the object was created or modified. Objects are then fetched using an inequality operation, -such as `SELECT * FROM objects WHERE object_id = id AND ledger_sequence <= seq order by ledger_sequence limit 1`, where `seq` is the ledger -in which we are trying to look up the object. When an object is deleted, we write an empty blob. +If the database is entirely empty, ETL must download an entire ledger in full +(as opposed to just the diff, as described above). This download is done via the +"GetLedgerData" gRPC request. "GetLedgerData" allows clients to page through an +entire ledger over several RPC calls. ETL will page through an entire ledger, +and write each object to the database. -Transactions are stored in a separate table, where the key is the hash. +If the database is not empty, the reporting node will first come up in a "soft" +read-only mode. In read-only mode, the server does not perform ETL and simply +publishes new ledgers as they are written to the database. +If the database is not updated within a certain time period +(currently hard coded at 20 seconds), the reporting node will begin the ETL +process and start writing to the database. Postgres will report an error when +trying to write a record with a key that already exists. ETL uses this error to +determine that another process is writing to the database, and subsequently +falls back to a soft read-only mode. Reporting nodes can also operate in strict +read-only mode, in which case they will never write to the database. -Ledger headers are stored in their own table. +# Database Nuances +The database schema for reporting mode does not allow any history gaps. +Attempting to write a ledger to a non-empty database where the previous ledger +does not exist will return an error. -The account_tx table maps accounts to a list of transactions that affect them. +The databases must be set up prior to running reporting mode. This requires +creating the Postgres database, and setting up the Cassandra keyspace. Reporting +mode will create the objects table in Cassandra if the table does not yet exist. +Creating the Postgres database: +``` +$ psql -h [host] -U [user] +postgres=# create database [database]; +``` +Creating the keyspace: +``` +$ cqlsh [host] [port] +> CREATE KEYSPACE rippled WITH REPLICATION = + {'class' : 'SimpleStrategy', 'replication_factor' : 3 }; +``` +A replication factor of 3 is recommended. However, when running locally, only a +replication factor of 1 is supported. -### Backend Indexer +Online delete is not supported by reporting mode and must be done manually. The +easiest way to do this would be to setup a second Cassandra keyspace and +Postgres database, bring up a single reporting mode instance that uses those +databases, and start ETL at a ledger of your choosing (via --startReporting on +the command line). Once this node is caught up, the other databases can be +deleted. + +To delete: +``` +$ psql -h [host] -U [user] -d [database] +reporting=$ truncate table ledgers cascade; +``` +``` +$ cqlsh [host] [port] +> truncate table objects; +``` +# Proxy +RPCs that require access to the p2p network and/or the open ledger are forwarded +from the reporting node to one of the ETL sources. The request is not processed +prior to forwarding, and the response is delivered as-is to the client. +Reporting will forward any requests that always require p2p/open ledger access +(fee and submit, for instance). In addition, any request that explicitly +requests data from the open or closed ledger (via setting +"ledger_index":"current" or "ledger_index":"closed"), will be forwarded to a +p2p node. + +For the stream "transactions_proposed" (AKA "rt_transactions"), reporting +subscribes to the "transactions_proposed" streams of each ETL source, and then +forwards those messages to any clients subscribed to the same stream on the +reporting node. A reporting node will subscribe to the stream on each ETL +source, but will only forward the messages from one of the streams at any given +time (to avoid sending the same message more than once to the same client). + +# API changes +A reporting node defaults to only returning validated data. If a ledger is not +specified, the most recently validated ledger is used. This is in contrast to +the normal rippled behavior, where the open ledger is used by default. + +Reporting will reject all subscribe requests for streams "server", "manifests", +"validations", "peer_status" and "consensus". -With the elimination of SHAMap inner nodes, iterating across a ledger becomes difficult. In order to iterate, -a keys table is maintained, which keeps a collection of all keys in a ledger. This table has one record for every -million ledgers, where each record has all of the keys in that ledger, as well as all of the keys that were deleted -between that ledger and the prior ledger written to the keys table. Most of this logic is contained in `BackendIndexer`. diff --git a/src/etl/ETLSource.h b/src/etl/ETLSource.h index 22a5ea7b..cd63c691 100644 --- a/src/etl/ETLSource.h +++ b/src/etl/ETLSource.h @@ -26,7 +26,7 @@ #include #include #include -#include +#include #include "org/xrpl/rpc/v1/xrp_ledger.grpc.pb.h" #include diff --git a/src/etl/ReportingETL.cpp b/src/etl/ReportingETL.cpp index 7ee872f0..50975957 100644 --- a/src/etl/ReportingETL.cpp +++ b/src/etl/ReportingETL.cpp @@ -26,9 +26,10 @@ #include #include #include +#include #include #include -#include +#include #include #include @@ -138,49 +139,18 @@ ReportingETL::loadInitialLedger(uint32_t startingSequence) return lgrInfo; } -std::optional -ReportingETL::getFees(std::uint32_t seq) -{ - ripple::Fees fees; - - auto key = ripple::keylet::fees().key; - auto bytes = backend_->fetchLedgerObject(key, seq); - - if (!bytes) - { - BOOST_LOG_TRIVIAL(error) << __func__ << " - could not find fees"; - return {}; - } - - ripple::SerialIter it(bytes->data(), bytes->size()); - ripple::SLE sle{it, key}; - - if (sle.getFieldIndex(ripple::sfBaseFee) != -1) - fees.base = sle.getFieldU64(ripple::sfBaseFee); - - if (sle.getFieldIndex(ripple::sfReferenceFeeUnits) != -1) - fees.units = sle.getFieldU32(ripple::sfReferenceFeeUnits); - - if (sle.getFieldIndex(ripple::sfReserveBase) != -1) - fees.reserve = sle.getFieldU32(ripple::sfReserveBase); - - if (sle.getFieldIndex(ripple::sfReserveIncrement) != -1) - fees.increment = sle.getFieldU32(ripple::sfReserveIncrement); - - return fees; -} - void ReportingETL::publishLedger(ripple::LedgerInfo const& lgrInfo) -{ +{ auto ledgerRange = backend_->fetchLedgerRangeNoThrow(); - auto fees = getFees(lgrInfo.seq); - std::vector transactions; - while (true) + std::optional fees; + std::vector transactions; + for(;;) { try { + fees = backend_->fetchFees(lgrInfo.seq); transactions = backend_->fetchAllTransactionsInLedger(lgrInfo.seq); break; } @@ -448,7 +418,6 @@ ReportingETL::runETLPipeline(uint32_t startSequence, int numExtractors) auto getNext = [&queues, &startSequence, &numExtractors]( uint32_t sequence) -> std::shared_ptr { - std::cout << std::to_string((sequence - startSequence) % numExtractors); return queues[(sequence - startSequence) % numExtractors]; }; std::vector extractors; diff --git a/src/etl/ReportingETL.h b/src/etl/ReportingETL.h index ad7d1455..806ae428 100644 --- a/src/etl/ReportingETL.h +++ b/src/etl/ReportingETL.h @@ -27,7 +27,7 @@ #include #include #include -#include +#include #include "org/xrpl/rpc/v1/xrp_ledger.grpc.pb.h" #include @@ -248,12 +248,6 @@ private: void publishLedger(ripple::LedgerInfo const& lgrInfo); - /// Get fees at a current ledger_index - /// @param seq the ledger index - /// @return nullopt if not found, fees if found. - std::optional - getFees(std::uint32_t seq); - bool isStopping() { diff --git a/src/handlers/AccountChannels.cpp b/src/handlers/AccountChannels.cpp deleted file mode 100644 index 6cf656a6..00000000 --- a/src/handlers/AccountChannels.cpp +++ /dev/null @@ -1,160 +0,0 @@ -#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(jDst); -} - -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; - } - } - - 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["channels"] = boost::json::value(boost::json::array_kind); - boost::json::array& jsonChannels = response.at("channels").as_array(); - - 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/src/handlers/AccountObjects.cpp b/src/handlers/AccountObjects.cpp deleted file mode 100644 index 5bb785f6..00000000 --- a/src/handlers/AccountObjects.cpp +++ /dev/null @@ -1,119 +0,0 @@ -#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(toJson(sle)); - } - - return true; - }; - - auto nextCursor = traverseOwnedNodes( - backend, accountID, *ledgerSequence, cursor, addToResponse); - - if (nextCursor) - response["next_cursor"] = ripple::strHex(*nextCursor); - - return response; -} diff --git a/src/handlers/AccountTx.cpp b/src/handlers/AccountTx.cpp deleted file mode 100644 index 8cc57c1c..00000000 --- a/src/handlers/AccountTx.cpp +++ /dev/null @@ -1,137 +0,0 @@ -//------------------------------------------------------------------------------ -/* - This file is part of rippled: https://github.com/ripple/rippled - Copyright (c) 2012-2014 Ripple Labs Inc. - - Permission to use, copy, modify, and/or distribute this software for any - purpose with or without fee is hereby granted, provided that the above - copyright notice and this permission notice appear in all copies. - - THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -*/ -//============================================================================== - -#include -#include - -// { -// account: account, -// ledger_index_min: ledger_index // optional, defaults to earliest -// ledger_index_max: ledger_index, // optional, defaults to latest -// binary: boolean, // optional, defaults to false -// forward: boolean, // optional, defaults to false -// limit: integer, // optional -// marker: object {ledger: ledger_index, seq: txn_sequence} // optional, -// resume previous query -// } -boost::json::object -doAccountTx(boost::json::object const& request, BackendInterface const& backend) -{ - boost::json::object response; - - if (!request.contains("account")) - { - response["error"] = "Please specify an account"; - return response; - } - - auto account = ripple::parseBase58( - request.at("account").as_string().c_str()); - if (!account) - { - account = ripple::AccountID(); - if (!account->parseHex(request.at("account").as_string().c_str())) - { - response["error"] = "account malformed"; - return response; - } - } - auto ledgerSequence = ledgerSequenceFromRequest(request, backend); - if (!ledgerSequence) - { - response["error"] = "Empty database"; - return response; - } - bool binary = - request.contains("binary") ? request.at("binary").as_bool() : false; - - std::optional cursor; - if (request.contains("cursor")) - { - auto const& obj = request.at("cursor").as_object(); - std::optional ledgerSequence; - if (obj.contains("ledger_sequence")) - { - ledgerSequence = (uint32_t)obj.at("ledger_sequence").as_int64(); - } - std::optional transactionIndex; - if (obj.contains("transaction_index")) - { - transactionIndex = (uint32_t)obj.at("transaction_index").as_int64(); - } - if (!ledgerSequence || !transactionIndex) - { - response["error"] = - "malformed cursor. include transaction_index and " - "ledger_sequence in an object named \"cursor\""; - return response; - } - cursor = {*ledgerSequence, *transactionIndex}; - } - - uint32_t limit = 200; - if (request.contains("limit") and - request.at("limit").kind() == boost::json::kind::int64) - limit = request.at("limit").as_int64(); - boost::json::array txns; - auto start = std::chrono::system_clock::now(); - auto [blobs, retCursor] = - backend.fetchAccountTransactions(*account, limit, cursor); - auto end = std::chrono::system_clock::now(); - BOOST_LOG_TRIVIAL(info) << __func__ << " db fetch took " - << ((end - start).count() / 1000000000.0) - << " num blobs = " << blobs.size(); - for (auto const& txnPlusMeta : blobs) - { - if (txnPlusMeta.ledgerSequence > ledgerSequence) - { - BOOST_LOG_TRIVIAL(debug) - << __func__ - << " skipping over transactions from incomplete ledger"; - continue; - } - boost::json::object obj; - if (!binary) - { - auto [txn, meta] = deserializeTxPlusMeta(txnPlusMeta); - obj["transaction"] = toJson(*txn); - obj["metadata"] = toJson(*meta); - } - else - { - obj["transaction"] = ripple::strHex(txnPlusMeta.transaction); - obj["metadata"] = ripple::strHex(txnPlusMeta.metadata); - } - obj["ledger_sequence"] = txnPlusMeta.ledgerSequence; - txns.push_back(obj); - } - response["transactions"] = txns; - if (retCursor) - { - boost::json::object cursorJson; - cursorJson["ledger_sequence"] = retCursor->ledgerSequence; - cursorJson["transaction_index"] = retCursor->transactionIndex; - response["cursor"] = cursorJson; - } - auto end2 = std::chrono::system_clock::now(); - BOOST_LOG_TRIVIAL(info) << __func__ << " serialization took " - << ((end2 - end).count() / 1000000000.0); - return response; -} - diff --git a/src/handlers/BookOffers.cpp b/src/handlers/BookOffers.cpp deleted file mode 100644 index b552f4dd..00000000 --- a/src/handlers/BookOffers.cpp +++ /dev/null @@ -1,302 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -boost::json::object -doBookOffers( - 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; - } - ripple::uint256 bookBase; - if (request.contains("book")) - { - if (!bookBase.parseHex(request.at("book").as_string().c_str())) - { - response["error"] = "Error parsing book"; - return response; - } - } - else - { - if (!request.contains("taker_pays")) - { - response["error"] = "Missing field taker_pays"; - return response; - } - - if (!request.contains("taker_gets")) - { - response["error"] = "Missing field taker_gets"; - return response; - } - - boost::json::object taker_pays; - if (request.at("taker_pays").kind() == boost::json::kind::object) - { - taker_pays = request.at("taker_pays").as_object(); - } - else - { - response["error"] = "Invalid field taker_pays"; - return response; - } - - boost::json::object taker_gets; - if (request.at("taker_gets").kind() == boost::json::kind::object) - { - taker_gets = request.at("taker_gets").as_object(); - } - else - { - response["error"] = "Invalid field taker_gets"; - return response; - } - - if (!taker_pays.contains("currency")) - { - response["error"] = "Missing field taker_pays.currency"; - return response; - } - - if (!taker_pays.at("currency").is_string()) - { - response["error"] = "taker_pays.currency should be string"; - return response; - } - - if (!taker_gets.contains("currency")) - { - response["error"] = "Missing field taker_gets.currency"; - return response; - } - - if (!taker_gets.at("currency").is_string()) - { - response["error"] = "taker_gets.currency should be string"; - return response; - } - - ripple::Currency pay_currency; - - if (!ripple::to_currency( - pay_currency, taker_pays.at("currency").as_string().c_str())) - { - response["error"] = - "Invalid field 'taker_pays.currency', bad currency."; - return response; - } - - ripple::Currency get_currency; - - if (!ripple::to_currency( - get_currency, taker_gets["currency"].as_string().c_str())) - { - response["error"] = - "Invalid field 'taker_gets.currency', bad currency."; - return response; - } - - ripple::AccountID pay_issuer; - - if (taker_pays.contains("issuer")) - { - if (!taker_pays.at("issuer").is_string()) - { - response["error"] = "taker_pays.issuer should be string"; - return response; - } - - if (!ripple::to_issuer( - pay_issuer, taker_pays.at("issuer").as_string().c_str())) - { - response["error"] = - "Invalid field 'taker_pays.issuer', bad issuer."; - return response; - } - - if (pay_issuer == ripple::noAccount()) - { - response["error"] = - "Invalid field 'taker_pays.issuer', bad issuer account " - "one."; - return response; - } - } - else - { - pay_issuer = ripple::xrpAccount(); - } - - if (isXRP(pay_currency) && !isXRP(pay_issuer)) - { - response["error"] = - "Unneeded field 'taker_pays.issuer' for XRP currency " - "specification."; - return response; - } - - if (!isXRP(pay_currency) && isXRP(pay_issuer)) - { - response["error"] = - "Invalid field 'taker_pays.issuer', expected non-XRP issuer."; - return response; - } - - ripple::AccountID get_issuer; - - if (taker_gets.contains("issuer")) - { - if (!taker_gets["issuer"].is_string()) - { - response["error"] = "taker_gets.issuer should be string"; - return response; - } - - if (!ripple::to_issuer( - get_issuer, taker_gets.at("issuer").as_string().c_str())) - { - response["error"] = - "Invalid field 'taker_gets.issuer', bad issuer."; - return response; - } - - if (get_issuer == ripple::noAccount()) - { - response["error"] = - "Invalid field 'taker_gets.issuer', bad issuer account " - "one."; - return response; - } - } - else - { - get_issuer = ripple::xrpAccount(); - } - - if (ripple::isXRP(get_currency) && !ripple::isXRP(get_issuer)) - { - response["error"] = - "Unneeded field 'taker_gets.issuer' for XRP currency " - "specification."; - return response; - } - - if (!ripple::isXRP(get_currency) && ripple::isXRP(get_issuer)) - { - response["error"] = - "Invalid field 'taker_gets.issuer', expected non-XRP issuer."; - return response; - } - - if (pay_currency == get_currency && pay_issuer == get_issuer) - { - response["error"] = "Bad market"; - return response; - } - ripple::Book book = { - {pay_currency, pay_issuer}, {get_currency, get_issuer}}; - - bookBase = getBookBase(book); - } - - std::uint32_t limit = 200; - if (request.contains("limit") and - request.at("limit").kind() == boost::json::kind::int64) - limit = request.at("limit").as_int64(); - - std::optional takerID = {}; - if (request.contains("taker")) - { - if (!request.at("taker").is_string()) - { - response["error"] = "Taker account must be string"; - return response; - } - - takerID = - accountFromStringStrict(request.at("taker").as_string().c_str()); - if (!takerID) - { - response["error"] = "Invalid taker account"; - return response; - } - } - - std::optional cursor; - if (request.contains("cursor")) - { - cursor = ripple::uint256{}; - if (!cursor->parseHex(request.at("cursor").as_string().c_str())) - { - response["error"] = "Bad cursor"; - return response; - } - } - - auto start = std::chrono::system_clock::now(); - auto [offers, retCursor, warning] = - backend.fetchBookOffers(bookBase, *ledgerSequence, limit, cursor); - auto end = std::chrono::system_clock::now(); - - BOOST_LOG_TRIVIAL(warning) - << "Time loading books: " << ((end - start).count() / 1000000000.0); - - if (warning) - response["warning"] = *warning; - - response["offers"] = boost::json::value(boost::json::array_kind); - boost::json::array& jsonOffers = response.at("offers").as_array(); - - start = std::chrono::system_clock::now(); - for (auto const& obj : offers) - { - if (jsonOffers.size() == limit) - break; - - try - { - ripple::SerialIter it{obj.blob.data(), obj.blob.size()}; - ripple::SLE offer{it, obj.key}; - ripple::uint256 bookDir = - offer.getFieldH256(ripple::sfBookDirectory); - - boost::json::object offerJson = toJson(offer); - offerJson["quality"] = - ripple::amountFromQuality(getQuality(bookDir)).getText(); - jsonOffers.push_back(offerJson); - } - catch (std::exception const& e) - { - } - } - - end = std::chrono::system_clock::now(); - - BOOST_LOG_TRIVIAL(warning) << "Time transforming to json: " - << ((end - start).count() / 1000000000.0); - if (retCursor) - response["cursor"] = ripple::strHex(*retCursor); - if (warning) - response["warning"] = - "Periodic database update in progress. Data for this book as of " - "this ledger " - "may be incomplete. Data should be complete within one minute"; - - return response; -} diff --git a/src/handlers/Context.cpp b/src/handlers/Context.cpp new file mode 100644 index 00000000..cca0e492 --- /dev/null +++ b/src/handlers/Context.cpp @@ -0,0 +1,71 @@ +#include + +namespace RPC +{ + +std::optional +make_WsContext( + boost::json::object const& request, + std::shared_ptr const& backend, + std::shared_ptr const& subscriptions, + std::shared_ptr const& balancer, + std::shared_ptr const& session, + Backend::LedgerRange const& range) +{ + if (!request.contains("command")) + return {}; + + std::string command = request.at("command").as_string().c_str(); + + return Context{ + command, + 1, + request, + backend, + subscriptions, + balancer, + session, + range + }; +} + +std::optional +make_HttpContext( + boost::json::object const& request, + std::shared_ptr const& backend, + std::shared_ptr const& subscriptions, + std::shared_ptr const& balancer, + Backend::LedgerRange const& range) +{ + if (!request.contains("method") || !request.at("method").is_string()) + return {}; + + std::string const& command = request.at("method").as_string().c_str(); + + if (command == "subscribe" || command == "unsubscribe") + return {}; + + if (!request.contains("params") || !request.at("params").is_array()) + return {}; + + boost::json::array const& array = request.at("params").as_array(); + + if (array.size() != 1) + return {}; + + if (!array.at(0).is_object()) + return {}; + + return Context{ + command, + 1, + array.at(0).as_object(), + backend, + subscriptions, + balancer, + nullptr, + range + }; +} + +} // namespace RPC \ No newline at end of file diff --git a/src/handlers/Context.h b/src/handlers/Context.h new file mode 100644 index 00000000..9deaed0f --- /dev/null +++ b/src/handlers/Context.h @@ -0,0 +1,85 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2021 Ripple Labs Inc. + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef REPORTING_CONTEXT_H_INCLUDED +#define REPORTING_CONTEXT_H_INCLUDED + +#include + +#include + +#include + +class WsBase; +class SubscriptionManager; +class ETLLoadBalancer; + +namespace RPC +{ + +struct Context +{ + std::string method; + std::uint32_t version; + boost::json::object const& params; + std::shared_ptr const& backend; + std::shared_ptr const& subscriptions; + std::shared_ptr const& balancer; + std::shared_ptr session; + Backend::LedgerRange const& range; + + Context( + std::string const& command_, + std::uint32_t version_, + boost::json::object const& params_, + std::shared_ptr const& backend_, + std::shared_ptr const& subscriptions_, + std::shared_ptr const& balancer_, + std::shared_ptr const& session_, + Backend::LedgerRange const& range_) + : method(command_) + , version(version_) + , params(params_) + , backend(backend_) + , subscriptions(subscriptions_) + , balancer(balancer_) + , session(session_) + , range(range_) + {} +}; + + +std::optional +make_WsContext( + boost::json::object const& request, + std::shared_ptr const& backend, + std::shared_ptr const& subscriptions, + std::shared_ptr const& balancer, + std::shared_ptr const& session, + Backend::LedgerRange const& range); + +std::optional +make_HttpContext( + boost::json::object const& request, + std::shared_ptr const& backend, + std::shared_ptr const& subscriptions, + std::shared_ptr const& balancer, + Backend::LedgerRange const& range); + +} // namespace RPC + +#endif //REPORTING_CONTEXT_H_INCLUDED \ No newline at end of file diff --git a/src/handlers/Handlers.cpp b/src/handlers/Handlers.cpp new file mode 100644 index 00000000..9c9fa7ea --- /dev/null +++ b/src/handlers/Handlers.cpp @@ -0,0 +1,90 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +namespace RPC +{ + +static std::unordered_map> + handlerTable +{ + { "account_channels", &doAccountChannels }, + { "account_currencies", &doAccountCurrencies }, + { "account_info", &doAccountInfo }, + { "account_lines", &doAccountLines }, + { "account_objects", &doAccountObjects }, + { "account_offers", &doAccountOffers }, + { "account_tx", &doAccountTx }, + { "book_offers", &doBookOffers }, + { "channel_authorize", &doChannelAuthorize }, + { "channel_verify", &doChannelVerify }, + { "ledger", &doLedger }, + { "ledger_data", &doLedgerData }, + { "ledger_entry", &doLedgerEntry }, + { "ledger_range", &doLedgerRange }, + { "ledger_data", &doLedgerData }, + { "subscribe", &doSubscribe }, + { "unsubscribe", &doUnsubscribe }, + { "tx", &doTx }, +}; + +static std::unordered_set forwardCommands { + "submit", + "submit_multisigned", + "fee", + "path_find", + "ripple_path_find", + "manifest" +}; + +bool +shouldForwardToRippled(Context const& ctx) +{ + auto request = ctx.params; + + if (request.contains("forward") && request.at("forward").is_bool()) + return request.at("forward").as_bool(); + + BOOST_LOG_TRIVIAL(debug) << "checked forward"; + + if (forwardCommands.find(ctx.method) != forwardCommands.end()) + return true; + + BOOST_LOG_TRIVIAL(debug) << "checked command"; + + if (request.contains("ledger_index")) + { + auto indexValue = request.at("ledger_index"); + if (indexValue.is_string()) + { + BOOST_LOG_TRIVIAL(debug) << "checking ledger as string"; + std::string index = indexValue.as_string().c_str(); + return index == "current" || index == "closed"; + } + } + + BOOST_LOG_TRIVIAL(debug) << "checked ledger"; + + return false; +} + +Result +buildResponse(Context const& ctx) +{ + if (shouldForwardToRippled(ctx)) + return ctx.balancer->forwardToRippled(ctx.params); + + if (handlerTable.find(ctx.method) == handlerTable.end()) + return Status{Error::rpcUNKNOWN_COMMAND}; + + auto method = handlerTable[ctx.method]; + + return method(ctx); +} + +} \ No newline at end of file diff --git a/src/handlers/Handlers.h b/src/handlers/Handlers.h new file mode 100644 index 00000000..25d6993c --- /dev/null +++ b/src/handlers/Handlers.h @@ -0,0 +1,29 @@ +#include +#include + +#include +#include +#include + +#ifndef RIPPLE_REPORTING_HANDLERS_H +#define RIPPLE_REPORTING_HANDLERS_H + +namespace RPC +{ + +// Maximum and minimum supported API versions +// Defaults to APIVersionIfUnspecified +constexpr unsigned int APIVersionIfUnspecified = 1; +constexpr unsigned int ApiMinimumSupportedVersion = 1; +constexpr unsigned int ApiMaximumSupportedVersion = 1; +constexpr unsigned int APINumberVersionSupported = + ApiMaximumSupportedVersion - ApiMinimumSupportedVersion + 1; + +static_assert(ApiMinimumSupportedVersion >= APIVersionIfUnspecified); +static_assert(ApiMaximumSupportedVersion >= ApiMinimumSupportedVersion); + +Result +buildResponse(Context const& ctx); + +} // namespace RPC +#endif // RIPPLE_REPORTING_HANDLERS_H diff --git a/src/handlers/Ledger.cpp b/src/handlers/Ledger.cpp deleted file mode 100644 index a823b265..00000000 --- a/src/handlers/Ledger.cpp +++ /dev/null @@ -1,89 +0,0 @@ -#include -#include - -boost::json::object -doLedger(boost::json::object const& request, BackendInterface const& backend) -{ - boost::json::object response; - if (!request.contains("ledger_index")) - { - response["error"] = "Please specify a ledger index"; - return response; - } - bool binary = - request.contains("binary") ? request.at("binary").as_bool() : false; - bool getTransactions = request.contains("transactions") - ? request.at("transactions").as_bool() - : false; - bool expand = - request.contains("expand") ? request.at("expand").as_bool() : false; - auto ledgerSequence = ledgerSequenceFromRequest(request, backend); - if (!ledgerSequence) - { - response["error"] = "Empty database"; - return response; - } - - auto lgrInfo = backend.fetchLedgerBySequence(*ledgerSequence); - if (!lgrInfo) - { - response["error"] = "ledger not found"; - return response; - } - boost::json::object header; - if (binary) - { - header["blob"] = ripple::strHex(ledgerInfoToBlob(*lgrInfo)); - } - else - { - header = toJson(*lgrInfo); - } - response["header"] = header; - if (getTransactions) - { - response["transactions"] = boost::json::value(boost::json::array_kind); - boost::json::array& jsonTransactions = - response.at("transactions").as_array(); - if (expand) - { - auto txns = backend.fetchAllTransactionsInLedger(*ledgerSequence); - - std::transform( - std::move_iterator(txns.begin()), - std::move_iterator(txns.end()), - std::back_inserter(jsonTransactions), - [binary](auto obj) { - boost::json::object entry; - if (!binary) - { - auto [sttx, meta] = deserializeTxPlusMeta(obj); - entry["transaction"] = toJson(*sttx); - entry["metadata"] = toJson(*meta); - } - else - { - entry["transaction"] = ripple::strHex(obj.transaction); - entry["metadata"] = ripple::strHex(obj.metadata); - } - entry["ledger_sequence"] = obj.ledgerSequence; - return entry; - }); - } - else - { - auto hashes = - backend.fetchAllTransactionHashesInLedger(*ledgerSequence); - std::transform( - std::move_iterator(hashes.begin()), - std::move_iterator(hashes.end()), - std::back_inserter(jsonTransactions), - [](auto hash) { - boost::json::object entry; - entry["hash"] = ripple::strHex(hash); - return entry; - }); - } - } - return response; -} diff --git a/src/handlers/LedgerData.cpp b/src/handlers/LedgerData.cpp deleted file mode 100644 index a5fc96a8..00000000 --- a/src/handlers/LedgerData.cpp +++ /dev/null @@ -1,185 +0,0 @@ -//------------------------------------------------------------------------------ -/* - This file is part of rippled: https://github.com/ripple/rippled - Copyright (c) 2012-2014 Ripple Labs Inc. - - Permission to use, copy, modify, and/or distribute this software for any - purpose with or without fee is hereby granted, provided that the above - copyright notice and this permission notice appear in all copies. - - THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -*/ -//============================================================================== - -#include -#include -#include -#include -#include -// Get state nodes from a ledger -// Inputs: -// limit: integer, maximum number of entries -// marker: opaque, resume point -// binary: boolean, format -// type: string // optional, defaults to all ledger node types -// Outputs: -// ledger_hash: chosen ledger's hash -// ledger_index: chosen ledger's index -// state: array of state nodes -// marker: resume point, if any -// -// -boost::json::object -doLedgerData( - 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; - } - auto ledger = backend.fetchLedgerBySequence(*ledgerSequence); - if (!ledger) - { - response["error"] = "Ledger not found"; - return response; - } - - std::optional cursor; - if (request.contains("cursor")) - { - BOOST_LOG_TRIVIAL(debug) << __func__ << " : parsing cursor"; - cursor = ripple::uint256{}; - if (!cursor->parseHex(request.at("cursor").as_string().c_str())) - { - response["error"] = "Invalid cursor"; - response["request"] = request; - return response; - } - } - bool binary = - request.contains("binary") ? request.at("binary").as_bool() : false; - size_t limit = request.contains("limit") ? request.at("limit").as_int64() - : (binary ? 2048 : 256); - Backend::LedgerPage page; - auto start = std::chrono::system_clock::now(); - page = backend.fetchLedgerPage(cursor, *ledgerSequence, limit); - - auto end = std::chrono::system_clock::now(); - - auto time = - std::chrono::duration_cast(end - start) - .count(); - boost::json::array objects; - std::vector& results = page.objects; - std::optional const& returnedCursor = page.cursor; - BOOST_LOG_TRIVIAL(debug) - << __func__ << " number of results = " << results.size(); - for (auto const& [key, object] : results) - { - ripple::STLedgerEntry sle{ - ripple::SerialIter{object.data(), object.size()}, key}; - if (binary) - { - boost::json::object entry; - entry["data"] = ripple::serializeHex(sle); - entry["index"] = ripple::to_string(sle.key()); - objects.push_back(entry); - } - else - objects.push_back(toJson(sle)); - } - response["objects"] = objects; - if (returnedCursor) - response["cursor"] = ripple::strHex(*returnedCursor); - - response["num_results"] = results.size(); - response["db_time"] = time; - response["time_per_result"] = time / (results.size() ? results.size() : 1); - if (page.warning) - { - response["warning"] = - "Periodic database update in progress. Data for this ledger may be " - "incomplete. Data should be complete " - "within a few minutes. Other RPC calls are not affected, " - "regardless of ledger. This " - "warning is only present on the first " - "page of the ledger"; - } - return response; -} - -/* -std::pair -doLedgerDataGrpc( - RPC::GRPCContext& context) -{ - org::xrpl::rpc::v1::GetLedgerDataRequest& request = context.params; - org::xrpl::rpc::v1::GetLedgerDataResponse response; - grpc::Status status = grpc::Status::OK; - - std::shared_ptr ledger; - if (RPC::ledgerFromRequest(ledger, context)) - { - grpc::Status errorStatus{ - grpc::StatusCode::NOT_FOUND, "ledger not found"}; - return {response, errorStatus}; - } - - ReadView::key_type key = ReadView::key_type(); - if (request.marker().size() != 0) - { - key = uint256::fromVoid(request.marker().data()); - if (key.size() != request.marker().size()) - { - grpc::Status errorStatus{ - grpc::StatusCode::INVALID_ARGUMENT, "marker malformed"}; - return {response, errorStatus}; - } - } - - auto e = ledger->sles.end(); - ReadView::key_type stopKey = ReadView::key_type(); - if (request.end_marker().size() != 0) - { - stopKey = uint256::fromVoid(request.end_marker().data()); - if (stopKey.size() != request.marker().size()) - { - grpc::Status errorStatus{ - grpc::StatusCode::INVALID_ARGUMENT, "end marker malformed"}; - return {response, errorStatus}; - } - e = ledger->sles.upper_bound(stopKey); - } - - int maxLimit = RPC::Tuning::pageLength(true); - - for (auto i = ledger->sles.upper_bound(key); i != e; ++i) - { - auto sle = ledger->read(keylet::unchecked((*i)->key())); - if (maxLimit-- <= 0) - { - // Stop processing before the current key. - auto k = sle->key(); - --k; - response.set_marker(k.data(), k.size()); - break; - } - auto stateObject = response.mutable_ledger_objects()->add_objects(); - Serializer s; - sle->add(s); - stateObject->set_data(s.peekData().data(), s.getLength()); - stateObject->set_key(sle->key().data(), sle->key().size()); - } - return {response, status}; -} -*/ diff --git a/src/handlers/LedgerEntry.cpp b/src/handlers/LedgerEntry.cpp deleted file mode 100644 index 1b6c9703..00000000 --- a/src/handlers/LedgerEntry.cpp +++ /dev/null @@ -1,55 +0,0 @@ -#include -#include -#include -#include -#include -// { -// ledger_hash : -// ledger_index : -// ... -// } -boost::json::object -doLedgerEntry( - boost::json::object const& request, - BackendInterface const& backend) -{ - boost::json::object response; - bool binary = - request.contains("binary") ? request.at("binary").as_bool() : false; - auto ledgerSequence = ledgerSequenceFromRequest(request, backend); - if (!ledgerSequence) - { - response["error"] = "Empty database"; - return response; - } - ripple::uint256 key; - if (!key.parseHex(request.at("index").as_string().c_str())) - { - response["error"] = "Error parsing index"; - return response; - } - auto start = std::chrono::system_clock::now(); - auto dbResponse = backend.fetchLedgerObject(key, *ledgerSequence); - auto end = std::chrono::system_clock::now(); - auto time = - std::chrono::duration_cast(end - start) - .count(); - if (!dbResponse or dbResponse->size() == 0) - { - response["error"] = "Object not found"; - return response; - } - if (binary) - { - response["object"] = ripple::strHex(*dbResponse); - } - else - { - ripple::STLedgerEntry sle{ - ripple::SerialIter{dbResponse->data(), dbResponse->size()}, key}; - response["object"] = toJson(sle); - } - - return response; -} - diff --git a/src/handlers/RPCHelpers.cpp b/src/handlers/RPCHelpers.cpp index e3aa0b31..9c3568c1 100644 --- a/src/handlers/RPCHelpers.cpp +++ b/src/handlers/RPCHelpers.cpp @@ -1,5 +1,6 @@ -#include #include +#include +#include std::optional accountFromStringStrict(std::string const& account) @@ -69,54 +70,38 @@ deserializeTxPlusMeta( boost::json::object toJson(ripple::STBase const& obj) { - auto start = std::chrono::system_clock::now(); boost::json::value value = boost::json::parse( obj.getJson(ripple::JsonOptions::none).toStyledString()); - auto end = std::chrono::system_clock::now(); - value.as_object()["deserialization_time_microseconds"] = - std::chrono::duration_cast(end - start) - .count(); + return value.as_object(); } -boost::json::value -getJson(Json::Value const& value) -{ - boost::json::value boostValue = boost::json::parse(value.toStyledString()); - - return boostValue; -} - boost::json::object toJson(ripple::TxMeta const& meta) { - auto start = std::chrono::system_clock::now(); boost::json::value value = boost::json::parse( meta.getJson(ripple::JsonOptions::none).toStyledString()); - auto end = std::chrono::system_clock::now(); - value.as_object()["deserialization_time_microseconds"] = - std::chrono::duration_cast(end - start) - .count(); + return value.as_object(); } +boost::json::value +toBoostJson(Json::Value const& value) +{ + boost::json::value boostValue = + boost::json::parse(value.toStyledString()); + + return boostValue; +} + boost::json::object toJson(ripple::SLE const& sle) { - auto start = std::chrono::system_clock::now(); boost::json::value value = boost::json::parse( sle.getJson(ripple::JsonOptions::none).toStyledString()); - auto end = std::chrono::system_clock::now(); - value.as_object()["deserialization_time_microseconds"] = - std::chrono::duration_cast(end - start) - .count(); + return value.as_object(); } -boost::json::value -toBoostJson(RippledJson const& value) -{ - return boost::json::parse(value.toStyledString()); -} boost::json::object toJson(ripple::LedgerInfo const& lgrInfo) @@ -138,20 +123,70 @@ toJson(ripple::LedgerInfo const& lgrInfo) return header; } -std::optional -ledgerSequenceFromRequest( - boost::json::object const& request, - BackendInterface const& backend) +std::variant +ledgerInfoFromRequest(RPC::Context const& ctx) { - if (!request.contains("ledger_index") || - request.at("ledger_index").is_string()) + auto indexValue = ctx.params.contains("ledger_index") + ? ctx.params.at("ledger_index") + : nullptr; + + auto hashValue = ctx.params.contains("ledger_hash") + ? ctx.params.at("ledger_hash") + : nullptr; + + std::optional lgrInfo; + if (!hashValue.is_null()) { - return backend.fetchLatestLedgerSequence(); + if (!hashValue.is_string()) + return RPC::Status{RPC::Error::rpcINVALID_PARAMS, "ledgerHashNotString"}; + + ripple::uint256 ledgerHash; + if (!ledgerHash.parseHex(hashValue.as_string().c_str())) + return RPC::Status{RPC::Error::rpcINVALID_PARAMS, "ledgerHashMalformed"}; + + lgrInfo = ctx.backend->fetchLedgerByHash(ledgerHash); + + } + else if (!indexValue.is_null()) + { + std::uint32_t ledgerSequence; + if (indexValue.is_string() && indexValue.as_string() == "validated") + ledgerSequence = ctx.range.maxSequence; + else if (!indexValue.is_string() && indexValue.is_int64()) + ledgerSequence = indexValue.as_int64(); + else + return RPC::Status{RPC::Error::rpcINVALID_PARAMS, "ledgerIndexMalformed"}; + + lgrInfo = + ctx.backend->fetchLedgerBySequence(ledgerSequence); } else { - return request.at("ledger_index").as_int64(); + lgrInfo = + ctx.backend->fetchLedgerBySequence(ctx.range.maxSequence); } + + if (!lgrInfo) + return RPC::Status{RPC::Error::rpcLGR_NOT_FOUND, "ledgerNotFound"}; + + return *lgrInfo; +} + +std::vector +ledgerInfoToBlob(ripple::LedgerInfo const& info) +{ + ripple::Serializer s; + s.add32(info.seq); + s.add64(info.drops.drops()); + s.addBitString(info.parentHash); + s.addBitString(info.txHash); + s.addBitString(info.accountHash); + s.add32(info.parentCloseTime.time_since_epoch().count()); + s.add32(info.closeTime.time_since_epoch().count()); + s.add8(info.closeTimeResolution.count()); + s.add8(info.closeFlags); + // s.addBitString(info.hash); + return s.peekData(); } std::optional @@ -239,8 +274,8 @@ parseRippleLibSeed(boost::json::value const& value) return {}; } -std::pair -keypairFromRequst(boost::json::object const& request, boost::json::value& error) +std::variant> +keypairFromRequst(boost::json::object const& request) { bool const has_key_type = request.contains("key_type"); @@ -262,17 +297,13 @@ keypairFromRequst(boost::json::object const& request, boost::json::value& error) } if (count == 0) - { - error = "missing field secret"; - return {}; - } + return RPC::Status{RPC::Error::rpcINVALID_PARAMS, "missing field secret"}; if (count > 1) { - error = - "Exactly one of the following must be specified: " - " passphrase, secret, seed, or seed_hex"; - return {}; + return RPC::Status{RPC::Error::rpcINVALID_PARAMS, + "Exactly one of the following must be specified: " + " passphrase, secret, seed, or seed_hex"}; } boost::optional keyType; @@ -281,25 +312,18 @@ keypairFromRequst(boost::json::object const& request, boost::json::value& error) if (has_key_type) { if (!request.at("key_type").is_string()) - { - error = "key_type must be string"; - return {}; - } + return RPC::Status{RPC::Error::rpcINVALID_PARAMS, "keyTypeNotString"}; std::string key_type = request.at("key_type").as_string().c_str(); keyType = ripple::keyTypeFromString(key_type); if (!keyType) - { - error = "Invalid field key_type"; - return {}; - } + return RPC::Status{RPC::Error::rpcINVALID_PARAMS, "invalidFieldKeyType"}; if (secretType == "secret") - { - error = "The secret field is not allowed if key_type is used."; - return {}; - } + return RPC::Status{RPC::Error::rpcINVALID_PARAMS, + "The secret field is not allowed if key_type is used."}; + } // ripple-lib encodes seed used to generate an Ed25519 wallet in a @@ -313,12 +337,9 @@ keypairFromRequst(boost::json::object const& request, boost::json::value& error) { // If the user passed in an Ed25519 seed but *explicitly* // requested another key type, return an error. - if (keyType.value_or(ripple::KeyType::ed25519) != - ripple::KeyType::ed25519) - { - error = "Specified seed is for an Ed25519 wallet."; - return {}; - } + if (keyType.value_or(ripple::KeyType::ed25519) != ripple::KeyType::ed25519) + return RPC::Status{RPC::Error::rpcINVALID_PARAMS, + "Specified seed is for an Ed25519 wallet."}; keyType = ripple::KeyType::ed25519; } @@ -332,10 +353,8 @@ keypairFromRequst(boost::json::object const& request, boost::json::value& error) if (has_key_type) { if (!request.at(secretType).is_string()) - { - error = "secret value must be string"; - return {}; - } + return RPC::Status{RPC::Error::rpcINVALID_PARAMS, + "secret value must be string"}; std::string key = request.at(secretType).as_string().c_str(); @@ -353,10 +372,8 @@ keypairFromRequst(boost::json::object const& request, boost::json::value& error) else { if (!request.at("secret").is_string()) - { - error = "field secret should be a string"; - return {}; - } + return RPC::Status{RPC::Error::rpcINVALID_PARAMS, + "field secret should be a string"}; std::string secret = request.at("secret").as_string().c_str(); seed = ripple::parseGenericSeed(secret); @@ -364,17 +381,13 @@ keypairFromRequst(boost::json::object const& request, boost::json::value& error) } if (!seed) - { - error = "Bad Seed: invalid field message secretType"; - return {}; - } + return RPC::Status{RPC::Error::rpcBAD_SEED, + "Bad Seed: invalid field message secretType"}; - if (keyType != ripple::KeyType::secp256k1 && - keyType != ripple::KeyType::ed25519) - { - error = "keypairForSignature: invalid key type"; - return {}; - } + if (keyType != ripple::KeyType::secp256k1 + && keyType != ripple::KeyType::ed25519) + return RPC::Status{RPC::Error::rpcINVALID_PARAMS, + "keypairForSignature: invalid key type"}; return generateKeyPair(*keyType, *seed); } @@ -402,19 +415,161 @@ getAccountsFromTransaction(boost::json::object const& transaction) return accounts; } -std::vector -ledgerInfoToBlob(ripple::LedgerInfo const& info) + +bool +isGlobalFrozen( + BackendInterface const& backend, + std::uint32_t sequence, + ripple::AccountID const& issuer) { - ripple::Serializer s; - s.add32(info.seq); - s.add64(info.drops.drops()); - s.addBitString(info.parentHash); - s.addBitString(info.txHash); - s.addBitString(info.accountHash); - s.add32(info.parentCloseTime.time_since_epoch().count()); - s.add32(info.closeTime.time_since_epoch().count()); - s.add8(info.closeTimeResolution.count()); - s.add8(info.closeFlags); - s.addBitString(info.hash); - return s.peekData(); + if (ripple::isXRP(issuer)) + return false; + + auto key = ripple::keylet::account(issuer).key; + auto blob = backend.fetchLedgerObject(key, sequence); + + if (!blob) + return false; + + ripple::SerialIter it{blob->data(), blob->size()}; + ripple::SLE sle{it, key}; + + return sle.isFlag(ripple::lsfGlobalFreeze); +} + +bool +isFrozen( + BackendInterface const& backend, + std::uint32_t sequence, + ripple::AccountID const& account, + ripple::Currency const& currency, + ripple::AccountID const& issuer) +{ + if (ripple::isXRP(currency)) + return false; + + auto key = ripple::keylet::account(issuer).key; + auto blob = backend.fetchLedgerObject(key, sequence); + + if (!blob) + return false; + + ripple::SerialIter it{blob->data(), blob->size()}; + ripple::SLE sle{it, key}; + + if (sle.isFlag(ripple::lsfGlobalFreeze)) + return true; + + if (issuer != account) + { + key = ripple::keylet::line(account, issuer, currency).key; + blob = backend.fetchLedgerObject(key, sequence); + + if (!blob) + return false; + + ripple::SerialIter issuerIt{blob->data(), blob->size()}; + ripple::SLE issuerLine{it, key}; + + auto frozen = + (issuer > account) ? ripple::lsfHighFreeze : ripple::lsfLowFreeze; + + if (issuerLine.isFlag(frozen)) + return true; + } + + return false; +} + +ripple::XRPAmount +xrpLiquid( + BackendInterface const& backend, + std::uint32_t sequence, + ripple::AccountID const& id) +{ + auto key = ripple::keylet::account(id).key; + auto blob = backend.fetchLedgerObject(key, sequence); + + if (!blob) + return beast::zero; + + ripple::SerialIter it{blob->data(), blob->size()}; + ripple::SLE sle{it, key}; + + std::uint32_t const ownerCount = sle.getFieldU32(ripple::sfOwnerCount); + + auto const reserve = backend.fetchFees(sequence)->accountReserve(ownerCount); + + auto const balance = sle.getFieldAmount(ripple::sfBalance); + + ripple::STAmount amount = balance - reserve; + if (balance < reserve) + amount.clear(); + + return amount.xrp(); +} + +ripple::STAmount +accountHolds( + BackendInterface const& backend, + std::uint32_t sequence, + ripple::AccountID const& account, + ripple::Currency const& currency, + ripple::AccountID const& issuer) +{ + ripple::STAmount amount; + if (ripple::isXRP(currency)) + { + return {xrpLiquid(backend, sequence, account)}; + } + + auto key = ripple::keylet::line(account, issuer, currency).key; + auto const blob = backend.fetchLedgerObject(key, sequence); + + if (!blob) + { + amount.clear({currency, issuer}); + return amount; + } + + ripple::SerialIter it{blob->data(), blob->size()}; + ripple::SLE sle{it, key}; + + if (isFrozen(backend, sequence, account, currency, issuer)) + { + amount.clear(ripple::Issue(currency, issuer)); + } + else + { + amount = sle.getFieldAmount(ripple::sfBalance); + if (account > issuer) + { + // Put balance in account terms. + amount.negate(); + } + amount.setIssuer(issuer); + } + + return amount; +} + +ripple::Rate +transferRate( + BackendInterface const& backend, + std::uint32_t sequence, + ripple::AccountID const& issuer) +{ + auto key = ripple::keylet::account(issuer).key; + auto blob = backend.fetchLedgerObject(key, sequence); + + if (blob) + { + ripple::SerialIter it{blob->data(), blob->size()}; + ripple::SLE sle{it, key}; + + if (sle.isFieldPresent(ripple::sfTransferRate)) + return ripple::Rate{sle.getFieldU32(ripple::sfTransferRate)}; + } + + return ripple::parityRate; } diff --git a/src/handlers/RPCHelpers.h b/src/handlers/RPCHelpers.h index 56849fce..c6181515 100644 --- a/src/handlers/RPCHelpers.h +++ b/src/handlers/RPCHelpers.h @@ -3,8 +3,13 @@ #define XRPL_REPORTING_RPCHELPERS_H_INCLUDED #include +#include +#include +#include #include #include +#include +#include #include std::optional @@ -38,10 +43,8 @@ using RippledJson = Json::Value; boost::json::value toBoostJson(RippledJson const& value); -std::optional -ledgerSequenceFromRequest( - boost::json::object const& request, - BackendInterface const& backend); +std::variant +ledgerInfoFromRequest(RPC::Context const& ctx); std::optional traverseOwnedNodes( @@ -51,10 +54,8 @@ traverseOwnedNodes( ripple::uint256 const& cursor, std::function atOwnedNode); -std::pair -keypairFromRequst( - boost::json::object const& request, - boost::json::value& error); +std::variant> +keypairFromRequst(boost::json::object const& request); std::vector getAccountsFromTransaction(boost::json::object const& transaction); @@ -62,4 +63,38 @@ getAccountsFromTransaction(boost::json::object const& transaction); std::vector ledgerInfoToBlob(ripple::LedgerInfo const& info); +bool +isGlobalFrozen( + BackendInterface const& backend, + std::uint32_t seq, + ripple::AccountID const& issuer); + +bool +isFrozen( + BackendInterface const& backend, + std::uint32_t sequence, + ripple::AccountID const& account, + ripple::Currency const& currency, + ripple::AccountID const& issuer); + +ripple::STAmount +accountHolds( + BackendInterface const& backend, + std::uint32_t sequence, + ripple::AccountID const& account, + ripple::Currency const& currency, + ripple::AccountID const& issuer); + +ripple::Rate +transferRate( + BackendInterface const& backend, + std::uint32_t sequence, + ripple::AccountID const& issuer); + +ripple::XRPAmount +xrpLiquid( + BackendInterface const& backend, + std::uint32_t sequence, + ripple::AccountID const& id); + #endif diff --git a/src/handlers/ServerInfo.cpp b/src/handlers/ServerInfo.cpp deleted file mode 100644 index 344643ce..00000000 --- a/src/handlers/ServerInfo.cpp +++ /dev/null @@ -1,53 +0,0 @@ -#include -#include -boost::json::object -doServerInfo( - boost::json::object const& request, - BackendInterface const& backend) -{ - boost::json::object response; - - auto rng = backend.fetchLedgerRange(); - if (!rng) - { - response["complete_ledgers"] = "empty"; - } - else - { - std::string completeLedgers = std::to_string(rng->minSequence); - if (rng->maxSequence != rng->minSequence) - completeLedgers += "-" + std::to_string(rng->maxSequence); - response["complete_ledgers"] = completeLedgers; - } - if (rng) - { - auto lgrInfo = backend.fetchLedgerBySequence(rng->maxSequence); - response["validated_ledger"] = toJson(*lgrInfo); - } - - boost::json::array indexes; - - if (rng) - { - uint32_t cur = rng->minSequence; - while (cur <= rng->maxSequence + 1) - { - auto keyIndex = backend.getKeyIndexOfSeq(cur); - assert(keyIndex.has_value()); - cur = keyIndex->keyIndex; - boost::json::object entry; - entry["complete"] = backend.isLedgerIndexed(cur); - entry["sequence"] = cur; - indexes.emplace_back(entry); - cur = cur + 1; - } - } - response["indexes"] = indexes; - auto indexing = backend.getIndexer().getCurrentlyIndexing(); - if (indexing) - response["indexing"] = *indexing; - else - response["indexing"] = "none"; - - return response; -} diff --git a/src/handlers/Status.cpp b/src/handlers/Status.cpp new file mode 100644 index 00000000..217af122 --- /dev/null +++ b/src/handlers/Status.cpp @@ -0,0 +1,62 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2021 Ripple Labs Inc. + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include +#include + +namespace RPC +{ + +void +inject_error(Error err, boost::json::object& json) +{ + ripple::RPC::ErrorInfo const& info(ripple::RPC::get_error_info(err)); + json["error"] = info.token; + json["error_code"] = static_cast(err); + json["error_message"] = info.message; + json["status"] = "error"; + json["type"] = "response"; +} + +void +inject_error(Error err, std::string const& message, boost::json::object& json) +{ + ripple::RPC::ErrorInfo const& info(ripple::RPC::get_error_info(err)); + json["error"] = info.token; + json["error_code"] = static_cast(err); + json["error_message"] = message; + json["status"] = "error"; + json["type"] = "response"; +} + +boost::json::object +make_error(Error err) +{ + boost::json::object json{}; + inject_error(err, json); + return json; +} + +boost::json::object +make_error(Error err, std::string const& message) +{ + boost::json::object json{}; + inject_error(err, message, json); + return json; +} +} \ No newline at end of file diff --git a/src/handlers/Status.h b/src/handlers/Status.h new file mode 100644 index 00000000..dd028a69 --- /dev/null +++ b/src/handlers/Status.h @@ -0,0 +1,72 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2021 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef RPC_ERRORCODES_H_INCLUDED +#define RPC_ERRORCODES_H_INCLUDED + +#include +#include +#include +#include + +namespace RPC +{ + +using Error = ripple::error_code_i; + +struct Status +{ + Error error = Error::rpcSUCCESS; + std::string message = ""; + + Status() {}; + + Status(Error error_) : error(error_) {}; + + Status(Error error_, std::string message_) + : error(error_) + , message(message_) + {} + + /** Returns true if the Status is *not* OK. */ + operator bool() const + { + return error != Error::rpcSUCCESS; + } +}; + +static Status OK; + +using Result = std::variant; + +void +inject_error(Error err, boost::json::object& json); + +void +inject_error(Error err, std::string const& message, boost::json::object& json); + +boost::json::object +make_error(Error err); + +boost::json::object +make_error(Error err, std::string const& message); + +} // namespace RPC + +#endif // RPC_ERRORCODES_H_INCLUDED \ No newline at end of file diff --git a/src/handlers/methods/Account.h b/src/handlers/methods/Account.h new file mode 100644 index 00000000..fc3fe4b2 --- /dev/null +++ b/src/handlers/methods/Account.h @@ -0,0 +1,51 @@ + +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2021 Ripple Labs Inc. + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + + +#ifndef REPORTING_ACCOUNT_HANDLER_H_INCLUDED +#define REPORTING_ACCOUNT_HANDLER_H_INCLUDED + +#include +#include +#include +#include + +namespace RPC +{ + +Result +doAccountInfo(Context const& context); + +Result +doAccountChannels(Context const& context); + +Result +doAccountCurrencies(Context const& context); + +Result +doAccountLines(Context const& context); + +Result +doAccountObjects(Context const& context); + +Result +doAccountOffers(Context const& context); + +} // namespace RPC + +#endif // REPORTING_ACCOUNT_HANDLER_H_INCLUDED \ No newline at end of file diff --git a/src/handlers/methods/Channel.h b/src/handlers/methods/Channel.h new file mode 100644 index 00000000..ad9bf70f --- /dev/null +++ b/src/handlers/methods/Channel.h @@ -0,0 +1,38 @@ + +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2021 Ripple Labs Inc. + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + + +#ifndef REPORTING_CHANNEL_HANDLER_H_INCLUDED +#define REPORTING_CHANNEL_HANDLER_H_INCLUDED + +#include +#include +#include + +namespace RPC +{ + +Result +doChannelAuthorize(Context const& context); + +Result +doChannelVerify(Context const& context); + +} // namespace RPC + +#endif // REPORTING_CHANNEL_HANDLER_H_INCLUDED \ No newline at end of file diff --git a/src/handlers/methods/Exchange.h b/src/handlers/methods/Exchange.h new file mode 100644 index 00000000..61b6ef82 --- /dev/null +++ b/src/handlers/methods/Exchange.h @@ -0,0 +1,36 @@ + +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2021 Ripple Labs Inc. + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + + +#ifndef REPORTING_EXCHANGE_HANDLER_H_INCLUDED +#define REPORTING_EXCHANGE_HANDLER_H_INCLUDED + +#include +#include +#include +#include + +namespace RPC +{ + +Result +doBookOffers(Context const& context); + +} // namespace RPC + +#endif // REPORTING_CHANNEL_HANDLER_H_INCLUDED \ No newline at end of file diff --git a/src/handlers/methods/Ledger.h b/src/handlers/methods/Ledger.h new file mode 100644 index 00000000..1ea159eb --- /dev/null +++ b/src/handlers/methods/Ledger.h @@ -0,0 +1,45 @@ + +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2021 Ripple Labs Inc. + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + + +#ifndef REPORTING_LEDGER_HANDLER_H_INCLUDED +#define REPORTING_LEDGER_HANDLER_H_INCLUDED + +#include +#include +#include +#include + +namespace RPC +{ + +Result +doLedger(Context const& context); + +Result +doLedgerEntry(Context const& context); + +Result +doLedgerData(Context const& context); + +Result +doLedgerRange(Context const& context); + +} // namespace RPC + +#endif // REPORTING_LEDGER_HANDLER_H_INCLUDED \ No newline at end of file diff --git a/src/handlers/methods/Subscribe.h b/src/handlers/methods/Subscribe.h new file mode 100644 index 00000000..b1baa207 --- /dev/null +++ b/src/handlers/methods/Subscribe.h @@ -0,0 +1,39 @@ + +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2021 Ripple Labs Inc. + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + + +#ifndef REPORTING_SUBSCRIBE_HANDLER_H_INCLUDED +#define REPORTING_SUBSCRIBE_HANDLER_H_INCLUDED + +#include +#include +#include +#include + +namespace RPC +{ + +Result +doSubscribe(Context const& context); + +Result +doUnsubscribe(Context const& context); + +} // namespace RPC + +#endif // REPORTING_SUBSCRIBE_HANDLER_H_INCLUDED \ No newline at end of file diff --git a/src/handlers/methods/Transaction.h b/src/handlers/methods/Transaction.h new file mode 100644 index 00000000..198a7951 --- /dev/null +++ b/src/handlers/methods/Transaction.h @@ -0,0 +1,39 @@ + +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2021 Ripple Labs Inc. + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + + +#ifndef REPORTING_TRANSACTION_HANDLER_H_INCLUDED +#define REPORTING_TRANSACTION_HANDLER_H_INCLUDED + +#include +#include +#include +#include + +namespace RPC +{ + +Result +doTx(Context const& context); + +Result +doAccountTx(Context const& context); + +} // namespace RPC + +#endif // REPORTING_TRANSACTION_HANDLER_H_INCLUDED \ No newline at end of file diff --git a/src/handlers/methods/impl/AccountChannels.cpp b/src/handlers/methods/impl/AccountChannels.cpp new file mode 100644 index 00000000..5ccf87e4 --- /dev/null +++ b/src/handlers/methods/impl/AccountChannels.cpp @@ -0,0 +1,141 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace RPC +{ + +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(jDst); +} + +Result +doAccountChannels(Context const& context) +{ + auto request = context.params; + boost::json::object response = {}; + + auto v = ledgerInfoFromRequest(context); + if (auto status = std::get_if(&v)) + return *status; + + auto lgrInfo = std::get(v); + + if(!request.contains("account")) + return Status{Error::rpcINVALID_PARAMS, "missingAccount"}; + + if(!request.at("account").is_string()) + return Status{Error::rpcINVALID_PARAMS, "accountNotString"}; + + auto accountID = + accountFromStringStrict(request.at("account").as_string().c_str()); + + if (!accountID) + return Status{Error::rpcINVALID_PARAMS, "malformedAccount"}; + + std::optional destAccount = {}; + if (request.contains("destination_account")) + { + if (!request.at("destination_account").is_string()) + return Status{Error::rpcINVALID_PARAMS, "destinationNotString"}; + + destAccount = accountFromStringStrict( + request.at("destination_account").as_string().c_str()); + + if (!destAccount) + return Status{Error::rpcINVALID_PARAMS, "destinationMalformed"}; + } + + std::uint32_t limit = 200; + if (request.contains("limit")) + { + if(!request.at("limit").is_int64()) + return Status{Error::rpcINVALID_PARAMS, "limitNotInt"}; + + limit = request.at("limit").as_int64(); + if (limit <= 0) + return Status{Error::rpcINVALID_PARAMS, "limitNotPositive"}; + } + + ripple::uint256 cursor; + if (request.contains("cursor")) + { + if(!request.at("cursor").is_string()) + return Status{Error::rpcINVALID_PARAMS, "cursorNotString"}; + + if (!cursor.parseHex(request.at("cursor").as_string().c_str())) + return Status{Error::rpcINVALID_PARAMS, "malformedCursor"}; + } + + response["account"] = ripple::to_string(*accountID); + response["channels"] = boost::json::value(boost::json::array_kind); + boost::json::array& jsonChannels = response.at("channels").as_array(); + + 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( + *context.backend, + *accountID, + lgrInfo.seq, + cursor, + addToResponse); + + response["ledger_hash"] = ripple::strHex(lgrInfo.hash); + response["ledger_index"] = lgrInfo.seq; + if (nextCursor) + response["marker"] = ripple::strHex(*nextCursor); + + return response; +} + +} // namespace RPC \ No newline at end of file diff --git a/src/handlers/AccountCurrencies.cpp b/src/handlers/methods/impl/AccountCurrencies.cpp similarity index 55% rename from src/handlers/AccountCurrencies.cpp rename to src/handlers/methods/impl/AccountCurrencies.cpp index 7032d19a..cdcc1160 100644 --- a/src/handlers/AccountCurrencies.cpp +++ b/src/handlers/methods/impl/AccountCurrencies.cpp @@ -6,53 +6,45 @@ #include #include #include -#include +#include #include +#include +#include +#include -boost::json::object -doAccountCurrencies( - boost::json::object const& request, - BackendInterface const& backend) +namespace RPC { - boost::json::object response; - auto ledgerSequence = ledgerSequenceFromRequest(request, backend); - if (!ledgerSequence) - { - response["error"] = "Empty database"; - return response; - } +Result +doAccountCurrencies(Context const& context) +{ + auto request = context.params; + boost::json::object response = {}; - if (!request.contains("account")) - { - response["error"] = "Must contain account"; - return response; - } + auto v = ledgerInfoFromRequest(context); + if (auto status = std::get_if(&v)) + return *status; - if (!request.at("account").is_string()) - { - response["error"] = "Account must be a string"; - return response; - } + auto lgrInfo = std::get(v); - ripple::AccountID accountID; - auto parsed = ripple::parseBase58( - request.at("account").as_string().c_str()); + if(!request.contains("account")) + return Status{Error::rpcINVALID_PARAMS, "missingAccount"}; - if (!parsed) - { - response["error"] = "Invalid account"; - return response; - } + if(!request.at("account").is_string()) + return Status{Error::rpcINVALID_PARAMS, "accountNotString"}; + + auto accountID = + accountFromStringStrict(request.at("account").as_string().c_str()); - accountID = *parsed; + if (!accountID) + return Status{Error::rpcINVALID_PARAMS, "malformedAccount"}; 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); + sle.getFieldAmount(ripple::sfBalance); auto lowLimit = sle.getFieldAmount(ripple::sfLowLimit); auto highLimit = sle.getFieldAmount(ripple::sfHighLimit); @@ -65,12 +57,25 @@ doAccountCurrencies( if ((-balance) < lineLimitPeer) send.insert(ripple::to_string(balance.getCurrency())); } - + return true; }; traverseOwnedNodes( - backend, accountID, *ledgerSequence, beast::zero, addToResponse); + *context.backend, + *accountID, + lgrInfo.seq, + beast::zero, + addToResponse); + + response["ledger_hash"] = ripple::strHex(lgrInfo.hash); + response["ledger_index"] = lgrInfo.seq; + + 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()); response["send_currencies"] = boost::json::value(boost::json::array_kind); boost::json::array& jsonSend = response.at("send_currencies").as_array(); @@ -78,13 +83,7 @@ doAccountCurrencies( 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; } + +} // namespace RPC diff --git a/src/handlers/AccountInfo.cpp b/src/handlers/methods/impl/AccountInfo.cpp similarity index 68% rename from src/handlers/AccountInfo.cpp rename to src/handlers/methods/impl/AccountInfo.cpp index 621e68cc..d7cb80d5 100644 --- a/src/handlers/AccountInfo.cpp +++ b/src/handlers/methods/impl/AccountInfo.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include @@ -38,78 +39,63 @@ // // error. // } -// TODO(tom): what is that "default"? -boost::json::object -doAccountInfo( - boost::json::object const& request, - BackendInterface const& backend) +namespace RPC { - boost::json::object response; + +Result +doAccountInfo(Context const& context) +{ + auto request = context.params; + boost::json::object response = {}; + std::string strIdent; if (request.contains("account")) strIdent = request.at("account").as_string().c_str(); else if (request.contains("ident")) strIdent = request.at("ident").as_string().c_str(); else - { - response["error"] = "missing account field"; - return response; - } - bool binary = - request.contains("binary") ? request.at("binary").as_bool() : false; - auto ledgerSequence = ledgerSequenceFromRequest(request, backend); - if (!ledgerSequence) - { - response["error"] = "Empty database"; - return response; - } + return Status{Error::rpcACT_MALFORMED}; - // bool bStrict = request.contains("strict") && - // params.at("strict").as_bool(); + auto v = ledgerInfoFromRequest(context); + if (auto status = std::get_if(&v)) + return *status; + + auto lgrInfo = std::get(v); // Get info on account. - std::optional accountID = - accountFromStringStrict(strIdent); + auto accountID = accountFromStringStrict(strIdent); - if (!accountID) - { - accountID = ripple::AccountID(); - if (!accountID->parseHex(request.at("account").as_string().c_str())) - { - response["error"] = "account malformed"; - return response; - } - } auto key = ripple::keylet::account(accountID.value()); auto start = std::chrono::system_clock::now(); std::optional> dbResponse = - backend.fetchLedgerObject(key.key, *ledgerSequence); + context.backend->fetchLedgerObject(key.key, lgrInfo.seq); auto end = std::chrono::system_clock::now(); + auto time = std::chrono::duration_cast(end - start) .count(); + if (!dbResponse) { - response["error"] = "no response from db"; + return Status{Error::rpcACT_NOT_FOUND}; } + ripple::STLedgerEntry sle{ ripple::SerialIter{dbResponse->data(), dbResponse->size()}, key.key}; + if (!key.check(sle)) - { - response["error"] = "error fetching record from db"; - return response; - } - else - { - response["success"] = "fetched successfully!"; - if (!binary) - response["object"] = toJson(sle); - else - response["object"] = ripple::strHex(*dbResponse); - response["db_time"] = time; - return response; - } + return Status{Error::rpcDB_DESERIALIZATION}; + + // if (!binary) + // response["account_data"] = getJson(sle); + // else + // response["account_data"] = ripple::strHex(*dbResponse); + // response["db_time"] = time; + + response["account_data"] = toJson(sle); + response["ledger_hash"] = ripple::strHex(lgrInfo.hash); + response["ledger_index"] = lgrInfo.seq; // Return SignerList(s) if that is requested. /* @@ -124,7 +110,7 @@ doAccountInfo( // support multiple SignerLists on one account. auto const sleSigners = ledger->read(keylet::signers(accountID)); if (sleSigners) - jvSignerList.append(sleSigners->toJson(JsonOptions::none)); + jvSignerList.append(sleSigners->getJson(JsonOptions::none)); result[jss::account_data][jss::signer_lists] = std::move(jvSignerList); @@ -134,3 +120,4 @@ doAccountInfo( return response; } +} //namespace RPC \ No newline at end of file diff --git a/src/handlers/AccountLines.cpp b/src/handlers/methods/impl/AccountLines.cpp similarity index 54% rename from src/handlers/AccountLines.cpp rename to src/handlers/methods/impl/AccountLines.cpp index d6f68de6..4b513770 100644 --- a/src/handlers/AccountLines.cpp +++ b/src/handlers/methods/impl/AccountLines.cpp @@ -1,21 +1,27 @@ -#include #include +#include #include #include #include #include #include #include +#include #include -#include #include +#include +#include + + +namespace RPC +{ void addLine( boost::json::array& jsonLines, ripple::SLE const& line, ripple::AccountID const& account, - boost::optional const& peerAccount) + std::optional const& peerAccount) { auto flags = line.getFieldU32(ripple::sfFlags); auto lowLimit = line.getFieldAmount(ripple::sfLowLimit); @@ -38,19 +44,16 @@ addLine( 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); + if (!viewLowest) + balance.negate(); + + 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); + 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); @@ -80,98 +83,67 @@ addLine( jsonLines.push_back(jPeer); } -boost::json::object -doAccountLines( - boost::json::object const& request, - BackendInterface const& backend) +Result +doAccountLines(Context const& context) { - boost::json::object response; + auto request = context.params; + boost::json::object response = {}; - auto ledgerSequence = ledgerSequenceFromRequest(request, backend); - if (!ledgerSequence) - { - response["error"] = "Empty database"; - return response; - } + auto v = ledgerInfoFromRequest(context); + if (auto status = std::get_if(&v)) + return *status; - if (!request.contains("account")) - { - response["error"] = "Must contain account"; - return response; - } + auto lgrInfo = std::get(v); - if (!request.at("account").is_string()) - { - response["error"] = "Account must be a string"; - return response; - } + if(!request.contains("account")) + return Status{Error::rpcINVALID_PARAMS, "missingAccount"}; - ripple::AccountID accountID; - auto parsed = ripple::parseBase58( - request.at("account").as_string().c_str()); + if(!request.at("account").is_string()) + return Status{Error::rpcINVALID_PARAMS, "accountNotString"}; + + auto accountID = + accountFromStringStrict(request.at("account").as_string().c_str()); - if (!parsed) - { - response["error"] = "Invalid account"; - return response; - } + if (!accountID) + return Status{Error::rpcINVALID_PARAMS, "malformedAccount"}; - accountID = *parsed; - - boost::optional peerAccount; + std::optional peerAccount; if (request.contains("peer")) { if (!request.at("peer").is_string()) - { - response["error"] = "peer should be a string"; - return response; - } + return Status{Error::rpcINVALID_PARAMS, "peerNotString"}; - peerAccount = ripple::parseBase58( + peerAccount = accountFromStringStrict( request.at("peer").as_string().c_str()); + if (!peerAccount) - { - response["error"] = "Invalid peer account"; - return response; - } + return Status{Error::rpcINVALID_PARAMS, "peerMalformed"}; } std::uint32_t limit = 200; if (request.contains("limit")) { - if (!request.at("limit").is_int64()) - { - response["error"] = "limit must be integer"; - return response; - } + if(!request.at("limit").is_int64()) + return Status{Error::rpcINVALID_PARAMS, "limitNotInt"}; limit = request.at("limit").as_int64(); if (limit <= 0) - { - response["error"] = "limit must be positive"; - return response; - } + return Status{Error::rpcINVALID_PARAMS, "limitNotPositive"}; } - ripple::uint256 cursor = beast::zero; + ripple::uint256 cursor; if (request.contains("cursor")) { - if (!request.at("cursor").is_string()) - { - response["error"] = "limit must be string"; - return response; - } + if(!request.at("cursor").is_string()) + return Status{Error::rpcINVALID_PARAMS, "cursorNotString"}; - 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()); + if (!cursor.parseHex(request.at("cursor").as_string().c_str())) + return Status{Error::rpcINVALID_PARAMS, "malformedCursor"}; } + response["account"] = ripple::to_string(*accountID); + response["ledger_hash"] = ripple::strHex(lgrInfo.hash); + response["ledger_index"] = lgrInfo.seq; response["lines"] = boost::json::value(boost::json::array_kind); boost::json::array& jsonLines = response.at("lines").as_array(); @@ -182,18 +154,25 @@ doAccountLines( { return false; } - - addLine(jsonLines, sle, accountID, peerAccount); + + addLine(jsonLines, sle, *accountID, peerAccount); } return true; }; - auto nextCursor = traverseOwnedNodes( - backend, accountID, *ledgerSequence, cursor, addToResponse); + auto nextCursor = + traverseOwnedNodes( + *context.backend, + *accountID, + lgrInfo.seq, + cursor, + addToResponse); if (nextCursor) - response["next_cursor"] = ripple::strHex(*nextCursor); + response["marker"] = ripple::strHex(*nextCursor); return response; } + +} // namespace RPC \ No newline at end of file diff --git a/src/handlers/methods/impl/AccountObjects.cpp b/src/handlers/methods/impl/AccountObjects.cpp new file mode 100644 index 00000000..fc61110c --- /dev/null +++ b/src/handlers/methods/impl/AccountObjects.cpp @@ -0,0 +1,123 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +namespace RPC +{ + +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}, +}; + +Result +doAccountObjects(Context const& context) +{ + auto request = context.params; + boost::json::object response = {}; + + auto v = ledgerInfoFromRequest(context); + if (auto status = std::get_if(&v)) + return *status; + + auto lgrInfo = std::get(v); + + if(!request.contains("account")) + return Status{Error::rpcINVALID_PARAMS, "missingAccount"}; + + if(!request.at("account").is_string()) + return Status{Error::rpcINVALID_PARAMS, "accountNotString"}; + + auto accountID = + accountFromStringStrict(request.at("account").as_string().c_str()); + + if (!accountID) + return Status{Error::rpcINVALID_PARAMS, "malformedAccount"}; + + std::uint32_t limit = 200; + if (request.contains("limit")) + { + if(!request.at("limit").is_int64()) + return Status{Error::rpcINVALID_PARAMS, "limitNotInt"}; + + limit = request.at("limit").as_int64(); + if (limit <= 0) + return Status{Error::rpcINVALID_PARAMS, "limitNotPositive"}; + } + + ripple::uint256 cursor; + if (request.contains("cursor")) + { + if(!request.at("cursor").is_string()) + return Status{Error::rpcINVALID_PARAMS, "cursorNotString"}; + + if (!cursor.parseHex(request.at("cursor").as_string().c_str())) + return Status{Error::rpcINVALID_PARAMS, "malformedCursor"}; + } + + std::optional objectType = {}; + if (request.contains("type")) + { + if(!request.at("type").is_string()) + return Status{Error::rpcINVALID_PARAMS, "typeNotString"}; + + std::string typeAsString = request.at("type").as_string().c_str(); + if(types.find(typeAsString) == types.end()) + return Status{Error::rpcINVALID_PARAMS, "typeInvalid"}; + + objectType = types[typeAsString]; + } + + response["account"] = ripple::to_string(*accountID); + response["account_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()) + { + if (limit-- == 0) + { + return false; + } + + jsonObjects.push_back(toJson(sle)); + } + + return true; + }; + + auto nextCursor = + traverseOwnedNodes( + *context.backend, + *accountID, + lgrInfo.seq, + cursor, + addToResponse); + + response["ledger_hash"] = ripple::strHex(lgrInfo.hash); + response["ledger_index"] = lgrInfo.seq; + + if (nextCursor) + response["marker"] = ripple::strHex(*nextCursor); + + return response; +} + +} // namespace RPC \ No newline at end of file diff --git a/src/handlers/AccountOffers.cpp b/src/handlers/methods/impl/AccountOffers.cpp similarity index 60% rename from src/handlers/AccountOffers.cpp rename to src/handlers/methods/impl/AccountOffers.cpp index 05c103ef..ce533fca 100644 --- a/src/handlers/AccountOffers.cpp +++ b/src/handlers/methods/impl/AccountOffers.cpp @@ -1,5 +1,5 @@ -#include #include +#include #include #include #include @@ -7,8 +7,14 @@ #include #include #include -#include #include +#include +#include +#include + + +namespace RPC +{ void addOffer(boost::json::array& offersJson, ripple::SLE const& offer) @@ -18,7 +24,7 @@ addOffer(boost::json::array& offersJson, ripple::SLE const& offer) ripple::STAmount takerPays = offer.getFieldAmount(ripple::sfTakerPays); ripple::STAmount takerGets = offer.getFieldAmount(ripple::sfTakerGets); - + boost::json::object obj; if (!takerPays.native()) @@ -54,83 +60,59 @@ addOffer(boost::json::array& offersJson, ripple::SLE const& offer) 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) + +Result +doAccountOffers(Context const& context) { - boost::json::object response; + auto request = context.params; + boost::json::object response = {}; - auto ledgerSequence = ledgerSequenceFromRequest(request, backend); - if (!ledgerSequence) - { - response["error"] = "Empty database"; - return response; - } + auto v = ledgerInfoFromRequest(context); + if (auto status = std::get_if(&v)) + return *status; - if (!request.contains("account")) - { - response["error"] = "Must contain account"; - return response; - } + auto lgrInfo = std::get(v); - if (!request.at("account").is_string()) - { - response["error"] = "Account must be a string"; - return response; - } + if(!request.contains("account")) + return Status{Error::rpcINVALID_PARAMS, "missingAccount"}; - ripple::AccountID accountID; - auto parsed = ripple::parseBase58( - request.at("account").as_string().c_str()); + if(!request.at("account").is_string()) + return Status{Error::rpcINVALID_PARAMS, "accountNotString"}; + + auto accountID = + accountFromStringStrict(request.at("account").as_string().c_str()); - if (!parsed) - { - response["error"] = "Invalid account"; - return response; - } + if (!accountID) + return Status{Error::rpcINVALID_PARAMS, "malformedAccount"}; - 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; - } + if(!request.at("limit").is_int64()) + return Status{Error::rpcINVALID_PARAMS, "limitNotInt"}; limit = request.at("limit").as_int64(); if (limit <= 0) - { - response["error"] = "limit must be positive"; - return response; - } + return Status{Error::rpcINVALID_PARAMS, "limitNotPositive"}; } - ripple::uint256 cursor = beast::zero; + ripple::uint256 cursor; if (request.contains("cursor")) { - if (!request.at("cursor").is_string()) - { - response["error"] = "limit must be string"; - return response; - } + if(!request.at("cursor").is_string()) + return Status{Error::rpcINVALID_PARAMS, "cursorNotString"}; - 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()); + if (!cursor.parseHex(request.at("cursor").as_string().c_str())) + return Status{Error::rpcINVALID_PARAMS, "malformedCursor"}; } - + + response["account"] = ripple::to_string(*accountID); + response["ledger_hash"] = ripple::strHex(lgrInfo.hash); + response["ledger_index"] = lgrInfo.seq; response["offers"] = boost::json::value(boost::json::array_kind); boost::json::array& jsonLines = response.at("offers").as_array(); @@ -141,18 +123,25 @@ doAccountOffers( { return false; } - + addOffer(jsonLines, sle); } return true; }; - - auto nextCursor = traverseOwnedNodes( - backend, accountID, *ledgerSequence, cursor, addToResponse); + + auto nextCursor = + traverseOwnedNodes( + *context.backend, + *accountID, + lgrInfo.seq, + cursor, + addToResponse); if (nextCursor) - response["next_cursor"] = ripple::strHex(*nextCursor); + response["marker"] = ripple::strHex(*nextCursor); return response; } + +} // namespace RPC \ No newline at end of file diff --git a/src/handlers/methods/impl/AccountTx.cpp b/src/handlers/methods/impl/AccountTx.cpp new file mode 100644 index 00000000..2b61dc2e --- /dev/null +++ b/src/handlers/methods/impl/AccountTx.cpp @@ -0,0 +1,182 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2012-2014 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include +#include +#include + +namespace RPC +{ + +Result +doAccountTx(Context const& context) +{ + auto request = context.params; + boost::json::object response = {}; + + if(!request.contains("account")) + return Status{Error::rpcINVALID_PARAMS, "missingAccount"}; + + if(!request.at("account").is_string()) + return Status{Error::rpcINVALID_PARAMS, "accountNotString"}; + + auto accountID = + accountFromStringStrict(request.at("account").as_string().c_str()); + + if (!accountID) + return Status{Error::rpcINVALID_PARAMS, "malformedAccount"}; + + bool binary = false; + if(request.contains("binary")) + { + if(!request.at("binary").is_bool()) + return Status{Error::rpcINVALID_PARAMS, "binaryFlagNotBool"}; + + binary = request.at("binary").as_bool(); + } + + auto minIndex = context.range.minSequence; + if (request.contains("ledger_index_min")) + { + if (!request.at("ledger_index_min").is_int64()) + return Status{Error::rpcINVALID_PARAMS, "ledgerSeqMinNotNumber"}; + + minIndex = value_to(request.at("ledger_index_min")); + } + + std::optional cursor; + cursor = {context.range.maxSequence, 0}; + + if (request.contains("cursor")) + { + auto const& obj = request.at("cursor").as_object(); + + std::optional transactionIndex = {}; + if (obj.contains("seq")) + { + if (!obj.at("seq").is_int64()) + return Status{Error::rpcINVALID_PARAMS, "transactionIndexNotInt"}; + + transactionIndex = value_to(obj.at("seq")); + } + + std::optional ledgerIndex = {}; + if (obj.contains("ledger")) + { + if (!obj.at("ledger").is_int64()) + return Status{Error::rpcINVALID_PARAMS, "transactionIndexNotInt"}; + + transactionIndex = value_to(obj.at("ledger")); + } + + if (!transactionIndex || !ledgerIndex) + return Status{Error::rpcINVALID_PARAMS, "missingLedgerOrSeq"}; + + cursor = {*ledgerIndex, *transactionIndex}; + } + else if (request.contains("ledger_index_max")) + { + if (!request.at("ledger_index_max").is_int64()) + return Status{Error::rpcINVALID_PARAMS, "ledgerSeqMaxNotNumber"}; + + auto maxIndex = value_to(request.at("ledger_index_max")); + + if (minIndex > maxIndex) + return Status{Error::rpcINVALID_PARAMS, "invalidIndex"}; + + cursor = {maxIndex, 0}; + } + + std::uint32_t limit = 200; + if (request.contains("limit")) + { + if(!request.at("limit").is_int64()) + return Status{Error::rpcINVALID_PARAMS, "limitNotInt"}; + + limit = request.at("limit").as_int64(); + if (limit <= 0) + return Status{Error::rpcINVALID_PARAMS, "limitNotPositive"}; + + response["limit"] = limit; + } + + + boost::json::array txns; + auto start = std::chrono::system_clock::now(); + auto [blobs, retCursor] = + context.backend->fetchAccountTransactions(*accountID, limit, cursor); + + auto end = std::chrono::system_clock::now(); + BOOST_LOG_TRIVIAL(info) << __func__ << " db fetch took " << ((end - start).count() / 1000000000.0) << " num blobs = " << blobs.size(); + + response["account"] = ripple::to_string(*accountID); + response["ledger_index_min"] = minIndex; + response["ledger_index_max"] = cursor->ledgerSequence; + + if (retCursor) + { + boost::json::object cursorJson; + cursorJson["ledger"] = retCursor->ledgerSequence; + cursorJson["seq"] = retCursor->transactionIndex; + response["marker"] = cursorJson; + } + + for (auto const& txnPlusMeta : blobs) + { + if (txnPlusMeta.ledgerSequence < minIndex) + { + BOOST_LOG_TRIVIAL(debug) + << __func__ + << " skipping over transactions from incomplete ledger"; + continue; + } + + boost::json::object obj; + + if (!binary) + { + auto [txn, meta] = deserializeTxPlusMeta(txnPlusMeta); + obj["meta"] = toJson(*meta); + obj["tx"] = toJson(*txn); + obj["tx"].as_object()["ledger_index"] = txnPlusMeta.ledgerSequence; + obj["tx"].as_object()["inLedger"] = txnPlusMeta.ledgerSequence; + + } + else + { + obj["meta"] = ripple::strHex(txnPlusMeta.metadata); + obj["tx_blob"] = ripple::strHex(txnPlusMeta.transaction); + obj["ledger_index"] = txnPlusMeta.ledgerSequence; + } + + obj["validated"] = true; + + txns.push_back(obj); + } + + response["transactions"] = txns; + + auto end2 = std::chrono::system_clock::now(); + BOOST_LOG_TRIVIAL(info) << __func__ << " serialization took " << ((end2 - end).count() / 1000000000.0); + + return response; +} + +} // namespace RPC \ No newline at end of file diff --git a/src/handlers/methods/impl/BookOffers.cpp b/src/handlers/methods/impl/BookOffers.cpp new file mode 100644 index 00000000..73d045a2 --- /dev/null +++ b/src/handlers/methods/impl/BookOffers.cpp @@ -0,0 +1,325 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace RPC +{ + +Result +doBookOffers(Context const& context) +{ + auto request = context.params; + boost::json::object response = {}; + + auto v = ledgerInfoFromRequest(context); + if (auto status = std::get_if(&v)) + return *status; + + auto lgrInfo = std::get(v); + + ripple::Book book; + ripple::uint256 bookBase; + if (request.contains("book")) + { + if (!request.at("book").is_string()) + return Status{Error::rpcINVALID_PARAMS, "bookNotString"}; + + if (!bookBase.parseHex(request.at("book").as_string().c_str())) + return Status{Error::rpcINVALID_PARAMS, "invalidBook"}; + } + else + { + if (!request.contains("taker_pays")) + return Status{Error::rpcINVALID_PARAMS, "missingTakerPays"}; + + if (!request.contains("taker_gets")) + return Status{Error::rpcINVALID_PARAMS, "missingTakerGets"}; + + if (!request.at("taker_pays").is_object()) + return Status{Error::rpcINVALID_PARAMS, "takerPaysNotObject"}; + + if (!request.at("taker_gets").is_object()) + return Status{Error::rpcINVALID_PARAMS, "takerGetsNotObject"}; + + auto taker_pays = request.at("taker_pays").as_object(); + if (!taker_pays.contains("currency")) + return Status{Error::rpcINVALID_PARAMS, "missingTakerPaysCurrency"}; + + if (!taker_pays.at("currency").is_string()) + return Status{Error::rpcINVALID_PARAMS, "takerPaysCurrencyNotString"}; + + auto taker_gets = request.at("taker_gets").as_object(); + if (!taker_gets.contains("currency")) + return Status{Error::rpcINVALID_PARAMS, "missingTakerGetsCurrency"}; + + if (!taker_gets.at("currency").is_string()) + return Status{Error::rpcINVALID_PARAMS, "takerGetsCurrencyNotString"}; + + ripple::Currency pay_currency; + if (!ripple::to_currency( + pay_currency, taker_pays.at("currency").as_string().c_str())) + return Status{Error::rpcINVALID_PARAMS, "badTakerPaysCurrency"}; + + ripple::Currency get_currency; + if (!ripple::to_currency( + get_currency, taker_gets["currency"].as_string().c_str())) + return Status{Error::rpcINVALID_PARAMS, "badTakerGetsCurrency"}; + + ripple::AccountID pay_issuer; + if (taker_pays.contains("issuer")) + { + if (!taker_pays.at("issuer").is_string()) + return Status{Error::rpcINVALID_PARAMS, "takerPaysIssuerNotString"}; + + if (!ripple::to_issuer( + pay_issuer, taker_pays.at("issuer").as_string().c_str())) + return Status{Error::rpcINVALID_PARAMS, "badTakerPaysIssuer"}; + + if (pay_issuer == ripple::noAccount()) + return Status{Error::rpcINVALID_PARAMS, "badTakerPaysIssuerAccountOne"}; + } + else + { + pay_issuer = ripple::xrpAccount(); + } + + if (isXRP(pay_currency) && !isXRP(pay_issuer)) + return Status{Error::rpcINVALID_PARAMS, + "Unneeded field 'taker_pays.issuer' for XRP currency " + "specification."}; + + if (!isXRP(pay_currency) && isXRP(pay_issuer)) + return Status{Error::rpcINVALID_PARAMS, + "Invalid field 'taker_pays.issuer', expected non-XRP " + "issuer."}; + + ripple::AccountID get_issuer; + + if (taker_gets.contains("issuer")) + { + if (!taker_gets["issuer"].is_string()) + return Status{Error::rpcINVALID_PARAMS, + "taker_gets.issuer should be string"}; + + if (!ripple::to_issuer( + get_issuer, taker_gets.at("issuer").as_string().c_str())) + return Status{Error::rpcINVALID_PARAMS, + "Invalid field 'taker_gets.issuer', bad issuer."}; + + if (get_issuer == ripple::noAccount()) + return Status{Error::rpcINVALID_PARAMS, + "Invalid field 'taker_gets.issuer', bad issuer account " + "one."}; + } + else + { + get_issuer = ripple::xrpAccount(); + } + + if (ripple::isXRP(get_currency) && !ripple::isXRP(get_issuer)) + return Status{Error::rpcINVALID_PARAMS, + "Unneeded field 'taker_gets.issuer' for XRP currency " + "specification."}; + + if (!ripple::isXRP(get_currency) && ripple::isXRP(get_issuer)) + return Status{Error::rpcINVALID_PARAMS, + "Invalid field 'taker_gets.issuer', expected non-XRP issuer."}; + + if (pay_currency == get_currency && pay_issuer == get_issuer) + return Status{Error::rpcINVALID_PARAMS, "badMarket"}; + + book = {{pay_currency, pay_issuer}, {get_currency, get_issuer}}; + bookBase = getBookBase(book); + } + + std::uint32_t limit = 200; + if (request.contains("limit")) + { + if(!request.at("limit").is_int64()) + return Status{Error::rpcINVALID_PARAMS, "limitNotInt"}; + + limit = request.at("limit").as_int64(); + if (limit <= 0) + return Status{Error::rpcINVALID_PARAMS, "limitNotPositive"}; + } + + + std::optional takerID = {}; + if (request.contains("taker")) + { + if (!request.at("taker").is_string()) + return Status{Error::rpcINVALID_PARAMS, "takerNotString"}; + + takerID = + accountFromStringStrict(request.at("taker").as_string().c_str()); + + if (!takerID) + return Status{Error::rpcINVALID_PARAMS, "invalidTakerAccount"}; + } + + ripple::uint256 cursor = beast::zero; + if (request.contains("cursor")) + { + if(!request.at("cursor").is_string()) + return Status{Error::rpcINVALID_PARAMS, "cursorNotString"}; + + if (!cursor.parseHex(request.at("cursor").as_string().c_str())) + return Status{Error::rpcINVALID_PARAMS, "malformedCursor"}; + } + + auto start = std::chrono::system_clock::now(); + auto [offers, retCursor, warning] = + context.backend->fetchBookOffers(bookBase, lgrInfo.seq, limit, cursor); + auto end = std::chrono::system_clock::now(); + + BOOST_LOG_TRIVIAL(warning) << "Time loading books: " + << ((end - start).count() / 1000000000.0); + + response["ledger_hash"] = ripple::strHex(lgrInfo.hash); + response["ledger_index"] = lgrInfo.seq; + + response["offers"] = boost::json::value(boost::json::array_kind); + boost::json::array& jsonOffers = response.at("offers").as_array(); + + std::map umBalance; + + bool globalFreeze = + isGlobalFrozen(*context.backend, lgrInfo.seq, book.out.account) || + isGlobalFrozen(*context.backend, lgrInfo.seq, book.out.account); + + auto rate = transferRate(*context.backend, lgrInfo.seq, book.out.account); + + start = std::chrono::system_clock::now(); + for (auto const& obj : offers) + { + if (jsonOffers.size() == limit) + break; + + try + { + ripple::SerialIter it{obj.blob.data(), obj.blob.size()}; + ripple::SLE offer{it, obj.key}; + ripple::uint256 bookDir = offer.getFieldH256(ripple::sfBookDirectory); + + auto const uOfferOwnerID = offer.getAccountID(ripple::sfAccount); + auto const& saTakerGets = offer.getFieldAmount(ripple::sfTakerGets); + auto const& saTakerPays = offer.getFieldAmount(ripple::sfTakerPays); + ripple::STAmount saOwnerFunds; + bool firstOwnerOffer = true; + + if (book.out.account == uOfferOwnerID) + { + // If an offer is selling issuer's own IOUs, it is fully + // funded. + saOwnerFunds = saTakerGets; + } + else if (globalFreeze) + { + // If either asset is globally frozen, consider all offers + // that aren't ours to be totally unfunded + saOwnerFunds.clear(book.out); + } + else + { + auto umBalanceEntry = umBalance.find(uOfferOwnerID); + if (umBalanceEntry != umBalance.end()) + { + // Found in running balance table. + + saOwnerFunds = umBalanceEntry->second; + firstOwnerOffer = false; + } + else { + saOwnerFunds = accountHolds( + *context.backend, + lgrInfo.seq, + uOfferOwnerID, + book.out.currency, + book.out.account); + + if (saOwnerFunds < beast::zero) + saOwnerFunds.clear(); + } + } + + boost::json::object offerJson = toJson(offer); + + ripple::STAmount saTakerGetsFunded; + ripple::STAmount saOwnerFundsLimit = saOwnerFunds; + ripple::Rate offerRate = ripple::parityRate; + ripple::STAmount dirRate = + ripple::amountFromQuality(getQuality(bookDir)); + + if (rate != ripple::parityRate + // Have a tranfer fee. + && takerID != book.out.account + // Not taking offers of own IOUs. + && book.out.account != uOfferOwnerID) + // Offer owner not issuing ownfunds + { + // Need to charge a transfer fee to offer owner. + offerRate = rate; + saOwnerFundsLimit = ripple::divide(saOwnerFunds, offerRate); + } + + if (saOwnerFundsLimit >= saTakerGets) + { + // Sufficient funds no shenanigans. + saTakerGetsFunded = saTakerGets; + } + else + { + saTakerGetsFunded = saOwnerFundsLimit; + offerJson["taker_gets_funded"] = saTakerGetsFunded.getText(); + offerJson["taker_pays_funded"] = toBoostJson(std::min( + saTakerPays, + ripple::multiply( + saTakerGetsFunded, dirRate, saTakerPays.issue())) + .getJson(ripple::JsonOptions::none)); + } + + ripple::STAmount saOwnerPays = (ripple::parityRate == offerRate) + ? saTakerGetsFunded + : std::min( + saOwnerFunds, + ripple::multiply(saTakerGetsFunded, offerRate)); + + umBalance[uOfferOwnerID] = saOwnerFunds - saOwnerPays; + + if (firstOwnerOffer) + offerJson["owner_funds"] = saOwnerFunds.getText(); + + offerJson["quality"] = dirRate.getText(); + + jsonOffers.push_back(offerJson); + } + catch (std::exception const& e) {} + } + + end = std::chrono::system_clock::now(); + + BOOST_LOG_TRIVIAL(warning) << "Time transforming to json: " + << ((end - start).count() / 1000000000.0); + + if (retCursor) + response["marker"] = ripple::strHex(*retCursor); + if (warning) + response["warning"] = + "Periodic database update in progress. Data for this book as of " + "this ledger " + "may be incomplete. Data should be complete within one minute"; + + return response; +} + +} // namespace RPC \ No newline at end of file diff --git a/src/handlers/ChannelAuthorize.cpp b/src/handlers/methods/impl/ChannelAuthorize.cpp similarity index 68% rename from src/handlers/ChannelAuthorize.cpp rename to src/handlers/methods/impl/ChannelAuthorize.cpp index ca57f7eb..c23bcbfe 100644 --- a/src/handlers/ChannelAuthorize.cpp +++ b/src/handlers/methods/impl/ChannelAuthorize.cpp @@ -23,9 +23,13 @@ #include #include #include +#include #include #include +namespace RPC +{ + void serializePayChanAuthorization( ripple::Serializer& msg, @@ -37,57 +41,42 @@ serializePayChanAuthorization( msg.add64(amt.drops()); } -boost::json::object -doChannelAuthorize(boost::json::object const& request) +Result +doChannelAuthorize(Context const& context) { - boost::json::object response; + auto request = context.params; + boost::json::object response = {}; + if(!request.contains("channel_id")) - { - response["error"] = "missing field channel_id"; - return response; - } + return Status{Error::rpcINVALID_PARAMS, "missingChannelID"}; + + if(!request.at("channel_id").is_string()) + return Status{Error::rpcINVALID_PARAMS, "channelIDNotString"}; if(!request.contains("amount")) - { - response["error"] = "missing field amount"; - return response; - } + return Status{Error::rpcINVALID_PARAMS, "missingAmount"}; + + if(!request.at("amount").is_string()) + return Status{Error::rpcINVALID_PARAMS, "amountNotString"}; if (!request.contains("key_type") && !request.contains("secret")) - { - response["error"] = "missing field secret"; - return response; - } + return Status{Error::rpcINVALID_PARAMS, "missingKeyTypeOrSecret"}; - boost::json::value error = nullptr; - auto const [pk, sk] = keypairFromRequst(request, error); - if (!error.is_null()) - { - response["error"] = error; - return response; - } + auto v = keypairFromRequst(request); + if (auto status = std::get_if(&v)) + return *status; + + auto const [pk, sk] = std::get>(v); ripple::uint256 channelId; if (!channelId.parseHex(request.at("channel_id").as_string().c_str())) - { - response["error"] = "channel id malformed"; - return response; - } - - if (!request.at("amount").is_string()) - { - response["error"] = "channel amount malformed"; - return response; - } + return Status{Error::rpcCHANNEL_MALFORMED, "malformedChannelID"}; auto optDrops = ripple::to_uint64(request.at("amount").as_string().c_str()); if (!optDrops) - { - response["error"] = "could not parse channel amount"; - return response; - } + return Status{Error::rpcCHANNEL_AMT_MALFORMED, "couldNotParseAmount"}; std::uint64_t drops = *optDrops; @@ -101,9 +90,10 @@ doChannelAuthorize(boost::json::object const& request) } catch (std::exception&) { - response["error"] = "Exception occurred during signing."; - return response; + return Status{Error::rpcINTERNAL}; } return response; -} \ No newline at end of file +} + +} // namesace RPC \ No newline at end of file diff --git a/src/handlers/ChannelVerify.cpp b/src/handlers/methods/impl/ChannelVerify.cpp similarity index 66% rename from src/handlers/ChannelVerify.cpp rename to src/handlers/methods/impl/ChannelVerify.cpp index c3cad63e..a9eb2319 100644 --- a/src/handlers/ChannelVerify.cpp +++ b/src/handlers/methods/impl/ChannelVerify.cpp @@ -23,36 +23,42 @@ #include #include #include +#include #include #include -boost::json::object -doChannelVerify(boost::json::object const& request) +namespace RPC { - boost::json::object response; + +Result +doChannelVerify(Context const& context) +{ + auto request = context.params; + boost::json::object response = {}; + if(!request.contains("channel_id")) - { - response["error"] = "missing field channel_id"; - return response; - } + return Status{Error::rpcINVALID_PARAMS, "missingChannelID"}; + + if(!request.at("channel_id").is_string()) + return Status{Error::rpcINVALID_PARAMS, "channelIDNotString"}; if(!request.contains("amount")) - { - response["error"] = "missing field amount"; - return response; - } + return Status{Error::rpcINVALID_PARAMS, "missingAmount"}; + + if(!request.at("amount").is_string()) + return Status{Error::rpcINVALID_PARAMS, "amountNotString"}; if (!request.contains("signature")) - { - response["error"] = "missing field signature"; - return response; - } - + return Status{Error::rpcINVALID_PARAMS, "missingSignature"}; + + if(!request.at("signature").is_string()) + return Status{Error::rpcINVALID_PARAMS, "signatureNotString"}; + if (!request.contains("public_key")) - { - response["error"] = "missing field public_key"; - return response; - } + return Status{Error::rpcINVALID_PARAMS, "missingPublicKey"}; + + if(!request.at("public_key").is_string()) + return Status{Error::rpcINVALID_PARAMS, "publicKeyNotString"}; boost::optional pk; { @@ -63,15 +69,11 @@ doChannelVerify(boost::json::object const& request) { auto pkHex = ripple::strUnHex(strPk); if (!pkHex) - { - response["error"] = "malformed public key"; - return response; - } + return Status{Error::rpcPUBLIC_MALFORMED, "malformedPublicKey"}; + auto const pkType = ripple::publicKeyType(ripple::makeSlice(*pkHex)); if (!pkType) - { - response["error"] = "invalid key type"; - } + return Status{Error::rpcPUBLIC_MALFORMED, "invalidKeyType"}; pk.emplace(ripple::makeSlice(*pkHex)); } @@ -79,35 +81,20 @@ doChannelVerify(boost::json::object const& request) ripple::uint256 channelId; if (!channelId.parseHex(request.at("channel_id").as_string().c_str())) - { - response["error"] = "channel id malformed"; - return response; - } + return Status{Error::rpcCHANNEL_MALFORMED, "malformedChannelID"}; auto optDrops = ripple::to_uint64(request.at("amount").as_string().c_str()); if (!optDrops) - { - response["error"] = "could not parse channel amount"; - return response; - } + return Status{Error::rpcCHANNEL_AMT_MALFORMED, "couldNotParseAmount"}; std::uint64_t drops = *optDrops; - if (!request.at("signature").is_string()) - { - response["error"] = "signature must be type string"; - return response; - } - auto sig = ripple::strUnHex(request.at("signature").as_string().c_str()); if (!sig || !sig->size()) - { - response["error"] = "Invalid signature"; - return response; - } + return Status{Error::rpcINVALID_PARAMS, "invalidSignature"}; ripple::Serializer msg; ripple::serializePayChanAuthorization(msg, channelId, ripple::XRPAmount(drops)); @@ -116,4 +103,6 @@ doChannelVerify(boost::json::object const& request) ripple::verify(*pk, msg.slice(), ripple::makeSlice(*sig), true); return response; -} \ No newline at end of file +} + +} // namespace RPC \ No newline at end of file diff --git a/src/handlers/methods/impl/Ledger.cpp b/src/handlers/methods/impl/Ledger.cpp new file mode 100644 index 00000000..f15da4b7 --- /dev/null +++ b/src/handlers/methods/impl/Ledger.cpp @@ -0,0 +1,123 @@ +#include +#include +#include + +namespace RPC +{ + +Result +doLedger(Context const& context) +{ + auto params = context.params; + boost::json::object response = {}; + + bool binary = false; + if(params.contains("binary")) + { + if(!params.at("binary").is_bool()) + return Status{Error::rpcINVALID_PARAMS, "binaryFlagNotBool"}; + + binary = params.at("binary").as_bool(); + } + + bool transactions = false; + if(params.contains("transactions")) + { + if(!params.at("transactions").is_bool()) + return Status{Error::rpcINVALID_PARAMS, "transactionsFlagNotBool"}; + + transactions = params.at("transactions").as_bool(); + } + + bool expand = false; + if(params.contains("expand")) + { + if(!params.at("expand").is_bool()) + return Status{Error::rpcINVALID_PARAMS, "expandFlagNotBool"}; + + expand = params.at("expand").as_bool(); + } + + auto v = ledgerInfoFromRequest(context); + if (auto status = std::get_if(&v)) + return *status; + + auto lgrInfo = std::get(v); + + boost::json::object header; + if (binary) + { + header["ledger_data"] = ripple::strHex(ledgerInfoToBlob(lgrInfo)); + } + else + { + header["accepted"] = true; + header["account_hash"] = ripple::strHex(lgrInfo.accountHash); + header["close_flags"] = lgrInfo.closeFlags; + header["close_time"] = lgrInfo.closeTime.time_since_epoch().count(); + header["close_time_human"] = ripple::to_string(lgrInfo.closeTime);; + header["close_time_resolution"] = lgrInfo.closeTimeResolution.count(); + header["closed"] = true; + header["hash"] = ripple::strHex(lgrInfo.hash); + header["ledger_hash"] = ripple::strHex(lgrInfo.hash); + header["ledger_index"] = std::to_string(lgrInfo.seq); + header["parent_close_time"] = + lgrInfo.parentCloseTime.time_since_epoch().count(); + header["parent_hash"] = ripple::strHex(lgrInfo.parentHash); + header["seqNum"] = std::to_string(lgrInfo.seq); + header["totalCoins"] = ripple::to_string(lgrInfo.drops); + header["total_coins"] = ripple::to_string(lgrInfo.drops); + header["transaction_hash"] = ripple::strHex(lgrInfo.txHash); + } + + if (transactions) + { + header["transactions"] = boost::json::value(boost::json::array_kind); + boost::json::array& jsonTxs = header.at("transactions").as_array(); + if (expand) + { + auto txns = context.backend->fetchAllTransactionsInLedger(lgrInfo.seq); + + std::transform( + std::move_iterator(txns.begin()), + std::move_iterator(txns.end()), + std::back_inserter(jsonTxs), + [binary](auto obj) { + boost::json::object entry; + if (!binary) + { + auto [sttx, meta] = deserializeTxPlusMeta(obj); + entry = toJson(*sttx); + entry["metaData"] = toJson(*meta); + } + else + { + entry["tx_blob"] = ripple::strHex(obj.transaction); + entry["meta"] = ripple::strHex(obj.metadata); + } + entry["ledger_sequence"] = obj.ledgerSequence; + return entry; + }); + } + else + { + auto hashes = + context.backend->fetchAllTransactionHashesInLedger(lgrInfo.seq); + std::transform( + std::move_iterator(hashes.begin()), + std::move_iterator(hashes.end()), + std::back_inserter(jsonTxs), + [](auto hash) { + boost::json::object entry; + return boost::json::string(ripple::strHex(hash)); + }); + } + } + + response["ledger"] = header; + response["ledger_hash"] = ripple::strHex(lgrInfo.hash); + response["ledger_index"] = lgrInfo.seq; + return response; +} + +} \ No newline at end of file diff --git a/src/handlers/methods/impl/LedgerData.cpp b/src/handlers/methods/impl/LedgerData.cpp new file mode 100644 index 00000000..5456f6e3 --- /dev/null +++ b/src/handlers/methods/impl/LedgerData.cpp @@ -0,0 +1,170 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2012-2014 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include +#include +#include +#include +#include +// Get state nodes from a ledger +// Inputs: +// limit: integer, maximum number of entries +// marker: opaque, resume point +// binary: boolean, format +// type: string // optional, defaults to all ledger node types +// Outputs: +// ledger_hash: chosen ledger's hash +// ledger_index: chosen ledger's index +// state: array of state nodes +// marker: resume point, if any +// +// + +namespace RPC +{ + +Result +doLedgerData(Context const& context) +{ + auto request = context.params; + boost::json::object response = {}; + + bool binary = false; + if(request.contains("binary")) + { + if(!request.at("binary").is_bool()) + return Status{Error::rpcINVALID_PARAMS, "binaryFlagNotBool"}; + + binary = request.at("binary").as_bool(); + } + + std::size_t limit = binary ? 2048 : 256; + if(request.contains("limit")) + { + if(!request.at("limit").is_int64()) + return Status{Error::rpcINVALID_PARAMS, "limitNotInteger"}; + + limit = value_to(request.at("limit")); + } + + std::optional cursor; + if(request.contains("cursor")) + { + if(!request.at("cursor").is_string()) + return Status{Error::rpcINVALID_PARAMS, "cursorNotString"}; + + BOOST_LOG_TRIVIAL(debug) << __func__ << " : parsing cursor"; + + cursor = ripple::uint256{}; + if(!cursor->parseHex(request.at("cursor").as_string().c_str())) + return Status{Error::rpcINVALID_PARAMS, "cursorMalformed"}; + } + + auto v = ledgerInfoFromRequest(context); + if (auto status = std::get_if(&v)) + return *status; + + auto lgrInfo = std::get(v); + + Backend::LedgerPage page; + auto start = std::chrono::system_clock::now(); + page = context.backend->fetchLedgerPage(cursor, lgrInfo.seq, limit); + + auto end = std::chrono::system_clock::now(); + + auto time = + std::chrono::duration_cast(end - start) + .count(); + + boost::json::object header; + if(!cursor) + { + if (binary) + { + header["ledger_data"] = ripple::strHex(ledgerInfoToBlob(lgrInfo)); + } + else + { + header["accepted"] = true; + header["account_hash"] = ripple::strHex(lgrInfo.accountHash); + header["close_flags"] = lgrInfo.closeFlags; + header["close_time"] = lgrInfo.closeTime.time_since_epoch().count(); + header["close_time_human"] = ripple::to_string(lgrInfo.closeTime);; + header["close_time_resolution"] = lgrInfo.closeTimeResolution.count(); + header["closed"] = true; + header["hash"] = ripple::strHex(lgrInfo.hash); + header["ledger_hash"] = ripple::strHex(lgrInfo.hash); + header["ledger_index"] = std::to_string(lgrInfo.seq); + header["parent_close_time"] = + lgrInfo.parentCloseTime.time_since_epoch().count(); + header["parent_hash"] = ripple::strHex(lgrInfo.parentHash); + header["seqNum"] = std::to_string(lgrInfo.seq); + header["totalCoins"] = ripple::to_string(lgrInfo.drops); + header["total_coins"] = ripple::to_string(lgrInfo.drops); + header["transaction_hash"] = ripple::strHex(lgrInfo.txHash); + + response["ledger"] = header; + } + } + + response["ledger_hash"] = ripple::strHex(lgrInfo.hash); + response["ledger_index"] = lgrInfo.seq; + + boost::json::array objects; + std::vector& results = page.objects; + std::optional const& returnedCursor = page.cursor; + + if(returnedCursor) + response["marker"] = ripple::strHex(*returnedCursor); + + BOOST_LOG_TRIVIAL(debug) + << __func__ << " number of results = " << results.size(); + for (auto const& [key, object] : results) + { + ripple::STLedgerEntry sle{ + ripple::SerialIter{object.data(), object.size()}, key}; + if (binary) + { + boost::json::object entry; + entry["data"] = ripple::serializeHex(sle); + entry["index"] = ripple::to_string(sle.key()); + objects.push_back(entry); + } + else + objects.push_back(toJson(sle)); + } + response["state"] = objects; + + + if (cursor && page.warning) + { + response["warning"] = + "Periodic database update in progress. Data for this ledger may be " + "incomplete. Data should be complete " + "within a few minutes. Other RPC calls are not affected, " + "regardless of ledger. This " + "warning is only present on the first " + "page of the ledger"; + } + + return response; +} + +} // namespace RPC \ No newline at end of file diff --git a/src/handlers/methods/impl/LedgerEntry.cpp b/src/handlers/methods/impl/LedgerEntry.cpp new file mode 100644 index 00000000..87a001d5 --- /dev/null +++ b/src/handlers/methods/impl/LedgerEntry.cpp @@ -0,0 +1,360 @@ +#include +#include +#include +#include +#include +#include +// { +// ledger_hash : +// ledger_index : +// ... +// } + +namespace RPC +{ + +Result +doLedgerEntry(Context const& context) +{ + auto request = context.params; + boost::json::object response = {}; + + bool binary = + request.contains("binary") ? request.at("binary").as_bool() : false; + + auto v = ledgerInfoFromRequest(context); + if (auto status = std::get_if(&v)) + return *status; + + auto lgrInfo = std::get(v); + + ripple::uint256 key; + if (request.contains("index")) + { + if(!request.at("index").is_string()) + return Status{Error::rpcINVALID_PARAMS, "indexNotString"}; + + if (!key.parseHex(request.at("index").as_string().c_str())) + return Status{Error::rpcINVALID_PARAMS, "malformedIndex"}; + } + else if (request.contains("account_root")) + { + if(!request.at("account_root").is_string()) + return Status{Error::rpcINVALID_PARAMS, "account_rootNotString"}; + + auto const account = ripple::parseBase58( + request.at("account_root").as_string().c_str()); + if (!account || account->isZero()) + return Status{Error::rpcINVALID_PARAMS, "malformedAddress"}; + else + key = ripple::keylet::account(*account).key; + } + else if (request.contains("check")) + { + if(!request.at("check").is_string()) + return Status{Error::rpcINVALID_PARAMS, "checkNotString"}; + + if (!key.parseHex(request.at("check").as_string().c_str())) + { + return Status{Error::rpcINVALID_PARAMS, "checkMalformed"}; + } + } + else if (request.contains("deposit_preauth")) + { + if (!request.at("deposit_preauth").is_object()) + { + if (!request.at("deposit_preauth").is_string() || + !key.parseHex( + request.at("deposit_preauth").as_string().c_str())) + { + return Status{Error::rpcINVALID_PARAMS, "deposit_preauthMalformed"}; + } + } + else if ( + !request.at("deposit_preauth").as_object().contains("owner") || + !request.at("deposit_preauth").as_object().at("owner").is_string()) + { + return Status{Error::rpcINVALID_PARAMS, "ownerNotString"}; + } + else if( + !request.at("deposit_preauth").as_object().contains("authorized") || + !request.at("deposit_preauth").as_object().at("authorized").is_string()) + { + return Status{Error::rpcINVALID_PARAMS, "authorizedNotString"}; + } + else + { + boost::json::object const& deposit_preauth = + request.at("deposit_preauth").as_object(); + + auto const owner = ripple::parseBase58( + deposit_preauth.at("owner").as_string().c_str()); + + auto const authorized = ripple::parseBase58( + deposit_preauth.at("authorized").as_string().c_str()); + + if (!owner) + return Status{Error::rpcINVALID_PARAMS, "malformedOwner"}; + else if (!authorized) + return Status{Error::rpcINVALID_PARAMS, "malformedAuthorized"}; + else + key = ripple::keylet::depositPreauth(*owner, *authorized).key; + } + } + else if (request.contains("directory")) + { + if (!request.at("directory").is_object()) + { + if(!request.at("directory").is_string()) + return Status{Error::rpcINVALID_PARAMS, "directoryNotString"}; + + if (!key.parseHex(request.at("directory").as_string().c_str())) + { + return Status{Error::rpcINVALID_PARAMS, "malformedDirectory"}; + } + } + else if ( + request.at("directory").as_object().contains("sub_index") && + !request.at("directory").as_object().at("sub_index").is_int64()) + { + return Status{Error::rpcINVALID_PARAMS, "sub_indexNotInt"}; + } + else + { + auto directory = request.at("directory").as_object(); + std::uint64_t subIndex = directory.contains("sub_index") + ? value_to(directory.at("sub_index")) + : 0; + + if (directory.contains("dir_root")) + { + ripple::uint256 uDirRoot; + + if (directory.contains("owner")) + { + // May not specify both dir_root and owner. + return Status{Error::rpcINVALID_PARAMS, "mayNotSpecifyBothDirRootAndOwner"}; + } + else if (!uDirRoot.parseHex( + directory.at("dir_root").as_string().c_str())) + { + return Status{Error::rpcINVALID_PARAMS, "malformedDirRoot"}; + } + else + { + key = ripple::keylet::page(uDirRoot, subIndex).key; + } + } + else if (directory.contains("owner")) + { + auto const ownerID = ripple::parseBase58( + directory.at("owner").as_string().c_str()); + + if (!ownerID) + { + return Status{Error::rpcINVALID_PARAMS, "malformedAddress"}; + } + else + { + key = + ripple::keylet::page( + ripple::keylet::ownerDir(*ownerID), subIndex).key; + } + } + else + { + return Status{Error::rpcINVALID_PARAMS, "missingOwnerOrDirRoot"}; + } + } + } + else if (request.contains("escrow")) + { + if (!request.at("escrow").is_object()) + { + if (!key.parseHex(request.at("escrow").as_string().c_str())) + return Status{Error::rpcINVALID_PARAMS, "malformedEscrow"}; + } + else if ( + !request.at("escrow").as_object().contains("owner") || + !request.at("escrow").as_object().at("owner").is_string()) + { + return Status{Error::rpcINVALID_PARAMS, "malformedOwner"}; + } + else if( + !request.at("escrow").as_object().contains("seq") || + !request.at("escrow").as_object().at("seq").is_int64()) + { + return Status{Error::rpcINVALID_PARAMS, "malformedSeq"}; + } + else + { + auto const id = ripple::parseBase58( + request.at("escrow").as_object().at("owner").as_string().c_str()); + + if (!id) + return Status{Error::rpcINVALID_PARAMS, "malformedOwner"}; + else + { + std::uint32_t seq = + request.at("escrow").as_object().at("seq").as_int64(); + key = ripple::keylet::escrow(*id, seq).key; + } + } + } + else if (request.contains("offer")) + { + if (!request.at("offer").is_object()) + { + if (!key.parseHex(request.at("offer").as_string().c_str())) + return Status{Error::rpcINVALID_PARAMS, "malformedOffer"}; + } + else if ( + !request.at("offer").as_object().contains("account") || + !request.at("offer").as_object().at("account").is_string()) + { + return Status{Error::rpcINVALID_PARAMS, "malformedAccount"}; + } + else if ( + !request.at("offer").as_object().contains("seq") || + !request.at("offer").as_object().at("seq").is_int64()) + { + return Status{Error::rpcINVALID_PARAMS, "malformedSeq"}; + } + else + { + auto offer = request.at("offer").as_object(); + auto const id = ripple::parseBase58( + offer.at("account").as_string().c_str()); + + if (!id) + return Status{Error::rpcINVALID_PARAMS, "malformedAccount"}; + else + { + std::uint32_t seq = value_to(offer.at("seq")); + key = ripple::keylet::offer(*id, seq).key; + } + } + } + else if (request.contains("payment_channel")) + { + if (!request.at("payment_channel").is_string()) + return Status{Error::rpcINVALID_PARAMS, "paymentChannelNotString"}; + + if (!key.parseHex(request.at("payment_channel").as_string().c_str())) + return Status{Error::rpcINVALID_PARAMS, "malformedPaymentChannel"}; + } + else if (request.contains("ripple_state")) + { + if (!request.at("ripple_state").is_object()) + return Status{Error::rpcINVALID_PARAMS, "rippleStateNotObject"}; + + ripple::Currency currency; + boost::json::object const& state = request.at("ripple_state").as_object(); + + if ( + !state.contains("currency") || + !state.at("currency").is_string()) + { + return Status{Error::rpcINVALID_PARAMS, "malformedCurrency"}; + } + + if ( + !state.contains("accounts") || + !state.at("accounts").is_array() || + 2 != state.at("accounts").as_array().size() || + !state.at("accounts").as_array().at(0).is_string() || + !state.at("accounts").as_array().at(1).is_string() || + (state.at("accounts").as_array().at(0).as_string() == + state.at("accounts").as_array().at(1).as_string())) + { + return Status{Error::rpcINVALID_PARAMS, "malformedAccounts"}; + } + + auto const id1 = ripple::parseBase58( + state.at("accounts").as_array().at(0).as_string().c_str()); + auto const id2 = ripple::parseBase58( + state.at("accounts").as_array().at(1).as_string().c_str()); + + if (!id1 || !id2) + return Status{Error::rpcINVALID_PARAMS, "malformedAccounts"}; + + else if (!ripple::to_currency( + currency, state.at("currency").as_string().c_str())) + return Status{Error::rpcINVALID_PARAMS, "malformedCurrency"}; + + key = ripple::keylet::line(*id1, *id2, currency).key; + + } + else if (request.contains("ticket")) + { + if (!request.at("ticket").is_object()) + { + if (!request.at("ticket").is_string()) + return Status{Error::rpcINVALID_PARAMS, "ticketNotString"}; + + if (!key.parseHex(request.at("ticket").as_string().c_str())) + return Status{Error::rpcINVALID_PARAMS, "malformedTicket"}; + + } + else if ( + !request.at("ticket").as_object().contains("account") || + !request.at("ticket").as_object().at("account").is_string()) + { + return Status{Error::rpcINVALID_PARAMS, "malformedTicketAccount"}; + } + else if ( + !request.at("ticket").as_object().contains("ticket_seq") || + !request.at("ticket").as_object().at("ticket_seq").is_int64()) + { + return Status{Error::rpcINVALID_PARAMS, "malformedTicketSeq"}; + } + else + { + auto const id = ripple::parseBase58( + request.at("ticket").as_object().at("account").as_string().c_str()); + + if (!id) + return Status{Error::rpcINVALID_PARAMS, "malformedTicketAccount"}; + else + { + std::uint32_t seq = + request.at("offer").as_object().at("ticket_seq").as_int64(); + + key = ripple::getTicketIndex(*id, seq); + } + } + } + else + { + return Status{Error::rpcINVALID_PARAMS, "unknownOption"}; + } + + auto start = std::chrono::system_clock::now(); + auto dbResponse = context.backend->fetchLedgerObject(key, lgrInfo.seq); + auto end = std::chrono::system_clock::now(); + auto time = + std::chrono::duration_cast(end - start) + .count(); + + if (!dbResponse or dbResponse->size() == 0) + return Status{Error::rpcLGR_NOT_FOUND}; + + response["index"] = ripple::strHex(key); + response["ledger_hash"] = ripple::strHex(lgrInfo.hash); + response["ledger_index"] = lgrInfo.seq; + + if (binary) + { + response["node_binary"] = ripple::strHex(*dbResponse); + } + else + { + ripple::STLedgerEntry sle{ + ripple::SerialIter{dbResponse->data(), dbResponse->size()}, key}; + response["node"] = toJson(sle); + } + + return response; +} + +} \ No newline at end of file diff --git a/src/handlers/LedgerRange.cpp b/src/handlers/methods/impl/LedgerRange.cpp similarity index 50% rename from src/handlers/LedgerRange.cpp rename to src/handlers/methods/impl/LedgerRange.cpp index 395b0174..ac2fbf48 100644 --- a/src/handlers/LedgerRange.cpp +++ b/src/handlers/methods/impl/LedgerRange.cpp @@ -1,22 +1,27 @@ +#include #include #include -boost::json::object -doLedgerRange( - boost::json::object const& request, - BackendInterface const& backend) +namespace RPC { - boost::json::object response; + +Result +doLedgerRange(Context const& context) +{ + boost::json::object response = {}; - auto range = backend.fetchLedgerRange(); + auto range = context.backend->fetchLedgerRange(); if (!range) { - response["error"] = "No data"; + return Status{Error::rpcNOT_READY, "rangeNotFound"}; } else { response["ledger_index_min"] = range->minSequence; response["ledger_index_max"] = range->maxSequence; } + return response; } + +} \ No newline at end of file diff --git a/src/handlers/Subscribe.cpp b/src/handlers/methods/impl/Subscribe.cpp similarity index 55% rename from src/handlers/Subscribe.cpp rename to src/handlers/methods/impl/Subscribe.cpp index 33141c54..824d72d7 100644 --- a/src/handlers/Subscribe.cpp +++ b/src/handlers/methods/impl/Subscribe.cpp @@ -1,45 +1,40 @@ #include +#include +#include +#include #include -#include -#include -static std::unordered_set validStreams{ +namespace RPC +{ + +static std::unordered_set validStreams { "ledger", "transactions", - "transactions_proposed"}; + "transactions_proposed" }; -boost::json::value +Status validateStreams(boost::json::object const& request) { - if (!request.at("streams").is_array()) - { - return "missing or invalid streams"; - } - boost::json::array const& streams = request.at("streams").as_array(); for (auto const& stream : streams) { if (!stream.is_string()) - { - return "streams must be strings"; - } + return Status{Error::rpcINVALID_PARAMS, "streamNotString"}; std::string s = stream.as_string().c_str(); if (validStreams.find(s) == validStreams.end()) - { - return boost::json::string("invalid stream " + s); - } + return Status{Error::rpcINVALID_PARAMS, "invalidStream" + s}; } - return nullptr; + return OK; } void subscribeToStreams( boost::json::object const& request, - std::shared_ptr& session, + std::shared_ptr session, SubscriptionManager& manager) { boost::json::array const& streams = request.at("streams").as_array(); @@ -62,7 +57,7 @@ subscribeToStreams( void unsubscribeToStreams( boost::json::object const& request, - std::shared_ptr& session, + std::shared_ptr session, SubscriptionManager& manager) { boost::json::array const& streams = request.at("streams").as_array(); @@ -82,34 +77,28 @@ unsubscribeToStreams( } } -boost::json::value -validateAccounts( - boost::json::object const& request, - boost::json::array const& accounts) +Status +validateAccounts(boost::json::array const& accounts) { for (auto const& account : accounts) { if (!account.is_string()) - { - return "account must be strings"; - } + return Status{Error::rpcINVALID_PARAMS, "accountNotString"}; std::string s = account.as_string().c_str(); auto id = accountFromStringStrict(s); if (!id) - { - return boost::json::string("invalid account " + s); - } + return Status{Error::rpcINVALID_PARAMS, "invalidAccount" + s}; } - return nullptr; + return OK; } void subscribeToAccounts( boost::json::object const& request, - std::shared_ptr& session, + std::shared_ptr session, SubscriptionManager& manager) { boost::json::array const& accounts = request.at("accounts").as_array(); @@ -120,7 +109,7 @@ subscribeToAccounts( auto accountID = accountFromStringStrict(s); - if (!accountID) + if(!accountID) { assert(false); continue; @@ -133,7 +122,7 @@ subscribeToAccounts( void unsubscribeToAccounts( boost::json::object const& request, - std::shared_ptr& session, + std::shared_ptr session, SubscriptionManager& manager) { boost::json::array const& accounts = request.at("accounts").as_array(); @@ -144,7 +133,7 @@ unsubscribeToAccounts( auto accountID = accountFromStringStrict(s); - if (!accountID) + if(!accountID) { assert(false); continue; @@ -157,11 +146,10 @@ unsubscribeToAccounts( void subscribeToAccountsProposed( boost::json::object const& request, - std::shared_ptr& session, + std::shared_ptr session, SubscriptionManager& manager) { - boost::json::array const& accounts = - request.at("accounts_proposed").as_array(); + boost::json::array const& accounts = request.at("accounts_proposed").as_array(); for (auto const& account : accounts) { @@ -169,7 +157,7 @@ subscribeToAccountsProposed( auto accountID = ripple::parseBase58(s); - if (!accountID) + if(!accountID) { assert(false); continue; @@ -182,11 +170,10 @@ subscribeToAccountsProposed( void unsubscribeToAccountsProposed( boost::json::object const& request, - std::shared_ptr& session, + std::shared_ptr session, SubscriptionManager& manager) { - boost::json::array const& accounts = - request.at("accounts_proposed").as_array(); + boost::json::array const& accounts = request.at("accounts_proposed").as_array(); for (auto const& account : accounts) { @@ -194,7 +181,7 @@ unsubscribeToAccountsProposed( auto accountID = ripple::parseBase58(s); - if (!accountID) + if(!accountID) { assert(false); continue; @@ -204,128 +191,114 @@ unsubscribeToAccountsProposed( } } -boost::json::object -doSubscribe( - boost::json::object const& request, - std::shared_ptr& session, - SubscriptionManager& manager) + +Result +doSubscribe(Context const& context) { - boost::json::object response; + auto request = context.params; if (request.contains("streams")) { - boost::json::value error = validateStreams(request); + if (!request.at("streams").is_array()) + return Status{Error::rpcINVALID_PARAMS, "streamsNotArray"}; - if (!error.is_null()) - { - response["error"] = error; - return response; - } + auto status = validateStreams(request); + + if (status) + return status; } if (request.contains("accounts")) { + if (!request.at("accounts").is_array()) - { - response["error"] = "accounts must be array"; - return response; - } + return Status{Error::rpcINVALID_PARAMS, "accountsNotArray"}; boost::json::array accounts = request.at("accounts").as_array(); - boost::json::value error = validateAccounts(request, accounts); + auto status = validateAccounts(accounts); - if (!error.is_null()) - { - response["error"] = error; - return response; - } + if (status) + return status; } if (request.contains("accounts_proposed")) { if (!request.at("accounts_proposed").is_array()) - { - response["error"] = "accounts_proposed must be array"; - return response; - } + return Status{Error::rpcINVALID_PARAMS, "accountsProposedNotArray"}; - boost::json::array accounts = - request.at("accounts_proposed").as_array(); - boost::json::value error = validateAccounts(request, accounts); + boost::json::array accounts = request.at("accounts_proposed").as_array(); + auto status = validateAccounts(accounts); - if (!error.is_null()) - { - response["error"] = error; - return response; - } + if(status) + return status; } if (request.contains("streams")) - subscribeToStreams(request, session, manager); + subscribeToStreams(request, context.session, *context.subscriptions); if (request.contains("accounts")) - subscribeToAccounts(request, session, manager); + subscribeToAccounts(request, context.session, *context.subscriptions); if (request.contains("accounts_proposed")) - subscribeToAccountsProposed(request, session, manager); + subscribeToAccountsProposed(request, context.session, *context.subscriptions); - response["status"] = "success"; + boost::json::object response = {{"status", "success"}}; return response; } -boost::json::object -doUnsubscribe( - boost::json::object const& request, - std::shared_ptr& session, - SubscriptionManager& manager) +Result +doUnsubscribe(Context const& context) { - boost::json::object response; + auto request = context.params; - if (request.contains("streams")) + + if (request.contains("streams")) { - boost::json::value error = validateStreams(request); + if (!request.at("streams").is_array()) + return Status{Error::rpcINVALID_PARAMS, "streamsNotArray"}; - if (!error.is_null()) - { - response["error"] = error; - return response; - } + auto status = validateStreams(request); + + if (status) + return status; } if (request.contains("accounts")) { + + if (!request.at("accounts").is_array()) + return Status{Error::rpcINVALID_PARAMS, "accountsNotArray"}; + boost::json::array accounts = request.at("accounts").as_array(); - boost::json::value error = validateAccounts(request, accounts); + auto status = validateAccounts(accounts); - if (!error.is_null()) - { - response["error"] = error; - return response; - } + if (status) + return status; } if (request.contains("accounts_proposed")) { - boost::json::array accounts = - request.at("accounts_proposed").as_array(); - boost::json::value error = validateAccounts(request, accounts); + if (!request.at("accounts_proposed").is_array()) + return Status{Error::rpcINVALID_PARAMS, "accountsProposedNotArray"}; - if (!error.is_null()) - { - response["error"] = error; - return response; - } + boost::json::array accounts = request.at("accounts_proposed").as_array(); + auto status = validateAccounts(accounts); + + if(status) + return status; } if (request.contains("streams")) - unsubscribeToStreams(request, session, manager); + unsubscribeToStreams(request, context.session, *context.subscriptions); if (request.contains("accounts")) - unsubscribeToAccounts(request, session, manager); + unsubscribeToAccounts(request, context.session, *context.subscriptions); if (request.contains("accounts_proposed")) - unsubscribeToAccountsProposed(request, session, manager); + unsubscribeToAccountsProposed(request, context.session, *context.subscriptions); - response["status"] = "success"; + boost::json::object response = {{"status", "success"}}; return response; } + +} // namespace RPC \ No newline at end of file diff --git a/src/handlers/Tx.cpp b/src/handlers/methods/impl/Tx.cpp similarity index 59% rename from src/handlers/Tx.cpp rename to src/handlers/methods/impl/Tx.cpp index 379d916a..9ce66d37 100644 --- a/src/handlers/Tx.cpp +++ b/src/handlers/methods/impl/Tx.cpp @@ -17,60 +17,64 @@ */ //============================================================================== -#include #include +#include +#include +#include + +namespace RPC +{ // { // transaction: // } -boost::json::object -doTx(boost::json::object const& request, BackendInterface const& backend) +Result +doTx(Context const& context) { - boost::json::object response; + auto request = context.params; + boost::json::object response = {}; + if (!request.contains("transaction")) - { - response["error"] = "Please specify a transaction hash"; - return response; - } + return Status{Error::rpcINVALID_PARAMS, "specifyTransaction"}; + + if (!request.at("transaction").is_string()) + return Status{Error::rpcINVALID_PARAMS, "transactionNotString"}; + ripple::uint256 hash; if (!hash.parseHex(request.at("transaction").as_string().c_str())) + return Status{Error::rpcINVALID_PARAMS, "malformedTransaction"}; + + bool binary = false; + if(request.contains("binary")) { - response["error"] = "Error parsing transaction hash"; - return response; + if(!request.at("binary").is_bool()) + return Status{Error::rpcINVALID_PARAMS, "binaryFlagNotBool"}; + + binary = request.at("binary").as_bool(); } - auto range = backend.fetchLedgerRange(); + auto range = context.backend->fetchLedgerRange(); if (!range) - { - response["error"] = "Database is empty"; - return response; - } + return Status{Error::rpcNOT_READY}; - auto dbResponse = backend.fetchTransaction(hash); + auto dbResponse = context.backend->fetchTransaction(hash); if (!dbResponse) - { - response["error"] = "Transaction not found in Cassandra"; - response["ledger_range"] = std::to_string(range->minSequence) + " - " + - std::to_string(range->maxSequence); + return Status{Error::rpcTXN_NOT_FOUND}; - return response; - } - - bool binary = - request.contains("binary") ? request.at("binary").as_bool() : false; if (!binary) { auto [sttx, meta] = deserializeTxPlusMeta(dbResponse.value()); - response["transaction"] = toJson(*sttx); - response["metadata"] = toJson(*meta); + response = toJson(*sttx); + response["meta"] = toJson(*meta); } else { - response["transaction"] = ripple::strHex(dbResponse->transaction); + response["tx"] = ripple::strHex(dbResponse->transaction); response["metadata"] = ripple::strHex(dbResponse->metadata); } - response["ledger_sequence"] = dbResponse->ledgerSequence; + return response; } +} // namespace RPC \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index f2bf90d0..1c313f1b 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,17 +1,21 @@ -// -// Copyright (c) 2016-2019 Vinnie Falco (vinnie dot falco at gmail dot com) -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// -// Official repository: https://github.com/boostorg/beast -// +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2021 Ripple Labs Inc. -//------------------------------------------------------------------------------ -// -// Example: WebSocket server, asynchronous -// -//------------------------------------------------------------------------------ + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== #include #include @@ -28,7 +32,7 @@ #include #include #include -#include +#include #include #include #include @@ -109,10 +113,10 @@ main(int argc, char* argv[]) // Check command line arguments. if (argc != 2) { - std::cerr << "Usage: websocket-server-async " + std::cerr << "Usage: clio_server " " \n" << "Example:\n" - << " websocket-server-async config.json \n"; + << " clio_server config.json \n"; return EXIT_FAILURE; } diff --git a/src/server/Handlers.cpp b/src/server/Handlers.cpp deleted file mode 100644 index 9b6e3ab7..00000000 --- a/src/server/Handlers.cpp +++ /dev/null @@ -1,136 +0,0 @@ -#include - -bool -shouldForwardToRippled(boost::json::object const& request) -{ - if (request.contains("forward") && request.at("forward").is_bool()) - return request.at("forward").as_bool(); - BOOST_LOG_TRIVIAL(info) << "checked forward"; - - std::string strCommand = request.contains("command") - ? request.at("command").as_string().c_str() - : request.at("method").as_string().c_str(); - BOOST_LOG_TRIVIAL(info) << "checked command"; - - if (forwardCommands.find(strCommand) != forwardCommands.end()) - return true; - - if (request.contains("ledger_index")) - { - auto indexValue = request.at("ledger_index"); - if (indexValue.is_string()) - { - BOOST_LOG_TRIVIAL(info) << "checking ledger as string"; - std::string index = indexValue.as_string().c_str(); - return index == "current" || index == "closed"; - } - } - BOOST_LOG_TRIVIAL(info) << "checked ledger"; - - return false; -} - -std::pair -buildResponse( - boost::json::object const& request, - std::shared_ptr backend, - std::shared_ptr manager, - std::shared_ptr balancer, - std::shared_ptr session) -{ - std::string command = request.at("command").as_string().c_str(); - BOOST_LOG_TRIVIAL(info) << "Received rpc command : " << request; - boost::json::object response; - - if (shouldForwardToRippled(request)) - return {balancer->forwardToRippled(request), 10}; - - BOOST_LOG_TRIVIAL(info) << "Not forwarding"; - switch (commandMap[command]) - { - case tx: - return {doTx(request, *backend), 1}; - case account_tx: { - auto res = doAccountTx(request, *backend); - if (res.contains("transactions")) - return {res, res["transactions"].as_array().size()}; - return {res, 1}; - } - case ledger: { - auto res = doLedger(request, *backend); - - BOOST_LOG_TRIVIAL(info) << "did command"; - if (res.contains("transactions")) - return {res, res["transactions"].as_array().size()}; - return {res, 1}; - } - case ledger_entry: - return {doLedgerEntry(request, *backend), 1}; - case ledger_range: - return {doLedgerRange(request, *backend), 1}; - case ledger_data: { - auto res = doLedgerData(request, *backend); - if (res.contains("objects")) - return {res, res["objects"].as_array().size() * 4}; - return {res, 1}; - } - case account_info: - return {doAccountInfo(request, *backend), 1}; - case book_offers: { - auto res = doBookOffers(request, *backend); - if (res.contains("offers")) - return {res, res["offers"].as_array().size() * 4}; - return {res, 1}; - } - case account_channels: { - auto res = doAccountChannels(request, *backend); - if (res.contains("channels")) - return {res, res["channels"].as_array().size()}; - return {res, 1}; - } - case account_lines: { - auto res = doAccountLines(request, *backend); - if (res.contains("lines")) - return {res, res["lines"].as_array().size()}; - return {res, 1}; - } - case account_currencies: { - auto res = doAccountCurrencies(request, *backend); - size_t count = 1; - if (res.contains("send_currencies")) - count = res["send_currencies"].as_array().size(); - if (res.contains("receive_currencies")) - count += res["receive_currencies"].as_array().size(); - return {res, count}; - } - - case account_offers: { - auto res = doAccountOffers(request, *backend); - if (res.contains("offers")) - return {res, res["offers"].as_array().size()}; - return {res, 1}; - } - case account_objects: { - auto res = doAccountObjects(request, *backend); - if (res.contains("objects")) - return {res, res["objects"].as_array().size()}; - return {res, 1}; - } - case channel_authorize: { - return {doChannelAuthorize(request), 1}; - }; - case channel_verify: - return {doChannelVerify(request), 1}; - case subscribe: - return {doSubscribe(request, session, *manager), 1}; - case unsubscribe: - return {doUnsubscribe(request, session, *manager), 1}; - case server_info: { - return {doServerInfo(request, *backend), 1}; - break; - } - default: - response["error"] = "Unknown command: " + command; - return {response, 1}; - } -} diff --git a/src/server/Handlers.h b/src/server/Handlers.h deleted file mode 100644 index 98f53fd7..00000000 --- a/src/server/Handlers.h +++ /dev/null @@ -1,154 +0,0 @@ -#include -#include -#include -#include - -#include -#include -#include - -#include -#include - -#include -#include - -#ifndef RIPPLE_REPORTING_HANDLERS_H -#define RIPPLE_REPORTING_HANDLERS_H - -class ReportingETL; -class SubscriptionManager; - -//------------------------------------------------------------------------------ - -static std::unordered_set forwardCommands{ - "submit", - "submit_multisigned", - "fee", - "path_find", - "ripple_path_find", - "manifest"}; - -enum RPCCommand { - tx, - account_tx, - ledger, - account_info, - ledger_data, - book_offers, - ledger_range, - ledger_entry, - account_channels, - account_lines, - account_currencies, - account_offers, - account_objects, - channel_authorize, - channel_verify, - subscribe, - unsubscribe, - server_info -}; - -static std::unordered_map commandMap{ - {"tx", tx}, - {"account_tx", account_tx}, - {"ledger", ledger}, - {"ledger_range", ledger_range}, - {"ledger_entry", ledger_entry}, - {"account_info", account_info}, - {"ledger_data", ledger_data}, - {"book_offers", book_offers}, - {"account_channels", account_channels}, - {"account_lines", account_lines}, - {"account_currencies", account_currencies}, - {"account_offers", account_offers}, - {"account_objects", account_objects}, - {"channel_authorize", channel_authorize}, - {"channel_verify", channel_verify}, - {"subscribe", subscribe}, - {"unsubscribe", unsubscribe}, - {"server_info", server_info}}; - -boost::json::object -doTx(boost::json::object const& request, BackendInterface const& backend); -boost::json::object -doAccountTx( - boost::json::object const& request, - BackendInterface const& backend); - -boost::json::object -doBookOffers( - boost::json::object const& request, - BackendInterface const& backend); - -boost::json::object -doLedgerData( - boost::json::object const& request, - BackendInterface const& backend); -boost::json::object -doLedgerEntry( - boost::json::object const& request, - BackendInterface const& backend); -boost::json::object -doLedger(boost::json::object const& request, BackendInterface const& backend); -boost::json::object -doLedgerRange( - boost::json::object const& request, - BackendInterface const& backend); - -boost::json::object -doAccountInfo( - 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 -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 -doChannelAuthorize(boost::json::object const& request); -boost::json::object -doChannelVerify(boost::json::object const& request); - -boost::json::object -doServerInfo( - boost::json::object const& request, - BackendInterface const& backend); - -boost::json::object -doSubscribe( - boost::json::object const& request, - std::shared_ptr& session, - SubscriptionManager& manager); -boost::json::object -doUnsubscribe( - boost::json::object const& request, - std::shared_ptr& session, - SubscriptionManager& manager); - -std::pair -buildResponse( - boost::json::object const& request, - std::shared_ptr backend, - std::shared_ptr manager, - std::shared_ptr balancer, - std::shared_ptr session); - -#endif // RIPPLE_REPORTING_HANDLERS_H diff --git a/src/server/DOSGuard.h b/src/webserver/DOSGuard.h similarity index 100% rename from src/server/DOSGuard.h rename to src/webserver/DOSGuard.h diff --git a/src/server/HttpBase.h b/src/webserver/HttpBase.h similarity index 77% rename from src/server/HttpBase.h rename to src/webserver/HttpBase.h index 44f420fd..ae861325 100644 --- a/src/server/HttpBase.h +++ b/src/webserver/HttpBase.h @@ -36,8 +36,8 @@ #include #include -#include -#include +#include +#include #include namespace http = boost::beast::http; @@ -77,29 +77,6 @@ httpFail(boost::beast::error_code ec, char const* what) std::cerr << what << ": " << ec.message() << "\n"; } -bool -validRequest(boost::json::object const& req) -{ - if (!req.contains("method") || !req.at("method").is_string()) - return false; - - if (!req.contains("params")) - return true; - - if (!req.at("params").is_array()) - return false; - - auto array = req.at("params").as_array(); - - if (array.size() != 1) - return false; - - if (!array.at(0).is_object()) - return false; - - return true; -} - // This function produces an HTTP response for the given // request. The type of the response object depends on the // contents of the request, so the interface requires the @@ -112,9 +89,10 @@ handle_request( Send&& send, std::shared_ptr backend, std::shared_ptr balancer, - DOSGuard& dosGuard) + DOSGuard& dosGuard, + std::string const& ip) { - auto const response = [&req]( + auto const httpResponse = [&req]( http::status status, std::string content_type, std::string message) { @@ -129,70 +107,108 @@ handle_request( if (req.method() == http::verb::get && req.body() == "") { - send(response(http::status::ok, "text/html", defaultResponse)); + send(httpResponse(http::status::ok, "text/html", defaultResponse)); return; } if (req.method() != http::verb::post) - { - send(response( + return send(httpResponse( http::status::bad_request, "text/html", "Expected a POST request")); - return; - } + if (!dosGuard.isOk(ip)) + return send(httpResponse( + http::status::ok, + "application/json", + boost::json::serialize( + RPC::make_error(RPC::Error::rpcSLOW_DOWN)))); try { BOOST_LOG_TRIVIAL(info) << "Received request: " << req.body(); boost::json::object request; + std::string responseStr = ""; try { request = boost::json::parse(req.body()).as_object(); } catch (std::runtime_error const& e) { - send(response( - http::status::bad_request, - "text/html", - "Cannot parse json in body")); - - return; + return send(httpResponse( + http::status::ok, + "application/json", + boost::json::serialize( + RPC::make_error(RPC::Error::rpcBAD_SYNTAX)))); } - if (!validRequest(request)) + if (!dosGuard.isOk(ip)) + return send(httpResponse( + http::status::ok, + "application/json", + boost::json::serialize( + RPC::make_error(RPC::Error::rpcSLOW_DOWN)))); + + auto range = backend->fetchLedgerRange(); + if (!range) + return send(httpResponse( + http::status::ok, + "application/json", + boost::json::serialize( + RPC::make_error(RPC::Error::rpcNOT_READY)))); + + std::optional context = RPC::make_HttpContext( + request, + backend, + nullptr, + balancer, + *range + ); + + if (!context) + return send(httpResponse( + http::status::ok, + "application/json", + boost::json::serialize( + RPC::make_error(RPC::Error::rpcBAD_SYNTAX)))); + + boost::json::object response{{"result", boost::json::object{}}}; + boost::json::object& result = response["result"].as_object(); + + auto v = RPC::buildResponse(*context); + + if (auto status = std::get_if(&v)) { - send(response( - http::status::bad_request, "text/html", "Malformed request")); + auto error = RPC::make_error(status->error); - return; + error["request"] = request; + + result = error; + + responseStr = boost::json::serialize(error); + } + else + { + result = std::get(v); + result["status"] = "success"; + result["validated"] = true; + + responseStr = boost::json::serialize(response); } - boost::json::object wsStyleRequest = request.contains("params") - ? request.at("params").as_array().at(0).as_object() - : boost::json::object{}; + if (!dosGuard.add(ip, responseStr.size())) + result["warning"] = "Too many requests"; - wsStyleRequest["command"] = request["method"]; - - auto [builtResponse, cost] = - buildResponse(wsStyleRequest, backend, nullptr, balancer, nullptr); - - send(response( + return send(httpResponse( http::status::ok, "application/json", - boost::json::serialize(builtResponse))); - - return; + responseStr)); } catch (std::exception const& e) { - std::cout << e.what() << std::endl; - send(response( + return send(httpResponse( http::status::internal_server_error, - "text/html", - "Internal server error occurred")); - - return; + "application/json", + boost::json::serialize(RPC::make_error(RPC::Error::rpcINTERNAL)))); } } @@ -300,7 +316,6 @@ public: if (ec) return httpFail(ec, "read"); - auto ip = derived().ip(); if (boost::beast::websocket::is_upgrade(req_)) { // Disable the timeout. @@ -316,9 +331,11 @@ public: dosGuard_); } + auto ip = derived().ip(); + // Send the response handle_request( - std::move(req_), lambda_, backend_, balancer_, dosGuard_); + std::move(req_), lambda_, backend_, balancer_, dosGuard_, ip); } void diff --git a/src/server/HttpSession.h b/src/webserver/HttpSession.h similarity index 98% rename from src/server/HttpSession.h rename to src/webserver/HttpSession.h index 60053b0e..002eee02 100644 --- a/src/server/HttpSession.h +++ b/src/webserver/HttpSession.h @@ -20,7 +20,7 @@ #ifndef RIPPLE_REPORTING_HTTP_SESSION_H #define RIPPLE_REPORTING_HTTP_SESSION_H -#include +#include namespace http = boost::beast::http; namespace net = boost::asio; @@ -35,7 +35,8 @@ class HttpSession : public HttpBase, public: // Take ownership of the socket - explicit HttpSession( + explicit + HttpSession( tcp::socket&& socket, std::shared_ptr backend, std::shared_ptr subscriptions, diff --git a/src/webserver/Listener.h b/src/webserver/Listener.h new file mode 100644 index 00000000..7c93c058 --- /dev/null +++ b/src/webserver/Listener.h @@ -0,0 +1,352 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2020 Ripple Labs Inc. + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef LISTENER_H +#define LISTENER_H + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +class SubscriptionManager; + +template +class Detector + : public std::enable_shared_from_this> +{ + using std::enable_shared_from_this< + Detector>::shared_from_this; + + boost::beast::tcp_stream stream_; + std::optional> ctx_; + std::shared_ptr backend_; + std::shared_ptr subscriptions_; + std::shared_ptr balancer_; + DOSGuard& dosGuard_; + boost::beast::flat_buffer buffer_; + +public: + Detector( + tcp::socket&& socket, + std::optional> ctx, + std::shared_ptr backend, + std::shared_ptr subscriptions, + std::shared_ptr balancer, + DOSGuard& dosGuard) + : stream_(std::move(socket)) + , ctx_(ctx) + , backend_(backend) + , subscriptions_(subscriptions) + , balancer_(balancer) + , dosGuard_(dosGuard) + { + } + + // Launch the detector + void + run() + { + // Set the timeout. + boost::beast::get_lowest_layer(stream_).expires_after( + std::chrono::seconds(30)); + // Detect a TLS handshake + async_detect_ssl( + stream_, + buffer_, + boost::beast::bind_front_handler( + &Detector::on_detect, shared_from_this())); + } + + void + on_detect(boost::beast::error_code ec, bool result) + { + if (ec) + return httpFail(ec, "detect"); + + if (result) + { + if (!ctx_) + return httpFail(ec, "ssl not supported by this server"); + // Launch SSL session + std::make_shared( + stream_.release_socket(), + *ctx_, + backend_, + subscriptions_, + balancer_, + dosGuard_, + std::move(buffer_)) + ->run(); + return; + } + + // Launch plain session + std::make_shared( + stream_.release_socket(), + backend_, + subscriptions_, + balancer_, + dosGuard_, + std::move(buffer_)) + ->run(); + } +}; + +void +make_websocket_session( + boost::beast::tcp_stream stream, + http::request req, + boost::beast::flat_buffer buffer, + std::shared_ptr backend, + std::shared_ptr subscriptions, + std::shared_ptr balancer, + DOSGuard& dosGuard) +{ + std::make_shared( + std::move(stream), + backend, + subscriptions, + balancer, + dosGuard, + std::move(buffer), + std::move(req)) + ->run(); +} + +void +make_websocket_session( + boost::beast::ssl_stream stream, + http::request req, + boost::beast::flat_buffer buffer, + std::shared_ptr backend, + std::shared_ptr subscriptions, + std::shared_ptr balancer, + DOSGuard& dosGuard) +{ + std::make_shared( + std::move(stream), + backend, + subscriptions, + balancer, + dosGuard, + std::move(buffer), + std::move(req)) + ->run(); +} + +template +class Listener + : public std::enable_shared_from_this> +{ + using std::enable_shared_from_this< + Listener>::shared_from_this; + + net::io_context& ioc_; + std::optional ctx_; + tcp::acceptor acceptor_; + std::shared_ptr backend_; + std::shared_ptr subscriptions_; + std::shared_ptr balancer_; + DOSGuard& dosGuard_; + +public: + Listener( + net::io_context& ioc, + std::optional&& ctx, + tcp::endpoint endpoint, + std::shared_ptr backend, + std::shared_ptr subscriptions, + std::shared_ptr balancer, + DOSGuard& dosGuard) + : ioc_(ioc) + , ctx_(std::move(ctx)) + , acceptor_(net::make_strand(ioc)) + , backend_(backend) + , subscriptions_(subscriptions) + , balancer_(balancer) + , dosGuard_(dosGuard) + { + boost::beast::error_code ec; + + // Open the acceptor + acceptor_.open(endpoint.protocol(), ec); + if (ec) + { + httpFail(ec, "open"); + return; + } + + // Allow address reuse + acceptor_.set_option(net::socket_base::reuse_address(true), ec); + if (ec) + { + httpFail(ec, "set_option"); + return; + } + + // Bind to the server address + acceptor_.bind(endpoint, ec); + if (ec) + { + httpFail(ec, "bind"); + return; + } + + // Start listening for connections + acceptor_.listen(net::socket_base::max_listen_connections, ec); + if (ec) + { + httpFail(ec, "listen"); + return; + } + } + + // Start accepting incoming connections + void + run() + { + do_accept(); + } + +private: + void + do_accept() + { + // The new connection gets its own strand + acceptor_.async_accept( + net::make_strand(ioc_), + boost::beast::bind_front_handler( + &Listener::on_accept, shared_from_this())); + } + + void + on_accept(boost::beast::error_code ec, tcp::socket socket) + { + if (ec) + { + httpFail(ec, "listener_accept"); + } + else + { + auto ctxRef = ctx_ + ? std::optional< + std::reference_wrapper>{ctx_.value()} + : std::nullopt; + // Create the detector session and run it + std::make_shared>( + std::move(socket), + ctxRef, + backend_, + subscriptions_, + balancer_, + dosGuard_) + ->run(); + } + + // Accept another connection + do_accept(); + } +}; + +namespace Server { +std::optional +parse_certs(const char* certFilename, const char* keyFilename) +{ + std::ifstream readCert(certFilename, std::ios::in | std::ios::binary); + if (!readCert) + return {}; + + std::stringstream contents; + contents << readCert.rdbuf(); + readCert.close(); + std::string cert = contents.str(); + + std::ifstream readKey(keyFilename, std::ios::in | std::ios::binary); + if (!readKey) + return {}; + + contents.str(""); + contents << readKey.rdbuf(); + readKey.close(); + std::string key = contents.str(); + + ssl::context ctx{ssl::context::tlsv12}; + + ctx.set_options( + boost::asio::ssl::context::default_workarounds | + boost::asio::ssl::context::no_sslv2); + + ctx.use_certificate_chain(boost::asio::buffer(cert.data(), cert.size())); + + ctx.use_private_key( + boost::asio::buffer(key.data(), key.size()), + boost::asio::ssl::context::file_format::pem); + + return ctx; +} + +using HttpServer = Listener; + +static std::shared_ptr +make_HttpServer( + boost::json::object const& config, + boost::asio::io_context& ioc, + std::shared_ptr backend, + std::shared_ptr subscriptions, + std::shared_ptr balancer, + DOSGuard& dosGuard) +{ + if (!config.contains("server")) + return nullptr; + + auto const& serverConfig = config.at("server").as_object(); + std::optional sslCtx; + if (serverConfig.contains("ssl_cert_file") && + serverConfig.contains("ssl_key_file")) + { + sslCtx = parse_certs( + serverConfig.at("ssl_cert_file").as_string().c_str(), + serverConfig.at("ssl_key_file").as_string().c_str()); + } + + auto const address = boost::asio::ip::make_address( + serverConfig.at("ip").as_string().c_str()); + auto const port = + static_cast(serverConfig.at("port").as_int64()); + + auto server = std::make_shared( + ioc, + std::move(sslCtx), + boost::asio::ip::tcp::endpoint{address, port}, + backend, + subscriptions, + balancer, + dosGuard); + + server->run(); + return server; +} +} // namespace Server + +#endif // LISTENER_H \ No newline at end of file diff --git a/src/server/PlainWsSession.h b/src/webserver/PlainWsSession.h similarity index 94% rename from src/server/PlainWsSession.h rename to src/webserver/PlainWsSession.h index 3984ef69..fef5ec71 100644 --- a/src/server/PlainWsSession.h +++ b/src/webserver/PlainWsSession.h @@ -27,9 +27,9 @@ #include #include -#include -#include -#include +#include +#include +#include #include @@ -133,7 +133,6 @@ public: void run() { - std::cout << "RUNNING" << std::endl; // We need to be executing within a strand to perform async operations // on the I/O objects in this session. Although not strictly necessary // for single-threaded contexts, this example code is written to be @@ -149,7 +148,6 @@ private: void do_upgrade() { - std::cout << "doing upgrade" << std::endl; parser_.emplace(); // Apply a reasonable limit to the allowed size @@ -168,16 +166,12 @@ private: { // See if it is a WebSocket Upgrade if (!websocket::is_upgrade(req_)) - { - std::cout << "is not upgrade" << std::endl; return; - } // Disable the timeout. // The websocket::stream uses its own timeout settings. boost::beast::get_lowest_layer(http_).expires_never(); - std::cout << "making session" << std::endl; std::make_shared( http_.release_socket(), backend_, diff --git a/src/server/README.md b/src/webserver/README.md similarity index 100% rename from src/server/README.md rename to src/webserver/README.md diff --git a/src/server/SslHttpSession.h b/src/webserver/SslHttpSession.h similarity index 97% rename from src/server/SslHttpSession.h rename to src/webserver/SslHttpSession.h index 94950374..6793c56c 100644 --- a/src/server/SslHttpSession.h +++ b/src/webserver/SslHttpSession.h @@ -20,7 +20,7 @@ #ifndef RIPPLE_REPORTING_HTTPS_SESSION_H #define RIPPLE_REPORTING_HTTPS_SESSION_H -#include +#include namespace http = boost::beast::http; namespace net = boost::asio; @@ -35,7 +35,8 @@ class SslHttpSession : public HttpBase, public: // Take ownership of the socket - explicit SslHttpSession( + explicit + SslHttpSession( tcp::socket&& socket, ssl::context& ctx, std::shared_ptr backend, @@ -129,4 +130,4 @@ public: } }; -#endif // RIPPLE_REPORTING_HTTPS_SESSION_H +#endif // RIPPLE_REPORTING_HTTPS_SESSION_H diff --git a/src/server/SslWsSession.h b/src/webserver/SslWsSession.h similarity index 98% rename from src/server/SslWsSession.h rename to src/webserver/SslWsSession.h index 098a6f20..e4ac76b5 100644 --- a/src/server/SslWsSession.h +++ b/src/webserver/SslWsSession.h @@ -27,9 +27,8 @@ #include #include -#include -#include +#include namespace http = boost::beast::http; namespace net = boost::asio; @@ -154,7 +153,6 @@ private: void do_upgrade() { - std::cout << "doing upgrade" << std::endl; parser_.emplace(); // Apply a reasonable limit to the allowed size diff --git a/src/server/SubscriptionManager.cpp b/src/webserver/SubscriptionManager.cpp similarity index 75% rename from src/server/SubscriptionManager.cpp rename to src/webserver/SubscriptionManager.cpp index 813569ee..854c4f02 100644 --- a/src/server/SubscriptionManager.cpp +++ b/src/webserver/SubscriptionManager.cpp @@ -1,16 +1,18 @@ #include -#include -#include +#include +#include void SubscriptionManager::subLedger(std::shared_ptr& session) { + std::lock_guard lk(m_); streamSubscribers_[Ledgers].emplace(std::move(session)); } void SubscriptionManager::unsubLedger(std::shared_ptr& session) { + std::lock_guard lk(m_); streamSubscribers_[Ledgers].erase(session); } @@ -36,19 +38,22 @@ SubscriptionManager::pubLedger( pubMsg["validated_ledgers"] = ledgerRange; pubMsg["txn_count"] = txnCount; - for (auto const& session : streamSubscribers_[Ledgers]) + std::lock_guard lk(m_); + for (auto const& session: streamSubscribers_[Ledgers]) session->send(boost::json::serialize(pubMsg)); } void SubscriptionManager::subTransactions(std::shared_ptr& session) { + std::lock_guard lk(m_); streamSubscribers_[Transactions].emplace(std::move(session)); } void SubscriptionManager::unsubTransactions(std::shared_ptr& session) { + std::lock_guard lk(m_); streamSubscribers_[Transactions].erase(session); } @@ -57,6 +62,7 @@ SubscriptionManager::subAccount( ripple::AccountID const& account, std::shared_ptr& session) { + std::lock_guard lk(m_); accountSubscribers_[account].emplace(std::move(session)); } @@ -65,6 +71,7 @@ SubscriptionManager::unsubAccount( ripple::AccountID const& account, std::shared_ptr& session) { + std::lock_guard lk(m_); accountSubscribers_[account].erase(session); } @@ -73,6 +80,8 @@ SubscriptionManager::pubTransaction( Backend::TransactionAndMetadata const& blob, std::uint32_t seq) { + std::lock_guard lk(m_); + auto [tx, meta] = deserializeTxPlusMeta(blob, seq); boost::json::object pubMsg; @@ -94,6 +103,7 @@ void SubscriptionManager::forwardProposedTransaction( boost::json::object const& response) { + std::lock_guard lk(m_); for (auto const& session : streamSubscribers_[TransactionsProposed]) session->send(boost::json::serialize(response)); @@ -110,6 +120,7 @@ SubscriptionManager::subProposedAccount( ripple::AccountID const& account, std::shared_ptr& session) { + std::lock_guard lk(m_); accountProposedSubscribers_[account].emplace(std::move(session)); } @@ -118,17 +129,43 @@ SubscriptionManager::unsubProposedAccount( ripple::AccountID const& account, std::shared_ptr& session) { + std::lock_guard lk(m_); accountProposedSubscribers_[account].erase(session); } void SubscriptionManager::subProposedTransactions(std::shared_ptr& session) { + std::lock_guard lk(m_); streamSubscribers_[TransactionsProposed].emplace(std::move(session)); } void SubscriptionManager::unsubProposedTransactions(std::shared_ptr& session) { + std::lock_guard lk(m_); streamSubscribers_[TransactionsProposed].erase(session); } + +void +SubscriptionManager::clearSession(WsBase* s) +{ + std::lock_guard lk(m_); + + // need the == operator. No-op delete + std::shared_ptr targetSession(s, [](WsBase*){}); + for(auto& stream : streamSubscribers_) + stream.erase(targetSession); + + for(auto& [account, subscribers] : accountSubscribers_) + { + if (subscribers.find(targetSession) != subscribers.end()) + accountSubscribers_[account].erase(targetSession); + } + + for(auto& [account, subscribers] : accountProposedSubscribers_) + { + if (subscribers.find(targetSession) != subscribers.end()) + accountProposedSubscribers_[account].erase(targetSession); + } +} diff --git a/src/server/SubscriptionManager.h b/src/webserver/SubscriptionManager.h similarity index 96% rename from src/server/SubscriptionManager.h rename to src/webserver/SubscriptionManager.h index 7b1954b0..71d27e70 100644 --- a/src/server/SubscriptionManager.h +++ b/src/webserver/SubscriptionManager.h @@ -22,6 +22,7 @@ #include #include +#include class WsBase; @@ -37,6 +38,7 @@ class SubscriptionManager finalEntry }; + std::mutex m_; std::array streamSubscribers_; std::unordered_map accountSubscribers_; std::unordered_map @@ -101,6 +103,9 @@ public: void unsubProposedTransactions(std::shared_ptr& session); + + void + clearSession(WsBase* session); }; #endif // SUBSCRIPTION_MANAGER_H diff --git a/src/server/WsBase.h b/src/webserver/WsBase.h similarity index 57% rename from src/server/WsBase.h rename to src/webserver/WsBase.h index 1227d99e..43668a61 100644 --- a/src/server/WsBase.h +++ b/src/webserver/WsBase.h @@ -1,14 +1,37 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2021 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + #ifndef RIPPLE_REPORTING_WS_BASE_SESSION_H #define RIPPLE_REPORTING_WS_BASE_SESSION_H #include #include -#include -#include + #include #include -#include -#include + +#include +#include +#include +#include +#include +#include namespace http = boost::beast::http; namespace net = boost::asio; @@ -22,6 +45,20 @@ wsFail(boost::beast::error_code ec, char const* what) std::cerr << what << ": " << ec.message() << "\n"; } +inline boost::json::object +getDefaultWsResponse(boost::json::value const& id) +{ + boost::json::object defaultResp = {}; + if(!id.is_null()) + defaultResp["id"] = id; + + defaultResp["result"] = boost::json::object_kind; + defaultResp["status"] = "success"; + defaultResp["type"] = "response"; + + return defaultResp; +} + class WsBase { public: @@ -34,11 +71,16 @@ public: } }; +class SubscriptionManager; +class ETLLoadBalancer; + // Echoes back all received WebSocket messages template class WsSession : public WsBase, public std::enable_shared_from_this> { + using std::enable_shared_from_this>::shared_from_this; + boost::beast::flat_buffer buffer_; std::string response_; @@ -64,6 +106,7 @@ public: virtual ~WsSession() { } + // Access the derived class, this is part of // the Curiously Recurring Template Pattern idiom. Derived& @@ -71,6 +114,7 @@ public: { return static_cast(*this); } + void send(std::string&& msg) { @@ -80,15 +124,14 @@ public: boost::beast::bind_front_handler( &WsSession::on_write, this->shared_from_this())); } + void run(http::request req) { - std::cout << "Running ws" << std::endl; // Set suggested timeout settings for the websocket derived().ws().set_option(websocket::stream_base::timeout::suggested( boost::beast::role_type::server)); - std::cout << "Trying to decorate" << std::endl; // Set a decorator to change the Server of the handshake derived().ws().set_option(websocket::stream_base::decorator( [](websocket::response_type& res) { @@ -98,8 +141,6 @@ public: " websocket-server-async"); })); - std::cout << "trying to async accept" << std::endl; - derived().ws().async_accept( req, boost::beast::bind_front_handler( @@ -116,6 +157,18 @@ public: do_read(); } + void + do_close() + { + std::shared_ptr mgr = subscriptions_.lock(); + + if(!mgr) + return; + + mgr->clearSession(this); + } + + void do_read() { @@ -134,7 +187,6 @@ public: // This indicates that the session was closed if (ec == boost::beast::websocket::error::closed) { - std::cout << "session closed" << std::endl; return; } @@ -159,53 +211,72 @@ public: BOOST_LOG_TRIVIAL(debug) << " received request : " << request; try { - std::shared_ptr subPtr = - subscriptions_.lock(); - if (!subPtr) - return; + auto range = backend_->fetchLedgerRange(); + if (!range) + return send(boost::json::serialize( + RPC::make_error(RPC::Error::rpcNOT_READY))); - auto [res, cost] = buildResponse( + std::optional context = RPC::make_WsContext( request, backend_, - subPtr, + subscriptions_.lock(), balancer_, - this->shared_from_this()); - auto start = std::chrono::system_clock::now(); - response = std::move(res); - if (!dosGuard_.add(ip, cost)) + shared_from_this(), + *range + ); + + if (!context) + return send(boost::json::serialize( + RPC::make_error(RPC::Error::rpcBAD_SYNTAX))); + + auto id = request.contains("id") + ? request.at("id") + : nullptr; + + auto response = getDefaultWsResponse(id); + boost::json::object& result = response["result"].as_object(); + + auto v = RPC::buildResponse(*context); + + if (auto status = std::get_if(&v)) { - response["warning"] = "Too many requests"; + auto error = RPC::make_error(status->error); + + if (!id.is_null()) + error["id"] = id; + error["request"] = request; + + response_ = boost::json::serialize(error); + } + else + { + auto json = std::get(v); + response_ = boost::json::serialize(json); } - auto end = std::chrono::system_clock::now(); - BOOST_LOG_TRIVIAL(info) - << __func__ << " RPC call took " - << ((end - start).count() / 1000000000.0) - << " . request = " << request; + if (!dosGuard_.add(ip, response_.size())) + result["warning"] = "Too many requests"; + } catch (Backend::DatabaseTimeout const& t) { BOOST_LOG_TRIVIAL(error) << __func__ << " Database timeout"; - response["error"] = - "Database read timeout. Please retry the request"; + return send(boost::json::serialize( + RPC::make_error(RPC::Error::rpcNOT_READY))); } } catch (std::exception const& e) { BOOST_LOG_TRIVIAL(error) - << __func__ << "caught exception : " << e.what(); - response["error"] = "Unknown exception"; + << __func__ << " caught exception : " << e.what(); + + return send(boost::json::serialize( + RPC::make_error(RPC::Error::rpcINTERNAL))); } } - BOOST_LOG_TRIVIAL(trace) << __func__ << response; - response_ = boost::json::serialize(response); + BOOST_LOG_TRIVIAL(trace) << __func__ << response_; - // Echo the message - derived().ws().text(derived().ws().got_text()); - derived().ws().async_write( - boost::asio::buffer(response_), - boost::beast::bind_front_handler( - &WsSession::on_write, this->shared_from_this())); + send(std::move(response_)); } void @@ -223,4 +294,5 @@ public: do_read(); } }; + #endif // RIPPLE_REPORTING_WS_BASE_SESSION_H diff --git a/src/server/listener.h b/src/webserver/listener.h similarity index 97% rename from src/server/listener.h rename to src/webserver/listener.h index a78bcdeb..7c93c058 100644 --- a/src/server/listener.h +++ b/src/webserver/listener.h @@ -2,11 +2,9 @@ /* This file is part of rippled: https://github.com/ripple/rippled Copyright (c) 2020 Ripple Labs Inc. - Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. - THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR @@ -23,11 +21,11 @@ #include #include #include -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include #include @@ -156,6 +154,7 @@ make_websocket_session( std::move(req)) ->run(); } + template class Listener : public std::enable_shared_from_this> @@ -306,7 +305,7 @@ parse_certs(const char* certFilename, const char* keyFilename) return ctx; } -using WebsocketServer = Listener; + using HttpServer = Listener; static std::shared_ptr @@ -350,4 +349,4 @@ make_HttpServer( } } // namespace Server -#endif // LISTENER_H +#endif // LISTENER_H \ No newline at end of file