mirror of
https://github.com/XRPLF/clio.git
synced 2026-10-01 01:18:06 +00:00
1788 lines
59 KiB
C++
1788 lines
59 KiB
C++
#include "rpc/RPCHelpers.hpp"
|
|
|
|
#include "data/AmendmentCenter.hpp"
|
|
#include "data/AmendmentCenterInterface.hpp"
|
|
#include "data/BackendInterface.hpp"
|
|
#include "data/Types.hpp"
|
|
#include "rpc/JS.hpp"
|
|
#include "rpc/common/Types.hpp"
|
|
#include "util/AccountUtils.hpp"
|
|
#include "util/Assert.hpp"
|
|
#include "util/JsonUtils.hpp"
|
|
#include "util/Profiler.hpp"
|
|
#include "util/log/Logger.hpp"
|
|
#include "web/Context.hpp"
|
|
|
|
#include <boost/algorithm/string/case_conv.hpp>
|
|
#include <boost/asio/spawn.hpp>
|
|
#include <boost/json/array.hpp>
|
|
#include <boost/json/object.hpp>
|
|
#include <boost/json/parse.hpp>
|
|
#include <boost/json/serialize.hpp>
|
|
#include <boost/json/string.hpp>
|
|
#include <boost/json/value.hpp>
|
|
#include <boost/json/value_to.hpp>
|
|
#include <boost/lexical_cast.hpp>
|
|
#include <boost/lexical_cast/bad_lexical_cast.hpp>
|
|
#include <fmt/format.h>
|
|
#include <rpcspec/Errors.hpp>
|
|
#include <rpcspec/Ledger.hpp>
|
|
#include <xrpl/basics/Slice.h>
|
|
#include <xrpl/basics/StringUtilities.h>
|
|
#include <xrpl/basics/base_uint.h>
|
|
#include <xrpl/basics/chrono.h>
|
|
#include <xrpl/basics/strHex.h>
|
|
#include <xrpl/beast/utility/Zero.h>
|
|
#include <xrpl/json/json_reader.h>
|
|
#include <xrpl/json/json_value.h>
|
|
#include <xrpl/protocol/AccountID.h>
|
|
#include <xrpl/protocol/Book.h>
|
|
#include <xrpl/protocol/ErrorCodes.h>
|
|
#include <xrpl/protocol/Indexes.h>
|
|
#include <xrpl/protocol/Issue.h>
|
|
#include <xrpl/protocol/Keylet.h>
|
|
#include <xrpl/protocol/LedgerFormats.h>
|
|
#include <xrpl/protocol/LedgerHeader.h>
|
|
#include <xrpl/protocol/MPTIssue.h>
|
|
#include <xrpl/protocol/NFTokenID.h>
|
|
#include <xrpl/protocol/NFTokenOfferID.h>
|
|
#include <xrpl/protocol/Protocol.h>
|
|
#include <xrpl/protocol/PublicKey.h>
|
|
#include <xrpl/protocol/Rate.h>
|
|
#include <xrpl/protocol/SField.h>
|
|
#include <xrpl/protocol/STAmount.h>
|
|
#include <xrpl/protocol/STBase.h>
|
|
#include <xrpl/protocol/STLedgerEntry.h>
|
|
#include <xrpl/protocol/STObject.h>
|
|
#include <xrpl/protocol/STTx.h>
|
|
#include <xrpl/protocol/Seed.h>
|
|
#include <xrpl/protocol/Serializer.h>
|
|
#include <xrpl/protocol/TER.h>
|
|
#include <xrpl/protocol/TxFormats.h>
|
|
#include <xrpl/protocol/TxMeta.h>
|
|
#include <xrpl/protocol/UintTypes.h>
|
|
#include <xrpl/protocol/XRPAmount.h>
|
|
#include <xrpl/protocol/jss.h>
|
|
#include <xrpl/protocol/nftPageMask.h>
|
|
#include <xrpl/protocol/tokens.h>
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <cassert>
|
|
#include <chrono>
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
#include <exception>
|
|
#include <functional>
|
|
#include <iterator>
|
|
#include <limits>
|
|
#include <map>
|
|
#include <memory>
|
|
#include <optional>
|
|
#include <sstream>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
namespace rpc {
|
|
|
|
std::optional<AccountCursor>
|
|
parseAccountCursor(std::optional<std::string> jsonCursor)
|
|
{
|
|
xrpl::uint256 cursorIndex = beast::kZero;
|
|
std::uint64_t startHint = 0;
|
|
|
|
if (!jsonCursor)
|
|
return AccountCursor({.index = cursorIndex, .hint = startHint});
|
|
|
|
// Cursor is composed of a comma separated index and start hint. The
|
|
// former will be read as hex, and the latter using boost lexical cast.
|
|
std::stringstream cursor(*jsonCursor);
|
|
std::string value;
|
|
if (!std::getline(cursor, value, ','))
|
|
return {};
|
|
|
|
if (!cursorIndex.parseHex(value))
|
|
return {};
|
|
|
|
if (!std::getline(cursor, value, ','))
|
|
return {};
|
|
|
|
try {
|
|
startHint = boost::lexical_cast<std::uint64_t>(value);
|
|
} catch (boost::bad_lexical_cast&) {
|
|
return {};
|
|
}
|
|
|
|
return AccountCursor({.index = cursorIndex, .hint = startHint});
|
|
}
|
|
|
|
std::optional<xrpl::STAmount>
|
|
getDeliveredAmount(
|
|
std::shared_ptr<xrpl::STTx const> const& txn,
|
|
std::shared_ptr<xrpl::TxMeta const> const& meta,
|
|
std::uint32_t const ledgerSequence,
|
|
uint32_t date
|
|
)
|
|
{
|
|
if (auto const delivered = meta->getDeliveredAmount(); delivered.has_value())
|
|
return delivered;
|
|
if (txn->isFieldPresent(xrpl::sfAmount)) {
|
|
// Ledger 4594095 is the first ledger in which the DeliveredAmount field
|
|
// was present when a partial payment was made and its absence indicates
|
|
// that the amount delivered is listed in the Amount field.
|
|
//
|
|
// If the ledger closed long after the DeliveredAmount code was deployed
|
|
// then its absence indicates that the amount delivered is listed in the
|
|
// Amount field. DeliveredAmount went live January 24, 2014.
|
|
// 446000000 is in Feb 2014, well after DeliveredAmount went live
|
|
static constexpr std::uint32_t kFirstLedgerWithDeliveredAmount = 4594095;
|
|
static constexpr std::uint32_t kDeliveredAmountLiveDate = 446000000;
|
|
if (ledgerSequence >= kFirstLedgerWithDeliveredAmount || date > kDeliveredAmountLiveDate) {
|
|
return txn->getFieldAmount(xrpl::sfAmount);
|
|
}
|
|
}
|
|
return {};
|
|
}
|
|
|
|
bool
|
|
canHaveDeliveredAmount(
|
|
std::shared_ptr<xrpl::STTx const> const& txn,
|
|
std::shared_ptr<xrpl::TxMeta const> const& meta
|
|
)
|
|
{
|
|
xrpl::TxType const tt{txn->getTxnType()};
|
|
if (tt != xrpl::ttPAYMENT && tt != xrpl::ttCHECK_CASH && tt != xrpl::ttACCOUNT_DELETE)
|
|
return false;
|
|
|
|
return meta->getResultTER() == xrpl::tesSUCCESS;
|
|
}
|
|
|
|
std::optional<xrpl::AccountID>
|
|
accountFromStringStrict(std::string const& account)
|
|
{
|
|
auto blob = xrpl::strUnHex(account);
|
|
|
|
std::optional<xrpl::PublicKey> publicKey = {};
|
|
if (blob && xrpl::publicKeyType(xrpl::makeSlice(*blob))) {
|
|
publicKey = xrpl::PublicKey(xrpl::Slice{blob->data(), blob->size()});
|
|
} else {
|
|
publicKey =
|
|
util::parseBase58Wrapper<xrpl::PublicKey>(xrpl::TokenType::AccountPublic, account);
|
|
}
|
|
|
|
std::optional<xrpl::AccountID> result;
|
|
if (publicKey) {
|
|
result = xrpl::calcAccountID(*publicKey);
|
|
} else {
|
|
result = util::parseBase58Wrapper<xrpl::AccountID>(account);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
std::pair<std::shared_ptr<xrpl::STTx const>, std::shared_ptr<xrpl::STObject const>>
|
|
deserializeTxPlusMeta(data::TransactionAndMetadata const& blobs)
|
|
{
|
|
static util::Logger const log{"RPC"}; // NOLINT(readability-identifier-naming)
|
|
|
|
try {
|
|
std::pair<std::shared_ptr<xrpl::STTx const>, std::shared_ptr<xrpl::STObject const>> result;
|
|
{
|
|
xrpl::SerialIter s{blobs.transaction.data(), blobs.transaction.size()};
|
|
result.first = std::make_shared<xrpl::STTx const>(s);
|
|
}
|
|
{
|
|
xrpl::SerialIter s{blobs.metadata.data(), blobs.metadata.size()};
|
|
result.second = std::make_shared<xrpl::STObject const>(s, xrpl::sfMetadata);
|
|
}
|
|
return result;
|
|
} catch (std::exception const& e) {
|
|
std::stringstream txn;
|
|
std::stringstream meta;
|
|
std::ranges::copy(blobs.transaction, std::ostream_iterator<unsigned char>(txn));
|
|
std::ranges::copy(blobs.metadata, std::ostream_iterator<unsigned char>(meta));
|
|
LOG(log.error()) << "Failed to deserialize transaction. txn = " << txn.str()
|
|
<< " - meta = " << meta.str()
|
|
<< " txn length = " << std::to_string(blobs.transaction.size())
|
|
<< " meta length = " << std::to_string(blobs.metadata.size());
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
std::pair<std::shared_ptr<xrpl::STTx const>, std::shared_ptr<xrpl::TxMeta const>>
|
|
deserializeTxPlusMeta(data::TransactionAndMetadata const& blobs, std::uint32_t seq)
|
|
{
|
|
auto [tx, meta] = deserializeTxPlusMeta(blobs);
|
|
|
|
std::shared_ptr<xrpl::TxMeta> const m =
|
|
std::make_shared<xrpl::TxMeta>(tx->getTransactionID(), seq, *meta);
|
|
|
|
return {tx, m};
|
|
}
|
|
|
|
boost::json::object
|
|
toJson(xrpl::STBase const& obj)
|
|
{
|
|
boost::json::value value =
|
|
boost::json::parse(obj.getJson(xrpl::JsonOptions::Values::None).toStyledString());
|
|
|
|
return value.as_object();
|
|
}
|
|
|
|
std::pair<boost::json::object, boost::json::object>
|
|
toExpandedJson(
|
|
data::TransactionAndMetadata const& blobs,
|
|
std::uint32_t const apiVersion,
|
|
NFTokenjson nftEnabled,
|
|
std::optional<uint16_t> networkId
|
|
)
|
|
{
|
|
auto [txn, meta] = deserializeTxPlusMeta(blobs, blobs.ledgerSequence);
|
|
auto txnJson = rpc::toJson(*txn);
|
|
auto metaJson = rpc::toJson(*meta);
|
|
insertDeliveredAmount(metaJson, txn, meta, blobs.date);
|
|
insertDeliverMaxAlias(txnJson, apiVersion);
|
|
insertMPTIssuanceID(txnJson, txn, metaJson, meta);
|
|
|
|
if (nftEnabled == NFTokenjson::ENABLE) {
|
|
json::Value nftJson;
|
|
xrpl::insertNFTokenID(nftJson[xrpl::jss::meta], txn, *meta);
|
|
xrpl::insertNFTokenOfferID(nftJson[xrpl::jss::meta], txn, *meta);
|
|
// if there is no nft fields, the nftJson will be {"meta":null}
|
|
auto const nftBoostJson = toBoostJson(nftJson).as_object();
|
|
if (nftBoostJson.contains(JS(meta)) and nftBoostJson.at(JS(meta)).is_object()) {
|
|
for (auto const& [k, v] : nftBoostJson.at(JS(meta)).as_object())
|
|
metaJson.insert_or_assign(k, v);
|
|
}
|
|
}
|
|
|
|
if (networkId) {
|
|
// networkId is available, insert ctid field to tx
|
|
if (auto const ctid = rpc::encodeCTID(meta->getLgrSeq(), meta->getIndex(), *networkId)) {
|
|
txnJson[JS(ctid)] = *ctid;
|
|
}
|
|
}
|
|
|
|
return {txnJson, metaJson};
|
|
}
|
|
|
|
std::optional<std::string>
|
|
encodeCTID(uint32_t ledgerSeq, uint16_t txnIndex, uint16_t networkId) noexcept
|
|
{
|
|
static constexpr uint32_t kMaxLedgerSeq = 0x0FFF'FFFF;
|
|
static constexpr uint32_t kMaxTxnIndex = 0xFFFF;
|
|
static constexpr uint32_t kMaxNetworkId = 0xFFFF;
|
|
|
|
if (ledgerSeq > kMaxLedgerSeq || txnIndex > kMaxTxnIndex || networkId > kMaxNetworkId)
|
|
return {};
|
|
|
|
static constexpr uint64_t kCtidPrefix = 0xC000'0000;
|
|
uint64_t const ctidValue = ((kCtidPrefix + static_cast<uint64_t>(ledgerSeq)) << 32) +
|
|
(static_cast<uint64_t>(txnIndex) << 16) + networkId;
|
|
|
|
return {fmt::format("{:016X}", ctidValue)};
|
|
}
|
|
|
|
bool
|
|
insertDeliveredAmount(
|
|
boost::json::object& metaJson,
|
|
std::shared_ptr<xrpl::STTx const> const& txn,
|
|
std::shared_ptr<xrpl::TxMeta const> const& meta,
|
|
uint32_t date
|
|
)
|
|
{
|
|
if (canHaveDeliveredAmount(txn, meta)) {
|
|
if (auto amt = getDeliveredAmount(txn, meta, meta->getLgrSeq(), date)) {
|
|
metaJson["delivered_amount"] =
|
|
toBoostJson(amt->getJson(xrpl::JsonOptions::Values::IncludeDate));
|
|
} else {
|
|
metaJson["delivered_amount"] = "unavailable";
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @brief Get the delivered amount
|
|
*
|
|
* @param meta The metadata
|
|
* @return The mpt_issuance_id or std::nullopt if not available
|
|
*/
|
|
static std::optional<xrpl::uint192>
|
|
getMPTIssuanceID(std::shared_ptr<xrpl::TxMeta const> const& meta)
|
|
{
|
|
xrpl::TxMeta const& transactionMeta = *meta;
|
|
|
|
for (xrpl::STObject const& node : transactionMeta.getNodes()) {
|
|
if (node.getFieldU16(xrpl::sfLedgerEntryType) != xrpl::ltMPTOKEN_ISSUANCE ||
|
|
node.getFName() != xrpl::sfCreatedNode)
|
|
continue;
|
|
|
|
auto const& mptNode = node.peekAtField(xrpl::sfNewFields).downcast<xrpl::STObject>();
|
|
return xrpl::makeMptID(mptNode[xrpl::sfSequence], mptNode[xrpl::sfIssuer]);
|
|
}
|
|
|
|
return {};
|
|
}
|
|
|
|
/**
|
|
* @brief Check if transaction has a new MPToken created
|
|
*
|
|
* @param txn The transaction object
|
|
* @param meta The metadata object
|
|
* @return true if the transaction can have a mpt_issuance_id
|
|
*/
|
|
static bool
|
|
canHaveMPTIssuanceID(
|
|
std::shared_ptr<xrpl::STTx const> const& txn,
|
|
std::shared_ptr<xrpl::TxMeta const> const& meta
|
|
)
|
|
{
|
|
if (txn->getTxnType() != xrpl::ttMPTOKEN_ISSUANCE_CREATE)
|
|
return false;
|
|
|
|
return (meta->getResultTER() == xrpl::tesSUCCESS);
|
|
}
|
|
|
|
bool
|
|
insertMPTIssuanceID(
|
|
boost::json::object& txnJson,
|
|
std::shared_ptr<xrpl::STTx const> const& txn,
|
|
boost::json::object& metaJson,
|
|
std::shared_ptr<xrpl::TxMeta const> const& meta
|
|
)
|
|
{
|
|
if (!canHaveMPTIssuanceID(txn, meta))
|
|
return false;
|
|
|
|
auto const id = getMPTIssuanceID(meta);
|
|
ASSERT(id.has_value(), "MPTIssuanceID must have value");
|
|
if (!id)
|
|
return false;
|
|
|
|
// For mpttokenissuance create, add mpt_issuance_id to metajson
|
|
// Otherwise, add it to txn json
|
|
if (txnJson.contains(JS(TransactionType)) && txnJson.at(JS(TransactionType)).is_string() and
|
|
txnJson.at(JS(TransactionType)).as_string() == JS(MPTokenIssuanceCreate)) {
|
|
metaJson[JS(mpt_issuance_id)] = xrpl::to_string(*id);
|
|
} else {
|
|
txnJson[JS(mpt_issuance_id)] = xrpl::to_string(*id);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void
|
|
insertDeliverMaxAlias(boost::json::object& txJson, std::uint32_t const apiVersion)
|
|
{
|
|
if (txJson.contains(JS(TransactionType)) and txJson.at(JS(TransactionType)).is_string() and
|
|
txJson.at(JS(TransactionType)).as_string() == JS(Payment) and txJson.contains(JS(Amount))) {
|
|
txJson.insert_or_assign(JS(DeliverMax), txJson[JS(Amount)]);
|
|
if (apiVersion > 1)
|
|
txJson.erase(JS(Amount));
|
|
}
|
|
}
|
|
|
|
boost::json::object
|
|
toJson(xrpl::TxMeta const& meta)
|
|
{
|
|
boost::json::value value =
|
|
boost::json::parse(meta.getJson(xrpl::JsonOptions::Values::None).toStyledString());
|
|
|
|
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(xrpl::SLE const& sle)
|
|
{
|
|
boost::json::value value =
|
|
boost::json::parse(sle.getJson(xrpl::JsonOptions::Values::None).toStyledString());
|
|
if (sle.getType() == xrpl::ltACCOUNT_ROOT) {
|
|
if (sle.isFieldPresent(xrpl::sfEmailHash)) {
|
|
auto const& hash = sle.getFieldH128(xrpl::sfEmailHash);
|
|
std::string md5 = strHex(hash);
|
|
boost::algorithm::to_lower(md5);
|
|
value.as_object()["urlgravatar"] =
|
|
fmt::format("http://www.gravatar.com/avatar/{}", md5);
|
|
}
|
|
}
|
|
return value.as_object();
|
|
}
|
|
|
|
boost::json::object
|
|
toJson(xrpl::LedgerHeader const& lgrInfo, bool const binary, std::uint32_t const apiVersion)
|
|
{
|
|
boost::json::object header;
|
|
if (binary) {
|
|
header[JS(ledger_data)] = xrpl::strHex(ledgerHeaderToBlob(lgrInfo));
|
|
} else {
|
|
header[JS(account_hash)] = xrpl::strHex(lgrInfo.accountHash);
|
|
header[JS(close_flags)] = lgrInfo.closeFlags;
|
|
header[JS(close_time)] = lgrInfo.closeTime.time_since_epoch().count();
|
|
header[JS(close_time_human)] = xrpl::to_string(lgrInfo.closeTime);
|
|
header[JS(close_time_resolution)] = lgrInfo.closeTimeResolution.count();
|
|
header[JS(close_time_iso)] = xrpl::toStringIso(lgrInfo.closeTime);
|
|
header[JS(ledger_hash)] = xrpl::strHex(lgrInfo.hash);
|
|
header[JS(parent_close_time)] = lgrInfo.parentCloseTime.time_since_epoch().count();
|
|
header[JS(parent_hash)] = xrpl::strHex(lgrInfo.parentHash);
|
|
header[JS(total_coins)] = xrpl::to_string(lgrInfo.drops);
|
|
header[JS(transaction_hash)] = xrpl::strHex(lgrInfo.txHash);
|
|
|
|
if (apiVersion < 2u) {
|
|
header[JS(ledger_index)] = std::to_string(lgrInfo.seq);
|
|
} else {
|
|
header[JS(ledger_index)] = lgrInfo.seq;
|
|
}
|
|
}
|
|
header[JS(closed)] = true;
|
|
return header;
|
|
}
|
|
|
|
std::optional<std::uint32_t>
|
|
parseStringAsUInt(std::string const& value)
|
|
{
|
|
std::optional<std::uint32_t> index = {};
|
|
try {
|
|
index = boost::lexical_cast<std::uint32_t>(value);
|
|
} catch (boost::bad_lexical_cast const&) {
|
|
index = std::nullopt;
|
|
}
|
|
|
|
return index;
|
|
}
|
|
|
|
std::expected<xrpl::LedgerHeader, Status>
|
|
ledgerHeaderFromRequest(
|
|
std::shared_ptr<data::BackendInterface const> const& backend,
|
|
web::Context const& ctx
|
|
)
|
|
{
|
|
auto hashValue = ctx.params.contains("ledger_hash") ? ctx.params.at("ledger_hash") : nullptr;
|
|
|
|
if (!hashValue.is_null()) {
|
|
if (!hashValue.is_string())
|
|
return std::unexpected{Status{RippledError::RpcInvalidParams, "ledgerHashNotString"}};
|
|
|
|
xrpl::uint256 ledgerHash;
|
|
if (!ledgerHash.parseHex(boost::json::value_to<std::string>(hashValue)))
|
|
return std::unexpected{Status{RippledError::RpcInvalidParams, "ledgerHashMalformed"}};
|
|
|
|
auto lgrInfo = backend->fetchLedgerByHash(ledgerHash, ctx.yield);
|
|
|
|
if (!lgrInfo || lgrInfo->seq > ctx.range.maxSequence)
|
|
return std::unexpected{Status{RippledError::RpcLgrNotFound, "ledgerNotFound"}};
|
|
|
|
return *lgrInfo;
|
|
}
|
|
|
|
auto indexValue = ctx.params.contains("ledger_index") ? ctx.params.at("ledger_index") : nullptr;
|
|
|
|
std::optional<std::uint32_t> ledgerSequence = {};
|
|
if (!indexValue.is_null()) {
|
|
if (indexValue.is_string()) {
|
|
auto const stringIndex = boost::json::value_to<std::string>(indexValue);
|
|
if (stringIndex == "validated") {
|
|
ledgerSequence = ctx.range.maxSequence;
|
|
} else {
|
|
ledgerSequence = parseStringAsUInt(stringIndex);
|
|
}
|
|
} else if (indexValue.is_int64() or indexValue.is_uint64()) {
|
|
ledgerSequence = util::integralValueAs<uint32_t>(indexValue);
|
|
}
|
|
} else {
|
|
ledgerSequence = ctx.range.maxSequence;
|
|
}
|
|
|
|
if (!ledgerSequence)
|
|
return std::unexpected{Status{RippledError::RpcInvalidParams, "ledgerIndexMalformed"}};
|
|
|
|
auto lgrInfo = backend->fetchLedgerBySequence(*ledgerSequence, ctx.yield);
|
|
|
|
if (!lgrInfo || lgrInfo->seq > ctx.range.maxSequence)
|
|
return std::unexpected{Status{RippledError::RpcLgrNotFound, "ledgerNotFound"}};
|
|
|
|
return *lgrInfo;
|
|
}
|
|
|
|
// extract ledgerHeaderFromRequest's parameter from context
|
|
std::expected<xrpl::LedgerHeader, Status>
|
|
getLedgerHeaderFromHashOrSeq(
|
|
BackendInterface const& backend,
|
|
boost::asio::yield_context yield,
|
|
std::optional<std::string> ledgerHash,
|
|
std::optional<uint32_t> ledgerIndex,
|
|
uint32_t maxSeq
|
|
)
|
|
{
|
|
std::optional<xrpl::LedgerHeader> lgrInfo;
|
|
auto const err = std::unexpected{Status{RippledError::RpcLgrNotFound, "ledgerNotFound"}};
|
|
if (ledgerHash) {
|
|
// invoke uint256's constructor to parse the hex string , instead of
|
|
// copying buffer
|
|
xrpl::uint256 const ledgerHash256{std::string_view(*ledgerHash)};
|
|
lgrInfo = backend.fetchLedgerByHash(ledgerHash256, yield);
|
|
if (!lgrInfo || lgrInfo->seq > maxSeq)
|
|
return err;
|
|
|
|
return *lgrInfo;
|
|
}
|
|
auto const ledgerSequence = ledgerIndex.value_or(maxSeq);
|
|
// return without check db
|
|
if (ledgerSequence > maxSeq)
|
|
return err;
|
|
|
|
lgrInfo = backend.fetchLedgerBySequence(ledgerSequence, yield);
|
|
if (!lgrInfo)
|
|
return err;
|
|
|
|
return *lgrInfo;
|
|
}
|
|
|
|
std::expected<xrpl::LedgerHeader, Status>
|
|
getLedgerHeaderFromLedgerSpecifier(
|
|
BackendInterface const& backend,
|
|
boost::asio::yield_context yield,
|
|
rpc::spec::LedgerSpecifier const& ledger,
|
|
uint32_t maxSeq
|
|
)
|
|
{
|
|
auto const err = std::unexpected{Status{RippledError::RpcLgrNotFound, "ledgerNotFound"}};
|
|
auto const resolved = ledger.resolved();
|
|
|
|
if (resolved.isHash()) {
|
|
auto const maybeLgrInfo =
|
|
backend.fetchLedgerByHash(std::get<xrpl::uint256>(resolved.value), yield);
|
|
if (not maybeLgrInfo.has_value() or maybeLgrInfo->seq > maxSeq)
|
|
return err;
|
|
|
|
return *maybeLgrInfo;
|
|
}
|
|
|
|
// `current` and `closed` name ledgers Clio does not hold. Forwarding diverts them for the
|
|
// methods xrpld can answer; Clio-only methods are not forwarded, so they reach here.
|
|
if (resolved.isShortcut() and
|
|
std::get<rpc::spec::LedgerShortcut>(resolved.value) !=
|
|
rpc::spec::LedgerShortcut::Validated) {
|
|
return std::unexpected{Status{RippledError::RpcInvalidParams, "ledgerIndexMalformed"}};
|
|
}
|
|
|
|
auto const ledgerSequence = resolved.isSequence() ? std::get<uint32_t>(resolved.value) : maxSeq;
|
|
|
|
// return without hitting the db
|
|
if (ledgerSequence > maxSeq)
|
|
return err;
|
|
|
|
auto const maybeLgrInfo = backend.fetchLedgerBySequence(ledgerSequence, yield);
|
|
if (not maybeLgrInfo.has_value())
|
|
return err;
|
|
|
|
return *maybeLgrInfo;
|
|
}
|
|
|
|
std::vector<unsigned char>
|
|
ledgerHeaderToBlob(xrpl::LedgerHeader const& info, bool includeHash)
|
|
{
|
|
xrpl::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);
|
|
if (includeHash)
|
|
s.addBitString(info.hash);
|
|
return s.peekData();
|
|
}
|
|
|
|
std::uint64_t
|
|
getStartHint(xrpl::SLE const& sle, xrpl::AccountID const& accountID)
|
|
{
|
|
if (sle.getType() == xrpl::ltRIPPLE_STATE) {
|
|
if (sle.getFieldAmount(xrpl::sfLowLimit).getIssuer() == accountID) {
|
|
return sle.getFieldU64(xrpl::sfLowNode);
|
|
}
|
|
if (sle.getFieldAmount(xrpl::sfHighLimit).getIssuer() == accountID)
|
|
return sle.getFieldU64(xrpl::sfHighNode);
|
|
}
|
|
|
|
if (!sle.isFieldPresent(xrpl::sfOwnerNode))
|
|
return 0;
|
|
|
|
return sle.getFieldU64(xrpl::sfOwnerNode);
|
|
}
|
|
|
|
// traverse account's nfts
|
|
// return Status if error occurs
|
|
// return [nextpage, count of nft already found] if success
|
|
std::expected<AccountCursor, Status>
|
|
traverseNFTObjects(
|
|
BackendInterface const& backend,
|
|
std::uint32_t sequence,
|
|
xrpl::AccountID const& accountID,
|
|
xrpl::uint256 nextPage,
|
|
std::uint32_t limit,
|
|
boost::asio::yield_context yield,
|
|
std::function<void(xrpl::SLE)> atOwnedNode
|
|
)
|
|
{
|
|
auto const firstNFTPage = xrpl::keylet::nftokenPageMin(accountID);
|
|
auto const lastNFTPage = xrpl::keylet::nftokenPageMax(accountID);
|
|
|
|
// check if nextPage is valid
|
|
if (nextPage != beast::kZero and firstNFTPage.key != (nextPage & ~xrpl::nft::kPageMask))
|
|
return std::unexpected{Status{RippledError::RpcInvalidParams, "Invalid marker."}};
|
|
|
|
// no marker, start from the last page
|
|
xrpl::uint256 const currentPage = nextPage == beast::kZero ? lastNFTPage.key : nextPage;
|
|
|
|
// read the current page
|
|
auto page = backend.fetchLedgerObject(currentPage, sequence, yield);
|
|
|
|
if (!page) {
|
|
if (nextPage == beast::kZero) { // no nft objects in lastNFTPage
|
|
return AccountCursor{.index = beast::kZero, .hint = 0};
|
|
}
|
|
// marker is in the right range, but still invalid
|
|
return std::unexpected{Status{RippledError::RpcInvalidParams, "Invalid marker."}};
|
|
}
|
|
|
|
// the object exists and the key is in right range, must be nft page
|
|
xrpl::SLE pageSLE{xrpl::SerialIter{page->data(), page->size()}, currentPage};
|
|
|
|
auto count = 0u;
|
|
// traverse the nft page linked list until the start of the list or reach the limit
|
|
while (true) {
|
|
auto const nftPreviousPage = pageSLE.getFieldH256(xrpl::sfPreviousPageMin);
|
|
atOwnedNode(std::move(pageSLE));
|
|
count++;
|
|
|
|
if (count == limit or nftPreviousPage == beast::kZero)
|
|
return AccountCursor{.index = nftPreviousPage, .hint = count};
|
|
|
|
page = backend.fetchLedgerObject(nftPreviousPage, sequence, yield);
|
|
if (!page)
|
|
break;
|
|
pageSLE = xrpl::SLE{xrpl::SerialIter{page->data(), page->size()}, nftPreviousPage};
|
|
}
|
|
|
|
return AccountCursor{.index = beast::kZero, .hint = 0};
|
|
}
|
|
|
|
std::expected<AccountCursor, Status>
|
|
traverseOwnedNodes(
|
|
BackendInterface const& backend,
|
|
xrpl::AccountID const& accountID,
|
|
std::uint32_t sequence,
|
|
std::uint32_t limit,
|
|
std::optional<std::string> jsonCursor,
|
|
boost::asio::yield_context yield,
|
|
std::function<void(xrpl::SLE)> atOwnedNode,
|
|
bool nftIncluded
|
|
)
|
|
{
|
|
auto const maybeCursor = parseAccountCursor(jsonCursor);
|
|
|
|
if (!maybeCursor)
|
|
return std::unexpected{Status{RippledError::RpcInvalidParams, "Malformed cursor."}};
|
|
|
|
// the format is checked in RPC framework level
|
|
auto [hexCursor, startHint] = *maybeCursor;
|
|
|
|
auto const isNftMarkerNonZero =
|
|
startHint == std::numeric_limits<uint32_t>::max() and hexCursor != beast::kZero;
|
|
auto const isNftMarkerZero =
|
|
startHint == std::numeric_limits<uint32_t>::max() and hexCursor == beast::kZero;
|
|
// if we need to traverse nft objects and this is the first request -> traverse nft objects
|
|
// if we need to traverse nft objects and the marker is still in nft page -> traverse nft
|
|
// objects if we need to traverse nft objects and the marker is still in nft page but next page
|
|
// is zero -> owned nodes if we need to traverse nft objects and the marker is not in nft page
|
|
// -> traverse owned nodes
|
|
if (nftIncluded and (!jsonCursor or isNftMarkerNonZero)) {
|
|
auto const cursorMaybe =
|
|
traverseNFTObjects(backend, sequence, accountID, hexCursor, limit, yield, atOwnedNode);
|
|
|
|
if (!cursorMaybe.has_value())
|
|
return cursorMaybe;
|
|
|
|
auto const [nextNFTPage, nftsCount] = cursorMaybe.value();
|
|
|
|
// if limit reach , we return the next page and max as marker
|
|
if (nftsCount >= limit) {
|
|
return AccountCursor{
|
|
.index = nextNFTPage, .hint = std::numeric_limits<uint32_t>::max()
|
|
};
|
|
}
|
|
|
|
// adjust limit ,continue traversing owned nodes
|
|
limit -= nftsCount;
|
|
hexCursor = beast::kZero;
|
|
startHint = 0;
|
|
} else if (nftIncluded and isNftMarkerZero) {
|
|
// the last request happen to fetch all the nft, adjust marker to continue traversing owned
|
|
// nodes
|
|
hexCursor = beast::kZero;
|
|
startHint = 0;
|
|
}
|
|
|
|
return traverseOwnedNodes(
|
|
backend,
|
|
xrpl::keylet::ownerDir(accountID),
|
|
hexCursor,
|
|
startHint,
|
|
sequence,
|
|
limit,
|
|
yield,
|
|
atOwnedNode
|
|
);
|
|
}
|
|
|
|
std::expected<AccountCursor, Status>
|
|
traverseOwnedNodes(
|
|
BackendInterface const& backend,
|
|
xrpl::Keylet const& owner,
|
|
xrpl::uint256 const& hexMarker,
|
|
std::uint32_t const startHint,
|
|
std::uint32_t sequence,
|
|
std::uint32_t limit,
|
|
boost::asio::yield_context yield,
|
|
std::function<void(xrpl::SLE)> atOwnedNode
|
|
)
|
|
{
|
|
auto cursor = AccountCursor({.index = beast::kZero, .hint = 0});
|
|
|
|
auto const rootIndex = owner;
|
|
auto currentIndex = rootIndex;
|
|
// track the current page we are accessing, will return it as the next hint
|
|
auto currentPage = startHint;
|
|
|
|
std::vector<xrpl::uint256> keys;
|
|
// Only reserve 2048 nodes when fetching all owned ledger objects. If there
|
|
// are more, then keys will allocate more memory, which is suboptimal, but
|
|
// should only occur occasionally.
|
|
static constexpr std::uint32_t kMinNodes = 2048;
|
|
keys.reserve(std::min(kMinNodes, limit));
|
|
|
|
auto start = std::chrono::system_clock::now();
|
|
|
|
// If startAfter is not zero try jumping to that page using the hint
|
|
if (hexMarker.isNonZero()) {
|
|
auto const hintIndex = xrpl::keylet::page(rootIndex, startHint);
|
|
auto hintDir = backend.fetchLedgerObject(hintIndex.key, sequence, yield);
|
|
|
|
if (!hintDir)
|
|
return std::unexpected{Status(xrpl::RpcInvalidParams, "Invalid marker.")};
|
|
|
|
xrpl::SerialIter hintDirIt{hintDir->data(), hintDir->size()};
|
|
xrpl::SLE const hintDirSle{hintDirIt, hintIndex.key};
|
|
|
|
if (auto const& indexes = hintDirSle.getFieldV256(xrpl::sfIndexes);
|
|
std::ranges::find(indexes, hexMarker) == std::end(indexes)) {
|
|
// the index specified by marker is not in the page specified by marker
|
|
return std::unexpected{Status(xrpl::RpcInvalidParams, "Invalid marker.")};
|
|
}
|
|
|
|
currentIndex = hintIndex;
|
|
bool found = false;
|
|
for (;;) {
|
|
auto const ownerDir = backend.fetchLedgerObject(currentIndex.key, sequence, yield);
|
|
|
|
if (!ownerDir) {
|
|
return std::unexpected{
|
|
Status(xrpl::RpcInvalidParams, "Owner directory not found.")
|
|
};
|
|
}
|
|
|
|
xrpl::SerialIter ownedDirIt{ownerDir->data(), ownerDir->size()};
|
|
xrpl::SLE const ownedDirSle{ownedDirIt, currentIndex.key};
|
|
|
|
for (auto const& key : ownedDirSle.getFieldV256(xrpl::sfIndexes)) {
|
|
if (!found) {
|
|
if (key == hexMarker)
|
|
found = true;
|
|
} else {
|
|
keys.push_back(key);
|
|
|
|
if (--limit == 0) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (limit == 0) {
|
|
cursor = AccountCursor({.index = keys.back(), .hint = currentPage});
|
|
break;
|
|
}
|
|
// the next page
|
|
auto const uNodeNext = ownedDirSle.getFieldU64(xrpl::sfIndexNext);
|
|
if (uNodeNext == 0)
|
|
break;
|
|
|
|
currentIndex = xrpl::keylet::page(rootIndex, uNodeNext);
|
|
currentPage = uNodeNext;
|
|
}
|
|
} else {
|
|
for (;;) {
|
|
auto const ownerDir = backend.fetchLedgerObject(currentIndex.key, sequence, yield);
|
|
|
|
if (!ownerDir)
|
|
break;
|
|
|
|
xrpl::SerialIter ownedDirIt{ownerDir->data(), ownerDir->size()};
|
|
xrpl::SLE const ownedDirSle{ownedDirIt, currentIndex.key};
|
|
|
|
for (auto const& key : ownedDirSle.getFieldV256(xrpl::sfIndexes)) {
|
|
keys.push_back(key);
|
|
|
|
if (--limit == 0)
|
|
break;
|
|
}
|
|
|
|
if (limit == 0) {
|
|
cursor = AccountCursor({.index = keys.back(), .hint = currentPage});
|
|
break;
|
|
}
|
|
|
|
auto const uNodeNext = ownedDirSle.getFieldU64(xrpl::sfIndexNext);
|
|
if (uNodeNext == 0)
|
|
break;
|
|
|
|
currentIndex = xrpl::keylet::page(rootIndex, uNodeNext);
|
|
currentPage = uNodeNext;
|
|
}
|
|
}
|
|
auto end = std::chrono::system_clock::now();
|
|
|
|
static util::Logger const log{"RPC"}; // NOLINT(readability-identifier-naming)
|
|
|
|
LOG(log.debug()) << fmt::format(
|
|
"Time loading owned directories: {} milliseconds, entries size: {}",
|
|
std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count(),
|
|
keys.size()
|
|
);
|
|
|
|
auto [objects, timeDiff] =
|
|
util::timed([&]() { return backend.fetchLedgerObjects(keys, sequence, yield); });
|
|
|
|
LOG(log.debug()) << "Time loading owned entries: " << timeDiff << " milliseconds";
|
|
|
|
for (auto i = 0u; i < objects.size(); ++i) {
|
|
xrpl::SerialIter it{objects[i].data(), objects[i].size()};
|
|
atOwnedNode(xrpl::SLE{it, keys[i]});
|
|
}
|
|
|
|
if (limit == 0)
|
|
return cursor;
|
|
|
|
return AccountCursor({.index = beast::kZero, .hint = 0});
|
|
}
|
|
|
|
std::shared_ptr<xrpl::SLE const>
|
|
read(
|
|
std::shared_ptr<data::BackendInterface const> const& backend,
|
|
xrpl::Keylet const& keylet,
|
|
xrpl::LedgerHeader const& lgrInfo,
|
|
web::Context const& context
|
|
)
|
|
{
|
|
if (auto const blob = backend->fetchLedgerObject(keylet.key, lgrInfo.seq, context.yield);
|
|
blob) {
|
|
return std::make_shared<xrpl::SLE const>(
|
|
xrpl::SerialIter{blob->data(), blob->size()}, keylet.key
|
|
);
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
std::optional<xrpl::Seed>
|
|
parseRippleLibSeed(boost::json::value const& value)
|
|
{
|
|
// ripple-lib encodes seed used to generate an Ed25519 wallet in a
|
|
// non-standard way. While rippled never encode seeds that way, we
|
|
// try to detect such keys to avoid user confusion.
|
|
if (!value.is_string())
|
|
return {};
|
|
|
|
auto const result =
|
|
xrpl::decodeBase58Token(boost::json::value_to<std::string>(value), xrpl::TokenType::None);
|
|
|
|
static constexpr std::size_t kSeedSize = 18;
|
|
static constexpr std::array<std::uint8_t, 2> kSeedPrefix = {0xE1, 0x4B};
|
|
if (result.size() == kSeedSize && static_cast<std::uint8_t>(result[0]) == kSeedPrefix[0] &&
|
|
static_cast<std::uint8_t>(result[1]) == kSeedPrefix[1])
|
|
return xrpl::Seed(xrpl::makeSlice(result.substr(2)));
|
|
|
|
return {};
|
|
}
|
|
|
|
std::vector<xrpl::AccountID>
|
|
getAccountsFromTransaction(boost::json::object const& transaction)
|
|
{
|
|
std::vector<xrpl::AccountID> accounts = {};
|
|
for (auto const& [key, value] : transaction) {
|
|
if (value.is_object()) {
|
|
auto inObject = getAccountsFromTransaction(value.as_object());
|
|
accounts.insert(accounts.end(), inObject.begin(), inObject.end());
|
|
} else if (value.is_string()) {
|
|
auto const account = util::parseBase58Wrapper<xrpl::AccountID>(
|
|
boost::json::value_to<std::string>(value)
|
|
);
|
|
if (account) {
|
|
accounts.push_back(*account);
|
|
}
|
|
}
|
|
}
|
|
|
|
return accounts;
|
|
}
|
|
|
|
bool
|
|
isGlobalFrozen(
|
|
BackendInterface const& backend,
|
|
std::uint32_t sequence,
|
|
xrpl::AccountID const& issuer,
|
|
boost::asio::yield_context yield
|
|
)
|
|
{
|
|
if (xrpl::isXRP(issuer))
|
|
return false;
|
|
|
|
auto key = xrpl::keylet::account(issuer).key;
|
|
auto blob = backend.fetchLedgerObject(key, sequence, yield);
|
|
|
|
if (!blob)
|
|
return false;
|
|
|
|
xrpl::SerialIter it{blob->data(), blob->size()};
|
|
xrpl::SLE const sle{it, key};
|
|
|
|
return sle.isFlag(xrpl::lsfGlobalFreeze);
|
|
}
|
|
|
|
bool
|
|
fetchAndCheckAnyFlagsExists(
|
|
BackendInterface const& backend,
|
|
std::uint32_t sequence,
|
|
xrpl::Keylet const& keylet,
|
|
std::vector<std::uint32_t> const& flags,
|
|
boost::asio::yield_context yield
|
|
)
|
|
{
|
|
auto const blob = backend.fetchLedgerObject(keylet.key, sequence, yield);
|
|
|
|
if (!blob)
|
|
return false;
|
|
|
|
xrpl::SerialIter it{blob->data(), blob->size()};
|
|
xrpl::SLE const sle{it, keylet.key};
|
|
|
|
return std::ranges::any_of(flags, [sle](std::uint32_t flag) { return sle.isFlag(flag); });
|
|
}
|
|
|
|
bool
|
|
isFrozen(
|
|
BackendInterface const& backend,
|
|
std::uint32_t sequence,
|
|
xrpl::AccountID const& account,
|
|
xrpl::Currency const& currency,
|
|
xrpl::AccountID const& issuer,
|
|
boost::asio::yield_context yield
|
|
)
|
|
{
|
|
if (xrpl::isXRP(currency))
|
|
return false;
|
|
|
|
if (fetchAndCheckAnyFlagsExists(
|
|
backend, sequence, xrpl::keylet::account(issuer), {xrpl::lsfGlobalFreeze}, yield
|
|
))
|
|
return true;
|
|
|
|
auto const trustLineKeylet = xrpl::keylet::trustLine(account, issuer, currency);
|
|
return issuer != account &&
|
|
fetchAndCheckAnyFlagsExists(
|
|
backend,
|
|
sequence,
|
|
trustLineKeylet,
|
|
{(issuer > account) ? xrpl::lsfHighFreeze : xrpl::lsfLowFreeze},
|
|
yield
|
|
);
|
|
}
|
|
|
|
bool
|
|
isDeepFrozen(
|
|
BackendInterface const& backend,
|
|
std::uint32_t sequence,
|
|
xrpl::AccountID const& account,
|
|
xrpl::Currency const& currency,
|
|
xrpl::AccountID const& issuer,
|
|
boost::asio::yield_context yield
|
|
)
|
|
{
|
|
if (xrpl::isXRP(currency))
|
|
return false;
|
|
|
|
if (issuer == account)
|
|
return false;
|
|
|
|
auto const trustLineKeylet = xrpl::keylet::trustLine(account, issuer, currency);
|
|
|
|
return fetchAndCheckAnyFlagsExists(
|
|
backend, sequence, trustLineKeylet, {xrpl::lsfHighDeepFreeze, xrpl::lsfLowDeepFreeze}, yield
|
|
);
|
|
}
|
|
|
|
bool
|
|
isLPTokenFrozen(
|
|
BackendInterface const& backend,
|
|
std::uint32_t sequence,
|
|
xrpl::AccountID const& account,
|
|
xrpl::Issue const& asset,
|
|
xrpl::Issue const& asset2,
|
|
boost::asio::yield_context yield
|
|
)
|
|
{
|
|
return isFrozen(backend, sequence, account, asset.currency, asset.account, yield) ||
|
|
isFrozen(backend, sequence, account, asset2.currency, asset2.account, yield);
|
|
}
|
|
|
|
xrpl::XRPAmount
|
|
xrpLiquid(
|
|
BackendInterface const& backend,
|
|
std::uint32_t sequence,
|
|
xrpl::AccountID const& id,
|
|
boost::asio::yield_context yield
|
|
)
|
|
{
|
|
auto const key = xrpl::keylet::account(id).key;
|
|
auto blob = backend.fetchLedgerObject(key, sequence, yield);
|
|
|
|
if (!blob)
|
|
return beast::kZero;
|
|
|
|
xrpl::SerialIter it{blob->data(), blob->size()};
|
|
xrpl::SLE const sle{it, key};
|
|
|
|
std::uint32_t const ownerCount = sle.getFieldU32(xrpl::sfOwnerCount);
|
|
|
|
// A sponsored account pays no base reserve of its own, and an account pays one extra base
|
|
// reserve for every account it sponsors. Before the Sponsor amendment activates neither field
|
|
// is ever set, so this evaluates to 1 and matches the previous behaviour.
|
|
std::uint32_t const accountCount = (sle.isFieldPresent(xrpl::sfSponsor) ? 0 : 1) +
|
|
sle.getFieldU32(xrpl::sfSponsoringAccountCount);
|
|
|
|
auto balance = sle.getFieldAmount(xrpl::sfBalance);
|
|
|
|
xrpl::STAmount const amount = [&]() {
|
|
// AMM doesn't require the reserves
|
|
if ((sle.getFlags() & xrpl::lsfAMMNode) != 0u)
|
|
return balance;
|
|
auto const reserve =
|
|
backend.fetchFees(sequence, yield)->accountReserve(ownerCount, accountCount);
|
|
xrpl::STAmount amount = balance - reserve;
|
|
if (balance < reserve)
|
|
amount.clear();
|
|
return amount;
|
|
}();
|
|
|
|
return amount.xrp();
|
|
}
|
|
|
|
xrpl::STAmount
|
|
accountFunds(
|
|
BackendInterface const& backend,
|
|
data::AmendmentCenterInterface const& amendmentCenter,
|
|
std::uint32_t const sequence,
|
|
xrpl::STAmount const& amount,
|
|
xrpl::AccountID const& id,
|
|
boost::asio::yield_context yield
|
|
)
|
|
{
|
|
if (!amount.native() && amount.getIssuer() == id) {
|
|
return amount;
|
|
}
|
|
|
|
return accountHolds(
|
|
backend,
|
|
amendmentCenter,
|
|
sequence,
|
|
id,
|
|
amount.get<xrpl::Issue>().currency,
|
|
amount.getIssuer(),
|
|
true,
|
|
yield
|
|
);
|
|
}
|
|
|
|
xrpl::STAmount
|
|
ammAccountHolds(
|
|
BackendInterface const& backend,
|
|
std::uint32_t sequence,
|
|
xrpl::AccountID const& account,
|
|
xrpl::Currency const& currency,
|
|
xrpl::AccountID const& issuer,
|
|
bool const zeroIfFrozen,
|
|
boost::asio::yield_context yield
|
|
)
|
|
{
|
|
xrpl::STAmount amount;
|
|
ASSERT(!xrpl::isXRP(currency), "LPToken currency can never be XRP");
|
|
if (xrpl::isXRP(currency))
|
|
return {xrpLiquid(backend, sequence, account, yield)};
|
|
|
|
auto const key = xrpl::keylet::trustLine(account, issuer, currency).key;
|
|
auto const blob = backend.fetchLedgerObject(key, sequence, yield);
|
|
|
|
if (!blob) {
|
|
amount.setIssue(xrpl::Issue(currency, issuer));
|
|
amount.clear();
|
|
return amount;
|
|
}
|
|
|
|
xrpl::SerialIter it{blob->data(), blob->size()};
|
|
xrpl::SLE const sle{it, key};
|
|
|
|
if (zeroIfFrozen &&
|
|
(isFrozen(backend, sequence, account, currency, issuer, yield) ||
|
|
isDeepFrozen(backend, sequence, account, currency, issuer, yield))) {
|
|
amount.setIssue(xrpl::Issue(currency, issuer));
|
|
amount.clear();
|
|
} else {
|
|
amount = sle.getFieldAmount(xrpl::sfBalance);
|
|
if (account > issuer) {
|
|
// Put balance in account terms.
|
|
amount.negate();
|
|
}
|
|
amount.setIssue(xrpl::Issue{amount.get<xrpl::Issue>().currency, issuer});
|
|
}
|
|
|
|
return amount;
|
|
}
|
|
|
|
xrpl::STAmount
|
|
accountHolds(
|
|
BackendInterface const& backend,
|
|
data::AmendmentCenterInterface const& amendmentCenter,
|
|
std::uint32_t sequence,
|
|
xrpl::AccountID const& account,
|
|
xrpl::Currency const& currency,
|
|
xrpl::AccountID const& issuer,
|
|
bool const zeroIfFrozen,
|
|
boost::asio::yield_context yield
|
|
)
|
|
{
|
|
xrpl::STAmount amount;
|
|
if (xrpl::isXRP(currency))
|
|
return {xrpLiquid(backend, sequence, account, yield)};
|
|
|
|
auto const key = xrpl::keylet::trustLine(account, issuer, currency).key;
|
|
auto const blob = backend.fetchLedgerObject(key, sequence, yield);
|
|
|
|
if (!blob) {
|
|
amount.setIssue(xrpl::Issue(currency, issuer));
|
|
amount.clear();
|
|
return amount;
|
|
}
|
|
|
|
auto const allowBalance = [&]() {
|
|
if (!zeroIfFrozen)
|
|
return true;
|
|
|
|
if (isFrozen(backend, sequence, account, currency, issuer, yield))
|
|
return false;
|
|
|
|
if (amendmentCenter.isEnabled(
|
|
yield, data::Amendments::fixFrozenLPTokenTransfer, sequence
|
|
)) {
|
|
auto const issuerBlob =
|
|
backend.fetchLedgerObject(xrpl::keylet::account(issuer).key, sequence, yield);
|
|
|
|
if (!issuerBlob)
|
|
return false;
|
|
|
|
xrpl::SLE const issuerSle{
|
|
xrpl::SerialIter{issuerBlob->data(), issuerBlob->size()},
|
|
xrpl::keylet::account(issuer).key
|
|
};
|
|
|
|
// if the issuer is an amm account, then currency is lptoken, so we will need to check
|
|
// if the assets in the pool are frozen as well
|
|
if (issuerSle.isFieldPresent(xrpl::sfAMMID)) {
|
|
auto const ammKeylet = xrpl::keylet::amm(issuerSle[xrpl::sfAMMID]);
|
|
auto const ammBlob = backend.fetchLedgerObject(ammKeylet.key, sequence, yield);
|
|
|
|
if (!ammBlob)
|
|
return false;
|
|
|
|
xrpl::SLE const ammSle{
|
|
xrpl::SerialIter{ammBlob->data(), ammBlob->size()}, ammKeylet.key
|
|
};
|
|
|
|
return !isLPTokenFrozen(
|
|
backend,
|
|
sequence,
|
|
account,
|
|
ammSle[xrpl::sfAsset].get<xrpl::Issue>(),
|
|
ammSle[xrpl::sfAsset2].get<xrpl::Issue>(),
|
|
yield
|
|
);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}();
|
|
|
|
if (allowBalance) {
|
|
xrpl::SerialIter it{blob->data(), blob->size()};
|
|
xrpl::SLE const sle{it, key};
|
|
|
|
amount = sle.getFieldAmount(xrpl::sfBalance);
|
|
if (account > issuer) {
|
|
// Put balance in account terms.
|
|
amount.negate();
|
|
}
|
|
amount.setIssue(xrpl::Issue{amount.get<xrpl::Issue>().currency, issuer});
|
|
} else {
|
|
amount.setIssue(xrpl::Issue(currency, issuer));
|
|
amount.clear();
|
|
}
|
|
|
|
return amount;
|
|
}
|
|
|
|
xrpl::Rate
|
|
transferRate(
|
|
BackendInterface const& backend,
|
|
std::uint32_t sequence,
|
|
xrpl::AccountID const& issuer,
|
|
boost::asio::yield_context yield
|
|
)
|
|
{
|
|
auto key = xrpl::keylet::account(issuer).key;
|
|
auto blob = backend.fetchLedgerObject(key, sequence, yield);
|
|
|
|
if (blob) {
|
|
xrpl::SerialIter it{blob->data(), blob->size()};
|
|
xrpl::SLE const sle{it, key};
|
|
|
|
if (sle.isFieldPresent(xrpl::sfTransferRate))
|
|
return xrpl::Rate{sle.getFieldU32(xrpl::sfTransferRate)};
|
|
}
|
|
|
|
return xrpl::kParityRate;
|
|
}
|
|
|
|
xrpl::STAmount
|
|
accountHoldsMPT(
|
|
BackendInterface const& backend,
|
|
std::uint32_t sequence,
|
|
xrpl::AccountID const& account,
|
|
xrpl::MPTIssue const& mptIssue,
|
|
bool zeroIfFrozen,
|
|
boost::asio::yield_context yield
|
|
)
|
|
{
|
|
xrpl::STAmount zero{mptIssue, 0};
|
|
|
|
if (account == mptIssue.getIssuer()) {
|
|
// Issuer: available = MaximumAmount - OutstandingAmount
|
|
auto const issuanceKey = xrpl::keylet::mptokenIssuance(mptIssue.getMptID()).key;
|
|
auto const issuanceBlob = backend.fetchLedgerObject(issuanceKey, sequence, yield);
|
|
if (!issuanceBlob)
|
|
return zero;
|
|
xrpl::SerialIter it{issuanceBlob->data(), issuanceBlob->size()};
|
|
xrpl::SLE const sle{it, issuanceKey};
|
|
auto const maxAmount = sle.isFieldPresent(xrpl::sfMaximumAmount)
|
|
? sle.getFieldU64(xrpl::sfMaximumAmount)
|
|
: xrpl::kMaxMpTokenAmount;
|
|
auto const outstanding = sle.isFieldPresent(xrpl::sfOutstandingAmount)
|
|
? sle.getFieldU64(xrpl::sfOutstandingAmount)
|
|
: 0u;
|
|
auto const available = (outstanding > maxAmount) ? 0u : (maxAmount - outstanding);
|
|
return xrpl::STAmount{mptIssue, available};
|
|
}
|
|
|
|
auto const key = xrpl::keylet::mptoken(mptIssue.getMptID(), account).key;
|
|
auto const blob = backend.fetchLedgerObject(key, sequence, yield);
|
|
if (!blob)
|
|
return zero;
|
|
|
|
xrpl::SerialIter it{blob->data(), blob->size()};
|
|
xrpl::SLE const sle{it, key};
|
|
|
|
auto const issuanceKey = xrpl::keylet::mptokenIssuance(mptIssue.getMptID()).key;
|
|
auto const issuanceBlob = backend.fetchLedgerObject(issuanceKey, sequence, yield);
|
|
std::optional<xrpl::SLE> issuanceSle;
|
|
if (issuanceBlob) {
|
|
xrpl::SerialIter issuanceIt{issuanceBlob->data(), issuanceBlob->size()};
|
|
issuanceSle.emplace(issuanceIt, issuanceKey);
|
|
}
|
|
|
|
if (zeroIfFrozen) {
|
|
if (issuanceSle && issuanceSle->isFlag(xrpl::lsfMPTLocked))
|
|
return zero;
|
|
if (sle.isFlag(xrpl::lsfMPTLocked))
|
|
return zero;
|
|
}
|
|
|
|
if (issuanceSle && issuanceSle->isFlag(xrpl::lsfMPTRequireAuth) &&
|
|
!sle.isFlag(xrpl::lsfMPTAuthorized))
|
|
return zero;
|
|
|
|
return xrpl::STAmount{mptIssue, sle.getFieldU64(xrpl::sfMPTAmount)};
|
|
}
|
|
|
|
boost::json::array
|
|
postProcessOrderBook(
|
|
std::vector<data::LedgerObject> const& offers,
|
|
xrpl::Book const& book,
|
|
xrpl::AccountID const& takerID,
|
|
data::BackendInterface const& backend,
|
|
data::AmendmentCenterInterface const& amendmentCenter,
|
|
std::uint32_t const ledgerSequence,
|
|
boost::asio::yield_context yield
|
|
)
|
|
{
|
|
boost::json::array jsonOffers;
|
|
|
|
std::map<xrpl::AccountID, xrpl::STAmount> umBalance;
|
|
|
|
bool const globalFreeze =
|
|
isGlobalFrozen(backend, ledgerSequence, book.out.getIssuer(), yield) ||
|
|
isGlobalFrozen(backend, ledgerSequence, book.in.getIssuer(), yield);
|
|
|
|
auto rate = transferRate(backend, ledgerSequence, book.out.getIssuer(), yield);
|
|
|
|
for (auto const& obj : offers) {
|
|
try {
|
|
xrpl::SerialIter it{obj.blob.data(), obj.blob.size()};
|
|
xrpl::SLE const offer{it, obj.key};
|
|
xrpl::uint256 const bookDir = offer.getFieldH256(xrpl::sfBookDirectory);
|
|
|
|
auto const uOfferOwnerID = offer.getAccountID(xrpl::sfAccount);
|
|
auto const& saTakerGets = offer.getFieldAmount(xrpl::sfTakerGets);
|
|
auto const& saTakerPays = offer.getFieldAmount(xrpl::sfTakerPays);
|
|
xrpl::STAmount saOwnerFunds;
|
|
bool firstOwnerOffer = true;
|
|
|
|
if (book.out.getIssuer() == uOfferOwnerID) {
|
|
// If an offer is selling the issuer's own asset, it is treated as fully funded.
|
|
// This mirrors xrpld's getBookPage for both IOU and MPT.
|
|
//
|
|
// NOTE: xrpld has a separate fix (issuerFundsToSelfIssue) that bounds an MPT
|
|
// issuer's self-issued offers by the remaining issuance capacity
|
|
// (MaximumAmount - OutstandingAmount). It is not part of the getBookPage logic
|
|
// mirrored here; port it if/when clio mirrors that updated getBookPage.
|
|
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 {
|
|
if (book.out.holds<xrpl::MPTIssue>()) {
|
|
saOwnerFunds = accountHoldsMPT(
|
|
backend,
|
|
ledgerSequence,
|
|
uOfferOwnerID,
|
|
book.out.get<xrpl::MPTIssue>(),
|
|
true,
|
|
yield
|
|
);
|
|
} else {
|
|
saOwnerFunds = accountHolds(
|
|
backend,
|
|
amendmentCenter,
|
|
ledgerSequence,
|
|
uOfferOwnerID,
|
|
book.out.get<xrpl::Issue>().currency,
|
|
book.out.getIssuer(),
|
|
true,
|
|
yield
|
|
);
|
|
}
|
|
|
|
if (saOwnerFunds < beast::kZero)
|
|
saOwnerFunds.clear();
|
|
}
|
|
}
|
|
|
|
boost::json::object offerJson = toJson(offer);
|
|
|
|
xrpl::STAmount saTakerGetsFunded;
|
|
xrpl::STAmount saOwnerFundsLimit = saOwnerFunds;
|
|
xrpl::Rate offerRate = xrpl::kParityRate;
|
|
xrpl::STAmount const dirRate = xrpl::amountFromQuality(getQuality(bookDir));
|
|
|
|
if (rate != xrpl::kParityRate
|
|
// Have a transfer fee.
|
|
&& takerID != book.out.getIssuer()
|
|
// Not taking offers of own IOUs.
|
|
&& book.out.getIssuer() != uOfferOwnerID)
|
|
// Offer owner not issuing ownfunds
|
|
{
|
|
// Need to charge a transfer fee to offer owner.
|
|
offerRate = rate;
|
|
saOwnerFundsLimit = xrpl::divide(saOwnerFunds, offerRate);
|
|
}
|
|
|
|
if (saOwnerFundsLimit >= saTakerGets) {
|
|
// Sufficient funds no shenanigans.
|
|
saTakerGetsFunded = saTakerGets;
|
|
} else {
|
|
saTakerGetsFunded = saOwnerFundsLimit;
|
|
offerJson["taker_gets_funded"] =
|
|
toBoostJson(saTakerGetsFunded.getJson(xrpl::JsonOptions::Values::None));
|
|
offerJson["taker_pays_funded"] = toBoostJson(
|
|
std::min(
|
|
saTakerPays, xrpl::multiply(saTakerGetsFunded, dirRate, saTakerPays.asset())
|
|
)
|
|
.getJson(xrpl::JsonOptions::Values::None)
|
|
);
|
|
}
|
|
|
|
xrpl::STAmount const saOwnerPays = (xrpl::kParityRate == offerRate)
|
|
? saTakerGetsFunded
|
|
: std::min(saOwnerFunds, xrpl::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) {
|
|
util::Logger const log{"RPC"};
|
|
LOG(log.error()) << "caught exception: " << e.what();
|
|
}
|
|
}
|
|
return jsonOffers;
|
|
}
|
|
|
|
std::expected<xrpl::Book, Status>
|
|
parseBook(
|
|
xrpl::Currency pays,
|
|
xrpl::AccountID payIssuer,
|
|
xrpl::Currency gets,
|
|
xrpl::AccountID getIssuer,
|
|
std::optional<std::string> const& domain
|
|
)
|
|
{
|
|
return parseBook(xrpl::Issue{pays, payIssuer}, xrpl::Issue{gets, getIssuer}, domain);
|
|
}
|
|
|
|
std::expected<xrpl::Book, Status>
|
|
parseBook(
|
|
xrpl::Asset const& pays,
|
|
xrpl::Asset const& gets,
|
|
std::optional<std::string> const& domain
|
|
)
|
|
{
|
|
auto const checkIssuer = [](xrpl::Asset const& asset,
|
|
std::string_view field,
|
|
RippledError error) -> std::optional<Status> {
|
|
if (!asset.holds<xrpl::Issue>())
|
|
return std::nullopt;
|
|
|
|
auto const& issue = asset.get<xrpl::Issue>();
|
|
if (xrpl::isXRP(issue.currency) && !xrpl::isXRP(issue.account)) {
|
|
return Status{
|
|
error,
|
|
fmt::format("Unneeded field '{}.issuer' for XRP currency specification.", field)
|
|
};
|
|
}
|
|
if (!xrpl::isXRP(issue.currency) && xrpl::isXRP(issue.account)) {
|
|
return Status{
|
|
error, fmt::format("Invalid field '{}.issuer', expected non-XRP issuer.", field)
|
|
};
|
|
}
|
|
return std::nullopt;
|
|
};
|
|
|
|
if (auto const err = checkIssuer(pays, JS(taker_pays), RippledError::RpcSrcIsrMalformed))
|
|
return std::unexpected{*err};
|
|
|
|
if (auto const err = checkIssuer(gets, JS(taker_gets), RippledError::RpcDstIsrMalformed))
|
|
return std::unexpected{*err};
|
|
|
|
std::optional<xrpl::uint256> domainID = std::nullopt;
|
|
if (domain.has_value()) {
|
|
xrpl::uint256 dom;
|
|
if (!dom.parseHex(*domain)) {
|
|
return std::unexpected{
|
|
Status{RippledError::RpcDomainMalformed, "Unable to parse domain."}
|
|
};
|
|
}
|
|
domainID = dom;
|
|
}
|
|
|
|
if (pays == gets)
|
|
return std::unexpected{Status{RippledError::RpcBadMarket, "badMarket"}};
|
|
|
|
return xrpl::Book{pays, gets, domainID};
|
|
}
|
|
|
|
std::expected<xrpl::Book, Status>
|
|
parseBook(boost::json::object const& request)
|
|
{
|
|
if (!request.contains("taker_pays")) {
|
|
return std::unexpected{
|
|
Status{RippledError::RpcInvalidParams, "Missing field 'taker_pays'"}
|
|
};
|
|
}
|
|
|
|
if (!request.contains("taker_gets")) {
|
|
return std::unexpected{
|
|
Status{RippledError::RpcInvalidParams, "Missing field 'taker_gets'"}
|
|
};
|
|
}
|
|
|
|
if (!request.at("taker_pays").is_object()) {
|
|
return std::unexpected{
|
|
Status{RippledError::RpcInvalidParams, "Field 'taker_pays' is not an object"}
|
|
};
|
|
}
|
|
|
|
if (!request.at("taker_gets").is_object()) {
|
|
return std::unexpected{
|
|
Status{RippledError::RpcInvalidParams, "Field 'taker_gets' is not an object"}
|
|
};
|
|
}
|
|
|
|
auto takerPays = request.at("taker_pays").as_object();
|
|
if (!takerPays.contains("currency"))
|
|
return std::unexpected{Status{RippledError::RpcSrcCurMalformed}};
|
|
|
|
if (!takerPays.at("currency").is_string())
|
|
return std::unexpected{Status{RippledError::RpcSrcCurMalformed}};
|
|
|
|
auto takerGets = request.at("taker_gets").as_object();
|
|
if (!takerGets.contains("currency"))
|
|
return std::unexpected{Status{RippledError::RpcDstAmtMalformed}};
|
|
|
|
if (!takerGets.at("currency").is_string()) {
|
|
return std::unexpected{Status{
|
|
RippledError::RpcDstAmtMalformed,
|
|
}};
|
|
}
|
|
|
|
if (request.contains("domain") && !request.at("domain").is_string())
|
|
return std::unexpected{Status{RippledError::RpcDomainMalformed}};
|
|
|
|
xrpl::Currency payCurrency;
|
|
if (!xrpl::toCurrency(
|
|
payCurrency, boost::json::value_to<std::string>(takerPays.at("currency"))
|
|
))
|
|
return std::unexpected{Status{RippledError::RpcSrcCurMalformed}};
|
|
|
|
xrpl::Currency getCurrency;
|
|
if (!xrpl::toCurrency(getCurrency, boost::json::value_to<std::string>(takerGets["currency"])))
|
|
return std::unexpected{Status{RippledError::RpcDstAmtMalformed}};
|
|
|
|
xrpl::AccountID payIssuer;
|
|
if (takerPays.contains("issuer")) {
|
|
if (!takerPays.at("issuer").is_string()) {
|
|
return std::unexpected{
|
|
Status{RippledError::RpcInvalidParams, "takerPaysIssuerNotString"}
|
|
};
|
|
}
|
|
|
|
if (!xrpl::toIssuer(payIssuer, boost::json::value_to<std::string>(takerPays.at("issuer"))))
|
|
return std::unexpected{Status{RippledError::RpcSrcIsrMalformed}};
|
|
|
|
if (payIssuer == xrpl::noAccount())
|
|
return std::unexpected{Status{RippledError::RpcSrcIsrMalformed}};
|
|
} else {
|
|
payIssuer = xrpl::xrpAccount();
|
|
}
|
|
|
|
if (isXRP(payCurrency) && !isXRP(payIssuer)) {
|
|
return std::unexpected{Status{
|
|
RippledError::RpcSrcIsrMalformed,
|
|
"Unneeded field 'taker_pays.issuer' for XRP currency specification."
|
|
}};
|
|
}
|
|
|
|
if (!isXRP(payCurrency) && isXRP(payIssuer)) {
|
|
return std::unexpected{Status{
|
|
RippledError::RpcSrcIsrMalformed,
|
|
"Invalid field 'taker_pays.issuer', expected non-XRP issuer."
|
|
}};
|
|
}
|
|
|
|
if ((!isXRP(payCurrency)) && (!takerPays.contains("issuer"))) {
|
|
return std::unexpected{Status{RippledError::RpcSrcIsrMalformed, "Missing non-XRP issuer."}};
|
|
}
|
|
|
|
xrpl::AccountID getIssuer;
|
|
|
|
if (takerGets.contains("issuer")) {
|
|
if (!takerGets["issuer"].is_string()) {
|
|
return std::unexpected{
|
|
Status{RippledError::RpcInvalidParams, "taker_gets.issuer should be string"}
|
|
};
|
|
}
|
|
|
|
if (!xrpl::toIssuer(
|
|
getIssuer, boost::json::value_to<std::string>(takerGets.at("issuer"))
|
|
)) {
|
|
return std::unexpected{Status{
|
|
RippledError::RpcDstIsrMalformed, "Invalid field 'taker_gets.issuer', bad issuer."
|
|
}};
|
|
}
|
|
|
|
if (getIssuer == xrpl::noAccount()) {
|
|
return std::unexpected{Status{
|
|
RippledError::RpcDstIsrMalformed,
|
|
"Invalid field 'taker_gets.issuer', bad issuer account one."
|
|
}};
|
|
}
|
|
} else {
|
|
getIssuer = xrpl::xrpAccount();
|
|
}
|
|
|
|
if (xrpl::isXRP(getCurrency) && !xrpl::isXRP(getIssuer)) {
|
|
return std::unexpected{Status{
|
|
RippledError::RpcDstIsrMalformed,
|
|
"Unneeded field 'taker_gets.issuer' for XRP currency specification."
|
|
}};
|
|
}
|
|
|
|
if (!xrpl::isXRP(getCurrency) && xrpl::isXRP(getIssuer)) {
|
|
return std::unexpected{Status{
|
|
RippledError::RpcDstIsrMalformed,
|
|
"Invalid field 'taker_gets.issuer', expected non-XRP issuer."
|
|
}};
|
|
}
|
|
|
|
if (payCurrency == getCurrency && payIssuer == getIssuer)
|
|
return std::unexpected{Status{RippledError::RpcBadMarket, "badMarket"}};
|
|
|
|
std::optional<xrpl::uint256> domainID;
|
|
if (request.contains("domain")) {
|
|
xrpl::uint256 dom;
|
|
if (!dom.parseHex(boost::json::value_to<std::string>(request.at("domain"))))
|
|
return std::unexpected{Status{RippledError::RpcDomainMalformed}};
|
|
domainID = dom;
|
|
}
|
|
|
|
return xrpl::Book{
|
|
xrpl::Issue{payCurrency, payIssuer}, xrpl::Issue{getCurrency, getIssuer}, domainID
|
|
};
|
|
}
|
|
|
|
std::expected<xrpl::AccountID, Status>
|
|
parseTaker(boost::json::value const& taker)
|
|
{
|
|
std::optional<xrpl::AccountID> takerID = {};
|
|
if (!taker.is_string())
|
|
return std::unexpected{Status{RippledError::RpcInvalidParams, "takerNotString"}};
|
|
|
|
takerID = accountFromStringStrict(boost::json::value_to<std::string>(taker));
|
|
|
|
if (!takerID)
|
|
return std::unexpected{Status{RippledError::RpcBadIssuer, "invalidTakerAccount"}};
|
|
return *takerID;
|
|
}
|
|
|
|
xrpl::Issue
|
|
parseIssue(boost::json::object const& issue)
|
|
{
|
|
json::Value jv;
|
|
if (issue.contains(JS(issuer)) && issue.at(JS(issuer)).is_string())
|
|
jv["issuer"] = boost::json::value_to<std::string>(issue.at(JS(issuer)));
|
|
if (issue.contains(JS(currency)) && issue.at(JS(currency)).is_string())
|
|
jv["currency"] = boost::json::value_to<std::string>(issue.at(JS(currency)));
|
|
|
|
return xrpl::issueFromJson(jv);
|
|
}
|
|
|
|
bool
|
|
specifiesCurrentOrClosedLedger(boost::json::object const& request)
|
|
{
|
|
if (request.contains("ledger_index")) {
|
|
auto indexValue = request.at("ledger_index");
|
|
if (indexValue.is_string()) {
|
|
auto const index = boost::json::value_to<std::string>(indexValue);
|
|
return index == "current" || index == "closed";
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool
|
|
isAdminCmd(std::string const& method, boost::json::object const& request)
|
|
{
|
|
if (method == JS(ledger)) {
|
|
auto const requestStr = boost::json::serialize(request);
|
|
json::Value jv;
|
|
json::Reader{}.parse(requestStr, jv);
|
|
// rippled considers string/non-zero int/non-empty array/ non-empty json as true.
|
|
// Use rippled's API asBool to get the same result.
|
|
// https://github.com/XRPLF/rippled/issues/5119
|
|
auto const isFieldSet = [&jv](auto const field) {
|
|
return jv.isMember(field) and jv[field].asBool();
|
|
};
|
|
|
|
// According to doc
|
|
// https://xrpl.org/docs/references/http-websocket-apis/public-api-methods/ledger-methods/ledger,
|
|
// full/accounts/type are admin only, but type only works when full/accounts are set, so we
|
|
// don't need to check type.
|
|
if (isFieldSet(JS(full)) or isFieldSet(JS(accounts)))
|
|
return true;
|
|
}
|
|
|
|
if (method == JS(feature) and request.contains(JS(vetoed)))
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
std::expected<xrpl::uint256, Status>
|
|
getNFTID(boost::json::object const& request)
|
|
{
|
|
if (!request.contains(JS(nft_id)))
|
|
return std::unexpected{Status{RippledError::RpcInvalidParams, "missingTokenID"}};
|
|
|
|
if (!request.at(JS(nft_id)).is_string())
|
|
return std::unexpected{Status{RippledError::RpcInvalidParams, "tokenIDNotString"}};
|
|
|
|
xrpl::uint256 tokenid;
|
|
if (!tokenid.parseHex(boost::json::value_to<std::string>(request.at(JS(nft_id)))))
|
|
return std::unexpected{Status{RippledError::RpcInvalidParams, "malformedTokenID"}};
|
|
|
|
return tokenid;
|
|
}
|
|
|
|
boost::json::object
|
|
toJsonWithBinaryTx(data::TransactionAndMetadata const& txnPlusMeta, std::uint32_t const apiVersion)
|
|
{
|
|
boost::json::object obj{};
|
|
auto const metaKey = apiVersion > 1 ? JS(meta_blob) : JS(meta);
|
|
obj[metaKey] = xrpl::strHex(txnPlusMeta.metadata);
|
|
obj[JS(tx_blob)] = xrpl::strHex(txnPlusMeta.transaction);
|
|
return obj;
|
|
}
|
|
|
|
} // namespace rpc
|