Achieve parity with rippled API

This commit is contained in:
Nathan Nichols
2021-06-22 13:09:40 -05:00
committed by CJ Cobb
parent 83b50bc261
commit 37abec1401
64 changed files with 3643 additions and 2215 deletions

View File

@@ -36,29 +36,22 @@ add_executable (clio_server
add_executable (clio_tests add_executable (clio_tests
unittests/main.cpp unittests/main.cpp
) )
add_library(clio src/backend/BackendInterface.h) add_library(clio)
include_directories(src) include_directories(src)
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/deps") list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/deps")
include(ExternalProject) include(ExternalProject)
message(${CMAKE_CURRENT_BINARY_DIR}) message(${CMAKE_CURRENT_BINARY_DIR})
message(${CMAKE_MODULE_PATH}) message(${CMAKE_MODULE_PATH})
#include(rippled/Builds/CMake/RippledCore.cmake)
add_subdirectory(rippled) add_subdirectory(rippled)
target_link_libraries(clio PUBLIC xrpl_core grpc_pbufs) target_link_libraries(clio PUBLIC xrpl_core grpc_pbufs)
add_dependencies(clio xrpl_core) add_dependencies(clio xrpl_core)
add_dependencies(clio grpc_pbufs) add_dependencies(clio grpc_pbufs)
get_target_property(grpc_includes grpc_pbufs INCLUDE_DIRECTORIES) 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("${grpc_includes}")
message("${proto_includes}") message("${proto_includes}")
# ExternalProject_Get_Property(protobuf_src SOURCE_DIR)
message("${SOURCE_DIR}") message("${SOURCE_DIR}")
INCLUDE_DIRECTORIES(${grpc_includes}) INCLUDE_DIRECTORIES(${grpc_includes})
#INCLUDE_DIRECTORIES(${xrpl_core_includes})
INCLUDE_DIRECTORIES(${SOURCE_DIR}/src) INCLUDE_DIRECTORIES(${SOURCE_DIR}/src)
# ExternalProject_Get_Property(grpc_src SOURCE_DIR)
INCLUDE_DIRECTORIES(${SOURCE_DIR}/include) INCLUDE_DIRECTORIES(${SOURCE_DIR}/include)
get_target_property(xrpl_core_includes xrpl_core INCLUDE_DIRECTORIES) get_target_property(xrpl_core_includes xrpl_core INCLUDE_DIRECTORIES)
message("${xrpl_core_includes}") message("${xrpl_core_includes}")
@@ -68,34 +61,46 @@ include(Postgres)
target_sources(clio PRIVATE target_sources(clio PRIVATE
## Backend
src/backend/CassandraBackend.cpp src/backend/CassandraBackend.cpp
src/backend/PostgresBackend.cpp src/backend/PostgresBackend.cpp
src/backend/BackendIndexer.cpp src/backend/BackendIndexer.cpp
src/backend/BackendInterface.cpp src/backend/BackendInterface.cpp
src/backend/Pg.cpp src/backend/Pg.cpp
src/backend/DBHelpers.cpp src/backend/DBHelpers.cpp
## Reporting
src/etl/ETLSource.cpp src/etl/ETLSource.cpp
src/etl/ReportingETL.cpp src/etl/ReportingETL.cpp
src/server/Handlers.cpp ## Server
src/server/SubscriptionManager.cpp src/webserver/SubscriptionManager.cpp
src/handlers/AccountInfo.cpp ## Handlers
src/handlers/Tx.cpp src/handlers/Status.cpp
src/handlers/RPCHelpers.cpp src/handlers/RPCHelpers.cpp
src/handlers/AccountTx.cpp src/handlers/Handlers.cpp
src/handlers/LedgerData.cpp src/handlers/Context.cpp
src/handlers/BookOffers.cpp ## Methods
src/handlers/LedgerRange.cpp # Account
src/handlers/Ledger.cpp src/handlers/methods/impl/AccountChannels.cpp
src/handlers/LedgerEntry.cpp src/handlers/methods/impl/AccountCurrencies.cpp
src/handlers/AccountChannels.cpp src/handlers/methods/impl/AccountInfo.cpp
src/handlers/AccountLines.cpp src/handlers/methods/impl/AccountLines.cpp
src/handlers/AccountCurrencies.cpp src/handlers/methods/impl/AccountOffers.cpp
src/handlers/AccountOffers.cpp src/handlers/methods/impl/AccountObjects.cpp
src/handlers/AccountObjects.cpp # Ledger
src/handlers/ChannelAuthorize.cpp src/handlers/methods/impl/Ledger.cpp
src/handlers/ChannelVerify.cpp src/handlers/methods/impl/LedgerData.cpp
src/handlers/Subscribe.cpp src/handlers/methods/impl/LedgerEntry.cpp
src/handlers/ServerInfo.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}) message(${Boost_LIBRARIES})

View File

@@ -224,6 +224,7 @@ BackendInterface::fetchLedgerPage(
page.objects.end(), partial.objects.begin(), partial.objects.end()); page.objects.end(), partial.objects.begin(), partial.objects.end());
page.cursor = partial.cursor; page.cursor = partial.cursor;
} while (page.objects.size() < limit && page.cursor); } while (page.objects.size() < limit && page.cursor);
if (incomplete) if (incomplete)
{ {
auto rng = fetchLedgerRange(); auto rng = fetchLedgerRange();
@@ -332,4 +333,37 @@ BackendInterface::checkFlagLedgers() const
} }
} }
} }
std::optional<ripple::Fees>
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 } // namespace Backend

View File

@@ -91,18 +91,23 @@ public:
virtual std::optional<ripple::LedgerInfo> virtual std::optional<ripple::LedgerInfo>
fetchLedgerBySequence(uint32_t sequence) const = 0; fetchLedgerBySequence(uint32_t sequence) const = 0;
virtual std::optional<ripple::LedgerInfo>
fetchLedgerByHash(ripple::uint256 const& hash) const = 0;
virtual std::optional<uint32_t> virtual std::optional<uint32_t>
fetchLatestLedgerSequence() const = 0; fetchLatestLedgerSequence() const = 0;
virtual std::optional<LedgerRange> virtual std::optional<LedgerRange>
fetchLedgerRange() const = 0; fetchLedgerRange() const = 0;
std::optional<ripple::Fees>
fetchFees(std::uint32_t seq) const;
// Doesn't throw DatabaseTimeout. Should be used with care. // Doesn't throw DatabaseTimeout. Should be used with care.
std::optional<LedgerRange> std::optional<LedgerRange>
fetchLedgerRangeNoThrow() const; fetchLedgerRangeNoThrow() const;
// *** transaction methods // *** transaction methods
virtual std::optional<TransactionAndMetadata> virtual std::optional<TransactionAndMetadata>
fetchTransaction(ripple::uint256 const& hash) const = 0; fetchTransaction(ripple::uint256 const& hash) const = 0;

View File

@@ -286,6 +286,7 @@ struct ReadDiffCallbackData
CassandraBackend const& backend; CassandraBackend const& backend;
uint32_t sequence; uint32_t sequence;
std::vector<LedgerObject>& result; std::vector<LedgerObject>& result;
std::mutex& mtx;
std::condition_variable& cv; std::condition_variable& cv;
std::atomic_uint32_t& numFinished; std::atomic_uint32_t& numFinished;
@@ -295,12 +296,14 @@ struct ReadDiffCallbackData
CassandraBackend const& backend, CassandraBackend const& backend,
uint32_t sequence, uint32_t sequence,
std::vector<LedgerObject>& result, std::vector<LedgerObject>& result,
std::mutex& mtx,
std::condition_variable& cv, std::condition_variable& cv,
std::atomic_uint32_t& numFinished, std::atomic_uint32_t& numFinished,
size_t batchSize) size_t batchSize)
: backend(backend) : backend(backend)
, sequence(sequence) , sequence(sequence)
, result(result) , result(result)
, mtx(mtx)
, cv(cv) , cv(cv)
, numFinished(numFinished) , numFinished(numFinished)
, batchSize(batchSize) , batchSize(batchSize)
@@ -355,6 +358,7 @@ CassandraBackend::fetchLedgerDiffs(std::vector<uint32_t> const& sequences) const
*this, *this,
sequences[i], sequences[i],
results[sequences[i]], results[sequences[i]],
mtx,
cv, cv,
numFinished, numFinished,
sequences.size())); sequences.size()));
@@ -414,7 +418,10 @@ CassandraBackend::doFetchLedgerPage(
CassandraStatement statement{selectKeys_}; CassandraStatement statement{selectKeys_};
statement.bindInt(index->keyIndex); statement.bindInt(index->keyIndex);
if (cursor) if (cursor)
statement.bindBytes(*cursor); {
auto thisCursor = *cursor;
statement.bindBytes(++thisCursor);
}
else else
{ {
ripple::uint256 zero; ripple::uint256 zero;
@@ -426,12 +433,13 @@ CassandraBackend::doFetchLedgerPage(
{ {
BOOST_LOG_TRIVIAL(debug) BOOST_LOG_TRIVIAL(debug)
<< __func__ << " - got keys - size = " << result.numRows(); << __func__ << " - got keys - size = " << result.numRows();
std::vector<ripple::uint256> keys;
std::vector<ripple::uint256> keys;
do do
{ {
keys.push_back(result.getUInt256()); keys.push_back(result.getUInt256());
} while (result.nextRow()); } while (result.nextRow());
if (keys.size() && keys.size() >= limit) if (keys.size() && keys.size() >= limit)
{ {
page.cursor = keys.back(); page.cursor = keys.back();
@@ -454,12 +462,17 @@ CassandraBackend::doFetchLedgerPage(
page.objects.push_back({std::move(key), std::move(obj)}); page.objects.push_back({std::move(key), std::move(obj)});
} }
} }
if (!cursor && (!keys.size() || !keys[0].isZero())) if (!cursor && (!keys.size() || !keys[0].isZero()))
{
page.warning = "Data may be incomplete"; page.warning = "Data may be incomplete";
}
return page; return page;
} }
if (!cursor) if (!cursor)
return {{}, {}, "Data may be incomplete"}; return {{}, {}, "Data may be incomplete"};
return {}; return {};
} }
std::vector<Blob> std::vector<Blob>
@@ -479,7 +492,7 @@ CassandraBackend::fetchLedgerObjects(
for (std::size_t i = 0; i < keys.size(); ++i) for (std::size_t i = 0; i < keys.size(); ++i)
{ {
cbs.push_back(std::make_shared<ReadObjectCallbackData>( cbs.push_back(std::make_shared<ReadObjectCallbackData>(
*this, keys[i], sequence, results[i], cv, numFinished, numKeys)); *this, keys[i], sequence, results[i], mtx, cv, numFinished, numKeys));
readObject(*cbs[i]); readObject(*cbs[i]);
} }
assert(results.size() == cbs.size()); assert(results.size() == cbs.size());
@@ -1574,6 +1587,12 @@ CassandraBackend::open(bool readOnly)
if (!insertLedgerHash_.prepareStatement(query, session_.get())) if (!insertLedgerHash_.prepareStatement(query, session_.get()))
continue; continue;
query.str("");
query << "SELECT sequence FROM " << tablePrefix << "ledger_hashes "
<< "WHERE hash = ? LIMIT 1";
if (!selectLedgerByHash_.prepareStatement(query, session_.get()))
continue;
query.str(""); query.str("");
query << " update " << tablePrefix << "ledger_range" query << " update " << tablePrefix << "ledger_range"
<< " set sequence = ? where is_latest = ? if sequence in " << " set sequence = ? where is_latest = ? if sequence in "

View File

@@ -575,6 +575,8 @@ public:
{ {
BOOST_LOG_TRIVIAL(trace) << "finished a request"; BOOST_LOG_TRIVIAL(trace) << "finished a request";
size_t batchSize = requestParams_.batchSize; size_t batchSize = requestParams_.batchSize;
std::unique_lock lk(requestParams_.mtx);
if (++(requestParams_.numFinished) == batchSize) if (++(requestParams_.numFinished) == batchSize)
requestParams_.cv.notify_all(); requestParams_.cv.notify_all();
} }
@@ -657,6 +659,7 @@ private:
CassandraPreparedStatement deleteLedgerRange_; CassandraPreparedStatement deleteLedgerRange_;
CassandraPreparedStatement updateLedgerHeader_; CassandraPreparedStatement updateLedgerHeader_;
CassandraPreparedStatement selectLedgerBySeq_; CassandraPreparedStatement selectLedgerBySeq_;
CassandraPreparedStatement selectLedgerByHash_;
CassandraPreparedStatement selectLatestLedger_; CassandraPreparedStatement selectLatestLedger_;
CassandraPreparedStatement selectLedgerRange_; CassandraPreparedStatement selectLedgerRange_;
CassandraPreparedStatement selectLedgerDiff_; CassandraPreparedStatement selectLedgerDiff_;
@@ -906,9 +909,7 @@ public:
{ {
BOOST_LOG_TRIVIAL(trace) << __func__; BOOST_LOG_TRIVIAL(trace) << __func__;
CassandraStatement statement{selectLatestLedger_}; CassandraStatement statement{selectLatestLedger_};
std::cout << "READ" << std::endl;
CassandraResult result = executeSyncRead(statement); CassandraResult result = executeSyncRead(statement);
std::cout << "ITS THE READ" << std::endl;
if (!result.hasResult()) if (!result.hasResult())
{ {
BOOST_LOG_TRIVIAL(error) BOOST_LOG_TRIVIAL(error)
@@ -934,6 +935,26 @@ public:
std::vector<unsigned char> header = result.getBytes(); std::vector<unsigned char> header = result.getBytes();
return deserializeHeader(ripple::makeSlice(header)); return deserializeHeader(ripple::makeSlice(header));
} }
std::optional<ripple::LedgerInfo>
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<LedgerRange> std::optional<LedgerRange>
fetchLedgerRange() const override; fetchLedgerRange() const override;
@@ -1033,6 +1054,7 @@ public:
CassandraBackend const& backend; CassandraBackend const& backend;
ripple::uint256 const& hash; ripple::uint256 const& hash;
TransactionAndMetadata& result; TransactionAndMetadata& result;
std::mutex& mtx;
std::condition_variable& cv; std::condition_variable& cv;
std::atomic_uint32_t& numFinished; std::atomic_uint32_t& numFinished;
@@ -1042,12 +1064,14 @@ public:
CassandraBackend const& backend, CassandraBackend const& backend,
ripple::uint256 const& hash, ripple::uint256 const& hash,
TransactionAndMetadata& result, TransactionAndMetadata& result,
std::mutex& mtx,
std::condition_variable& cv, std::condition_variable& cv,
std::atomic_uint32_t& numFinished, std::atomic_uint32_t& numFinished,
size_t batchSize) size_t batchSize)
: backend(backend) : backend(backend)
, hash(hash) , hash(hash)
, result(result) , result(result)
, mtx(mtx)
, cv(cv) , cv(cv)
, numFinished(numFinished) , numFinished(numFinished)
, batchSize(batchSize) , batchSize(batchSize)
@@ -1072,7 +1096,7 @@ public:
for (std::size_t i = 0; i < hashes.size(); ++i) for (std::size_t i = 0; i < hashes.size(); ++i)
{ {
cbs.push_back(std::make_shared<ReadCallbackData>( cbs.push_back(std::make_shared<ReadCallbackData>(
*this, hashes[i], results[i], cv, numFinished, numHashes)); *this, hashes[i], results[i], mtx, cv, numFinished, numHashes));
read(*cbs[i]); read(*cbs[i]);
} }
assert(results.size() == cbs.size()); assert(results.size() == cbs.size());
@@ -1106,6 +1130,7 @@ public:
ripple::uint256 const& key; ripple::uint256 const& key;
uint32_t sequence; uint32_t sequence;
Blob& result; Blob& result;
std::mutex& mtx;
std::condition_variable& cv; std::condition_variable& cv;
std::atomic_uint32_t& numFinished; std::atomic_uint32_t& numFinished;
@@ -1116,6 +1141,7 @@ public:
ripple::uint256 const& key, ripple::uint256 const& key,
uint32_t sequence, uint32_t sequence,
Blob& result, Blob& result,
std::mutex& mtx,
std::condition_variable& cv, std::condition_variable& cv,
std::atomic_uint32_t& numFinished, std::atomic_uint32_t& numFinished,
size_t batchSize) size_t batchSize)
@@ -1123,6 +1149,7 @@ public:
, key(key) , key(key)
, sequence(sequence) , sequence(sequence)
, result(result) , result(result)
, mtx(mtx)
, cv(cv) , cv(cv)
, numFinished(numFinished) , numFinished(numFinished)
, batchSize(batchSize) , batchSize(batchSize)

View File

@@ -199,6 +199,20 @@ PostgresBackend::fetchLedgerBySequence(uint32_t sequence) const
return {}; return {};
} }
std::optional<ripple::LedgerInfo>
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<LedgerRange> std::optional<LedgerRange>
PostgresBackend::fetchLedgerRange() const PostgresBackend::fetchLedgerRange() const
{ {

View File

@@ -27,6 +27,9 @@ public:
std::optional<ripple::LedgerInfo> std::optional<ripple::LedgerInfo>
fetchLedgerBySequence(uint32_t sequence) const override; fetchLedgerBySequence(uint32_t sequence) const override;
std::optional<ripple::LedgerInfo>
fetchLedgerByHash(ripple::uint256 const& hash) const override;
std::optional<LedgerRange> std::optional<LedgerRange>
fetchLedgerRange() const override; fetchLedgerRange() const override;

View File

@@ -1,26 +1,108 @@
The backend is clio's view into the database. The database could be either PostgreSQL or Cassandra. Reporting mode is a special operating mode of rippled, designed to handle RPCs
Multiple clio servers can share access to the same database. 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, # Reporting ETL
and generally don't access any internal data structures. Nearly all of the methods are const. 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 If the database is entirely empty, ETL must download an entire ledger in full
SHAMap inner nodes, and instead only stores the raw ledger objects contained in the leaf node. Ledger objects (as opposed to just the diff, as described above). This download is done via the
are stored in the database with a compound key of `(object_id, ledger_sequence)`, where `ledger_sequence` is the "GetLedgerData" gRPC request. "GetLedgerData" allows clients to page through an
ledger in which the object was created or modified. Objects are then fetched using an inequality operation, entire ledger over several RPC calls. ETL will page through an entire ledger,
such as `SELECT * FROM objects WHERE object_id = id AND ledger_sequence <= seq order by ledger_sequence limit 1`, where `seq` is the ledger and write each object to the database.
in which we are trying to look up the object. When an object is deleted, we write an empty blob.
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`.

View File

@@ -26,7 +26,7 @@
#include <boost/beast/core/string.hpp> #include <boost/beast/core/string.hpp>
#include <boost/beast/websocket.hpp> #include <boost/beast/websocket.hpp>
#include <backend/BackendInterface.h> #include <backend/BackendInterface.h>
#include <server/SubscriptionManager.h> #include <webserver/SubscriptionManager.h>
#include "org/xrpl/rpc/v1/xrp_ledger.grpc.pb.h" #include "org/xrpl/rpc/v1/xrp_ledger.grpc.pb.h"
#include <grpcpp/grpcpp.h> #include <grpcpp/grpcpp.h>

View File

@@ -26,9 +26,10 @@
#include <boost/asio/ip/tcp.hpp> #include <boost/asio/ip/tcp.hpp>
#include <boost/beast/core.hpp> #include <boost/beast/core.hpp>
#include <boost/beast/websocket.hpp> #include <boost/beast/websocket.hpp>
#include <webserver/SubscriptionManager.h>
#include <cstdlib> #include <cstdlib>
#include <iostream> #include <iostream>
#include <server/SubscriptionManager.h> #include <webserver/SubscriptionManager.h>
#include <string> #include <string>
#include <variant> #include <variant>
@@ -138,49 +139,18 @@ ReportingETL::loadInitialLedger(uint32_t startingSequence)
return lgrInfo; return lgrInfo;
} }
std::optional<ripple::Fees>
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 void
ReportingETL::publishLedger(ripple::LedgerInfo const& lgrInfo) ReportingETL::publishLedger(ripple::LedgerInfo const& lgrInfo)
{ {
auto ledgerRange = backend_->fetchLedgerRangeNoThrow(); auto ledgerRange = backend_->fetchLedgerRangeNoThrow();
auto fees = getFees(lgrInfo.seq); std::optional<ripple::Fees> fees;
std::vector<Backend::TransactionAndMetadata> transactions; std::vector<Backend::TransactionAndMetadata> transactions;
while (true) for(;;)
{ {
try try
{ {
fees = backend_->fetchFees(lgrInfo.seq);
transactions = backend_->fetchAllTransactionsInLedger(lgrInfo.seq); transactions = backend_->fetchAllTransactionsInLedger(lgrInfo.seq);
break; break;
} }
@@ -448,7 +418,6 @@ ReportingETL::runETLPipeline(uint32_t startSequence, int numExtractors)
auto getNext = [&queues, &startSequence, &numExtractors]( auto getNext = [&queues, &startSequence, &numExtractors](
uint32_t sequence) -> std::shared_ptr<QueueType> { uint32_t sequence) -> std::shared_ptr<QueueType> {
std::cout << std::to_string((sequence - startSequence) % numExtractors);
return queues[(sequence - startSequence) % numExtractors]; return queues[(sequence - startSequence) % numExtractors];
}; };
std::vector<std::thread> extractors; std::vector<std::thread> extractors;

View File

@@ -27,7 +27,7 @@
#include <boost/beast/websocket.hpp> #include <boost/beast/websocket.hpp>
#include <backend/BackendInterface.h> #include <backend/BackendInterface.h>
#include <etl/ETLSource.h> #include <etl/ETLSource.h>
#include <server/SubscriptionManager.h> #include <webserver/SubscriptionManager.h>
#include "org/xrpl/rpc/v1/xrp_ledger.grpc.pb.h" #include "org/xrpl/rpc/v1/xrp_ledger.grpc.pb.h"
#include <grpcpp/grpcpp.h> #include <grpcpp/grpcpp.h>
@@ -248,12 +248,6 @@ private:
void void
publishLedger(ripple::LedgerInfo const& lgrInfo); 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<ripple::Fees>
getFees(std::uint32_t seq);
bool bool
isStopping() isStopping()
{ {

View File

@@ -1,160 +0,0 @@
#include <ripple/app/ledger/Ledger.h>
#include <ripple/basics/StringUtilities.h>
#include <ripple/protocol/ErrorCodes.h>
#include <ripple/protocol/Indexes.h>
#include <ripple/protocol/STLedgerEntry.h>
#include <ripple/protocol/jss.h>
#include <boost/json.hpp>
#include <algorithm>
#include <backend/BackendInterface.h>
#include <handlers/RPCHelpers.h>
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<ripple::AccountID>(
request.at("account").as_string().c_str());
if (!parsed)
{
response["error"] = "Invalid account";
return response;
}
accountID = *parsed;
boost::optional<ripple::AccountID> 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<ripple::AccountID>(
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;
}

View File

@@ -1,119 +0,0 @@
#include <ripple/app/ledger/Ledger.h>
#include <ripple/app/paths/RippleState.h>
#include <ripple/basics/StringUtilities.h>
#include <ripple/protocol/ErrorCodes.h>
#include <ripple/protocol/Indexes.h>
#include <ripple/protocol/STLedgerEntry.h>
#include <ripple/protocol/jss.h>
#include <boost/json.hpp>
#include <algorithm>
#include <backend/BackendInterface.h>
#include <handlers/RPCHelpers.h>
std::unordered_map<std::string, ripple::LedgerEntryType> 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<ripple::AccountID>(
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<ripple::LedgerEntryType> 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;
}

View File

@@ -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 <handlers/RPCHelpers.h>
#include <backend/BackendInterface.h>
// {
// 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<ripple::AccountID>(
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<Backend::AccountTransactionsCursor> cursor;
if (request.contains("cursor"))
{
auto const& obj = request.at("cursor").as_object();
std::optional<uint32_t> ledgerSequence;
if (obj.contains("ledger_sequence"))
{
ledgerSequence = (uint32_t)obj.at("ledger_sequence").as_int64();
}
std::optional<uint32_t> 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;
}

View File

@@ -1,302 +0,0 @@
#include <ripple/app/ledger/Ledger.h>
#include <ripple/basics/StringUtilities.h>
#include <ripple/protocol/ErrorCodes.h>
#include <ripple/protocol/Indexes.h>
#include <ripple/protocol/STLedgerEntry.h>
#include <ripple/protocol/jss.h>
#include <boost/json.hpp>
#include <algorithm>
#include <backend/BackendInterface.h>
#include <backend/DBHelpers.h>
#include <handlers/RPCHelpers.h>
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<ripple::AccountID> 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<ripple::uint256> 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;
}

71
src/handlers/Context.cpp Normal file
View File

@@ -0,0 +1,71 @@
#include <handlers/Context.h>
namespace RPC
{
std::optional<Context>
make_WsContext(
boost::json::object const& request,
std::shared_ptr<BackendInterface> const& backend,
std::shared_ptr<SubscriptionManager> const& subscriptions,
std::shared_ptr<ETLLoadBalancer> const& balancer,
std::shared_ptr<WsBase> 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<Context>
make_HttpContext(
boost::json::object const& request,
std::shared_ptr<BackendInterface> const& backend,
std::shared_ptr<SubscriptionManager> const& subscriptions,
std::shared_ptr<ETLLoadBalancer> 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

85
src/handlers/Context.h Normal file
View File

@@ -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 <boost/json.hpp>
#include <optional>
#include <backend/BackendInterface.h>
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<BackendInterface> const& backend;
std::shared_ptr<SubscriptionManager> const& subscriptions;
std::shared_ptr<ETLLoadBalancer> const& balancer;
std::shared_ptr<WsBase> session;
Backend::LedgerRange const& range;
Context(
std::string const& command_,
std::uint32_t version_,
boost::json::object const& params_,
std::shared_ptr<BackendInterface> const& backend_,
std::shared_ptr<SubscriptionManager> const& subscriptions_,
std::shared_ptr<ETLLoadBalancer> const& balancer_,
std::shared_ptr<WsBase> const& session_,
Backend::LedgerRange const& range_)
: method(command_)
, version(version_)
, params(params_)
, backend(backend_)
, subscriptions(subscriptions_)
, balancer(balancer_)
, session(session_)
, range(range_)
{}
};
std::optional<Context>
make_WsContext(
boost::json::object const& request,
std::shared_ptr<BackendInterface> const& backend,
std::shared_ptr<SubscriptionManager> const& subscriptions,
std::shared_ptr<ETLLoadBalancer> const& balancer,
std::shared_ptr<WsBase> const& session,
Backend::LedgerRange const& range);
std::optional<Context>
make_HttpContext(
boost::json::object const& request,
std::shared_ptr<BackendInterface> const& backend,
std::shared_ptr<SubscriptionManager> const& subscriptions,
std::shared_ptr<ETLLoadBalancer> const& balancer,
Backend::LedgerRange const& range);
} // namespace RPC
#endif //REPORTING_CONTEXT_H_INCLUDED

90
src/handlers/Handlers.cpp Normal file
View File

@@ -0,0 +1,90 @@
#include <handlers/Handlers.h>
#include <handlers/methods/Account.h>
#include <handlers/methods/Channel.h>
#include <handlers/methods/Exchange.h>
#include <handlers/methods/Ledger.h>
#include <handlers/methods/Subscribe.h>
#include <handlers/methods/Transaction.h>
#include <etl/ETLSource.h>
namespace RPC
{
static std::unordered_map<std::string, std::function<Result(Context const&)>>
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<std::string> 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);
}
}

29
src/handlers/Handlers.h Normal file
View File

@@ -0,0 +1,29 @@
#include <handlers/Context.h>
#include <handlers/Status.h>
#include <unordered_map>
#include <functional>
#include <iostream>
#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

View File

@@ -1,89 +0,0 @@
#include <handlers/RPCHelpers.h>
#include <backend/BackendInterface.h>
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;
}

View File

@@ -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 <ripple/app/ledger/LedgerToJson.h>
#include <ripple/protocol/STLedgerEntry.h>
#include <boost/json.hpp>
#include <handlers/RPCHelpers.h>
#include <backend/BackendInterface.h>
// 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<ripple::uint256> 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<std::chrono::microseconds>(end - start)
.count();
boost::json::array objects;
std::vector<Backend::LedgerObject>& results = page.objects;
std::optional<ripple::uint256> 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<org::xrpl::rpc::v1::GetLedgerDataResponse, grpc::Status>
doLedgerDataGrpc(
RPC::GRPCContext<org::xrpl::rpc::v1::GetLedgerDataRequest>& context)
{
org::xrpl::rpc::v1::GetLedgerDataRequest& request = context.params;
org::xrpl::rpc::v1::GetLedgerDataResponse response;
grpc::Status status = grpc::Status::OK;
std::shared_ptr<ReadView const> 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};
}
*/

View File

@@ -1,55 +0,0 @@
#include <ripple/protocol/Indexes.h>
#include <ripple/protocol/STLedgerEntry.h>
#include <boost/json.hpp>
#include <handlers/RPCHelpers.h>
#include <backend/BackendInterface.h>
// {
// ledger_hash : <ledger>
// ledger_index : <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<std::chrono::microseconds>(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;
}

View File

@@ -1,5 +1,6 @@
#include <backend/BackendInterface.h>
#include <handlers/RPCHelpers.h> #include <handlers/RPCHelpers.h>
#include <handlers/Status.h>
#include <backend/BackendInterface.h>
std::optional<ripple::AccountID> std::optional<ripple::AccountID>
accountFromStringStrict(std::string const& account) accountFromStringStrict(std::string const& account)
@@ -69,54 +70,38 @@ deserializeTxPlusMeta(
boost::json::object boost::json::object
toJson(ripple::STBase const& obj) toJson(ripple::STBase const& obj)
{ {
auto start = std::chrono::system_clock::now();
boost::json::value value = boost::json::parse( boost::json::value value = boost::json::parse(
obj.getJson(ripple::JsonOptions::none).toStyledString()); obj.getJson(ripple::JsonOptions::none).toStyledString());
auto end = std::chrono::system_clock::now();
value.as_object()["deserialization_time_microseconds"] =
std::chrono::duration_cast<std::chrono::microseconds>(end - start)
.count();
return value.as_object(); 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 boost::json::object
toJson(ripple::TxMeta const& meta) toJson(ripple::TxMeta const& meta)
{ {
auto start = std::chrono::system_clock::now();
boost::json::value value = boost::json::parse( boost::json::value value = boost::json::parse(
meta.getJson(ripple::JsonOptions::none).toStyledString()); meta.getJson(ripple::JsonOptions::none).toStyledString());
auto end = std::chrono::system_clock::now();
value.as_object()["deserialization_time_microseconds"] =
std::chrono::duration_cast<std::chrono::microseconds>(end - start)
.count();
return value.as_object(); 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 boost::json::object
toJson(ripple::SLE const& sle) toJson(ripple::SLE const& sle)
{ {
auto start = std::chrono::system_clock::now();
boost::json::value value = boost::json::parse( boost::json::value value = boost::json::parse(
sle.getJson(ripple::JsonOptions::none).toStyledString()); sle.getJson(ripple::JsonOptions::none).toStyledString());
auto end = std::chrono::system_clock::now();
value.as_object()["deserialization_time_microseconds"] =
std::chrono::duration_cast<std::chrono::microseconds>(end - start)
.count();
return value.as_object(); return value.as_object();
} }
boost::json::value
toBoostJson(RippledJson const& value)
{
return boost::json::parse(value.toStyledString());
}
boost::json::object boost::json::object
toJson(ripple::LedgerInfo const& lgrInfo) toJson(ripple::LedgerInfo const& lgrInfo)
@@ -138,20 +123,70 @@ toJson(ripple::LedgerInfo const& lgrInfo)
return header; return header;
} }
std::optional<uint32_t> std::variant<RPC::Status, ripple::LedgerInfo>
ledgerSequenceFromRequest( ledgerInfoFromRequest(RPC::Context const& ctx)
boost::json::object const& request,
BackendInterface const& backend)
{ {
if (!request.contains("ledger_index") || auto indexValue = ctx.params.contains("ledger_index")
request.at("ledger_index").is_string()) ? ctx.params.at("ledger_index")
: nullptr;
auto hashValue = ctx.params.contains("ledger_hash")
? ctx.params.at("ledger_hash")
: nullptr;
std::optional<ripple::LedgerInfo> 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 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<unsigned char>
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<ripple::uint256> std::optional<ripple::uint256>
@@ -239,8 +274,8 @@ parseRippleLibSeed(boost::json::value const& value)
return {}; return {};
} }
std::pair<ripple::PublicKey, ripple::SecretKey> std::variant<RPC::Status, std::pair<ripple::PublicKey, ripple::SecretKey>>
keypairFromRequst(boost::json::object const& request, boost::json::value& error) keypairFromRequst(boost::json::object const& request)
{ {
bool const has_key_type = request.contains("key_type"); 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) if (count == 0)
{ return RPC::Status{RPC::Error::rpcINVALID_PARAMS, "missing field secret"};
error = "missing field secret";
return {};
}
if (count > 1) if (count > 1)
{ {
error = return RPC::Status{RPC::Error::rpcINVALID_PARAMS,
"Exactly one of the following must be specified: " "Exactly one of the following must be specified: "
" passphrase, secret, seed, or seed_hex"; " passphrase, secret, seed, or seed_hex"};
return {};
} }
boost::optional<ripple::KeyType> keyType; boost::optional<ripple::KeyType> keyType;
@@ -281,25 +312,18 @@ keypairFromRequst(boost::json::object const& request, boost::json::value& error)
if (has_key_type) if (has_key_type)
{ {
if (!request.at("key_type").is_string()) if (!request.at("key_type").is_string())
{ return RPC::Status{RPC::Error::rpcINVALID_PARAMS, "keyTypeNotString"};
error = "key_type must be string";
return {};
}
std::string key_type = request.at("key_type").as_string().c_str(); std::string key_type = request.at("key_type").as_string().c_str();
keyType = ripple::keyTypeFromString(key_type); keyType = ripple::keyTypeFromString(key_type);
if (!keyType) if (!keyType)
{ return RPC::Status{RPC::Error::rpcINVALID_PARAMS, "invalidFieldKeyType"};
error = "Invalid field key_type";
return {};
}
if (secretType == "secret") if (secretType == "secret")
{ return RPC::Status{RPC::Error::rpcINVALID_PARAMS,
error = "The secret field is not allowed if key_type is used."; "The secret field is not allowed if key_type is used."};
return {};
}
} }
// ripple-lib encodes seed used to generate an Ed25519 wallet in a // 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* // If the user passed in an Ed25519 seed but *explicitly*
// requested another key type, return an error. // requested another key type, return an error.
if (keyType.value_or(ripple::KeyType::ed25519) != if (keyType.value_or(ripple::KeyType::ed25519) != ripple::KeyType::ed25519)
ripple::KeyType::ed25519) return RPC::Status{RPC::Error::rpcINVALID_PARAMS,
{ "Specified seed is for an Ed25519 wallet."};
error = "Specified seed is for an Ed25519 wallet.";
return {};
}
keyType = ripple::KeyType::ed25519; keyType = ripple::KeyType::ed25519;
} }
@@ -332,10 +353,8 @@ keypairFromRequst(boost::json::object const& request, boost::json::value& error)
if (has_key_type) if (has_key_type)
{ {
if (!request.at(secretType).is_string()) if (!request.at(secretType).is_string())
{ return RPC::Status{RPC::Error::rpcINVALID_PARAMS,
error = "secret value must be string"; "secret value must be string"};
return {};
}
std::string key = request.at(secretType).as_string().c_str(); 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 else
{ {
if (!request.at("secret").is_string()) if (!request.at("secret").is_string())
{ return RPC::Status{RPC::Error::rpcINVALID_PARAMS,
error = "field secret should be a string"; "field secret should be a string"};
return {};
}
std::string secret = request.at("secret").as_string().c_str(); std::string secret = request.at("secret").as_string().c_str();
seed = ripple::parseGenericSeed(secret); seed = ripple::parseGenericSeed(secret);
@@ -364,17 +381,13 @@ keypairFromRequst(boost::json::object const& request, boost::json::value& error)
} }
if (!seed) if (!seed)
{ return RPC::Status{RPC::Error::rpcBAD_SEED,
error = "Bad Seed: invalid field message secretType"; "Bad Seed: invalid field message secretType"};
return {};
}
if (keyType != ripple::KeyType::secp256k1 && if (keyType != ripple::KeyType::secp256k1
keyType != ripple::KeyType::ed25519) && keyType != ripple::KeyType::ed25519)
{ return RPC::Status{RPC::Error::rpcINVALID_PARAMS,
error = "keypairForSignature: invalid key type"; "keypairForSignature: invalid key type"};
return {};
}
return generateKeyPair(*keyType, *seed); return generateKeyPair(*keyType, *seed);
} }
@@ -402,19 +415,161 @@ getAccountsFromTransaction(boost::json::object const& transaction)
return accounts; return accounts;
} }
std::vector<unsigned char>
ledgerInfoToBlob(ripple::LedgerInfo const& info) bool
isGlobalFrozen(
BackendInterface const& backend,
std::uint32_t sequence,
ripple::AccountID const& issuer)
{ {
ripple::Serializer s; if (ripple::isXRP(issuer))
s.add32(info.seq); return false;
s.add64(info.drops.drops());
s.addBitString(info.parentHash); auto key = ripple::keylet::account(issuer).key;
s.addBitString(info.txHash); auto blob = backend.fetchLedgerObject(key, sequence);
s.addBitString(info.accountHash);
s.add32(info.parentCloseTime.time_since_epoch().count()); if (!blob)
s.add32(info.closeTime.time_since_epoch().count()); return false;
s.add8(info.closeTimeResolution.count());
s.add8(info.closeFlags); ripple::SerialIter it{blob->data(), blob->size()};
s.addBitString(info.hash); ripple::SLE sle{it, key};
return s.peekData();
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;
} }

View File

@@ -3,8 +3,13 @@
#define XRPL_REPORTING_RPCHELPERS_H_INCLUDED #define XRPL_REPORTING_RPCHELPERS_H_INCLUDED
#include <ripple/protocol/STLedgerEntry.h> #include <ripple/protocol/STLedgerEntry.h>
#include <ripple/app/ledger/Ledger.h>
#include <ripple/protocol/Indexes.h>
#include <ripple/protocol/jss.h>
#include <ripple/protocol/STTx.h> #include <ripple/protocol/STTx.h>
#include <boost/json.hpp> #include <boost/json.hpp>
#include <handlers/Status.h>
#include <handlers/Context.h>
#include <backend/BackendInterface.h> #include <backend/BackendInterface.h>
std::optional<ripple::AccountID> std::optional<ripple::AccountID>
@@ -38,10 +43,8 @@ using RippledJson = Json::Value;
boost::json::value boost::json::value
toBoostJson(RippledJson const& value); toBoostJson(RippledJson const& value);
std::optional<uint32_t> std::variant<RPC::Status, ripple::LedgerInfo>
ledgerSequenceFromRequest( ledgerInfoFromRequest(RPC::Context const& ctx);
boost::json::object const& request,
BackendInterface const& backend);
std::optional<ripple::uint256> std::optional<ripple::uint256>
traverseOwnedNodes( traverseOwnedNodes(
@@ -51,10 +54,8 @@ traverseOwnedNodes(
ripple::uint256 const& cursor, ripple::uint256 const& cursor,
std::function<bool(ripple::SLE)> atOwnedNode); std::function<bool(ripple::SLE)> atOwnedNode);
std::pair<ripple::PublicKey, ripple::SecretKey> std::variant<RPC::Status, std::pair<ripple::PublicKey, ripple::SecretKey>>
keypairFromRequst( keypairFromRequst(boost::json::object const& request);
boost::json::object const& request,
boost::json::value& error);
std::vector<ripple::AccountID> std::vector<ripple::AccountID>
getAccountsFromTransaction(boost::json::object const& transaction); getAccountsFromTransaction(boost::json::object const& transaction);
@@ -62,4 +63,38 @@ getAccountsFromTransaction(boost::json::object const& transaction);
std::vector<unsigned char> std::vector<unsigned char>
ledgerInfoToBlob(ripple::LedgerInfo const& info); 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 #endif

View File

@@ -1,53 +0,0 @@
#include <handlers/RPCHelpers.h>
#include <backend/BackendInterface.h>
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;
}

62
src/handlers/Status.cpp Normal file
View File

@@ -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 <handlers/Status.h>
#include <unordered_map>
#include <variant>
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<std::uint32_t>(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<std::uint32_t>(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;
}
}

72
src/handlers/Status.h Normal file
View File

@@ -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 <string>
#include <variant>
#include <boost/json.hpp>
#include <ripple/protocol/ErrorCodes.h>
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<Status, boost::json::object>;
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

View File

@@ -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 <handlers/Context.h>
#include <handlers/Status.h>
#include <handlers/Handlers.h>
#include <boost/json.hpp>
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

View File

@@ -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 <handlers/Context.h>
#include <handlers/Status.h>
#include <handlers/Handlers.h>
namespace RPC
{
Result
doChannelAuthorize(Context const& context);
Result
doChannelVerify(Context const& context);
} // namespace RPC
#endif // REPORTING_CHANNEL_HANDLER_H_INCLUDED

View File

@@ -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 <handlers/Context.h>
#include <handlers/Status.h>
#include <handlers/Handlers.h>
#include <boost/json.hpp>
namespace RPC
{
Result
doBookOffers(Context const& context);
} // namespace RPC
#endif // REPORTING_CHANNEL_HANDLER_H_INCLUDED

View File

@@ -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 <handlers/Context.h>
#include <handlers/Status.h>
#include <handlers/Handlers.h>
#include <boost/json.hpp>
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

View File

@@ -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 <handlers/Context.h>
#include <handlers/Status.h>
#include <handlers/Handlers.h>
#include <boost/json.hpp>
namespace RPC
{
Result
doSubscribe(Context const& context);
Result
doUnsubscribe(Context const& context);
} // namespace RPC
#endif // REPORTING_SUBSCRIBE_HANDLER_H_INCLUDED

View File

@@ -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 <handlers/Context.h>
#include <handlers/Status.h>
#include <handlers/Handlers.h>
#include <boost/json.hpp>
namespace RPC
{
Result
doTx(Context const& context);
Result
doAccountTx(Context const& context);
} // namespace RPC
#endif // REPORTING_TRANSACTION_HANDLER_H_INCLUDED

View File

@@ -0,0 +1,141 @@
#include <ripple/app/ledger/Ledger.h>
#include <ripple/basics/StringUtilities.h>
#include <ripple/protocol/ErrorCodes.h>
#include <ripple/protocol/Indexes.h>
#include <ripple/protocol/STLedgerEntry.h>
#include <ripple/protocol/jss.h>
#include <boost/json.hpp>
#include <algorithm>
#include <handlers/methods/Account.h>
#include <handlers/RPCHelpers.h>
#include <backend/BackendInterface.h>
#include <backend/DBHelpers.h>
#include <backend/Pg.h>
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<Status>(&v))
return *status;
auto lgrInfo = std::get<ripple::LedgerInfo>(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<ripple::AccountID> 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

View File

@@ -6,46 +6,38 @@
#include <ripple/protocol/jss.h> #include <ripple/protocol/jss.h>
#include <boost/json.hpp> #include <boost/json.hpp>
#include <algorithm> #include <algorithm>
#include <backend/BackendInterface.h> #include <handlers/methods/Account.h>
#include <handlers/RPCHelpers.h> #include <handlers/RPCHelpers.h>
#include <backend/BackendInterface.h>
#include <backend/DBHelpers.h>
#include <backend/Pg.h>
boost::json::object namespace RPC
doAccountCurrencies(
boost::json::object const& request,
BackendInterface const& backend)
{ {
boost::json::object response;
auto ledgerSequence = ledgerSequenceFromRequest(request, backend); Result
if (!ledgerSequence) doAccountCurrencies(Context const& context)
{ {
response["error"] = "Empty database"; auto request = context.params;
return response; boost::json::object response = {};
}
if (!request.contains("account")) auto v = ledgerInfoFromRequest(context);
{ if (auto status = std::get_if<Status>(&v))
response["error"] = "Must contain account"; return *status;
return response;
}
if (!request.at("account").is_string()) auto lgrInfo = std::get<ripple::LedgerInfo>(v);
{
response["error"] = "Account must be a string";
return response;
}
ripple::AccountID accountID; if(!request.contains("account"))
auto parsed = ripple::parseBase58<ripple::AccountID>( return Status{Error::rpcINVALID_PARAMS, "missingAccount"};
request.at("account").as_string().c_str());
if (!parsed) if(!request.at("account").is_string())
{ return Status{Error::rpcINVALID_PARAMS, "accountNotString"};
response["error"] = "Invalid account";
return response;
}
accountID = *parsed; auto accountID =
accountFromStringStrict(request.at("account").as_string().c_str());
if (!accountID)
return Status{Error::rpcINVALID_PARAMS, "malformedAccount"};
std::set<std::string> send, receive; std::set<std::string> send, receive;
auto const addToResponse = [&](ripple::SLE const& sle) { auto const addToResponse = [&](ripple::SLE const& sle) {
@@ -70,7 +62,20 @@ doAccountCurrencies(
}; };
traverseOwnedNodes( 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); response["send_currencies"] = boost::json::value(boost::json::array_kind);
boost::json::array& jsonSend = response.at("send_currencies").as_array(); boost::json::array& jsonSend = response.at("send_currencies").as_array();
@@ -78,13 +83,7 @@ doAccountCurrencies(
for (auto const& currency : send) for (auto const& currency : send)
jsonSend.push_back(currency.c_str()); 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; return response;
} }
} // namespace RPC

View File

@@ -20,6 +20,7 @@
#include <ripple/protocol/Indexes.h> #include <ripple/protocol/Indexes.h>
#include <ripple/protocol/STLedgerEntry.h> #include <ripple/protocol/STLedgerEntry.h>
#include <boost/json.hpp> #include <boost/json.hpp>
#include <handlers/methods/Account.h>
#include <handlers/RPCHelpers.h> #include <handlers/RPCHelpers.h>
#include <backend/BackendInterface.h> #include <backend/BackendInterface.h>
@@ -38,78 +39,63 @@
// // error. // // error.
// } // }
// TODO(tom): what is that "default"? namespace RPC
boost::json::object
doAccountInfo(
boost::json::object const& request,
BackendInterface const& backend)
{ {
boost::json::object response;
Result
doAccountInfo(Context const& context)
{
auto request = context.params;
boost::json::object response = {};
std::string strIdent; std::string strIdent;
if (request.contains("account")) if (request.contains("account"))
strIdent = request.at("account").as_string().c_str(); strIdent = request.at("account").as_string().c_str();
else if (request.contains("ident")) else if (request.contains("ident"))
strIdent = request.at("ident").as_string().c_str(); strIdent = request.at("ident").as_string().c_str();
else else
{ return Status{Error::rpcACT_MALFORMED};
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;
}
// bool bStrict = request.contains("strict") && auto v = ledgerInfoFromRequest(context);
// params.at("strict").as_bool(); if (auto status = std::get_if<Status>(&v))
return *status;
auto lgrInfo = std::get<ripple::LedgerInfo>(v);
// Get info on account. // Get info on account.
std::optional<ripple::AccountID> accountID = auto accountID = accountFromStringStrict(strIdent);
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 key = ripple::keylet::account(accountID.value());
auto start = std::chrono::system_clock::now(); auto start = std::chrono::system_clock::now();
std::optional<std::vector<unsigned char>> dbResponse = std::optional<std::vector<unsigned char>> dbResponse =
backend.fetchLedgerObject(key.key, *ledgerSequence); context.backend->fetchLedgerObject(key.key, lgrInfo.seq);
auto end = std::chrono::system_clock::now(); auto end = std::chrono::system_clock::now();
auto time = auto time =
std::chrono::duration_cast<std::chrono::microseconds>(end - start) std::chrono::duration_cast<std::chrono::microseconds>(end - start)
.count(); .count();
if (!dbResponse) if (!dbResponse)
{ {
response["error"] = "no response from db"; return Status{Error::rpcACT_NOT_FOUND};
} }
ripple::STLedgerEntry sle{ ripple::STLedgerEntry sle{
ripple::SerialIter{dbResponse->data(), dbResponse->size()}, key.key}; ripple::SerialIter{dbResponse->data(), dbResponse->size()}, key.key};
if (!key.check(sle)) if (!key.check(sle))
{ return Status{Error::rpcDB_DESERIALIZATION};
response["error"] = "error fetching record from db";
return response; // if (!binary)
} // response["account_data"] = getJson(sle);
else // else
{ // response["account_data"] = ripple::strHex(*dbResponse);
response["success"] = "fetched successfully!"; // response["db_time"] = time;
if (!binary)
response["object"] = toJson(sle); response["account_data"] = toJson(sle);
else response["ledger_hash"] = ripple::strHex(lgrInfo.hash);
response["object"] = ripple::strHex(*dbResponse); response["ledger_index"] = lgrInfo.seq;
response["db_time"] = time;
return response;
}
// Return SignerList(s) if that is requested. // Return SignerList(s) if that is requested.
/* /*
@@ -124,7 +110,7 @@ doAccountInfo(
// support multiple SignerLists on one account. // support multiple SignerLists on one account.
auto const sleSigners = ledger->read(keylet::signers(accountID)); auto const sleSigners = ledger->read(keylet::signers(accountID));
if (sleSigners) if (sleSigners)
jvSignerList.append(sleSigners->toJson(JsonOptions::none)); jvSignerList.append(sleSigners->getJson(JsonOptions::none));
result[jss::account_data][jss::signer_lists] = result[jss::account_data][jss::signer_lists] =
std::move(jvSignerList); std::move(jvSignerList);
@@ -134,3 +120,4 @@ doAccountInfo(
return response; return response;
} }
} //namespace RPC

View File

@@ -1,21 +1,27 @@
#include <ripple/app/ledger/Ledger.h>
#include <ripple/app/paths/RippleState.h> #include <ripple/app/paths/RippleState.h>
#include <ripple/app/ledger/Ledger.h>
#include <ripple/basics/StringUtilities.h> #include <ripple/basics/StringUtilities.h>
#include <ripple/protocol/ErrorCodes.h> #include <ripple/protocol/ErrorCodes.h>
#include <ripple/protocol/Indexes.h> #include <ripple/protocol/Indexes.h>
#include <ripple/protocol/STLedgerEntry.h> #include <ripple/protocol/STLedgerEntry.h>
#include <ripple/protocol/jss.h> #include <ripple/protocol/jss.h>
#include <boost/json.hpp> #include <boost/json.hpp>
#include <handlers/methods/Account.h>
#include <algorithm> #include <algorithm>
#include <backend/BackendInterface.h>
#include <handlers/RPCHelpers.h> #include <handlers/RPCHelpers.h>
#include <backend/BackendInterface.h>
#include <backend/DBHelpers.h>
namespace RPC
{
void void
addLine( addLine(
boost::json::array& jsonLines, boost::json::array& jsonLines,
ripple::SLE const& line, ripple::SLE const& line,
ripple::AccountID const& account, ripple::AccountID const& account,
boost::optional<ripple::AccountID> const& peerAccount) std::optional<ripple::AccountID> const& peerAccount)
{ {
auto flags = line.getFieldU32(ripple::sfFlags); auto flags = line.getFieldU32(ripple::sfFlags);
auto lowLimit = line.getFieldAmount(ripple::sfLowLimit); auto lowLimit = line.getFieldAmount(ripple::sfLowLimit);
@@ -38,19 +44,16 @@ addLine(
if (peerAccount and peerAccount != lineAccountIDPeer) if (peerAccount and peerAccount != lineAccountIDPeer)
return; return;
bool lineAuth = if (!viewLowest)
flags & (viewLowest ? ripple::lsfLowAuth : ripple::lsfHighAuth); balance.negate();
bool lineAuthPeer =
flags & (!viewLowest ? ripple::lsfLowAuth : ripple::lsfHighAuth); bool lineAuth = flags & (viewLowest ? ripple::lsfLowAuth : ripple::lsfHighAuth);
bool lineNoRipple = bool lineAuthPeer = flags & (!viewLowest ? ripple::lsfLowAuth : ripple::lsfHighAuth);
flags & (viewLowest ? ripple::lsfLowNoRipple : ripple::lsfHighNoRipple); bool lineNoRipple = flags & (viewLowest ? ripple::lsfLowNoRipple : ripple::lsfHighNoRipple);
bool lineDefaultRipple = flags & ripple::lsfDefaultRipple; bool lineDefaultRipple = flags & ripple::lsfDefaultRipple;
bool lineNoRipplePeer = flags & bool lineNoRipplePeer = flags & (!viewLowest ? ripple::lsfLowNoRipple : ripple::lsfHighNoRipple);
(!viewLowest ? ripple::lsfLowNoRipple : ripple::lsfHighNoRipple); bool lineFreeze = flags & (viewLowest ? ripple::lsfLowFreeze : ripple::lsfHighFreeze);
bool lineFreeze = bool lineFreezePeer = flags & (!viewLowest ? ripple::lsfLowFreeze : ripple::lsfHighFreeze);
flags & (viewLowest ? ripple::lsfLowFreeze : ripple::lsfHighFreeze);
bool lineFreezePeer =
flags & (!viewLowest ? ripple::lsfLowFreeze : ripple::lsfHighFreeze);
ripple::STAmount const& saBalance(balance); ripple::STAmount const& saBalance(balance);
ripple::STAmount const& saLimit(lineLimit); ripple::STAmount const& saLimit(lineLimit);
@@ -80,98 +83,67 @@ addLine(
jsonLines.push_back(jPeer); jsonLines.push_back(jPeer);
} }
boost::json::object Result
doAccountLines( doAccountLines(Context const& context)
boost::json::object const& request,
BackendInterface const& backend)
{ {
boost::json::object response; auto request = context.params;
boost::json::object response = {};
auto ledgerSequence = ledgerSequenceFromRequest(request, backend); auto v = ledgerInfoFromRequest(context);
if (!ledgerSequence) if (auto status = std::get_if<Status>(&v))
{ return *status;
response["error"] = "Empty database";
return response;
}
if (!request.contains("account")) auto lgrInfo = std::get<ripple::LedgerInfo>(v);
{
response["error"] = "Must contain account";
return response;
}
if (!request.at("account").is_string()) if(!request.contains("account"))
{ return Status{Error::rpcINVALID_PARAMS, "missingAccount"};
response["error"] = "Account must be a string";
return response;
}
ripple::AccountID accountID; if(!request.at("account").is_string())
auto parsed = ripple::parseBase58<ripple::AccountID>( return Status{Error::rpcINVALID_PARAMS, "accountNotString"};
request.at("account").as_string().c_str());
if (!parsed) auto accountID =
{ accountFromStringStrict(request.at("account").as_string().c_str());
response["error"] = "Invalid account";
return response;
}
accountID = *parsed; if (!accountID)
return Status{Error::rpcINVALID_PARAMS, "malformedAccount"};
boost::optional<ripple::AccountID> peerAccount; std::optional<ripple::AccountID> peerAccount;
if (request.contains("peer")) if (request.contains("peer"))
{ {
if (!request.at("peer").is_string()) if (!request.at("peer").is_string())
{ return Status{Error::rpcINVALID_PARAMS, "peerNotString"};
response["error"] = "peer should be a string";
return response;
}
peerAccount = ripple::parseBase58<ripple::AccountID>( peerAccount = accountFromStringStrict(
request.at("peer").as_string().c_str()); request.at("peer").as_string().c_str());
if (!peerAccount) if (!peerAccount)
{ return Status{Error::rpcINVALID_PARAMS, "peerMalformed"};
response["error"] = "Invalid peer account";
return response;
}
} }
std::uint32_t limit = 200; std::uint32_t limit = 200;
if (request.contains("limit")) if (request.contains("limit"))
{ {
if (!request.at("limit").is_int64()) if(!request.at("limit").is_int64())
{ return Status{Error::rpcINVALID_PARAMS, "limitNotInt"};
response["error"] = "limit must be integer";
return response;
}
limit = request.at("limit").as_int64(); limit = request.at("limit").as_int64();
if (limit <= 0) if (limit <= 0)
{ return Status{Error::rpcINVALID_PARAMS, "limitNotPositive"};
response["error"] = "limit must be positive";
return response;
}
} }
ripple::uint256 cursor = beast::zero; ripple::uint256 cursor;
if (request.contains("cursor")) if (request.contains("cursor"))
{ {
if (!request.at("cursor").is_string()) if(!request.at("cursor").is_string())
{ return Status{Error::rpcINVALID_PARAMS, "cursorNotString"};
response["error"] = "limit must be string";
return response; if (!cursor.parseHex(request.at("cursor").as_string().c_str()))
} return Status{Error::rpcINVALID_PARAMS, "malformedCursor"};
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["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); response["lines"] = boost::json::value(boost::json::array_kind);
boost::json::array& jsonLines = response.at("lines").as_array(); boost::json::array& jsonLines = response.at("lines").as_array();
@@ -183,17 +155,24 @@ doAccountLines(
return false; return false;
} }
addLine(jsonLines, sle, accountID, peerAccount); addLine(jsonLines, sle, *accountID, peerAccount);
} }
return true; return true;
}; };
auto nextCursor = traverseOwnedNodes( auto nextCursor =
backend, accountID, *ledgerSequence, cursor, addToResponse); traverseOwnedNodes(
*context.backend,
*accountID,
lgrInfo.seq,
cursor,
addToResponse);
if (nextCursor) if (nextCursor)
response["next_cursor"] = ripple::strHex(*nextCursor); response["marker"] = ripple::strHex(*nextCursor);
return response; return response;
} }
} // namespace RPC

View File

@@ -0,0 +1,123 @@
#include <ripple/app/paths/RippleState.h>
#include <ripple/app/ledger/Ledger.h>
#include <ripple/basics/StringUtilities.h>
#include <ripple/protocol/ErrorCodes.h>
#include <ripple/protocol/Indexes.h>
#include <ripple/protocol/STLedgerEntry.h>
#include <ripple/protocol/jss.h>
#include <boost/json.hpp>
#include <algorithm>
#include <handlers/RPCHelpers.h>
#include <handlers/methods/Account.h>
#include <backend/BackendInterface.h>
#include <backend/DBHelpers.h>
namespace RPC
{
std::unordered_map<std::string, ripple::LedgerEntryType> 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<Status>(&v))
return *status;
auto lgrInfo = std::get<ripple::LedgerInfo>(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<ripple::LedgerEntryType> 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

View File

@@ -1,5 +1,5 @@
#include <ripple/app/ledger/Ledger.h>
#include <ripple/app/paths/RippleState.h> #include <ripple/app/paths/RippleState.h>
#include <ripple/app/ledger/Ledger.h>
#include <ripple/basics/StringUtilities.h> #include <ripple/basics/StringUtilities.h>
#include <ripple/protocol/ErrorCodes.h> #include <ripple/protocol/ErrorCodes.h>
#include <ripple/protocol/Indexes.h> #include <ripple/protocol/Indexes.h>
@@ -7,8 +7,14 @@
#include <ripple/protocol/jss.h> #include <ripple/protocol/jss.h>
#include <boost/json.hpp> #include <boost/json.hpp>
#include <algorithm> #include <algorithm>
#include <backend/BackendInterface.h>
#include <handlers/RPCHelpers.h> #include <handlers/RPCHelpers.h>
#include <handlers/methods/Account.h>
#include <backend/BackendInterface.h>
#include <backend/DBHelpers.h>
namespace RPC
{
void void
addOffer(boost::json::array& offersJson, ripple::SLE const& offer) addOffer(boost::json::array& offersJson, ripple::SLE const& offer)
@@ -57,80 +63,56 @@ addOffer(boost::json::array& offersJson, ripple::SLE const& offer)
offersJson.push_back(obj); offersJson.push_back(obj);
}; };
boost::json::object
doAccountOffers( Result
boost::json::object const& request, doAccountOffers(Context const& context)
BackendInterface const& backend)
{ {
boost::json::object response; auto request = context.params;
boost::json::object response = {};
auto ledgerSequence = ledgerSequenceFromRequest(request, backend); auto v = ledgerInfoFromRequest(context);
if (!ledgerSequence) if (auto status = std::get_if<Status>(&v))
{ return *status;
response["error"] = "Empty database";
return response;
}
if (!request.contains("account")) auto lgrInfo = std::get<ripple::LedgerInfo>(v);
{
response["error"] = "Must contain account";
return response;
}
if (!request.at("account").is_string()) if(!request.contains("account"))
{ return Status{Error::rpcINVALID_PARAMS, "missingAccount"};
response["error"] = "Account must be a string";
return response;
}
ripple::AccountID accountID; if(!request.at("account").is_string())
auto parsed = ripple::parseBase58<ripple::AccountID>( return Status{Error::rpcINVALID_PARAMS, "accountNotString"};
request.at("account").as_string().c_str());
if (!parsed) auto accountID =
{ accountFromStringStrict(request.at("account").as_string().c_str());
response["error"] = "Invalid account";
return response; if (!accountID)
} return Status{Error::rpcINVALID_PARAMS, "malformedAccount"};
accountID = *parsed;
std::uint32_t limit = 200; std::uint32_t limit = 200;
if (request.contains("limit")) if (request.contains("limit"))
{ {
if (!request.at("limit").is_int64()) if(!request.at("limit").is_int64())
{ return Status{Error::rpcINVALID_PARAMS, "limitNotInt"};
response["error"] = "limit must be integer";
return response;
}
limit = request.at("limit").as_int64(); limit = request.at("limit").as_int64();
if (limit <= 0) if (limit <= 0)
{ return Status{Error::rpcINVALID_PARAMS, "limitNotPositive"};
response["error"] = "limit must be positive";
return response;
}
} }
ripple::uint256 cursor = beast::zero; ripple::uint256 cursor;
if (request.contains("cursor")) if (request.contains("cursor"))
{ {
if (!request.at("cursor").is_string()) if(!request.at("cursor").is_string())
{ return Status{Error::rpcINVALID_PARAMS, "cursorNotString"};
response["error"] = "limit must be string";
return response; if (!cursor.parseHex(request.at("cursor").as_string().c_str()))
} return Status{Error::rpcINVALID_PARAMS, "malformedCursor"};
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["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); response["offers"] = boost::json::value(boost::json::array_kind);
boost::json::array& jsonLines = response.at("offers").as_array(); boost::json::array& jsonLines = response.at("offers").as_array();
@@ -148,11 +130,18 @@ doAccountOffers(
return true; return true;
}; };
auto nextCursor = traverseOwnedNodes( auto nextCursor =
backend, accountID, *ledgerSequence, cursor, addToResponse); traverseOwnedNodes(
*context.backend,
*accountID,
lgrInfo.seq,
cursor,
addToResponse);
if (nextCursor) if (nextCursor)
response["next_cursor"] = ripple::strHex(*nextCursor); response["marker"] = ripple::strHex(*nextCursor);
return response; return response;
} }
} // namespace RPC

View File

@@ -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 <handlers/RPCHelpers.h>
#include <handlers/methods/Transaction.h>
#include <backend/BackendInterface.h>
#include <backend/Pg.h>
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<std::uint32_t>(request.at("ledger_index_min"));
}
std::optional<Backend::AccountTransactionsCursor> cursor;
cursor = {context.range.maxSequence, 0};
if (request.contains("cursor"))
{
auto const& obj = request.at("cursor").as_object();
std::optional<std::uint32_t> transactionIndex = {};
if (obj.contains("seq"))
{
if (!obj.at("seq").is_int64())
return Status{Error::rpcINVALID_PARAMS, "transactionIndexNotInt"};
transactionIndex = value_to<std::uint32_t>(obj.at("seq"));
}
std::optional<std::uint32_t> ledgerIndex = {};
if (obj.contains("ledger"))
{
if (!obj.at("ledger").is_int64())
return Status{Error::rpcINVALID_PARAMS, "transactionIndexNotInt"};
transactionIndex = value_to<std::uint32_t>(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<std::uint32_t>(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

View File

@@ -0,0 +1,325 @@
#include <ripple/app/ledger/Ledger.h>
#include <ripple/basics/StringUtilities.h>
#include <ripple/protocol/ErrorCodes.h>
#include <ripple/protocol/Indexes.h>
#include <ripple/protocol/STLedgerEntry.h>
#include <ripple/protocol/jss.h>
#include <boost/json.hpp>
#include <algorithm>
#include <handlers/RPCHelpers.h>
#include <handlers/methods/Exchange.h>
#include <backend/BackendInterface.h>
#include <backend/DBHelpers.h>
#include <backend/Pg.h>
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<Status>(&v))
return *status;
auto lgrInfo = std::get<ripple::LedgerInfo>(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<ripple::AccountID> 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<ripple::AccountID, ripple::STAmount> 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

View File

@@ -23,9 +23,13 @@
#include <ripple/protocol/STAccount.h> #include <ripple/protocol/STAccount.h>
#include <ripple/protocol/jss.h> #include <ripple/protocol/jss.h>
#include <ripple/resource/Fees.h> #include <ripple/resource/Fees.h>
#include <handlers/methods/Channel.h>
#include <handlers/RPCHelpers.h> #include <handlers/RPCHelpers.h>
#include <optional> #include <optional>
namespace RPC
{
void void
serializePayChanAuthorization( serializePayChanAuthorization(
ripple::Serializer& msg, ripple::Serializer& msg,
@@ -37,57 +41,42 @@ serializePayChanAuthorization(
msg.add64(amt.drops()); msg.add64(amt.drops());
} }
boost::json::object Result
doChannelAuthorize(boost::json::object const& request) doChannelAuthorize(Context const& context)
{ {
boost::json::object response; auto request = context.params;
boost::json::object response = {};
if(!request.contains("channel_id")) if(!request.contains("channel_id"))
{ return Status{Error::rpcINVALID_PARAMS, "missingChannelID"};
response["error"] = "missing field channel_id";
return response; if(!request.at("channel_id").is_string())
} return Status{Error::rpcINVALID_PARAMS, "channelIDNotString"};
if(!request.contains("amount")) if(!request.contains("amount"))
{ return Status{Error::rpcINVALID_PARAMS, "missingAmount"};
response["error"] = "missing field amount";
return response; if(!request.at("amount").is_string())
} return Status{Error::rpcINVALID_PARAMS, "amountNotString"};
if (!request.contains("key_type") && !request.contains("secret")) if (!request.contains("key_type") && !request.contains("secret"))
{ return Status{Error::rpcINVALID_PARAMS, "missingKeyTypeOrSecret"};
response["error"] = "missing field secret";
return response;
}
boost::json::value error = nullptr; auto v = keypairFromRequst(request);
auto const [pk, sk] = keypairFromRequst(request, error); if (auto status = std::get_if<Status>(&v))
if (!error.is_null()) return *status;
{
response["error"] = error; auto const [pk, sk] = std::get<std::pair<ripple::PublicKey, ripple::SecretKey>>(v);
return response;
}
ripple::uint256 channelId; ripple::uint256 channelId;
if (!channelId.parseHex(request.at("channel_id").as_string().c_str())) if (!channelId.parseHex(request.at("channel_id").as_string().c_str()))
{ return Status{Error::rpcCHANNEL_MALFORMED, "malformedChannelID"};
response["error"] = "channel id malformed";
return response;
}
if (!request.at("amount").is_string())
{
response["error"] = "channel amount malformed";
return response;
}
auto optDrops = auto optDrops =
ripple::to_uint64(request.at("amount").as_string().c_str()); ripple::to_uint64(request.at("amount").as_string().c_str());
if (!optDrops) if (!optDrops)
{ return Status{Error::rpcCHANNEL_AMT_MALFORMED, "couldNotParseAmount"};
response["error"] = "could not parse channel amount";
return response;
}
std::uint64_t drops = *optDrops; std::uint64_t drops = *optDrops;
@@ -101,9 +90,10 @@ doChannelAuthorize(boost::json::object const& request)
} }
catch (std::exception&) catch (std::exception&)
{ {
response["error"] = "Exception occurred during signing."; return Status{Error::rpcINTERNAL};
return response;
} }
return response; return response;
} }
} // namesace RPC

View File

@@ -23,36 +23,42 @@
#include <ripple/protocol/STAccount.h> #include <ripple/protocol/STAccount.h>
#include <ripple/protocol/jss.h> #include <ripple/protocol/jss.h>
#include <ripple/resource/Fees.h> #include <ripple/resource/Fees.h>
#include <handlers/methods/Channel.h>
#include <handlers/RPCHelpers.h> #include <handlers/RPCHelpers.h>
#include <optional> #include <optional>
boost::json::object namespace RPC
doChannelVerify(boost::json::object const& request)
{ {
boost::json::object response;
Result
doChannelVerify(Context const& context)
{
auto request = context.params;
boost::json::object response = {};
if(!request.contains("channel_id")) if(!request.contains("channel_id"))
{ return Status{Error::rpcINVALID_PARAMS, "missingChannelID"};
response["error"] = "missing field channel_id";
return response; if(!request.at("channel_id").is_string())
} return Status{Error::rpcINVALID_PARAMS, "channelIDNotString"};
if(!request.contains("amount")) if(!request.contains("amount"))
{ return Status{Error::rpcINVALID_PARAMS, "missingAmount"};
response["error"] = "missing field amount";
return response; if(!request.at("amount").is_string())
} return Status{Error::rpcINVALID_PARAMS, "amountNotString"};
if (!request.contains("signature")) if (!request.contains("signature"))
{ return Status{Error::rpcINVALID_PARAMS, "missingSignature"};
response["error"] = "missing field signature";
return response; if(!request.at("signature").is_string())
} return Status{Error::rpcINVALID_PARAMS, "signatureNotString"};
if (!request.contains("public_key")) if (!request.contains("public_key"))
{ return Status{Error::rpcINVALID_PARAMS, "missingPublicKey"};
response["error"] = "missing field public_key";
return response; if(!request.at("public_key").is_string())
} return Status{Error::rpcINVALID_PARAMS, "publicKeyNotString"};
boost::optional<ripple::PublicKey> pk; boost::optional<ripple::PublicKey> pk;
{ {
@@ -63,15 +69,11 @@ doChannelVerify(boost::json::object const& request)
{ {
auto pkHex = ripple::strUnHex(strPk); auto pkHex = ripple::strUnHex(strPk);
if (!pkHex) if (!pkHex)
{ return Status{Error::rpcPUBLIC_MALFORMED, "malformedPublicKey"};
response["error"] = "malformed public key";
return response;
}
auto const pkType = ripple::publicKeyType(ripple::makeSlice(*pkHex)); auto const pkType = ripple::publicKeyType(ripple::makeSlice(*pkHex));
if (!pkType) if (!pkType)
{ return Status{Error::rpcPUBLIC_MALFORMED, "invalidKeyType"};
response["error"] = "invalid key type";
}
pk.emplace(ripple::makeSlice(*pkHex)); pk.emplace(ripple::makeSlice(*pkHex));
} }
@@ -79,35 +81,20 @@ doChannelVerify(boost::json::object const& request)
ripple::uint256 channelId; ripple::uint256 channelId;
if (!channelId.parseHex(request.at("channel_id").as_string().c_str())) if (!channelId.parseHex(request.at("channel_id").as_string().c_str()))
{ return Status{Error::rpcCHANNEL_MALFORMED, "malformedChannelID"};
response["error"] = "channel id malformed";
return response;
}
auto optDrops = auto optDrops =
ripple::to_uint64(request.at("amount").as_string().c_str()); ripple::to_uint64(request.at("amount").as_string().c_str());
if (!optDrops) if (!optDrops)
{ return Status{Error::rpcCHANNEL_AMT_MALFORMED, "couldNotParseAmount"};
response["error"] = "could not parse channel amount";
return response;
}
std::uint64_t drops = *optDrops; 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()); auto sig = ripple::strUnHex(request.at("signature").as_string().c_str());
if (!sig || !sig->size()) if (!sig || !sig->size())
{ return Status{Error::rpcINVALID_PARAMS, "invalidSignature"};
response["error"] = "Invalid signature";
return response;
}
ripple::Serializer msg; ripple::Serializer msg;
ripple::serializePayChanAuthorization(msg, channelId, ripple::XRPAmount(drops)); ripple::serializePayChanAuthorization(msg, channelId, ripple::XRPAmount(drops));
@@ -117,3 +104,5 @@ doChannelVerify(boost::json::object const& request)
return response; return response;
} }
} // namespace RPC

View File

@@ -0,0 +1,123 @@
#include <handlers/methods/Ledger.h>
#include <handlers/RPCHelpers.h>
#include <backend/BackendInterface.h>
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<Status>(&v))
return *status;
auto lgrInfo = std::get<ripple::LedgerInfo>(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;
}
}

View File

@@ -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 <ripple/app/ledger/LedgerToJson.h>
#include <ripple/protocol/STLedgerEntry.h>
#include <boost/json.hpp>
#include <handlers/methods/Ledger.h>
#include <handlers/RPCHelpers.h>
#include <backend/BackendInterface.h>
// 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<int>(request.at("limit"));
}
std::optional<ripple::uint256> 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<Status>(&v))
return *status;
auto lgrInfo = std::get<ripple::LedgerInfo>(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<std::chrono::microseconds>(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<Backend::LedgerObject>& results = page.objects;
std::optional<ripple::uint256> 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

View File

@@ -0,0 +1,360 @@
#include <ripple/protocol/Indexes.h>
#include <ripple/protocol/STLedgerEntry.h>
#include <boost/json.hpp>
#include <handlers/methods/Ledger.h>
#include <handlers/RPCHelpers.h>
#include <backend/BackendInterface.h>
// {
// ledger_hash : <ledger>
// ledger_index : <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<Status>(&v))
return *status;
auto lgrInfo = std::get<ripple::LedgerInfo>(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<ripple::AccountID>(
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<ripple::AccountID>(
deposit_preauth.at("owner").as_string().c_str());
auto const authorized = ripple::parseBase58<ripple::AccountID>(
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<std::uint64_t>(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<ripple::AccountID>(
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<ripple::AccountID>(
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<ripple::AccountID>(
offer.at("account").as_string().c_str());
if (!id)
return Status{Error::rpcINVALID_PARAMS, "malformedAccount"};
else
{
std::uint32_t seq = value_to<std::uint32_t>(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<ripple::AccountID>(
state.at("accounts").as_array().at(0).as_string().c_str());
auto const id2 = ripple::parseBase58<ripple::AccountID>(
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<ripple::AccountID>(
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<std::chrono::microseconds>(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;
}
}

View File

@@ -1,22 +1,27 @@
#include <handlers/methods/Ledger.h>
#include <handlers/RPCHelpers.h> #include <handlers/RPCHelpers.h>
#include <backend/BackendInterface.h> #include <backend/BackendInterface.h>
boost::json::object namespace RPC
doLedgerRange(
boost::json::object const& request,
BackendInterface const& backend)
{ {
boost::json::object response;
auto range = backend.fetchLedgerRange(); Result
doLedgerRange(Context const& context)
{
boost::json::object response = {};
auto range = context.backend->fetchLedgerRange();
if (!range) if (!range)
{ {
response["error"] = "No data"; return Status{Error::rpcNOT_READY, "rangeNotFound"};
} }
else else
{ {
response["ledger_index_min"] = range->minSequence; response["ledger_index_min"] = range->minSequence;
response["ledger_index_max"] = range->maxSequence; response["ledger_index_max"] = range->maxSequence;
} }
return response; return response;
} }
}

View File

@@ -1,45 +1,40 @@
#include <boost/json.hpp> #include <boost/json.hpp>
#include <webserver/WsBase.h>
#include <webserver/SubscriptionManager.h>
#include <handlers/methods/Subscribe.h>
#include <handlers/RPCHelpers.h> #include <handlers/RPCHelpers.h>
#include <server/WsBase.h>
#include <server/SubscriptionManager.h>
static std::unordered_set<std::string> validStreams{ namespace RPC
{
static std::unordered_set<std::string> validStreams {
"ledger", "ledger",
"transactions", "transactions",
"transactions_proposed"}; "transactions_proposed" };
boost::json::value Status
validateStreams(boost::json::object const& request) 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(); boost::json::array const& streams = request.at("streams").as_array();
for (auto const& stream : streams) for (auto const& stream : streams)
{ {
if (!stream.is_string()) if (!stream.is_string())
{ return Status{Error::rpcINVALID_PARAMS, "streamNotString"};
return "streams must be strings";
}
std::string s = stream.as_string().c_str(); std::string s = stream.as_string().c_str();
if (validStreams.find(s) == validStreams.end()) if (validStreams.find(s) == validStreams.end())
{ return Status{Error::rpcINVALID_PARAMS, "invalidStream" + s};
return boost::json::string("invalid stream " + s);
}
} }
return nullptr; return OK;
} }
void void
subscribeToStreams( subscribeToStreams(
boost::json::object const& request, boost::json::object const& request,
std::shared_ptr<WsBase>& session, std::shared_ptr<WsBase> session,
SubscriptionManager& manager) SubscriptionManager& manager)
{ {
boost::json::array const& streams = request.at("streams").as_array(); boost::json::array const& streams = request.at("streams").as_array();
@@ -62,7 +57,7 @@ subscribeToStreams(
void void
unsubscribeToStreams( unsubscribeToStreams(
boost::json::object const& request, boost::json::object const& request,
std::shared_ptr<WsBase>& session, std::shared_ptr<WsBase> session,
SubscriptionManager& manager) SubscriptionManager& manager)
{ {
boost::json::array const& streams = request.at("streams").as_array(); boost::json::array const& streams = request.at("streams").as_array();
@@ -82,34 +77,28 @@ unsubscribeToStreams(
} }
} }
boost::json::value Status
validateAccounts( validateAccounts(boost::json::array const& accounts)
boost::json::object const& request,
boost::json::array const& accounts)
{ {
for (auto const& account : accounts) for (auto const& account : accounts)
{ {
if (!account.is_string()) if (!account.is_string())
{ return Status{Error::rpcINVALID_PARAMS, "accountNotString"};
return "account must be strings";
}
std::string s = account.as_string().c_str(); std::string s = account.as_string().c_str();
auto id = accountFromStringStrict(s); auto id = accountFromStringStrict(s);
if (!id) if (!id)
{ return Status{Error::rpcINVALID_PARAMS, "invalidAccount" + s};
return boost::json::string("invalid account " + s);
}
} }
return nullptr; return OK;
} }
void void
subscribeToAccounts( subscribeToAccounts(
boost::json::object const& request, boost::json::object const& request,
std::shared_ptr<WsBase>& session, std::shared_ptr<WsBase> session,
SubscriptionManager& manager) SubscriptionManager& manager)
{ {
boost::json::array const& accounts = request.at("accounts").as_array(); boost::json::array const& accounts = request.at("accounts").as_array();
@@ -120,7 +109,7 @@ subscribeToAccounts(
auto accountID = accountFromStringStrict(s); auto accountID = accountFromStringStrict(s);
if (!accountID) if(!accountID)
{ {
assert(false); assert(false);
continue; continue;
@@ -133,7 +122,7 @@ subscribeToAccounts(
void void
unsubscribeToAccounts( unsubscribeToAccounts(
boost::json::object const& request, boost::json::object const& request,
std::shared_ptr<WsBase>& session, std::shared_ptr<WsBase> session,
SubscriptionManager& manager) SubscriptionManager& manager)
{ {
boost::json::array const& accounts = request.at("accounts").as_array(); boost::json::array const& accounts = request.at("accounts").as_array();
@@ -144,7 +133,7 @@ unsubscribeToAccounts(
auto accountID = accountFromStringStrict(s); auto accountID = accountFromStringStrict(s);
if (!accountID) if(!accountID)
{ {
assert(false); assert(false);
continue; continue;
@@ -157,11 +146,10 @@ unsubscribeToAccounts(
void void
subscribeToAccountsProposed( subscribeToAccountsProposed(
boost::json::object const& request, boost::json::object const& request,
std::shared_ptr<WsBase>& session, std::shared_ptr<WsBase> session,
SubscriptionManager& manager) SubscriptionManager& manager)
{ {
boost::json::array const& accounts = boost::json::array const& accounts = request.at("accounts_proposed").as_array();
request.at("accounts_proposed").as_array();
for (auto const& account : accounts) for (auto const& account : accounts)
{ {
@@ -169,7 +157,7 @@ subscribeToAccountsProposed(
auto accountID = ripple::parseBase58<ripple::AccountID>(s); auto accountID = ripple::parseBase58<ripple::AccountID>(s);
if (!accountID) if(!accountID)
{ {
assert(false); assert(false);
continue; continue;
@@ -182,11 +170,10 @@ subscribeToAccountsProposed(
void void
unsubscribeToAccountsProposed( unsubscribeToAccountsProposed(
boost::json::object const& request, boost::json::object const& request,
std::shared_ptr<WsBase>& session, std::shared_ptr<WsBase> session,
SubscriptionManager& manager) SubscriptionManager& manager)
{ {
boost::json::array const& accounts = boost::json::array const& accounts = request.at("accounts_proposed").as_array();
request.at("accounts_proposed").as_array();
for (auto const& account : accounts) for (auto const& account : accounts)
{ {
@@ -194,7 +181,7 @@ unsubscribeToAccountsProposed(
auto accountID = ripple::parseBase58<ripple::AccountID>(s); auto accountID = ripple::parseBase58<ripple::AccountID>(s);
if (!accountID) if(!accountID)
{ {
assert(false); assert(false);
continue; continue;
@@ -204,128 +191,114 @@ unsubscribeToAccountsProposed(
} }
} }
boost::json::object
doSubscribe( Result
boost::json::object const& request, doSubscribe(Context const& context)
std::shared_ptr<WsBase>& session,
SubscriptionManager& manager)
{ {
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()) auto status = validateStreams(request);
{
response["error"] = error; if (status)
return response; return status;
}
} }
if (request.contains("accounts")) if (request.contains("accounts"))
{ {
if (!request.at("accounts").is_array()) if (!request.at("accounts").is_array())
{ return Status{Error::rpcINVALID_PARAMS, "accountsNotArray"};
response["error"] = "accounts must be array";
return response;
}
boost::json::array accounts = request.at("accounts").as_array(); boost::json::array accounts = request.at("accounts").as_array();
boost::json::value error = validateAccounts(request, accounts); auto status = validateAccounts(accounts);
if (!error.is_null()) if (status)
{ return status;
response["error"] = error;
return response;
}
} }
if (request.contains("accounts_proposed")) if (request.contains("accounts_proposed"))
{ {
if (!request.at("accounts_proposed").is_array()) if (!request.at("accounts_proposed").is_array())
{ return Status{Error::rpcINVALID_PARAMS, "accountsProposedNotArray"};
response["error"] = "accounts_proposed must be array";
return response;
}
boost::json::array accounts = boost::json::array accounts = request.at("accounts_proposed").as_array();
request.at("accounts_proposed").as_array(); auto status = validateAccounts(accounts);
boost::json::value error = validateAccounts(request, accounts);
if (!error.is_null()) if(status)
{ return status;
response["error"] = error;
return response;
}
} }
if (request.contains("streams")) if (request.contains("streams"))
subscribeToStreams(request, session, manager); subscribeToStreams(request, context.session, *context.subscriptions);
if (request.contains("accounts")) if (request.contains("accounts"))
subscribeToAccounts(request, session, manager); subscribeToAccounts(request, context.session, *context.subscriptions);
if (request.contains("accounts_proposed")) 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; return response;
} }
boost::json::object Result
doUnsubscribe( doUnsubscribe(Context const& context)
boost::json::object const& request,
std::shared_ptr<WsBase>& session,
SubscriptionManager& manager)
{ {
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()) auto status = validateStreams(request);
{
response["error"] = error; if (status)
return response; return status;
}
} }
if (request.contains("accounts")) 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::array accounts = request.at("accounts").as_array();
boost::json::value error = validateAccounts(request, accounts); auto status = validateAccounts(accounts);
if (!error.is_null()) if (status)
{ return status;
response["error"] = error;
return response;
}
} }
if (request.contains("accounts_proposed")) if (request.contains("accounts_proposed"))
{ {
boost::json::array accounts = if (!request.at("accounts_proposed").is_array())
request.at("accounts_proposed").as_array(); return Status{Error::rpcINVALID_PARAMS, "accountsProposedNotArray"};
boost::json::value error = validateAccounts(request, accounts);
if (!error.is_null()) boost::json::array accounts = request.at("accounts_proposed").as_array();
{ auto status = validateAccounts(accounts);
response["error"] = error;
return response; if(status)
} return status;
} }
if (request.contains("streams")) if (request.contains("streams"))
unsubscribeToStreams(request, session, manager); unsubscribeToStreams(request, context.session, *context.subscriptions);
if (request.contains("accounts")) if (request.contains("accounts"))
unsubscribeToAccounts(request, session, manager); unsubscribeToAccounts(request, context.session, *context.subscriptions);
if (request.contains("accounts_proposed")) 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; return response;
} }
} // namespace RPC

View File

@@ -17,60 +17,64 @@
*/ */
//============================================================================== //==============================================================================
#include <backend/BackendInterface.h>
#include <handlers/RPCHelpers.h> #include <handlers/RPCHelpers.h>
#include <handlers/methods/Transaction.h>
#include <backend/BackendInterface.h>
#include <backend/Pg.h>
namespace RPC
{
// { // {
// transaction: <hex> // transaction: <hex>
// } // }
boost::json::object Result
doTx(boost::json::object const& request, BackendInterface const& backend) doTx(Context const& context)
{ {
boost::json::object response; auto request = context.params;
boost::json::object response = {};
if (!request.contains("transaction")) if (!request.contains("transaction"))
{ return Status{Error::rpcINVALID_PARAMS, "specifyTransaction"};
response["error"] = "Please specify a transaction hash";
return response; if (!request.at("transaction").is_string())
} return Status{Error::rpcINVALID_PARAMS, "transactionNotString"};
ripple::uint256 hash; ripple::uint256 hash;
if (!hash.parseHex(request.at("transaction").as_string().c_str())) 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"; if(!request.at("binary").is_bool())
return response; return Status{Error::rpcINVALID_PARAMS, "binaryFlagNotBool"};
binary = request.at("binary").as_bool();
} }
auto range = backend.fetchLedgerRange(); auto range = context.backend->fetchLedgerRange();
if (!range) if (!range)
{ return Status{Error::rpcNOT_READY};
response["error"] = "Database is empty";
return response;
}
auto dbResponse = backend.fetchTransaction(hash); auto dbResponse = context.backend->fetchTransaction(hash);
if (!dbResponse) if (!dbResponse)
{ return Status{Error::rpcTXN_NOT_FOUND};
response["error"] = "Transaction not found in Cassandra";
response["ledger_range"] = std::to_string(range->minSequence) + " - " +
std::to_string(range->maxSequence);
return response;
}
bool binary =
request.contains("binary") ? request.at("binary").as_bool() : false;
if (!binary) if (!binary)
{ {
auto [sttx, meta] = deserializeTxPlusMeta(dbResponse.value()); auto [sttx, meta] = deserializeTxPlusMeta(dbResponse.value());
response["transaction"] = toJson(*sttx); response = toJson(*sttx);
response["metadata"] = toJson(*meta); response["meta"] = toJson(*meta);
} }
else else
{ {
response["transaction"] = ripple::strHex(dbResponse->transaction); response["tx"] = ripple::strHex(dbResponse->transaction);
response["metadata"] = ripple::strHex(dbResponse->metadata); response["metadata"] = ripple::strHex(dbResponse->metadata);
} }
response["ledger_sequence"] = dbResponse->ledgerSequence;
return response; return response;
} }
} // namespace RPC

View File

@@ -1,17 +1,21 @@
// //------------------------------------------------------------------------------
// Copyright (c) 2016-2019 Vinnie Falco (vinnie dot falco at gmail dot com) /*
// This file is part of rippled: https://github.com/ripple/rippled
// Distributed under the Boost Software License, Version 1.0. (See accompanying Copyright (c) 2021 Ripple Labs Inc.
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//
// Official repository: https://github.com/boostorg/beast
//
//------------------------------------------------------------------------------ Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// Example: WebSocket server, asynchronous 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 <boost/asio/dispatch.hpp> #include <boost/asio/dispatch.hpp>
#include <boost/asio/strand.hpp> #include <boost/asio/strand.hpp>
@@ -28,7 +32,7 @@
#include <functional> #include <functional>
#include <iostream> #include <iostream>
#include <memory> #include <memory>
#include <server/listener.h> #include <webserver/Listener.h>
#include <sstream> #include <sstream>
#include <string> #include <string>
#include <thread> #include <thread>
@@ -109,10 +113,10 @@ main(int argc, char* argv[])
// Check command line arguments. // Check command line arguments.
if (argc != 2) if (argc != 2)
{ {
std::cerr << "Usage: websocket-server-async " std::cerr << "Usage: clio_server "
"<config_file> \n" "<config_file> \n"
<< "Example:\n" << "Example:\n"
<< " websocket-server-async config.json \n"; << " clio_server config.json \n";
return EXIT_FAILURE; return EXIT_FAILURE;
} }

View File

@@ -1,136 +0,0 @@
#include <server/Handlers.h>
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<boost::json::object, uint32_t>
buildResponse(
boost::json::object const& request,
std::shared_ptr<BackendInterface> backend,
std::shared_ptr<SubscriptionManager> manager,
std::shared_ptr<ETLLoadBalancer> balancer,
std::shared_ptr<WsBase> 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};
}
}

View File

@@ -1,154 +0,0 @@
#include <boost/asio/dispatch.hpp>
#include <boost/beast/core.hpp>
#include <boost/beast/websocket.hpp>
#include <boost/json.hpp>
#include <boost/log/core.hpp>
#include <boost/log/expressions.hpp>
#include <boost/log/trivial.hpp>
#include <etl/ReportingETL.h>
#include <server/WsBase.h>
#include <iostream>
#include <unordered_map>
#ifndef RIPPLE_REPORTING_HANDLERS_H
#define RIPPLE_REPORTING_HANDLERS_H
class ReportingETL;
class SubscriptionManager;
//------------------------------------------------------------------------------
static std::unordered_set<std::string> 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<std::string, RPCCommand> 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<WsBase>& session,
SubscriptionManager& manager);
boost::json::object
doUnsubscribe(
boost::json::object const& request,
std::shared_ptr<WsBase>& session,
SubscriptionManager& manager);
std::pair<boost::json::object, uint32_t>
buildResponse(
boost::json::object const& request,
std::shared_ptr<BackendInterface> backend,
std::shared_ptr<SubscriptionManager> manager,
std::shared_ptr<ETLLoadBalancer> balancer,
std::shared_ptr<WsBase> session);
#endif // RIPPLE_REPORTING_HANDLERS_H

View File

@@ -36,8 +36,8 @@
#include <string> #include <string>
#include <thread> #include <thread>
#include <server/DOSGuard.h> #include <handlers/Handlers.h>
#include <server/Handlers.h> #include <webserver/DOSGuard.h>
#include <vector> #include <vector>
namespace http = boost::beast::http; namespace http = boost::beast::http;
@@ -77,29 +77,6 @@ httpFail(boost::beast::error_code ec, char const* what)
std::cerr << what << ": " << ec.message() << "\n"; 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 // This function produces an HTTP response for the given
// request. The type of the response object depends on the // request. The type of the response object depends on the
// contents of the request, so the interface requires the // contents of the request, so the interface requires the
@@ -112,9 +89,10 @@ handle_request(
Send&& send, Send&& send,
std::shared_ptr<BackendInterface> backend, std::shared_ptr<BackendInterface> backend,
std::shared_ptr<ETLLoadBalancer> balancer, std::shared_ptr<ETLLoadBalancer> balancer,
DOSGuard& dosGuard) DOSGuard& dosGuard,
std::string const& ip)
{ {
auto const response = [&req]( auto const httpResponse = [&req](
http::status status, http::status status,
std::string content_type, std::string content_type,
std::string message) { std::string message) {
@@ -129,70 +107,108 @@ handle_request(
if (req.method() == http::verb::get && req.body() == "") 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; return;
} }
if (req.method() != http::verb::post) if (req.method() != http::verb::post)
{ return send(httpResponse(
send(response(
http::status::bad_request, "text/html", "Expected a POST request")); 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 try
{ {
BOOST_LOG_TRIVIAL(info) << "Received request: " << req.body(); BOOST_LOG_TRIVIAL(info) << "Received request: " << req.body();
boost::json::object request; boost::json::object request;
std::string responseStr = "";
try try
{ {
request = boost::json::parse(req.body()).as_object(); request = boost::json::parse(req.body()).as_object();
} }
catch (std::runtime_error const& e) catch (std::runtime_error const& e)
{ {
send(response( return send(httpResponse(
http::status::bad_request,
"text/html",
"Cannot parse json in body"));
return;
}
if (!validRequest(request))
{
send(response(
http::status::bad_request, "text/html", "Malformed request"));
return;
}
boost::json::object wsStyleRequest = request.contains("params")
? request.at("params").as_array().at(0).as_object()
: boost::json::object{};
wsStyleRequest["command"] = request["method"];
auto [builtResponse, cost] =
buildResponse(wsStyleRequest, backend, nullptr, balancer, nullptr);
send(response(
http::status::ok, http::status::ok,
"application/json", "application/json",
boost::json::serialize(builtResponse))); boost::json::serialize(
RPC::make_error(RPC::Error::rpcBAD_SYNTAX))));
}
return; 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<RPC::Context> 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<RPC::Status>(&v))
{
auto error = RPC::make_error(status->error);
error["request"] = request;
result = error;
responseStr = boost::json::serialize(error);
}
else
{
result = std::get<boost::json::object>(v);
result["status"] = "success";
result["validated"] = true;
responseStr = boost::json::serialize(response);
}
if (!dosGuard.add(ip, responseStr.size()))
result["warning"] = "Too many requests";
return send(httpResponse(
http::status::ok,
"application/json",
responseStr));
} }
catch (std::exception const& e) catch (std::exception const& e)
{ {
std::cout << e.what() << std::endl; return send(httpResponse(
send(response(
http::status::internal_server_error, http::status::internal_server_error,
"text/html", "application/json",
"Internal server error occurred")); boost::json::serialize(RPC::make_error(RPC::Error::rpcINTERNAL))));
return;
} }
} }
@@ -300,7 +316,6 @@ public:
if (ec) if (ec)
return httpFail(ec, "read"); return httpFail(ec, "read");
auto ip = derived().ip();
if (boost::beast::websocket::is_upgrade(req_)) if (boost::beast::websocket::is_upgrade(req_))
{ {
// Disable the timeout. // Disable the timeout.
@@ -316,9 +331,11 @@ public:
dosGuard_); dosGuard_);
} }
auto ip = derived().ip();
// Send the response // Send the response
handle_request( handle_request(
std::move(req_), lambda_, backend_, balancer_, dosGuard_); std::move(req_), lambda_, backend_, balancer_, dosGuard_, ip);
} }
void void

View File

@@ -20,7 +20,7 @@
#ifndef RIPPLE_REPORTING_HTTP_SESSION_H #ifndef RIPPLE_REPORTING_HTTP_SESSION_H
#define RIPPLE_REPORTING_HTTP_SESSION_H #define RIPPLE_REPORTING_HTTP_SESSION_H
#include <server/HttpBase.h> #include <webserver/HttpBase.h>
namespace http = boost::beast::http; namespace http = boost::beast::http;
namespace net = boost::asio; namespace net = boost::asio;
@@ -35,7 +35,8 @@ class HttpSession : public HttpBase<HttpSession>,
public: public:
// Take ownership of the socket // Take ownership of the socket
explicit HttpSession( explicit
HttpSession(
tcp::socket&& socket, tcp::socket&& socket,
std::shared_ptr<BackendInterface> backend, std::shared_ptr<BackendInterface> backend,
std::shared_ptr<SubscriptionManager> subscriptions, std::shared_ptr<SubscriptionManager> subscriptions,

352
src/webserver/Listener.h Normal file
View File

@@ -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 <boost/asio/dispatch.hpp>
#include <boost/beast/core.hpp>
#include <boost/beast/websocket.hpp>
#include <webserver/HttpSession.h>
#include <webserver/PlainWsSession.h>
#include <webserver/SslHttpSession.h>
#include <webserver/SslWsSession.h>
#include <webserver/SubscriptionManager.h>
#include <iostream>
class SubscriptionManager;
template <class PlainSession, class SslSession>
class Detector
: public std::enable_shared_from_this<Detector<PlainSession, SslSession>>
{
using std::enable_shared_from_this<
Detector<PlainSession, SslSession>>::shared_from_this;
boost::beast::tcp_stream stream_;
std::optional<std::reference_wrapper<ssl::context>> ctx_;
std::shared_ptr<BackendInterface> backend_;
std::shared_ptr<SubscriptionManager> subscriptions_;
std::shared_ptr<ETLLoadBalancer> balancer_;
DOSGuard& dosGuard_;
boost::beast::flat_buffer buffer_;
public:
Detector(
tcp::socket&& socket,
std::optional<std::reference_wrapper<ssl::context>> ctx,
std::shared_ptr<BackendInterface> backend,
std::shared_ptr<SubscriptionManager> subscriptions,
std::shared_ptr<ETLLoadBalancer> 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<SslSession>(
stream_.release_socket(),
*ctx_,
backend_,
subscriptions_,
balancer_,
dosGuard_,
std::move(buffer_))
->run();
return;
}
// Launch plain session
std::make_shared<PlainSession>(
stream_.release_socket(),
backend_,
subscriptions_,
balancer_,
dosGuard_,
std::move(buffer_))
->run();
}
};
void
make_websocket_session(
boost::beast::tcp_stream stream,
http::request<http::string_body> req,
boost::beast::flat_buffer buffer,
std::shared_ptr<BackendInterface> backend,
std::shared_ptr<SubscriptionManager> subscriptions,
std::shared_ptr<ETLLoadBalancer> balancer,
DOSGuard& dosGuard)
{
std::make_shared<WsUpgrader>(
std::move(stream),
backend,
subscriptions,
balancer,
dosGuard,
std::move(buffer),
std::move(req))
->run();
}
void
make_websocket_session(
boost::beast::ssl_stream<boost::beast::tcp_stream> stream,
http::request<http::string_body> req,
boost::beast::flat_buffer buffer,
std::shared_ptr<BackendInterface> backend,
std::shared_ptr<SubscriptionManager> subscriptions,
std::shared_ptr<ETLLoadBalancer> balancer,
DOSGuard& dosGuard)
{
std::make_shared<SslWsUpgrader>(
std::move(stream),
backend,
subscriptions,
balancer,
dosGuard,
std::move(buffer),
std::move(req))
->run();
}
template <class PlainSession, class SslSession>
class Listener
: public std::enable_shared_from_this<Listener<PlainSession, SslSession>>
{
using std::enable_shared_from_this<
Listener<PlainSession, SslSession>>::shared_from_this;
net::io_context& ioc_;
std::optional<ssl::context> ctx_;
tcp::acceptor acceptor_;
std::shared_ptr<BackendInterface> backend_;
std::shared_ptr<SubscriptionManager> subscriptions_;
std::shared_ptr<ETLLoadBalancer> balancer_;
DOSGuard& dosGuard_;
public:
Listener(
net::io_context& ioc,
std::optional<ssl::context>&& ctx,
tcp::endpoint endpoint,
std::shared_ptr<BackendInterface> backend,
std::shared_ptr<SubscriptionManager> subscriptions,
std::shared_ptr<ETLLoadBalancer> 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<ssl::context>>{ctx_.value()}
: std::nullopt;
// Create the detector session and run it
std::make_shared<Detector<PlainSession, SslSession>>(
std::move(socket),
ctxRef,
backend_,
subscriptions_,
balancer_,
dosGuard_)
->run();
}
// Accept another connection
do_accept();
}
};
namespace Server {
std::optional<ssl::context>
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<HttpSession, SslHttpSession>;
static std::shared_ptr<HttpServer>
make_HttpServer(
boost::json::object const& config,
boost::asio::io_context& ioc,
std::shared_ptr<BackendInterface> backend,
std::shared_ptr<SubscriptionManager> subscriptions,
std::shared_ptr<ETLLoadBalancer> balancer,
DOSGuard& dosGuard)
{
if (!config.contains("server"))
return nullptr;
auto const& serverConfig = config.at("server").as_object();
std::optional<ssl::context> 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<unsigned short>(serverConfig.at("port").as_int64());
auto server = std::make_shared<HttpServer>(
ioc,
std::move(sslCtx),
boost::asio::ip::tcp::endpoint{address, port},
backend,
subscriptions,
balancer,
dosGuard);
server->run();
return server;
}
} // namespace Server
#endif // LISTENER_H

View File

@@ -27,9 +27,9 @@
#include <boost/beast/websocket/ssl.hpp> #include <boost/beast/websocket/ssl.hpp>
#include <etl/ReportingETL.h> #include <etl/ReportingETL.h>
#include <server/Handlers.h> #include <handlers/Handlers.h>
#include <server/WsBase.h> #include <webserver/WsBase.h>
#include <server/listener.h> #include <webserver/Listener.h>
#include <iostream> #include <iostream>
@@ -133,7 +133,6 @@ public:
void void
run() run()
{ {
std::cout << "RUNNING" << std::endl;
// We need to be executing within a strand to perform async operations // We need to be executing within a strand to perform async operations
// on the I/O objects in this session. Although not strictly necessary // on the I/O objects in this session. Although not strictly necessary
// for single-threaded contexts, this example code is written to be // for single-threaded contexts, this example code is written to be
@@ -149,7 +148,6 @@ private:
void void
do_upgrade() do_upgrade()
{ {
std::cout << "doing upgrade" << std::endl;
parser_.emplace(); parser_.emplace();
// Apply a reasonable limit to the allowed size // Apply a reasonable limit to the allowed size
@@ -168,16 +166,12 @@ private:
{ {
// See if it is a WebSocket Upgrade // See if it is a WebSocket Upgrade
if (!websocket::is_upgrade(req_)) if (!websocket::is_upgrade(req_))
{
std::cout << "is not upgrade" << std::endl;
return; return;
}
// Disable the timeout. // Disable the timeout.
// The websocket::stream uses its own timeout settings. // The websocket::stream uses its own timeout settings.
boost::beast::get_lowest_layer(http_).expires_never(); boost::beast::get_lowest_layer(http_).expires_never();
std::cout << "making session" << std::endl;
std::make_shared<PlainWsSession>( std::make_shared<PlainWsSession>(
http_.release_socket(), http_.release_socket(),
backend_, backend_,

View File

@@ -20,7 +20,7 @@
#ifndef RIPPLE_REPORTING_HTTPS_SESSION_H #ifndef RIPPLE_REPORTING_HTTPS_SESSION_H
#define RIPPLE_REPORTING_HTTPS_SESSION_H #define RIPPLE_REPORTING_HTTPS_SESSION_H
#include <server/HttpBase.h> #include <webserver/HttpBase.h>
namespace http = boost::beast::http; namespace http = boost::beast::http;
namespace net = boost::asio; namespace net = boost::asio;
@@ -35,7 +35,8 @@ class SslHttpSession : public HttpBase<SslHttpSession>,
public: public:
// Take ownership of the socket // Take ownership of the socket
explicit SslHttpSession( explicit
SslHttpSession(
tcp::socket&& socket, tcp::socket&& socket,
ssl::context& ctx, ssl::context& ctx,
std::shared_ptr<BackendInterface> backend, std::shared_ptr<BackendInterface> backend,

View File

@@ -27,9 +27,8 @@
#include <boost/beast/websocket/ssl.hpp> #include <boost/beast/websocket/ssl.hpp>
#include <etl/ReportingETL.h> #include <etl/ReportingETL.h>
#include <server/Handlers.h>
#include <server/WsBase.h> #include <webserver/WsBase.h>
namespace http = boost::beast::http; namespace http = boost::beast::http;
namespace net = boost::asio; namespace net = boost::asio;
@@ -154,7 +153,6 @@ private:
void void
do_upgrade() do_upgrade()
{ {
std::cout << "doing upgrade" << std::endl;
parser_.emplace(); parser_.emplace();
// Apply a reasonable limit to the allowed size // Apply a reasonable limit to the allowed size

View File

@@ -1,16 +1,18 @@
#include <handlers/RPCHelpers.h> #include <handlers/RPCHelpers.h>
#include <server/SubscriptionManager.h> #include <webserver/SubscriptionManager.h>
#include <server/WsBase.h> #include <webserver/WsBase.h>
void void
SubscriptionManager::subLedger(std::shared_ptr<WsBase>& session) SubscriptionManager::subLedger(std::shared_ptr<WsBase>& session)
{ {
std::lock_guard lk(m_);
streamSubscribers_[Ledgers].emplace(std::move(session)); streamSubscribers_[Ledgers].emplace(std::move(session));
} }
void void
SubscriptionManager::unsubLedger(std::shared_ptr<WsBase>& session) SubscriptionManager::unsubLedger(std::shared_ptr<WsBase>& session)
{ {
std::lock_guard lk(m_);
streamSubscribers_[Ledgers].erase(session); streamSubscribers_[Ledgers].erase(session);
} }
@@ -36,19 +38,22 @@ SubscriptionManager::pubLedger(
pubMsg["validated_ledgers"] = ledgerRange; pubMsg["validated_ledgers"] = ledgerRange;
pubMsg["txn_count"] = txnCount; 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)); session->send(boost::json::serialize(pubMsg));
} }
void void
SubscriptionManager::subTransactions(std::shared_ptr<WsBase>& session) SubscriptionManager::subTransactions(std::shared_ptr<WsBase>& session)
{ {
std::lock_guard lk(m_);
streamSubscribers_[Transactions].emplace(std::move(session)); streamSubscribers_[Transactions].emplace(std::move(session));
} }
void void
SubscriptionManager::unsubTransactions(std::shared_ptr<WsBase>& session) SubscriptionManager::unsubTransactions(std::shared_ptr<WsBase>& session)
{ {
std::lock_guard lk(m_);
streamSubscribers_[Transactions].erase(session); streamSubscribers_[Transactions].erase(session);
} }
@@ -57,6 +62,7 @@ SubscriptionManager::subAccount(
ripple::AccountID const& account, ripple::AccountID const& account,
std::shared_ptr<WsBase>& session) std::shared_ptr<WsBase>& session)
{ {
std::lock_guard lk(m_);
accountSubscribers_[account].emplace(std::move(session)); accountSubscribers_[account].emplace(std::move(session));
} }
@@ -65,6 +71,7 @@ SubscriptionManager::unsubAccount(
ripple::AccountID const& account, ripple::AccountID const& account,
std::shared_ptr<WsBase>& session) std::shared_ptr<WsBase>& session)
{ {
std::lock_guard lk(m_);
accountSubscribers_[account].erase(session); accountSubscribers_[account].erase(session);
} }
@@ -73,6 +80,8 @@ SubscriptionManager::pubTransaction(
Backend::TransactionAndMetadata const& blob, Backend::TransactionAndMetadata const& blob,
std::uint32_t seq) std::uint32_t seq)
{ {
std::lock_guard lk(m_);
auto [tx, meta] = deserializeTxPlusMeta(blob, seq); auto [tx, meta] = deserializeTxPlusMeta(blob, seq);
boost::json::object pubMsg; boost::json::object pubMsg;
@@ -94,6 +103,7 @@ void
SubscriptionManager::forwardProposedTransaction( SubscriptionManager::forwardProposedTransaction(
boost::json::object const& response) boost::json::object const& response)
{ {
std::lock_guard lk(m_);
for (auto const& session : streamSubscribers_[TransactionsProposed]) for (auto const& session : streamSubscribers_[TransactionsProposed])
session->send(boost::json::serialize(response)); session->send(boost::json::serialize(response));
@@ -110,6 +120,7 @@ SubscriptionManager::subProposedAccount(
ripple::AccountID const& account, ripple::AccountID const& account,
std::shared_ptr<WsBase>& session) std::shared_ptr<WsBase>& session)
{ {
std::lock_guard lk(m_);
accountProposedSubscribers_[account].emplace(std::move(session)); accountProposedSubscribers_[account].emplace(std::move(session));
} }
@@ -118,17 +129,43 @@ SubscriptionManager::unsubProposedAccount(
ripple::AccountID const& account, ripple::AccountID const& account,
std::shared_ptr<WsBase>& session) std::shared_ptr<WsBase>& session)
{ {
std::lock_guard lk(m_);
accountProposedSubscribers_[account].erase(session); accountProposedSubscribers_[account].erase(session);
} }
void void
SubscriptionManager::subProposedTransactions(std::shared_ptr<WsBase>& session) SubscriptionManager::subProposedTransactions(std::shared_ptr<WsBase>& session)
{ {
std::lock_guard lk(m_);
streamSubscribers_[TransactionsProposed].emplace(std::move(session)); streamSubscribers_[TransactionsProposed].emplace(std::move(session));
} }
void void
SubscriptionManager::unsubProposedTransactions(std::shared_ptr<WsBase>& session) SubscriptionManager::unsubProposedTransactions(std::shared_ptr<WsBase>& session)
{ {
std::lock_guard lk(m_);
streamSubscribers_[TransactionsProposed].erase(session); streamSubscribers_[TransactionsProposed].erase(session);
} }
void
SubscriptionManager::clearSession(WsBase* s)
{
std::lock_guard lk(m_);
// need the == operator. No-op delete
std::shared_ptr<WsBase> 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);
}
}

View File

@@ -22,6 +22,7 @@
#include <backend/BackendInterface.h> #include <backend/BackendInterface.h>
#include <memory> #include <memory>
#include <backend/BackendInterface.h>
class WsBase; class WsBase;
@@ -37,6 +38,7 @@ class SubscriptionManager
finalEntry finalEntry
}; };
std::mutex m_;
std::array<subscriptions, finalEntry> streamSubscribers_; std::array<subscriptions, finalEntry> streamSubscribers_;
std::unordered_map<ripple::AccountID, subscriptions> accountSubscribers_; std::unordered_map<ripple::AccountID, subscriptions> accountSubscribers_;
std::unordered_map<ripple::AccountID, subscriptions> std::unordered_map<ripple::AccountID, subscriptions>
@@ -101,6 +103,9 @@ public:
void void
unsubProposedTransactions(std::shared_ptr<WsBase>& session); unsubProposedTransactions(std::shared_ptr<WsBase>& session);
void
clearSession(WsBase* session);
}; };
#endif // SUBSCRIPTION_MANAGER_H #endif // SUBSCRIPTION_MANAGER_H

View File

@@ -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 #ifndef RIPPLE_REPORTING_WS_BASE_SESSION_H
#define RIPPLE_REPORTING_WS_BASE_SESSION_H #define RIPPLE_REPORTING_WS_BASE_SESSION_H
#include <boost/beast/core.hpp> #include <boost/beast/core.hpp>
#include <boost/beast/websocket.hpp> #include <boost/beast/websocket.hpp>
#include <backend/BackendInterface.h>
#include <etl/ETLSource.h>
#include <iostream> #include <iostream>
#include <memory> #include <memory>
#include <server/DOSGuard.h>
#include <server/SubscriptionManager.h> #include <backend/BackendInterface.h>
#include <etl/ETLSource.h>
#include <handlers/Handlers.h>
#include <handlers/Status.h>
#include <webserver/DOSGuard.h>
#include <webserver/SubscriptionManager.h>
namespace http = boost::beast::http; namespace http = boost::beast::http;
namespace net = boost::asio; namespace net = boost::asio;
@@ -22,6 +45,20 @@ wsFail(boost::beast::error_code ec, char const* what)
std::cerr << what << ": " << ec.message() << "\n"; 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 class WsBase
{ {
public: public:
@@ -34,11 +71,16 @@ public:
} }
}; };
class SubscriptionManager;
class ETLLoadBalancer;
// Echoes back all received WebSocket messages // Echoes back all received WebSocket messages
template <class Derived> template <class Derived>
class WsSession : public WsBase, class WsSession : public WsBase,
public std::enable_shared_from_this<WsSession<Derived>> public std::enable_shared_from_this<WsSession<Derived>>
{ {
using std::enable_shared_from_this<WsSession<Derived>>::shared_from_this;
boost::beast::flat_buffer buffer_; boost::beast::flat_buffer buffer_;
std::string response_; std::string response_;
@@ -64,6 +106,7 @@ public:
virtual ~WsSession() virtual ~WsSession()
{ {
} }
// Access the derived class, this is part of // Access the derived class, this is part of
// the Curiously Recurring Template Pattern idiom. // the Curiously Recurring Template Pattern idiom.
Derived& Derived&
@@ -71,6 +114,7 @@ public:
{ {
return static_cast<Derived&>(*this); return static_cast<Derived&>(*this);
} }
void void
send(std::string&& msg) send(std::string&& msg)
{ {
@@ -80,15 +124,14 @@ public:
boost::beast::bind_front_handler( boost::beast::bind_front_handler(
&WsSession::on_write, this->shared_from_this())); &WsSession::on_write, this->shared_from_this()));
} }
void void
run(http::request<http::string_body> req) run(http::request<http::string_body> req)
{ {
std::cout << "Running ws" << std::endl;
// Set suggested timeout settings for the websocket // Set suggested timeout settings for the websocket
derived().ws().set_option(websocket::stream_base::timeout::suggested( derived().ws().set_option(websocket::stream_base::timeout::suggested(
boost::beast::role_type::server)); boost::beast::role_type::server));
std::cout << "Trying to decorate" << std::endl;
// Set a decorator to change the Server of the handshake // Set a decorator to change the Server of the handshake
derived().ws().set_option(websocket::stream_base::decorator( derived().ws().set_option(websocket::stream_base::decorator(
[](websocket::response_type& res) { [](websocket::response_type& res) {
@@ -98,8 +141,6 @@ public:
" websocket-server-async"); " websocket-server-async");
})); }));
std::cout << "trying to async accept" << std::endl;
derived().ws().async_accept( derived().ws().async_accept(
req, req,
boost::beast::bind_front_handler( boost::beast::bind_front_handler(
@@ -116,6 +157,18 @@ public:
do_read(); do_read();
} }
void
do_close()
{
std::shared_ptr<SubscriptionManager> mgr = subscriptions_.lock();
if(!mgr)
return;
mgr->clearSession(this);
}
void void
do_read() do_read()
{ {
@@ -134,7 +187,6 @@ public:
// This indicates that the session was closed // This indicates that the session was closed
if (ec == boost::beast::websocket::error::closed) if (ec == boost::beast::websocket::error::closed)
{ {
std::cout << "session closed" << std::endl;
return; return;
} }
@@ -159,53 +211,72 @@ public:
BOOST_LOG_TRIVIAL(debug) << " received request : " << request; BOOST_LOG_TRIVIAL(debug) << " received request : " << request;
try try
{ {
std::shared_ptr<SubscriptionManager> subPtr = auto range = backend_->fetchLedgerRange();
subscriptions_.lock(); if (!range)
if (!subPtr) return send(boost::json::serialize(
return; RPC::make_error(RPC::Error::rpcNOT_READY)));
auto [res, cost] = buildResponse( std::optional<RPC::Context> context = RPC::make_WsContext(
request, request,
backend_, backend_,
subPtr, subscriptions_.lock(),
balancer_, balancer_,
this->shared_from_this()); shared_from_this(),
auto start = std::chrono::system_clock::now(); *range
response = std::move(res); );
if (!dosGuard_.add(ip, cost))
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<RPC::Status>(&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<boost::json::object>(v);
response_ = boost::json::serialize(json);
} }
auto end = std::chrono::system_clock::now(); if (!dosGuard_.add(ip, response_.size()))
BOOST_LOG_TRIVIAL(info) result["warning"] = "Too many requests";
<< __func__ << " RPC call took "
<< ((end - start).count() / 1000000000.0)
<< " . request = " << request;
} }
catch (Backend::DatabaseTimeout const& t) catch (Backend::DatabaseTimeout const& t)
{ {
BOOST_LOG_TRIVIAL(error) << __func__ << " Database timeout"; BOOST_LOG_TRIVIAL(error) << __func__ << " Database timeout";
response["error"] = return send(boost::json::serialize(
"Database read timeout. Please retry the request"; RPC::make_error(RPC::Error::rpcNOT_READY)));
} }
} }
catch (std::exception const& e) catch (std::exception const& e)
{ {
BOOST_LOG_TRIVIAL(error) BOOST_LOG_TRIVIAL(error)
<< __func__ << "caught exception : " << e.what(); << __func__ << " caught exception : " << e.what();
response["error"] = "Unknown exception";
}
}
BOOST_LOG_TRIVIAL(trace) << __func__ << response;
response_ = boost::json::serialize(response);
// Echo the message return send(boost::json::serialize(
derived().ws().text(derived().ws().got_text()); RPC::make_error(RPC::Error::rpcINTERNAL)));
derived().ws().async_write( }
boost::asio::buffer(response_), }
boost::beast::bind_front_handler( BOOST_LOG_TRIVIAL(trace) << __func__ << response_;
&WsSession::on_write, this->shared_from_this()));
send(std::move(response_));
} }
void void
@@ -223,4 +294,5 @@ public:
do_read(); do_read();
} }
}; };
#endif // RIPPLE_REPORTING_WS_BASE_SESSION_H #endif // RIPPLE_REPORTING_WS_BASE_SESSION_H

View File

@@ -2,11 +2,9 @@
/* /*
This file is part of rippled: https://github.com/ripple/rippled This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2020 Ripple Labs Inc. Copyright (c) 2020 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies. copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
@@ -23,11 +21,11 @@
#include <boost/asio/dispatch.hpp> #include <boost/asio/dispatch.hpp>
#include <boost/beast/core.hpp> #include <boost/beast/core.hpp>
#include <boost/beast/websocket.hpp> #include <boost/beast/websocket.hpp>
#include <server/HttpSession.h> #include <webserver/HttpSession.h>
#include <server/PlainWsSession.h> #include <webserver/PlainWsSession.h>
#include <server/SslHttpSession.h> #include <webserver/SslHttpSession.h>
#include <server/SslWsSession.h> #include <webserver/SslWsSession.h>
#include <server/SubscriptionManager.h> #include <webserver/SubscriptionManager.h>
#include <iostream> #include <iostream>
@@ -156,6 +154,7 @@ make_websocket_session(
std::move(req)) std::move(req))
->run(); ->run();
} }
template <class PlainSession, class SslSession> template <class PlainSession, class SslSession>
class Listener class Listener
: public std::enable_shared_from_this<Listener<PlainSession, SslSession>> : public std::enable_shared_from_this<Listener<PlainSession, SslSession>>
@@ -306,7 +305,7 @@ parse_certs(const char* certFilename, const char* keyFilename)
return ctx; return ctx;
} }
using WebsocketServer = Listener<WsUpgrader, SslWsUpgrader>;
using HttpServer = Listener<HttpSession, SslHttpSession>; using HttpServer = Listener<HttpSession, SslHttpSession>;
static std::shared_ptr<HttpServer> static std::shared_ptr<HttpServer>