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

View File

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

View File

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

View File

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

View File

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

View File

@@ -199,6 +199,20 @@ PostgresBackend::fetchLedgerBySequence(uint32_t sequence) const
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>
PostgresBackend::fetchLedgerRange() const
{

View File

@@ -27,6 +27,9 @@ public:
std::optional<ripple::LedgerInfo>
fetchLedgerBySequence(uint32_t sequence) const override;
std::optional<ripple::LedgerInfo>
fetchLedgerByHash(ripple::uint256 const& hash) const override;
std::optional<LedgerRange>
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.
Multiple clio servers can share access to the same database.
Reporting mode is a special operating mode of rippled, designed to handle RPCs
for validated data. A server running in reporting mode does not connect to the
p2p network, but rather extracts validated data from a node that is connected
to the p2p network. To run rippled in reporting mode, you must also run a
separate rippled node in p2p mode, to use as an ETL source. Multiple reporting
nodes can share access to the same network accessible databases (Postgres and
Cassandra); at any given time, only one reporting node will be performing ETL
and writing to the databases, while the others simply read from the databases.
A server running in reporting mode will forward any requests that require access
to the p2p network to a p2p node.
`BackendInterface`, and it's derived classes, store very little state. The read methods go directly to the database,
and generally don't access any internal data structures. Nearly all of the methods are const.
# Reporting ETL
A single reporting node has one or more ETL sources, specified in the config
file. A reporting node will subscribe to the "ledgers" stream of each of the ETL
sources. This stream sends a message whenever a new ledger is validated. Upon
receiving a message on the stream, reporting will then fetch the data associated
with the newly validated ledger from one of the ETL sources. The fetch is
performed via a gRPC request ("GetLedger"). This request returns the ledger
header, transactions+metadata blobs, and every ledger object
added/modified/deleted as part of this ledger. ETL then writes all of this data
to the databases, and moves on to the next ledger. ETL does not apply
transactions, but rather extracts the already computed results of those
transactions (all of the added/modified/deleted SHAMap leaf nodes of the state
tree). The new SHAMap inner nodes are computed by the ETL writer; this computation mainly
involves manipulating child pointers and recomputing hashes, logic which is
buried inside of SHAMap.
The data model used by clio is called the flat map data model. The flat map data model does not store any
SHAMap inner nodes, and instead only stores the raw ledger objects contained in the leaf node. Ledger objects
are stored in the database with a compound key of `(object_id, ledger_sequence)`, where `ledger_sequence` is the
ledger in which the object was created or modified. Objects are then fetched using an inequality operation,
such as `SELECT * FROM objects WHERE object_id = id AND ledger_sequence <= seq order by ledger_sequence limit 1`, where `seq` is the ledger
in which we are trying to look up the object. When an object is deleted, we write an empty blob.
If the database is entirely empty, ETL must download an entire ledger in full
(as opposed to just the diff, as described above). This download is done via the
"GetLedgerData" gRPC request. "GetLedgerData" allows clients to page through an
entire ledger over several RPC calls. ETL will page through an entire ledger,
and write each object to the database.
Transactions are stored in a separate table, where the key is the hash.
If the database is not empty, the reporting node will first come up in a "soft"
read-only mode. In read-only mode, the server does not perform ETL and simply
publishes new ledgers as they are written to the database.
If the database is not updated within a certain time period
(currently hard coded at 20 seconds), the reporting node will begin the ETL
process and start writing to the database. Postgres will report an error when
trying to write a record with a key that already exists. ETL uses this error to
determine that another process is writing to the database, and subsequently
falls back to a soft read-only mode. Reporting nodes can also operate in strict
read-only mode, in which case they will never write to the database.
Ledger headers are stored in their own table.
# Database Nuances
The database schema for reporting mode does not allow any history gaps.
Attempting to write a ledger to a non-empty database where the previous ledger
does not exist will return an error.
The account_tx table maps accounts to a list of transactions that affect them.
The databases must be set up prior to running reporting mode. This requires
creating the Postgres database, and setting up the Cassandra keyspace. Reporting
mode will create the objects table in Cassandra if the table does not yet exist.
Creating the Postgres database:
```
$ psql -h [host] -U [user]
postgres=# create database [database];
```
Creating the keyspace:
```
$ cqlsh [host] [port]
> CREATE KEYSPACE rippled WITH REPLICATION =
{'class' : 'SimpleStrategy', 'replication_factor' : 3 };
```
A replication factor of 3 is recommended. However, when running locally, only a
replication factor of 1 is supported.
### Backend Indexer
Online delete is not supported by reporting mode and must be done manually. The
easiest way to do this would be to setup a second Cassandra keyspace and
Postgres database, bring up a single reporting mode instance that uses those
databases, and start ETL at a ledger of your choosing (via --startReporting on
the command line). Once this node is caught up, the other databases can be
deleted.
To delete:
```
$ psql -h [host] -U [user] -d [database]
reporting=$ truncate table ledgers cascade;
```
```
$ cqlsh [host] [port]
> truncate table objects;
```
# Proxy
RPCs that require access to the p2p network and/or the open ledger are forwarded
from the reporting node to one of the ETL sources. The request is not processed
prior to forwarding, and the response is delivered as-is to the client.
Reporting will forward any requests that always require p2p/open ledger access
(fee and submit, for instance). In addition, any request that explicitly
requests data from the open or closed ledger (via setting
"ledger_index":"current" or "ledger_index":"closed"), will be forwarded to a
p2p node.
For the stream "transactions_proposed" (AKA "rt_transactions"), reporting
subscribes to the "transactions_proposed" streams of each ETL source, and then
forwards those messages to any clients subscribed to the same stream on the
reporting node. A reporting node will subscribe to the stream on each ETL
source, but will only forward the messages from one of the streams at any given
time (to avoid sending the same message more than once to the same client).
# API changes
A reporting node defaults to only returning validated data. If a ledger is not
specified, the most recently validated ledger is used. This is in contrast to
the normal rippled behavior, where the open ledger is used by default.
Reporting will reject all subscribe requests for streams "server", "manifests",
"validations", "peer_status" and "consensus".
With the elimination of SHAMap inner nodes, iterating across a ledger becomes difficult. In order to iterate,
a keys table is maintained, which keeps a collection of all keys in a ledger. This table has one record for every
million ledgers, where each record has all of the keys in that ledger, as well as all of the keys that were deleted
between that ledger and the prior ledger written to the keys table. Most of this logic is contained in `BackendIndexer`.

View File

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

View File

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

View File

@@ -27,7 +27,7 @@
#include <boost/beast/websocket.hpp>
#include <backend/BackendInterface.h>
#include <etl/ETLSource.h>
#include <server/SubscriptionManager.h>
#include <webserver/SubscriptionManager.h>
#include "org/xrpl/rpc/v1/xrp_ledger.grpc.pb.h"
#include <grpcpp/grpcpp.h>
@@ -248,12 +248,6 @@ private:
void
publishLedger(ripple::LedgerInfo const& lgrInfo);
/// Get fees at a current ledger_index
/// @param seq the ledger index
/// @return nullopt if not found, fees if found.
std::optional<ripple::Fees>
getFees(std::uint32_t seq);
bool
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/Status.h>
#include <backend/BackendInterface.h>
std::optional<ripple::AccountID>
accountFromStringStrict(std::string const& account)
@@ -69,54 +70,38 @@ deserializeTxPlusMeta(
boost::json::object
toJson(ripple::STBase const& obj)
{
auto start = std::chrono::system_clock::now();
boost::json::value value = boost::json::parse(
obj.getJson(ripple::JsonOptions::none).toStyledString());
auto end = std::chrono::system_clock::now();
value.as_object()["deserialization_time_microseconds"] =
std::chrono::duration_cast<std::chrono::microseconds>(end - start)
.count();
return value.as_object();
}
boost::json::value
getJson(Json::Value const& value)
{
boost::json::value boostValue = boost::json::parse(value.toStyledString());
return boostValue;
}
boost::json::object
toJson(ripple::TxMeta const& meta)
{
auto start = std::chrono::system_clock::now();
boost::json::value value = boost::json::parse(
meta.getJson(ripple::JsonOptions::none).toStyledString());
auto end = std::chrono::system_clock::now();
value.as_object()["deserialization_time_microseconds"] =
std::chrono::duration_cast<std::chrono::microseconds>(end - start)
.count();
return value.as_object();
}
boost::json::value
toBoostJson(Json::Value const& value)
{
boost::json::value boostValue =
boost::json::parse(value.toStyledString());
return boostValue;
}
boost::json::object
toJson(ripple::SLE const& sle)
{
auto start = std::chrono::system_clock::now();
boost::json::value value = boost::json::parse(
sle.getJson(ripple::JsonOptions::none).toStyledString());
auto end = std::chrono::system_clock::now();
value.as_object()["deserialization_time_microseconds"] =
std::chrono::duration_cast<std::chrono::microseconds>(end - start)
.count();
return value.as_object();
}
boost::json::value
toBoostJson(RippledJson const& value)
{
return boost::json::parse(value.toStyledString());
}
boost::json::object
toJson(ripple::LedgerInfo const& lgrInfo)
@@ -138,20 +123,70 @@ toJson(ripple::LedgerInfo const& lgrInfo)
return header;
}
std::optional<uint32_t>
ledgerSequenceFromRequest(
boost::json::object const& request,
BackendInterface const& backend)
std::variant<RPC::Status, ripple::LedgerInfo>
ledgerInfoFromRequest(RPC::Context const& ctx)
{
if (!request.contains("ledger_index") ||
request.at("ledger_index").is_string())
auto indexValue = ctx.params.contains("ledger_index")
? ctx.params.at("ledger_index")
: nullptr;
auto hashValue = ctx.params.contains("ledger_hash")
? ctx.params.at("ledger_hash")
: nullptr;
std::optional<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
{
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>
@@ -239,8 +274,8 @@ parseRippleLibSeed(boost::json::value const& value)
return {};
}
std::pair<ripple::PublicKey, ripple::SecretKey>
keypairFromRequst(boost::json::object const& request, boost::json::value& error)
std::variant<RPC::Status, std::pair<ripple::PublicKey, ripple::SecretKey>>
keypairFromRequst(boost::json::object const& request)
{
bool const has_key_type = request.contains("key_type");
@@ -262,17 +297,13 @@ keypairFromRequst(boost::json::object const& request, boost::json::value& error)
}
if (count == 0)
{
error = "missing field secret";
return {};
}
return RPC::Status{RPC::Error::rpcINVALID_PARAMS, "missing field secret"};
if (count > 1)
{
error =
"Exactly one of the following must be specified: "
" passphrase, secret, seed, or seed_hex";
return {};
return RPC::Status{RPC::Error::rpcINVALID_PARAMS,
"Exactly one of the following must be specified: "
" passphrase, secret, seed, or seed_hex"};
}
boost::optional<ripple::KeyType> keyType;
@@ -281,25 +312,18 @@ keypairFromRequst(boost::json::object const& request, boost::json::value& error)
if (has_key_type)
{
if (!request.at("key_type").is_string())
{
error = "key_type must be string";
return {};
}
return RPC::Status{RPC::Error::rpcINVALID_PARAMS, "keyTypeNotString"};
std::string key_type = request.at("key_type").as_string().c_str();
keyType = ripple::keyTypeFromString(key_type);
if (!keyType)
{
error = "Invalid field key_type";
return {};
}
return RPC::Status{RPC::Error::rpcINVALID_PARAMS, "invalidFieldKeyType"};
if (secretType == "secret")
{
error = "The secret field is not allowed if key_type is used.";
return {};
}
return RPC::Status{RPC::Error::rpcINVALID_PARAMS,
"The secret field is not allowed if key_type is used."};
}
// ripple-lib encodes seed used to generate an Ed25519 wallet in a
@@ -313,12 +337,9 @@ keypairFromRequst(boost::json::object const& request, boost::json::value& error)
{
// If the user passed in an Ed25519 seed but *explicitly*
// requested another key type, return an error.
if (keyType.value_or(ripple::KeyType::ed25519) !=
ripple::KeyType::ed25519)
{
error = "Specified seed is for an Ed25519 wallet.";
return {};
}
if (keyType.value_or(ripple::KeyType::ed25519) != ripple::KeyType::ed25519)
return RPC::Status{RPC::Error::rpcINVALID_PARAMS,
"Specified seed is for an Ed25519 wallet."};
keyType = ripple::KeyType::ed25519;
}
@@ -332,10 +353,8 @@ keypairFromRequst(boost::json::object const& request, boost::json::value& error)
if (has_key_type)
{
if (!request.at(secretType).is_string())
{
error = "secret value must be string";
return {};
}
return RPC::Status{RPC::Error::rpcINVALID_PARAMS,
"secret value must be string"};
std::string key = request.at(secretType).as_string().c_str();
@@ -353,10 +372,8 @@ keypairFromRequst(boost::json::object const& request, boost::json::value& error)
else
{
if (!request.at("secret").is_string())
{
error = "field secret should be a string";
return {};
}
return RPC::Status{RPC::Error::rpcINVALID_PARAMS,
"field secret should be a string"};
std::string secret = request.at("secret").as_string().c_str();
seed = ripple::parseGenericSeed(secret);
@@ -364,17 +381,13 @@ keypairFromRequst(boost::json::object const& request, boost::json::value& error)
}
if (!seed)
{
error = "Bad Seed: invalid field message secretType";
return {};
}
return RPC::Status{RPC::Error::rpcBAD_SEED,
"Bad Seed: invalid field message secretType"};
if (keyType != ripple::KeyType::secp256k1 &&
keyType != ripple::KeyType::ed25519)
{
error = "keypairForSignature: invalid key type";
return {};
}
if (keyType != ripple::KeyType::secp256k1
&& keyType != ripple::KeyType::ed25519)
return RPC::Status{RPC::Error::rpcINVALID_PARAMS,
"keypairForSignature: invalid key type"};
return generateKeyPair(*keyType, *seed);
}
@@ -402,19 +415,161 @@ getAccountsFromTransaction(boost::json::object const& transaction)
return accounts;
}
std::vector<unsigned char>
ledgerInfoToBlob(ripple::LedgerInfo const& info)
bool
isGlobalFrozen(
BackendInterface const& backend,
std::uint32_t sequence,
ripple::AccountID const& issuer)
{
ripple::Serializer s;
s.add32(info.seq);
s.add64(info.drops.drops());
s.addBitString(info.parentHash);
s.addBitString(info.txHash);
s.addBitString(info.accountHash);
s.add32(info.parentCloseTime.time_since_epoch().count());
s.add32(info.closeTime.time_since_epoch().count());
s.add8(info.closeTimeResolution.count());
s.add8(info.closeFlags);
s.addBitString(info.hash);
return s.peekData();
if (ripple::isXRP(issuer))
return false;
auto key = ripple::keylet::account(issuer).key;
auto blob = backend.fetchLedgerObject(key, sequence);
if (!blob)
return false;
ripple::SerialIter it{blob->data(), blob->size()};
ripple::SLE sle{it, key};
return sle.isFlag(ripple::lsfGlobalFreeze);
}
bool
isFrozen(
BackendInterface const& backend,
std::uint32_t sequence,
ripple::AccountID const& account,
ripple::Currency const& currency,
ripple::AccountID const& issuer)
{
if (ripple::isXRP(currency))
return false;
auto key = ripple::keylet::account(issuer).key;
auto blob = backend.fetchLedgerObject(key, sequence);
if (!blob)
return false;
ripple::SerialIter it{blob->data(), blob->size()};
ripple::SLE sle{it, key};
if (sle.isFlag(ripple::lsfGlobalFreeze))
return true;
if (issuer != account)
{
key = ripple::keylet::line(account, issuer, currency).key;
blob = backend.fetchLedgerObject(key, sequence);
if (!blob)
return false;
ripple::SerialIter issuerIt{blob->data(), blob->size()};
ripple::SLE issuerLine{it, key};
auto frozen =
(issuer > account) ? ripple::lsfHighFreeze : ripple::lsfLowFreeze;
if (issuerLine.isFlag(frozen))
return true;
}
return false;
}
ripple::XRPAmount
xrpLiquid(
BackendInterface const& backend,
std::uint32_t sequence,
ripple::AccountID const& id)
{
auto key = ripple::keylet::account(id).key;
auto blob = backend.fetchLedgerObject(key, sequence);
if (!blob)
return beast::zero;
ripple::SerialIter it{blob->data(), blob->size()};
ripple::SLE sle{it, key};
std::uint32_t const ownerCount = sle.getFieldU32(ripple::sfOwnerCount);
auto const reserve = backend.fetchFees(sequence)->accountReserve(ownerCount);
auto const balance = sle.getFieldAmount(ripple::sfBalance);
ripple::STAmount amount = balance - reserve;
if (balance < reserve)
amount.clear();
return amount.xrp();
}
ripple::STAmount
accountHolds(
BackendInterface const& backend,
std::uint32_t sequence,
ripple::AccountID const& account,
ripple::Currency const& currency,
ripple::AccountID const& issuer)
{
ripple::STAmount amount;
if (ripple::isXRP(currency))
{
return {xrpLiquid(backend, sequence, account)};
}
auto key = ripple::keylet::line(account, issuer, currency).key;
auto const blob = backend.fetchLedgerObject(key, sequence);
if (!blob)
{
amount.clear({currency, issuer});
return amount;
}
ripple::SerialIter it{blob->data(), blob->size()};
ripple::SLE sle{it, key};
if (isFrozen(backend, sequence, account, currency, issuer))
{
amount.clear(ripple::Issue(currency, issuer));
}
else
{
amount = sle.getFieldAmount(ripple::sfBalance);
if (account > issuer)
{
// Put balance in account terms.
amount.negate();
}
amount.setIssuer(issuer);
}
return amount;
}
ripple::Rate
transferRate(
BackendInterface const& backend,
std::uint32_t sequence,
ripple::AccountID const& issuer)
{
auto key = ripple::keylet::account(issuer).key;
auto blob = backend.fetchLedgerObject(key, sequence);
if (blob)
{
ripple::SerialIter it{blob->data(), blob->size()};
ripple::SLE sle{it, key};
if (sle.isFieldPresent(ripple::sfTransferRate))
return ripple::Rate{sle.getFieldU32(ripple::sfTransferRate)};
}
return ripple::parityRate;
}

View File

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

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,53 +6,45 @@
#include <ripple/protocol/jss.h>
#include <boost/json.hpp>
#include <algorithm>
#include <backend/BackendInterface.h>
#include <handlers/methods/Account.h>
#include <handlers/RPCHelpers.h>
#include <backend/BackendInterface.h>
#include <backend/DBHelpers.h>
#include <backend/Pg.h>
boost::json::object
doAccountCurrencies(
boost::json::object const& request,
BackendInterface const& backend)
namespace RPC
{
boost::json::object response;
auto ledgerSequence = ledgerSequenceFromRequest(request, backend);
if (!ledgerSequence)
{
response["error"] = "Empty database";
return response;
}
Result
doAccountCurrencies(Context const& context)
{
auto request = context.params;
boost::json::object response = {};
if (!request.contains("account"))
{
response["error"] = "Must contain account";
return response;
}
auto v = ledgerInfoFromRequest(context);
if (auto status = std::get_if<Status>(&v))
return *status;
if (!request.at("account").is_string())
{
response["error"] = "Account must be a string";
return response;
}
auto lgrInfo = std::get<ripple::LedgerInfo>(v);
ripple::AccountID accountID;
auto parsed = ripple::parseBase58<ripple::AccountID>(
request.at("account").as_string().c_str());
if(!request.contains("account"))
return Status{Error::rpcINVALID_PARAMS, "missingAccount"};
if (!parsed)
{
response["error"] = "Invalid account";
return response;
}
if(!request.at("account").is_string())
return Status{Error::rpcINVALID_PARAMS, "accountNotString"};
auto accountID =
accountFromStringStrict(request.at("account").as_string().c_str());
accountID = *parsed;
if (!accountID)
return Status{Error::rpcINVALID_PARAMS, "malformedAccount"};
std::set<std::string> send, receive;
auto const addToResponse = [&](ripple::SLE const& sle) {
if (sle.getType() == ripple::ltRIPPLE_STATE)
{
ripple::STAmount const& balance =
sle.getFieldAmount(ripple::sfBalance);
sle.getFieldAmount(ripple::sfBalance);
auto lowLimit = sle.getFieldAmount(ripple::sfLowLimit);
auto highLimit = sle.getFieldAmount(ripple::sfHighLimit);
@@ -65,12 +57,25 @@ doAccountCurrencies(
if ((-balance) < lineLimitPeer)
send.insert(ripple::to_string(balance.getCurrency()));
}
return true;
};
traverseOwnedNodes(
backend, accountID, *ledgerSequence, beast::zero, addToResponse);
*context.backend,
*accountID,
lgrInfo.seq,
beast::zero,
addToResponse);
response["ledger_hash"] = ripple::strHex(lgrInfo.hash);
response["ledger_index"] = lgrInfo.seq;
response["receive_currencies"] = boost::json::value(boost::json::array_kind);
boost::json::array& jsonReceive = response.at("receive_currencies").as_array();
for (auto const& currency : receive)
jsonReceive.push_back(currency.c_str());
response["send_currencies"] = boost::json::value(boost::json::array_kind);
boost::json::array& jsonSend = response.at("send_currencies").as_array();
@@ -78,13 +83,7 @@ doAccountCurrencies(
for (auto const& currency : send)
jsonSend.push_back(currency.c_str());
response["receive_currencies"] =
boost::json::value(boost::json::array_kind);
boost::json::array& jsonReceive =
response.at("receive_currencies").as_array();
for (auto const& currency : receive)
jsonReceive.push_back(currency.c_str());
return response;
}
} // namespace RPC

View File

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

View File

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

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/ledger/Ledger.h>
#include <ripple/basics/StringUtilities.h>
#include <ripple/protocol/ErrorCodes.h>
#include <ripple/protocol/Indexes.h>
@@ -7,8 +7,14 @@
#include <ripple/protocol/jss.h>
#include <boost/json.hpp>
#include <algorithm>
#include <backend/BackendInterface.h>
#include <handlers/RPCHelpers.h>
#include <handlers/methods/Account.h>
#include <backend/BackendInterface.h>
#include <backend/DBHelpers.h>
namespace RPC
{
void
addOffer(boost::json::array& offersJson, ripple::SLE const& offer)
@@ -18,7 +24,7 @@ addOffer(boost::json::array& offersJson, ripple::SLE const& offer)
ripple::STAmount takerPays = offer.getFieldAmount(ripple::sfTakerPays);
ripple::STAmount takerGets = offer.getFieldAmount(ripple::sfTakerGets);
boost::json::object obj;
if (!takerPays.native())
@@ -54,83 +60,59 @@ addOffer(boost::json::array& offersJson, ripple::SLE const& offer)
obj["quality"] = rate.getText();
if (offer.isFieldPresent(ripple::sfExpiration))
obj["expiration"] = offer.getFieldU32(ripple::sfExpiration);
offersJson.push_back(obj);
};
boost::json::object
doAccountOffers(
boost::json::object const& request,
BackendInterface const& backend)
Result
doAccountOffers(Context const& context)
{
boost::json::object response;
auto request = context.params;
boost::json::object response = {};
auto ledgerSequence = ledgerSequenceFromRequest(request, backend);
if (!ledgerSequence)
{
response["error"] = "Empty database";
return response;
}
auto v = ledgerInfoFromRequest(context);
if (auto status = std::get_if<Status>(&v))
return *status;
if (!request.contains("account"))
{
response["error"] = "Must contain account";
return response;
}
auto lgrInfo = std::get<ripple::LedgerInfo>(v);
if (!request.at("account").is_string())
{
response["error"] = "Account must be a string";
return response;
}
if(!request.contains("account"))
return Status{Error::rpcINVALID_PARAMS, "missingAccount"};
ripple::AccountID accountID;
auto parsed = ripple::parseBase58<ripple::AccountID>(
request.at("account").as_string().c_str());
if(!request.at("account").is_string())
return Status{Error::rpcINVALID_PARAMS, "accountNotString"};
auto accountID =
accountFromStringStrict(request.at("account").as_string().c_str());
if (!parsed)
{
response["error"] = "Invalid account";
return response;
}
if (!accountID)
return Status{Error::rpcINVALID_PARAMS, "malformedAccount"};
accountID = *parsed;
std::uint32_t limit = 200;
if (request.contains("limit"))
{
if (!request.at("limit").is_int64())
{
response["error"] = "limit must be integer";
return response;
}
if(!request.at("limit").is_int64())
return Status{Error::rpcINVALID_PARAMS, "limitNotInt"};
limit = request.at("limit").as_int64();
if (limit <= 0)
{
response["error"] = "limit must be positive";
return response;
}
return Status{Error::rpcINVALID_PARAMS, "limitNotPositive"};
}
ripple::uint256 cursor = beast::zero;
ripple::uint256 cursor;
if (request.contains("cursor"))
{
if (!request.at("cursor").is_string())
{
response["error"] = "limit must be string";
return response;
}
if(!request.at("cursor").is_string())
return Status{Error::rpcINVALID_PARAMS, "cursorNotString"};
auto bytes = ripple::strUnHex(request.at("cursor").as_string().c_str());
if (bytes and bytes->size() != 32)
{
response["error"] = "invalid cursor";
return response;
}
cursor = ripple::uint256::fromVoid(bytes->data());
if (!cursor.parseHex(request.at("cursor").as_string().c_str()))
return Status{Error::rpcINVALID_PARAMS, "malformedCursor"};
}
response["account"] = ripple::to_string(*accountID);
response["ledger_hash"] = ripple::strHex(lgrInfo.hash);
response["ledger_index"] = lgrInfo.seq;
response["offers"] = boost::json::value(boost::json::array_kind);
boost::json::array& jsonLines = response.at("offers").as_array();
@@ -141,18 +123,25 @@ doAccountOffers(
{
return false;
}
addOffer(jsonLines, sle);
}
return true;
};
auto nextCursor = traverseOwnedNodes(
backend, accountID, *ledgerSequence, cursor, addToResponse);
auto nextCursor =
traverseOwnedNodes(
*context.backend,
*accountID,
lgrInfo.seq,
cursor,
addToResponse);
if (nextCursor)
response["next_cursor"] = ripple::strHex(*nextCursor);
response["marker"] = ripple::strHex(*nextCursor);
return response;
}
} // namespace RPC

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

View File

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

View File

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

View File

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

View File

@@ -1,17 +1,21 @@
//
// Copyright (c) 2016-2019 Vinnie Falco (vinnie dot falco at gmail dot com)
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//
// Official repository: https://github.com/boostorg/beast
//
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2021 Ripple Labs Inc.
//------------------------------------------------------------------------------
//
// Example: WebSocket server, asynchronous
//
//------------------------------------------------------------------------------
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <boost/asio/dispatch.hpp>
#include <boost/asio/strand.hpp>
@@ -28,7 +32,7 @@
#include <functional>
#include <iostream>
#include <memory>
#include <server/listener.h>
#include <webserver/Listener.h>
#include <sstream>
#include <string>
#include <thread>
@@ -109,10 +113,10 @@ main(int argc, char* argv[])
// Check command line arguments.
if (argc != 2)
{
std::cerr << "Usage: websocket-server-async "
std::cerr << "Usage: clio_server "
"<config_file> \n"
<< "Example:\n"
<< " websocket-server-async config.json \n";
<< " clio_server config.json \n";
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 <thread>
#include <server/DOSGuard.h>
#include <server/Handlers.h>
#include <handlers/Handlers.h>
#include <webserver/DOSGuard.h>
#include <vector>
namespace http = boost::beast::http;
@@ -77,29 +77,6 @@ httpFail(boost::beast::error_code ec, char const* what)
std::cerr << what << ": " << ec.message() << "\n";
}
bool
validRequest(boost::json::object const& req)
{
if (!req.contains("method") || !req.at("method").is_string())
return false;
if (!req.contains("params"))
return true;
if (!req.at("params").is_array())
return false;
auto array = req.at("params").as_array();
if (array.size() != 1)
return false;
if (!array.at(0).is_object())
return false;
return true;
}
// This function produces an HTTP response for the given
// request. The type of the response object depends on the
// contents of the request, so the interface requires the
@@ -112,9 +89,10 @@ handle_request(
Send&& send,
std::shared_ptr<BackendInterface> backend,
std::shared_ptr<ETLLoadBalancer> balancer,
DOSGuard& dosGuard)
DOSGuard& dosGuard,
std::string const& ip)
{
auto const response = [&req](
auto const httpResponse = [&req](
http::status status,
std::string content_type,
std::string message) {
@@ -129,70 +107,108 @@ handle_request(
if (req.method() == http::verb::get && req.body() == "")
{
send(response(http::status::ok, "text/html", defaultResponse));
send(httpResponse(http::status::ok, "text/html", defaultResponse));
return;
}
if (req.method() != http::verb::post)
{
send(response(
return send(httpResponse(
http::status::bad_request, "text/html", "Expected a POST request"));
return;
}
if (!dosGuard.isOk(ip))
return send(httpResponse(
http::status::ok,
"application/json",
boost::json::serialize(
RPC::make_error(RPC::Error::rpcSLOW_DOWN))));
try
{
BOOST_LOG_TRIVIAL(info) << "Received request: " << req.body();
boost::json::object request;
std::string responseStr = "";
try
{
request = boost::json::parse(req.body()).as_object();
}
catch (std::runtime_error const& e)
{
send(response(
http::status::bad_request,
"text/html",
"Cannot parse json in body"));
return;
return send(httpResponse(
http::status::ok,
"application/json",
boost::json::serialize(
RPC::make_error(RPC::Error::rpcBAD_SYNTAX))));
}
if (!validRequest(request))
if (!dosGuard.isOk(ip))
return send(httpResponse(
http::status::ok,
"application/json",
boost::json::serialize(
RPC::make_error(RPC::Error::rpcSLOW_DOWN))));
auto range = backend->fetchLedgerRange();
if (!range)
return send(httpResponse(
http::status::ok,
"application/json",
boost::json::serialize(
RPC::make_error(RPC::Error::rpcNOT_READY))));
std::optional<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))
{
send(response(
http::status::bad_request, "text/html", "Malformed request"));
auto error = RPC::make_error(status->error);
return;
error["request"] = request;
result = error;
responseStr = boost::json::serialize(error);
}
else
{
result = std::get<boost::json::object>(v);
result["status"] = "success";
result["validated"] = true;
responseStr = boost::json::serialize(response);
}
boost::json::object wsStyleRequest = request.contains("params")
? request.at("params").as_array().at(0).as_object()
: boost::json::object{};
if (!dosGuard.add(ip, responseStr.size()))
result["warning"] = "Too many requests";
wsStyleRequest["command"] = request["method"];
auto [builtResponse, cost] =
buildResponse(wsStyleRequest, backend, nullptr, balancer, nullptr);
send(response(
return send(httpResponse(
http::status::ok,
"application/json",
boost::json::serialize(builtResponse)));
return;
responseStr));
}
catch (std::exception const& e)
{
std::cout << e.what() << std::endl;
send(response(
return send(httpResponse(
http::status::internal_server_error,
"text/html",
"Internal server error occurred"));
return;
"application/json",
boost::json::serialize(RPC::make_error(RPC::Error::rpcINTERNAL))));
}
}
@@ -300,7 +316,6 @@ public:
if (ec)
return httpFail(ec, "read");
auto ip = derived().ip();
if (boost::beast::websocket::is_upgrade(req_))
{
// Disable the timeout.
@@ -316,9 +331,11 @@ public:
dosGuard_);
}
auto ip = derived().ip();
// Send the response
handle_request(
std::move(req_), lambda_, backend_, balancer_, dosGuard_);
std::move(req_), lambda_, backend_, balancer_, dosGuard_, ip);
}
void

View File

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

View File

@@ -20,7 +20,7 @@
#ifndef 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 net = boost::asio;
@@ -35,7 +35,8 @@ class SslHttpSession : public HttpBase<SslHttpSession>,
public:
// Take ownership of the socket
explicit SslHttpSession(
explicit
SslHttpSession(
tcp::socket&& socket,
ssl::context& ctx,
std::shared_ptr<BackendInterface> backend,
@@ -129,4 +130,4 @@ public:
}
};
#endif // RIPPLE_REPORTING_HTTPS_SESSION_H
#endif // RIPPLE_REPORTING_HTTPS_SESSION_H

View File

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

View File

@@ -1,16 +1,18 @@
#include <handlers/RPCHelpers.h>
#include <server/SubscriptionManager.h>
#include <server/WsBase.h>
#include <webserver/SubscriptionManager.h>
#include <webserver/WsBase.h>
void
SubscriptionManager::subLedger(std::shared_ptr<WsBase>& session)
{
std::lock_guard lk(m_);
streamSubscribers_[Ledgers].emplace(std::move(session));
}
void
SubscriptionManager::unsubLedger(std::shared_ptr<WsBase>& session)
{
std::lock_guard lk(m_);
streamSubscribers_[Ledgers].erase(session);
}
@@ -36,19 +38,22 @@ SubscriptionManager::pubLedger(
pubMsg["validated_ledgers"] = ledgerRange;
pubMsg["txn_count"] = txnCount;
for (auto const& session : streamSubscribers_[Ledgers])
std::lock_guard lk(m_);
for (auto const& session: streamSubscribers_[Ledgers])
session->send(boost::json::serialize(pubMsg));
}
void
SubscriptionManager::subTransactions(std::shared_ptr<WsBase>& session)
{
std::lock_guard lk(m_);
streamSubscribers_[Transactions].emplace(std::move(session));
}
void
SubscriptionManager::unsubTransactions(std::shared_ptr<WsBase>& session)
{
std::lock_guard lk(m_);
streamSubscribers_[Transactions].erase(session);
}
@@ -57,6 +62,7 @@ SubscriptionManager::subAccount(
ripple::AccountID const& account,
std::shared_ptr<WsBase>& session)
{
std::lock_guard lk(m_);
accountSubscribers_[account].emplace(std::move(session));
}
@@ -65,6 +71,7 @@ SubscriptionManager::unsubAccount(
ripple::AccountID const& account,
std::shared_ptr<WsBase>& session)
{
std::lock_guard lk(m_);
accountSubscribers_[account].erase(session);
}
@@ -73,6 +80,8 @@ SubscriptionManager::pubTransaction(
Backend::TransactionAndMetadata const& blob,
std::uint32_t seq)
{
std::lock_guard lk(m_);
auto [tx, meta] = deserializeTxPlusMeta(blob, seq);
boost::json::object pubMsg;
@@ -94,6 +103,7 @@ void
SubscriptionManager::forwardProposedTransaction(
boost::json::object const& response)
{
std::lock_guard lk(m_);
for (auto const& session : streamSubscribers_[TransactionsProposed])
session->send(boost::json::serialize(response));
@@ -110,6 +120,7 @@ SubscriptionManager::subProposedAccount(
ripple::AccountID const& account,
std::shared_ptr<WsBase>& session)
{
std::lock_guard lk(m_);
accountProposedSubscribers_[account].emplace(std::move(session));
}
@@ -118,17 +129,43 @@ SubscriptionManager::unsubProposedAccount(
ripple::AccountID const& account,
std::shared_ptr<WsBase>& session)
{
std::lock_guard lk(m_);
accountProposedSubscribers_[account].erase(session);
}
void
SubscriptionManager::subProposedTransactions(std::shared_ptr<WsBase>& session)
{
std::lock_guard lk(m_);
streamSubscribers_[TransactionsProposed].emplace(std::move(session));
}
void
SubscriptionManager::unsubProposedTransactions(std::shared_ptr<WsBase>& session)
{
std::lock_guard lk(m_);
streamSubscribers_[TransactionsProposed].erase(session);
}
void
SubscriptionManager::clearSession(WsBase* s)
{
std::lock_guard lk(m_);
// need the == operator. No-op delete
std::shared_ptr<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 <memory>
#include <backend/BackendInterface.h>
class WsBase;
@@ -37,6 +38,7 @@ class SubscriptionManager
finalEntry
};
std::mutex m_;
std::array<subscriptions, finalEntry> streamSubscribers_;
std::unordered_map<ripple::AccountID, subscriptions> accountSubscribers_;
std::unordered_map<ripple::AccountID, subscriptions>
@@ -101,6 +103,9 @@ public:
void
unsubProposedTransactions(std::shared_ptr<WsBase>& session);
void
clearSession(WsBase* session);
};
#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
#define RIPPLE_REPORTING_WS_BASE_SESSION_H
#include <boost/beast/core.hpp>
#include <boost/beast/websocket.hpp>
#include <backend/BackendInterface.h>
#include <etl/ETLSource.h>
#include <iostream>
#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 net = boost::asio;
@@ -22,6 +45,20 @@ wsFail(boost::beast::error_code ec, char const* what)
std::cerr << what << ": " << ec.message() << "\n";
}
inline boost::json::object
getDefaultWsResponse(boost::json::value const& id)
{
boost::json::object defaultResp = {};
if(!id.is_null())
defaultResp["id"] = id;
defaultResp["result"] = boost::json::object_kind;
defaultResp["status"] = "success";
defaultResp["type"] = "response";
return defaultResp;
}
class WsBase
{
public:
@@ -34,11 +71,16 @@ public:
}
};
class SubscriptionManager;
class ETLLoadBalancer;
// Echoes back all received WebSocket messages
template <class Derived>
class WsSession : public WsBase,
public std::enable_shared_from_this<WsSession<Derived>>
{
using std::enable_shared_from_this<WsSession<Derived>>::shared_from_this;
boost::beast::flat_buffer buffer_;
std::string response_;
@@ -64,6 +106,7 @@ public:
virtual ~WsSession()
{
}
// Access the derived class, this is part of
// the Curiously Recurring Template Pattern idiom.
Derived&
@@ -71,6 +114,7 @@ public:
{
return static_cast<Derived&>(*this);
}
void
send(std::string&& msg)
{
@@ -80,15 +124,14 @@ public:
boost::beast::bind_front_handler(
&WsSession::on_write, this->shared_from_this()));
}
void
run(http::request<http::string_body> req)
{
std::cout << "Running ws" << std::endl;
// Set suggested timeout settings for the websocket
derived().ws().set_option(websocket::stream_base::timeout::suggested(
boost::beast::role_type::server));
std::cout << "Trying to decorate" << std::endl;
// Set a decorator to change the Server of the handshake
derived().ws().set_option(websocket::stream_base::decorator(
[](websocket::response_type& res) {
@@ -98,8 +141,6 @@ public:
" websocket-server-async");
}));
std::cout << "trying to async accept" << std::endl;
derived().ws().async_accept(
req,
boost::beast::bind_front_handler(
@@ -116,6 +157,18 @@ public:
do_read();
}
void
do_close()
{
std::shared_ptr<SubscriptionManager> mgr = subscriptions_.lock();
if(!mgr)
return;
mgr->clearSession(this);
}
void
do_read()
{
@@ -134,7 +187,6 @@ public:
// This indicates that the session was closed
if (ec == boost::beast::websocket::error::closed)
{
std::cout << "session closed" << std::endl;
return;
}
@@ -159,53 +211,72 @@ public:
BOOST_LOG_TRIVIAL(debug) << " received request : " << request;
try
{
std::shared_ptr<SubscriptionManager> subPtr =
subscriptions_.lock();
if (!subPtr)
return;
auto range = backend_->fetchLedgerRange();
if (!range)
return send(boost::json::serialize(
RPC::make_error(RPC::Error::rpcNOT_READY)));
auto [res, cost] = buildResponse(
std::optional<RPC::Context> context = RPC::make_WsContext(
request,
backend_,
subPtr,
subscriptions_.lock(),
balancer_,
this->shared_from_this());
auto start = std::chrono::system_clock::now();
response = std::move(res);
if (!dosGuard_.add(ip, cost))
shared_from_this(),
*range
);
if (!context)
return send(boost::json::serialize(
RPC::make_error(RPC::Error::rpcBAD_SYNTAX)));
auto id = request.contains("id")
? request.at("id")
: nullptr;
auto response = getDefaultWsResponse(id);
boost::json::object& result = response["result"].as_object();
auto v = RPC::buildResponse(*context);
if (auto status = std::get_if<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();
BOOST_LOG_TRIVIAL(info)
<< __func__ << " RPC call took "
<< ((end - start).count() / 1000000000.0)
<< " . request = " << request;
if (!dosGuard_.add(ip, response_.size()))
result["warning"] = "Too many requests";
}
catch (Backend::DatabaseTimeout const& t)
{
BOOST_LOG_TRIVIAL(error) << __func__ << " Database timeout";
response["error"] =
"Database read timeout. Please retry the request";
return send(boost::json::serialize(
RPC::make_error(RPC::Error::rpcNOT_READY)));
}
}
catch (std::exception const& e)
{
BOOST_LOG_TRIVIAL(error)
<< __func__ << "caught exception : " << e.what();
response["error"] = "Unknown exception";
<< __func__ << " caught exception : " << e.what();
return send(boost::json::serialize(
RPC::make_error(RPC::Error::rpcINTERNAL)));
}
}
BOOST_LOG_TRIVIAL(trace) << __func__ << response;
response_ = boost::json::serialize(response);
BOOST_LOG_TRIVIAL(trace) << __func__ << response_;
// Echo the message
derived().ws().text(derived().ws().got_text());
derived().ws().async_write(
boost::asio::buffer(response_),
boost::beast::bind_front_handler(
&WsSession::on_write, this->shared_from_this()));
send(std::move(response_));
}
void
@@ -223,4 +294,5 @@ public:
do_read();
}
};
#endif // RIPPLE_REPORTING_WS_BASE_SESSION_H

View File

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