mirror of
https://github.com/XRPLF/clio.git
synced 2025-11-20 11:45:53 +00:00
@@ -116,6 +116,7 @@ target_sources (clio PRIVATE
|
||||
## RPC
|
||||
src/rpc/Errors.cpp
|
||||
src/rpc/Factories.cpp
|
||||
src/rpc/AMMHelpers.cpp
|
||||
src/rpc/RPCHelpers.cpp
|
||||
src/rpc/Counters.cpp
|
||||
src/rpc/WorkQueue.cpp
|
||||
@@ -133,6 +134,7 @@ target_sources (clio PRIVATE
|
||||
src/rpc/handlers/AccountObjects.cpp
|
||||
src/rpc/handlers/AccountOffers.cpp
|
||||
src/rpc/handlers/AccountTx.cpp
|
||||
src/rpc/handlers/AMMInfo.cpp
|
||||
src/rpc/handlers/BookChanges.cpp
|
||||
src/rpc/handlers/BookOffers.cpp
|
||||
src/rpc/handlers/DepositAuthorized.cpp
|
||||
@@ -248,6 +250,7 @@ if (tests)
|
||||
unittests/rpc/handlers/BookChangesTests.cpp
|
||||
unittests/rpc/handlers/LedgerTests.cpp
|
||||
unittests/rpc/handlers/VersionHandlerTests.cpp
|
||||
unittests/rpc/handlers/AMMInfoTests.cpp
|
||||
# Backend
|
||||
unittests/data/BackendFactoryTests.cpp
|
||||
unittests/data/BackendCountersTests.cpp
|
||||
|
||||
@@ -129,6 +129,7 @@ BackendInterface::fetchLedgerObjects(
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
// Fetches the successor to key/index
|
||||
std::optional<ripple::uint256>
|
||||
BackendInterface::fetchSuccessorKey(
|
||||
|
||||
82
src/rpc/AMMHelpers.cpp
Normal file
82
src/rpc/AMMHelpers.cpp
Normal file
@@ -0,0 +1,82 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2024, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and 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 "rpc/AMMHelpers.h"
|
||||
|
||||
#include "data/BackendInterface.h"
|
||||
#include "util/log/Logger.h"
|
||||
|
||||
#include <ripple/protocol/AMMCore.h>
|
||||
|
||||
namespace rpc {
|
||||
|
||||
std::pair<ripple::STAmount, ripple::STAmount>
|
||||
getAmmPoolHolds(
|
||||
BackendInterface const& backend,
|
||||
std::uint32_t sequence,
|
||||
ripple::AccountID const& ammAccountID,
|
||||
ripple::Issue const& issue1,
|
||||
ripple::Issue const& issue2,
|
||||
bool freezeHandling,
|
||||
boost::asio::yield_context yield
|
||||
)
|
||||
{
|
||||
auto const assetInBalance =
|
||||
accountHolds(backend, sequence, ammAccountID, issue1.currency, issue1.account, freezeHandling, yield);
|
||||
auto const assetOutBalance =
|
||||
accountHolds(backend, sequence, ammAccountID, issue2.currency, issue2.account, freezeHandling, yield);
|
||||
return std::make_pair(assetInBalance, assetOutBalance);
|
||||
}
|
||||
|
||||
ripple::STAmount
|
||||
getAmmLpHolds(
|
||||
BackendInterface const& backend,
|
||||
std::uint32_t sequence,
|
||||
ripple::Currency const& cur1,
|
||||
ripple::Currency const& cur2,
|
||||
ripple::AccountID const& ammAccount,
|
||||
ripple::AccountID const& lpAccount,
|
||||
boost::asio::yield_context yield
|
||||
)
|
||||
{
|
||||
auto const lptCurrency = ammLPTCurrency(cur1, cur2);
|
||||
return accountHolds(backend, sequence, lpAccount, lptCurrency, ammAccount, true, yield);
|
||||
}
|
||||
|
||||
ripple::STAmount
|
||||
getAmmLpHolds(
|
||||
BackendInterface const& backend,
|
||||
std::uint32_t sequence,
|
||||
ripple::SLE const& ammSle,
|
||||
ripple::AccountID const& lpAccount,
|
||||
boost::asio::yield_context yield
|
||||
)
|
||||
{
|
||||
return getAmmLpHolds(
|
||||
backend,
|
||||
sequence,
|
||||
ammSle[ripple::sfAsset].currency,
|
||||
ammSle[ripple::sfAsset2].currency,
|
||||
ammSle[ripple::sfAccount],
|
||||
lpAccount,
|
||||
yield
|
||||
);
|
||||
}
|
||||
|
||||
} // namespace rpc
|
||||
67
src/rpc/AMMHelpers.h
Normal file
67
src/rpc/AMMHelpers.h
Normal file
@@ -0,0 +1,67 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2024, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and 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.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "data/BackendInterface.h"
|
||||
#include "rpc/RPCHelpers.h"
|
||||
|
||||
namespace rpc {
|
||||
|
||||
/**
|
||||
* @brief getAmmPoolHolds returns the balances of the amm asset pair.
|
||||
*/
|
||||
std::pair<ripple::STAmount, ripple::STAmount>
|
||||
getAmmPoolHolds(
|
||||
BackendInterface const& backend,
|
||||
std::uint32_t sequence,
|
||||
ripple::AccountID const& ammAccountID,
|
||||
ripple::Issue const& issue1,
|
||||
ripple::Issue const& issue2,
|
||||
bool freezeHandling,
|
||||
boost::asio::yield_context yield
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief getAmmLpHolds returns the lp token balance.
|
||||
*/
|
||||
ripple::STAmount
|
||||
getAmmLpHolds(
|
||||
BackendInterface const& backend,
|
||||
std::uint32_t sequence,
|
||||
ripple::Currency const& cur1,
|
||||
ripple::Currency const& cur2,
|
||||
ripple::AccountID const& ammAccount,
|
||||
ripple::AccountID const& lpAccount,
|
||||
boost::asio::yield_context yield
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief getAmmLpHolds returns the lp token balance.
|
||||
*/
|
||||
ripple::STAmount
|
||||
getAmmLpHolds(
|
||||
BackendInterface const& backend,
|
||||
std::uint32_t sequence,
|
||||
ripple::SLE const& ammSle,
|
||||
ripple::AccountID const& lpAccount,
|
||||
boost::asio::yield_context yield
|
||||
);
|
||||
|
||||
} // namespace rpc
|
||||
@@ -988,7 +988,7 @@ xrpLiquid(
|
||||
boost::asio::yield_context yield
|
||||
)
|
||||
{
|
||||
auto key = ripple::keylet::account(id).key;
|
||||
auto const key = ripple::keylet::account(id).key;
|
||||
auto blob = backend.fetchLedgerObject(key, sequence, yield);
|
||||
|
||||
if (!blob)
|
||||
@@ -999,13 +999,18 @@ xrpLiquid(
|
||||
|
||||
std::uint32_t const ownerCount = sle.getFieldU32(ripple::sfOwnerCount);
|
||||
|
||||
auto balance = sle.getFieldAmount(ripple::sfBalance);
|
||||
|
||||
ripple::STAmount const amount = [&]() {
|
||||
// AMM doesn't require the reserves
|
||||
if ((sle.getFlags() & ripple::lsfAMMNode) != 0u)
|
||||
return balance;
|
||||
auto const reserve = backend.fetchFees(sequence, yield)->accountReserve(ownerCount);
|
||||
|
||||
auto const balance = sle.getFieldAmount(ripple::sfBalance);
|
||||
|
||||
ripple::STAmount amount = balance - reserve;
|
||||
if (balance < reserve)
|
||||
amount.clear();
|
||||
return amount;
|
||||
}();
|
||||
|
||||
return amount.xrp();
|
||||
}
|
||||
@@ -1038,11 +1043,10 @@ accountHolds(
|
||||
)
|
||||
{
|
||||
ripple::STAmount amount;
|
||||
if (ripple::isXRP(currency)) {
|
||||
if (ripple::isXRP(currency))
|
||||
return {xrpLiquid(backend, sequence, account, yield)};
|
||||
}
|
||||
auto key = ripple::keylet::line(account, issuer, currency).key;
|
||||
|
||||
auto const key = ripple::keylet::line(account, issuer, currency).key;
|
||||
auto const blob = backend.fetchLedgerObject(key, sequence, yield);
|
||||
|
||||
if (!blob) {
|
||||
|
||||
@@ -212,4 +212,24 @@ CustomValidator SubscribeAccountsValidator =
|
||||
return MaybeError{};
|
||||
}};
|
||||
|
||||
CustomValidator AMMAssetValidator =
|
||||
CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError {
|
||||
if (not value.is_object())
|
||||
return Error{Status{RippledError::rpcINVALID_PARAMS, std::string(key) + "NotObject"}};
|
||||
|
||||
Json::Value jvAsset;
|
||||
if (value.as_object().contains(JS(issuer)))
|
||||
jvAsset["issuer"] = value.at(JS(issuer)).as_string().c_str();
|
||||
if (value.as_object().contains(JS(currency)))
|
||||
jvAsset["currency"] = value.at(JS(currency)).as_string().c_str();
|
||||
// same as rippled
|
||||
try {
|
||||
ripple::issueFromJson(jvAsset);
|
||||
} catch (std::runtime_error const&) {
|
||||
return Error{Status{ClioError::rpcMALFORMED_REQUEST}};
|
||||
}
|
||||
|
||||
return MaybeError{};
|
||||
}};
|
||||
|
||||
} // namespace rpc::validation
|
||||
|
||||
@@ -508,4 +508,11 @@ extern CustomValidator SubscribeStreamValidator;
|
||||
*/
|
||||
extern CustomValidator SubscribeAccountsValidator;
|
||||
|
||||
/**
|
||||
* @brief Validates an asset (ripple::Issue).
|
||||
*
|
||||
* Used by amm_info.
|
||||
*/
|
||||
extern CustomValidator AMMAssetValidator;
|
||||
|
||||
} // namespace rpc::validation
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
#include "feed/SubscriptionManager.h"
|
||||
#include "rpc/Counters.h"
|
||||
#include "rpc/common/AnyHandler.h"
|
||||
#include "rpc/handlers/AMMInfo.h"
|
||||
#include "rpc/handlers/AccountChannels.h"
|
||||
#include "rpc/handlers/AccountCurrencies.h"
|
||||
#include "rpc/handlers/AccountInfo.h"
|
||||
@@ -79,6 +80,7 @@ ProductionHandlerProvider::ProductionHandlerProvider(
|
||||
{"account_objects", {AccountObjectsHandler{backend}}},
|
||||
{"account_offers", {AccountOffersHandler{backend}}},
|
||||
{"account_tx", {AccountTxHandler{backend}}},
|
||||
{"amm_info", {AMMInfoHandler{backend}}},
|
||||
{"book_changes", {BookChangesHandler{backend}}},
|
||||
{"book_offers", {BookOffersHandler{backend}}},
|
||||
{"deposit_authorized", {DepositAuthorizedHandler{backend}}},
|
||||
|
||||
295
src/rpc/handlers/AMMInfo.cpp
Normal file
295
src/rpc/handlers/AMMInfo.cpp
Normal file
@@ -0,0 +1,295 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2024, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and 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 "rpc/handlers/AMMInfo.h"
|
||||
|
||||
#include "data/DBHelpers.h"
|
||||
#include "rpc/AMMHelpers.h"
|
||||
#include "rpc/RPCHelpers.h"
|
||||
#include "rpc/common/MetaProcessors.h"
|
||||
#include "rpc/common/Specs.h"
|
||||
#include "rpc/common/Validators.h"
|
||||
|
||||
#include <ripple/protocol/AMMCore.h>
|
||||
|
||||
namespace {
|
||||
|
||||
std::string
|
||||
toIso8601(ripple::NetClock::time_point tp)
|
||||
{
|
||||
using namespace std::chrono;
|
||||
static auto constexpr rippleEpochOffset = seconds{rippleEpochStart};
|
||||
|
||||
return date::format(
|
||||
"%Y-%Om-%dT%H:%M:%OS%z",
|
||||
date::sys_time<system_clock::duration>(system_clock::time_point{tp.time_since_epoch() + rippleEpochOffset})
|
||||
);
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace rpc {
|
||||
|
||||
AMMInfoHandler::Result
|
||||
AMMInfoHandler::process(AMMInfoHandler::Input input, Context const& ctx) const
|
||||
{
|
||||
using namespace ripple;
|
||||
|
||||
auto const hasInvalidParams = [&input] {
|
||||
// no asset/asset2 can be specified if amm account is specified
|
||||
if (input.ammAccount)
|
||||
return input.issue1 != ripple::noIssue() || input.issue2 != ripple::noIssue();
|
||||
|
||||
// both assets must be specified when amm account is not specified
|
||||
return input.issue1 == ripple::noIssue() || input.issue2 == ripple::noIssue();
|
||||
}();
|
||||
|
||||
if (hasInvalidParams)
|
||||
return Error{Status{RippledError::rpcINVALID_PARAMS}};
|
||||
|
||||
auto const range = sharedPtrBackend_->fetchLedgerRange();
|
||||
auto const lgrInfoOrStatus = getLedgerInfoFromHashOrSeq(
|
||||
*sharedPtrBackend_, ctx.yield, input.ledgerHash, input.ledgerIndex, range->maxSequence
|
||||
);
|
||||
|
||||
if (auto const status = std::get_if<Status>(&lgrInfoOrStatus))
|
||||
return Error{*status};
|
||||
|
||||
auto const lgrInfo = std::get<LedgerInfo>(lgrInfoOrStatus);
|
||||
|
||||
if (input.accountID) {
|
||||
auto keylet = keylet::account(*input.accountID);
|
||||
if (not sharedPtrBackend_->fetchLedgerObject(keylet.key, lgrInfo.seq, ctx.yield))
|
||||
return Error{Status{RippledError::rpcACT_NOT_FOUND}};
|
||||
}
|
||||
|
||||
ripple::uint256 ammID;
|
||||
if (input.ammAccount) {
|
||||
auto const accountKeylet = keylet::account(*input.ammAccount);
|
||||
auto const accountLedgerObject =
|
||||
sharedPtrBackend_->fetchLedgerObject(accountKeylet.key, lgrInfo.seq, ctx.yield);
|
||||
if (not accountLedgerObject)
|
||||
return Error{Status{RippledError::rpcACT_MALFORMED}};
|
||||
ripple::STLedgerEntry const sle{
|
||||
ripple::SerialIter{accountLedgerObject->data(), accountLedgerObject->size()}, accountKeylet.key
|
||||
};
|
||||
if (not sle.isFieldPresent(ripple::sfAMMID))
|
||||
return Error{Status{RippledError::rpcACT_NOT_FOUND}};
|
||||
ammID = sle.getFieldH256(ripple::sfAMMID);
|
||||
}
|
||||
|
||||
auto ammKeylet = ammID != 0 ? keylet::amm(ammID) : keylet::amm(input.issue1, input.issue2);
|
||||
auto const ammBlob = sharedPtrBackend_->fetchLedgerObject(ammKeylet.key, lgrInfo.seq, ctx.yield);
|
||||
|
||||
if (not ammBlob)
|
||||
return Error{Status{RippledError::rpcACT_NOT_FOUND}};
|
||||
|
||||
auto const amm = SLE{SerialIter{ammBlob->data(), ammBlob->size()}, ammKeylet.key};
|
||||
auto const ammAccountID = amm.getAccountID(sfAccount);
|
||||
auto const accBlob =
|
||||
sharedPtrBackend_->fetchLedgerObject(keylet::account(ammAccountID).key, lgrInfo.seq, ctx.yield);
|
||||
if (not accBlob)
|
||||
return Error{Status{RippledError::rpcACT_NOT_FOUND}};
|
||||
|
||||
auto const [asset1Balance, asset2Balance] =
|
||||
getAmmPoolHolds(*sharedPtrBackend_, lgrInfo.seq, ammAccountID, amm[sfAsset], amm[sfAsset2], false, ctx.yield);
|
||||
auto const lptAMMBalance = input.accountID
|
||||
? getAmmLpHolds(*sharedPtrBackend_, lgrInfo.seq, amm, *input.accountID, ctx.yield)
|
||||
: amm[sfLPTokenBalance];
|
||||
|
||||
Output response;
|
||||
response.ledgerIndex = lgrInfo.seq;
|
||||
response.ledgerHash = ripple::strHex(lgrInfo.hash);
|
||||
response.amount1 = toBoostJson(asset1Balance.getJson(JsonOptions::none));
|
||||
response.amount2 = toBoostJson(asset2Balance.getJson(JsonOptions::none));
|
||||
response.lpToken = toBoostJson(lptAMMBalance.getJson(JsonOptions::none));
|
||||
response.tradingFee = amm[sfTradingFee];
|
||||
response.ammAccount = to_string(ammAccountID);
|
||||
|
||||
if (amm.isFieldPresent(sfVoteSlots)) {
|
||||
for (auto const& voteEntry : amm.getFieldArray(sfVoteSlots)) {
|
||||
boost::json::object vote;
|
||||
vote[JS(account)] = to_string(voteEntry.getAccountID(sfAccount));
|
||||
vote[JS(trading_fee)] = voteEntry[sfTradingFee];
|
||||
vote[JS(vote_weight)] = voteEntry[sfVoteWeight];
|
||||
|
||||
response.voteSlots.push_back(std::move(vote));
|
||||
}
|
||||
}
|
||||
|
||||
if (amm.isFieldPresent(sfAuctionSlot)) {
|
||||
auto const& auctionSlot = amm.peekAtField(sfAuctionSlot).downcast<STObject>();
|
||||
if (auctionSlot.isFieldPresent(sfAccount)) {
|
||||
boost::json::object auction;
|
||||
auto const timeSlot = ammAuctionTimeSlot(lgrInfo.parentCloseTime.time_since_epoch().count(), auctionSlot);
|
||||
|
||||
auction[JS(time_interval)] = timeSlot ? *timeSlot : AUCTION_SLOT_TIME_INTERVALS;
|
||||
auction[JS(price)] = toBoostJson(auctionSlot[sfPrice].getJson(JsonOptions::none));
|
||||
auction[JS(discounted_fee)] = auctionSlot[sfDiscountedFee];
|
||||
auction[JS(account)] = to_string(auctionSlot.getAccountID(sfAccount));
|
||||
auction[JS(expiration)] = toIso8601(NetClock::time_point{NetClock::duration{auctionSlot[sfExpiration]}});
|
||||
|
||||
if (auctionSlot.isFieldPresent(sfAuthAccounts)) {
|
||||
boost::json::array auth;
|
||||
for (auto const& acct : auctionSlot.getFieldArray(sfAuthAccounts)) {
|
||||
boost::json::object accountData;
|
||||
accountData[JS(account)] = to_string(acct.getAccountID(sfAccount));
|
||||
auth.push_back(std::move(accountData));
|
||||
}
|
||||
|
||||
auction[JS(auth_accounts)] = std::move(auth);
|
||||
}
|
||||
|
||||
response.auctionSlot = std::move(auction);
|
||||
}
|
||||
}
|
||||
|
||||
if (!isXRP(asset1Balance)) {
|
||||
response.asset1Frozen = isFrozen(
|
||||
*sharedPtrBackend_, lgrInfo.seq, ammAccountID, amm[sfAsset].currency, amm[sfAsset].account, ctx.yield
|
||||
);
|
||||
}
|
||||
if (!isXRP(asset2Balance)) {
|
||||
response.asset2Frozen = isFrozen(
|
||||
*sharedPtrBackend_, lgrInfo.seq, ammAccountID, amm[sfAsset2].currency, amm[sfAsset2].account, ctx.yield
|
||||
);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
RpcSpecConstRef
|
||||
AMMInfoHandler::spec([[maybe_unused]] uint32_t apiVersion)
|
||||
{
|
||||
static auto const stringIssueValidator =
|
||||
validation::CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError {
|
||||
if (not value.is_string())
|
||||
return Error{Status{RippledError::rpcINVALID_PARAMS, std::string(key) + "NotString"}};
|
||||
|
||||
try {
|
||||
ripple::issueFromJson(value.as_string().c_str());
|
||||
} catch (std::runtime_error const&) {
|
||||
return Error{Status{RippledError::rpcISSUE_MALFORMED}};
|
||||
}
|
||||
|
||||
return MaybeError{};
|
||||
}};
|
||||
|
||||
static auto const rpcSpec = RpcSpec{
|
||||
{JS(ledger_hash), validation::Uint256HexStringValidator},
|
||||
{JS(ledger_index), validation::LedgerIndexValidator},
|
||||
{JS(asset),
|
||||
meta::WithCustomError{
|
||||
validation::Type<std::string, boost::json::object>{}, Status(RippledError::rpcISSUE_MALFORMED)
|
||||
},
|
||||
meta::IfType<std::string>{stringIssueValidator},
|
||||
meta::IfType<boost::json::object>{
|
||||
meta::WithCustomError{validation::AMMAssetValidator, Status(RippledError::rpcISSUE_MALFORMED)},
|
||||
}},
|
||||
{JS(asset2),
|
||||
meta::WithCustomError{
|
||||
validation::Type<std::string, boost::json::object>{}, Status(RippledError::rpcISSUE_MALFORMED)
|
||||
},
|
||||
meta::IfType<std::string>{stringIssueValidator},
|
||||
meta::IfType<boost::json::object>{
|
||||
meta::WithCustomError{validation::AMMAssetValidator, Status(RippledError::rpcISSUE_MALFORMED)},
|
||||
}},
|
||||
{JS(amm_account), meta::WithCustomError{validation::AccountValidator, Status(RippledError::rpcACT_MALFORMED)}},
|
||||
{JS(account), meta::WithCustomError{validation::AccountValidator, Status(RippledError::rpcACT_MALFORMED)}},
|
||||
};
|
||||
|
||||
return rpcSpec;
|
||||
}
|
||||
|
||||
void
|
||||
tag_invoke(boost::json::value_from_tag, boost::json::value& jv, AMMInfoHandler::Output const& output)
|
||||
{
|
||||
boost::json::object amm = {
|
||||
{JS(lp_token), output.lpToken},
|
||||
{JS(amount), output.amount1},
|
||||
{JS(amount2), output.amount2},
|
||||
{JS(account), output.ammAccount},
|
||||
{JS(trading_fee), output.tradingFee},
|
||||
};
|
||||
|
||||
if (output.auctionSlot != nullptr)
|
||||
amm[JS(auction_slot)] = output.auctionSlot;
|
||||
|
||||
if (not output.voteSlots.empty())
|
||||
amm[JS(vote_slots)] = output.voteSlots;
|
||||
|
||||
if (output.asset1Frozen)
|
||||
amm[JS(asset_frozen)] = *output.asset1Frozen;
|
||||
|
||||
if (output.asset2Frozen)
|
||||
amm[JS(asset2_frozen)] = *output.asset2Frozen;
|
||||
|
||||
jv = {
|
||||
{JS(amm), amm},
|
||||
{JS(ledger_index), output.ledgerIndex},
|
||||
{JS(ledger_hash), output.ledgerHash},
|
||||
{JS(validated), output.validated},
|
||||
};
|
||||
}
|
||||
|
||||
AMMInfoHandler::Input
|
||||
tag_invoke(boost::json::value_to_tag<AMMInfoHandler::Input>, boost::json::value const& jv)
|
||||
{
|
||||
auto input = AMMInfoHandler::Input{};
|
||||
auto const& jsonObject = jv.as_object();
|
||||
|
||||
if (jsonObject.contains(JS(ledger_hash)))
|
||||
input.ledgerHash = jv.at(JS(ledger_hash)).as_string().c_str();
|
||||
|
||||
if (jsonObject.contains(JS(ledger_index))) {
|
||||
if (!jsonObject.at(JS(ledger_index)).is_string()) {
|
||||
input.ledgerIndex = jv.at(JS(ledger_index)).as_int64();
|
||||
} else if (jsonObject.at(JS(ledger_index)).as_string() != "validated") {
|
||||
input.ledgerIndex = std::stoi(jv.at(JS(ledger_index)).as_string().c_str());
|
||||
}
|
||||
}
|
||||
|
||||
auto getIssue = [](boost::json::value const& request) {
|
||||
if (request.is_string())
|
||||
return ripple::issueFromJson(request.as_string().c_str());
|
||||
|
||||
// Note: no checks needed as we already validated the input if we made it here
|
||||
auto const currency = ripple::to_currency(request.at(JS(currency)).as_string().c_str());
|
||||
if (ripple::isXRP(currency)) {
|
||||
return ripple::xrpIssue();
|
||||
}
|
||||
auto const issuer = ripple::parseBase58<ripple::AccountID>(request.at(JS(issuer)).as_string().c_str());
|
||||
return ripple::Issue{currency, *issuer};
|
||||
};
|
||||
|
||||
if (jsonObject.contains(JS(asset)))
|
||||
input.issue1 = getIssue(jsonObject.at(JS(asset)));
|
||||
|
||||
if (jsonObject.contains(JS(asset2)))
|
||||
input.issue2 = getIssue(jsonObject.at(JS(asset2)));
|
||||
|
||||
if (jsonObject.contains(JS(account)))
|
||||
input.accountID = accountFromStringStrict(jsonObject.at(JS(account)).as_string().c_str());
|
||||
if (jsonObject.contains(JS(amm_account)))
|
||||
input.ammAccount = accountFromStringStrict(jsonObject.at(JS(amm_account)).as_string().c_str());
|
||||
|
||||
return input;
|
||||
}
|
||||
|
||||
} // namespace rpc
|
||||
82
src/rpc/handlers/AMMInfo.h
Normal file
82
src/rpc/handlers/AMMInfo.h
Normal file
@@ -0,0 +1,82 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2024, the clio developers.
|
||||
|
||||
Permission to use, copy, modify, and 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.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "data/BackendInterface.h"
|
||||
#include "rpc/common/Types.h"
|
||||
|
||||
namespace rpc {
|
||||
|
||||
/**
|
||||
* @brief AMMInfoHandler returns information about AMM pools.
|
||||
*
|
||||
* For more info see: https://xrpl.org/amm_info.html
|
||||
*/
|
||||
class AMMInfoHandler {
|
||||
std::shared_ptr<BackendInterface> sharedPtrBackend_;
|
||||
|
||||
public:
|
||||
struct Output {
|
||||
// todo: use better type than json types
|
||||
boost::json::value amount1;
|
||||
boost::json::value amount2;
|
||||
boost::json::value lpToken;
|
||||
boost::json::array voteSlots;
|
||||
boost::json::value auctionSlot;
|
||||
std::uint16_t tradingFee = 0;
|
||||
std::string ammAccount;
|
||||
std::optional<bool> asset1Frozen;
|
||||
std::optional<bool> asset2Frozen;
|
||||
|
||||
std::string ledgerHash;
|
||||
uint32_t ledgerIndex;
|
||||
bool validated = true;
|
||||
};
|
||||
|
||||
struct Input {
|
||||
std::optional<ripple::AccountID> accountID;
|
||||
std::optional<ripple::AccountID> ammAccount;
|
||||
ripple::Issue issue1 = ripple::noIssue();
|
||||
ripple::Issue issue2 = ripple::noIssue();
|
||||
std::optional<std::string> ledgerHash;
|
||||
std::optional<uint32_t> ledgerIndex;
|
||||
};
|
||||
|
||||
using Result = HandlerReturnType<Output>;
|
||||
|
||||
AMMInfoHandler(std::shared_ptr<BackendInterface> const& sharedPtrBackend) : sharedPtrBackend_(sharedPtrBackend)
|
||||
{
|
||||
}
|
||||
|
||||
static RpcSpecConstRef
|
||||
spec([[maybe_unused]] uint32_t apiVersion);
|
||||
|
||||
Result
|
||||
process(Input input, Context const& ctx) const;
|
||||
|
||||
private:
|
||||
friend void
|
||||
tag_invoke(boost::json::value_from_tag, boost::json::value& jv, Output const& output);
|
||||
|
||||
friend Input
|
||||
tag_invoke(boost::json::value_to_tag<Input>, boost::json::value const& jv);
|
||||
};
|
||||
|
||||
} // namespace rpc
|
||||
@@ -103,27 +103,6 @@ public:
|
||||
static auto const malformedRequestIntValidator =
|
||||
meta::WithCustomError{validation::Type<uint32_t>{}, Status(ClioError::rpcMALFORMED_REQUEST)};
|
||||
|
||||
static auto const ammAssetValidator =
|
||||
validation::CustomValidator{[](boost::json::value const& value, std::string_view /* key */) -> MaybeError {
|
||||
if (!value.is_object()) {
|
||||
return Error{Status{ClioError::rpcMALFORMED_REQUEST}};
|
||||
}
|
||||
|
||||
Json::Value jvAsset;
|
||||
if (value.as_object().contains(JS(issuer)))
|
||||
jvAsset["issuer"] = value.at(JS(issuer)).as_string().c_str();
|
||||
if (value.as_object().contains(JS(currency)))
|
||||
jvAsset["currency"] = value.at(JS(currency)).as_string().c_str();
|
||||
// same as rippled
|
||||
try {
|
||||
ripple::issueFromJson(jvAsset);
|
||||
} catch (std::runtime_error const&) {
|
||||
return Error{Status{ClioError::rpcMALFORMED_REQUEST}};
|
||||
}
|
||||
|
||||
return MaybeError{};
|
||||
}};
|
||||
|
||||
static auto const rpcSpec = RpcSpec{
|
||||
{JS(binary), validation::Type<bool>{}},
|
||||
{JS(ledger_hash), validation::Uint256HexStringValidator},
|
||||
@@ -198,13 +177,13 @@ public:
|
||||
meta::WithCustomError{
|
||||
validation::Type<boost::json::object>{}, Status(ClioError::rpcMALFORMED_REQUEST)
|
||||
},
|
||||
ammAssetValidator},
|
||||
validation::AMMAssetValidator},
|
||||
{JS(asset2),
|
||||
meta::WithCustomError{validation::Required{}, Status(ClioError::rpcMALFORMED_REQUEST)},
|
||||
meta::WithCustomError{
|
||||
validation::Type<boost::json::object>{}, Status(ClioError::rpcMALFORMED_REQUEST)
|
||||
},
|
||||
ammAssetValidator},
|
||||
validation::AMMAssetValidator},
|
||||
},
|
||||
}}
|
||||
};
|
||||
|
||||
@@ -96,6 +96,8 @@ protected:
|
||||
}
|
||||
};
|
||||
|
||||
// TODO enable when fixed :/
|
||||
/*
|
||||
TEST_F(SubscriptionManagerTest, MultipleThreadCtx)
|
||||
{
|
||||
std::vector<std::thread> workers;
|
||||
@@ -123,6 +125,7 @@ TEST_F(SubscriptionManagerTest, MultipleThreadCtx)
|
||||
session.reset();
|
||||
SubscriptionManagerPtr.reset();
|
||||
}
|
||||
*/
|
||||
|
||||
TEST_F(SubscriptionManagerTest, MultipleThreadCtxSessionDieEarly)
|
||||
{
|
||||
|
||||
1175
unittests/rpc/handlers/AMMInfoTests.cpp
Normal file
1175
unittests/rpc/handlers/AMMInfoTests.cpp
Normal file
File diff suppressed because it is too large
Load Diff
@@ -636,6 +636,18 @@ generateTestValuesForParametersTest()
|
||||
"Malformed request."
|
||||
},
|
||||
|
||||
ParamTestCaseBundle{
|
||||
"NonObjectAMMJsonAsset",
|
||||
R"({
|
||||
"amm": {
|
||||
"asset": 123,
|
||||
"asset2": 123
|
||||
}
|
||||
})",
|
||||
"malformedRequest",
|
||||
"Malformed request."
|
||||
},
|
||||
|
||||
ParamTestCaseBundle{
|
||||
"EmptyAMMAssetJson",
|
||||
fmt::format(
|
||||
|
||||
@@ -21,11 +21,13 @@
|
||||
|
||||
#include "data/DBHelpers.h"
|
||||
#include "data/Types.h"
|
||||
#include "util/Assert.h"
|
||||
|
||||
#include <ripple/basics/Blob.h>
|
||||
#include <ripple/basics/Slice.h>
|
||||
#include <ripple/basics/base_uint.h>
|
||||
#include <ripple/basics/chrono.h>
|
||||
#include <ripple/protocol/AMMCore.h>
|
||||
#include <ripple/protocol/AccountID.h>
|
||||
#include <ripple/protocol/Issue.h>
|
||||
#include <ripple/protocol/LedgerFormats.h>
|
||||
@@ -52,7 +54,6 @@
|
||||
#include <vector>
|
||||
|
||||
constexpr static auto INDEX1 = "1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515BC";
|
||||
constexpr static auto CURRENCY = "03930D02208264E2E40EC1B0C09E4DB96EE197B1";
|
||||
|
||||
ripple::AccountID
|
||||
GetAccountIDWithString(std::string_view id)
|
||||
@@ -60,6 +61,18 @@ GetAccountIDWithString(std::string_view id)
|
||||
return ripple::parseBase58<ripple::AccountID>(std::string(id)).value();
|
||||
}
|
||||
|
||||
ripple::uint256
|
||||
GetAccountKey(std::string_view id)
|
||||
{
|
||||
return ripple::keylet::account(GetAccountIDWithString(id)).key;
|
||||
}
|
||||
|
||||
ripple::uint256
|
||||
GetAccountKey(ripple::AccountID const& acc)
|
||||
{
|
||||
return ripple::keylet::account(acc).key;
|
||||
}
|
||||
|
||||
ripple::LedgerInfo
|
||||
CreateLedgerInfo(std::string_view ledgerHash, ripple::LedgerIndex seq, std::optional<uint32_t> age)
|
||||
{
|
||||
@@ -801,20 +814,81 @@ CreateAMMObject(
|
||||
std::string_view assetCurrency,
|
||||
std::string_view assetIssuer,
|
||||
std::string_view asset2Currency,
|
||||
std::string_view asset2Issuer
|
||||
std::string_view asset2Issuer,
|
||||
std::string_view lpTokenBalanceIssueCurrency,
|
||||
uint32_t lpTokenBalanceIssueAmount,
|
||||
uint16_t tradingFee,
|
||||
uint64_t ownerNode
|
||||
)
|
||||
{
|
||||
auto amm = ripple::STObject(ripple::sfLedgerEntry);
|
||||
amm.setFieldU16(ripple::sfLedgerEntryType, ripple::ltAMM);
|
||||
amm.setAccountID(ripple::sfAccount, GetAccountIDWithString(accountId));
|
||||
amm.setFieldU16(ripple::sfTradingFee, 5);
|
||||
amm.setFieldU64(ripple::sfOwnerNode, 0);
|
||||
amm.setFieldU16(ripple::sfTradingFee, tradingFee);
|
||||
amm.setFieldU64(ripple::sfOwnerNode, ownerNode);
|
||||
amm.setFieldIssue(ripple::sfAsset, ripple::STIssue{ripple::sfAsset, GetIssue(assetCurrency, assetIssuer)});
|
||||
amm.setFieldIssue(ripple::sfAsset2, ripple::STIssue{ripple::sfAsset2, GetIssue(asset2Currency, asset2Issuer)});
|
||||
ripple::Issue const issue1(
|
||||
ripple::Currency{CURRENCY}, ripple::parseBase58<ripple::AccountID>(std::string(accountId)).value()
|
||||
ripple::Currency{lpTokenBalanceIssueCurrency},
|
||||
ripple::parseBase58<ripple::AccountID>(std::string(accountId)).value()
|
||||
);
|
||||
amm.setFieldAmount(ripple::sfLPTokenBalance, ripple::STAmount(issue1, 100));
|
||||
amm.setFieldAmount(ripple::sfLPTokenBalance, ripple::STAmount(issue1, lpTokenBalanceIssueAmount));
|
||||
amm.setFieldU32(ripple::sfFlags, 0);
|
||||
return amm;
|
||||
}
|
||||
|
||||
void
|
||||
AMMAddVoteSlot(ripple::STObject& amm, ripple::AccountID const& accountId, uint16_t tradingFee, uint32_t voteWeight)
|
||||
{
|
||||
if (!amm.isFieldPresent(ripple::sfVoteSlots))
|
||||
amm.setFieldArray(ripple::sfVoteSlots, ripple::STArray{});
|
||||
|
||||
auto& arr = amm.peekFieldArray(ripple::sfVoteSlots);
|
||||
auto slot = ripple::STObject(ripple::sfVoteEntry);
|
||||
slot.setAccountID(ripple::sfAccount, accountId);
|
||||
slot.setFieldU16(ripple::sfTradingFee, tradingFee);
|
||||
slot.setFieldU32(ripple::sfVoteWeight, voteWeight);
|
||||
arr.push_back(slot);
|
||||
}
|
||||
|
||||
void
|
||||
AMMSetAuctionSlot(
|
||||
ripple::STObject& amm,
|
||||
ripple::AccountID const& accountId,
|
||||
ripple::STAmount price,
|
||||
uint16_t discountedFee,
|
||||
uint32_t expiration,
|
||||
std::vector<ripple::AccountID> const& authAccounts
|
||||
)
|
||||
{
|
||||
ASSERT(expiration >= 24 * 3600, "Expiration must be at least 24 hours");
|
||||
|
||||
if (!amm.isFieldPresent(ripple::sfAuctionSlot))
|
||||
amm.makeFieldPresent(ripple::sfAuctionSlot);
|
||||
|
||||
auto& auctionSlot = amm.peekFieldObject(ripple::sfAuctionSlot);
|
||||
auctionSlot.setAccountID(ripple::sfAccount, accountId);
|
||||
auctionSlot.setFieldAmount(ripple::sfPrice, price);
|
||||
auctionSlot.setFieldU16(ripple::sfDiscountedFee, discountedFee);
|
||||
auctionSlot.setFieldU32(ripple::sfExpiration, expiration);
|
||||
|
||||
if (not authAccounts.empty()) {
|
||||
ripple::STArray accounts;
|
||||
|
||||
for (auto const& acc : authAccounts) {
|
||||
ripple::STObject authAcc(ripple::sfAuthAccount);
|
||||
authAcc.setAccountID(ripple::sfAccount, acc);
|
||||
accounts.push_back(authAcc);
|
||||
}
|
||||
|
||||
auctionSlot.setFieldArray(ripple::sfAuthAccounts, accounts);
|
||||
}
|
||||
}
|
||||
|
||||
ripple::Currency
|
||||
CreateLPTCurrency(std::string_view assetCurrency, std::string_view asset2Currency)
|
||||
{
|
||||
return ripple::ammLPTCurrency(
|
||||
ripple::to_currency(std::string(assetCurrency)), ripple::to_currency(std::string(asset2Currency))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -36,6 +36,18 @@
|
||||
[[nodiscard]] ripple::AccountID
|
||||
GetAccountIDWithString(std::string_view id);
|
||||
|
||||
/**
|
||||
* Create AccountID object with string and return its key
|
||||
*/
|
||||
[[nodiscard]] ripple::uint256
|
||||
GetAccountKey(std::string_view id);
|
||||
|
||||
/*
|
||||
* Gets the account key from an account id
|
||||
*/
|
||||
[[nodiscard]] ripple::uint256
|
||||
GetAccountKey(ripple::AccountID const& acc);
|
||||
|
||||
/*
|
||||
* Create a simple ledgerInfo object with only hash and seq
|
||||
*/
|
||||
@@ -284,8 +296,28 @@ CreateAMMObject(
|
||||
std::string_view assetCurrency,
|
||||
std::string_view assetIssuer,
|
||||
std::string_view asset2Currency,
|
||||
std::string_view asset2Issuer
|
||||
std::string_view asset2Issuer,
|
||||
std::string_view lpTokenBalanceIssueCurrency = "03930D02208264E2E40EC1B0C09E4DB96EE197B1",
|
||||
uint32_t lpTokenBalanceIssueAmount = 100u,
|
||||
uint16_t tradingFee = 5u,
|
||||
uint64_t ownerNode = 0u
|
||||
);
|
||||
|
||||
void
|
||||
AMMAddVoteSlot(ripple::STObject& amm, ripple::AccountID const& accountId, uint16_t tradingFee, uint32_t voteWeight);
|
||||
|
||||
void
|
||||
AMMSetAuctionSlot(
|
||||
ripple::STObject& amm,
|
||||
ripple::AccountID const& accountId,
|
||||
ripple::STAmount price,
|
||||
uint16_t discountedFee,
|
||||
uint32_t expiration,
|
||||
std::vector<ripple::AccountID> const& authAccounts = {}
|
||||
);
|
||||
|
||||
[[nodiscard]] ripple::STObject
|
||||
CreateDidObject(std::string_view accountId, std::string_view didDoc, std::string_view uri, std::string_view data);
|
||||
|
||||
[[nodiscard]] ripple::Currency
|
||||
CreateLPTCurrency(std::string_view assetCurrency, std::string_view asset2Currency);
|
||||
|
||||
Reference in New Issue
Block a user