Files
rippled/src/test/app/AMMCalc_test.cpp
Bart 1eb0fdac65 refactor: Rename ripple namespace to xrpl (#5982)
This change renames all occurrences of `namespace ripple` and `ripple::` to `namespace xrpl` and `xrpl::`, respectively, as well as the names of test suites. It also provides a script to allow developers to replicate the changes in their local branch or fork to avoid conflicts.
2025-12-11 16:51:49 +00:00

446 lines
15 KiB
C++

#include <test/jtx.h>
#include <xrpld/app/misc/AMMHelpers.h>
#include <xrpl/protocol/Quality.h>
#include <boost/regex.hpp>
namespace xrpl {
namespace test {
/** AMM Calculator. Uses AMM formulas to simulate the payment engine
* expected results. Assuming the formulas are correct some unit-tests can
* be verified. Currently supported operations are:
* - swapIn, find out given in. in can flow through multiple AMM/Offer steps.
* - swapOut, find in given out. out can flow through multiple AMM/Offer steps.
* - lptokens, find lptokens given pool composition.
* - changespq, change AMM spot price (SP) quality. given AMM and Offer
* find out AMM offer, which changes AMM's SP quality to
* the Offer's quality.
*/
class AMMCalc_test : public beast::unit_test::suite
{
using token_iter = boost::sregex_token_iterator;
using steps = std::vector<std::pair<Amounts, bool>>;
using trates = std::map<std::string, std::uint32_t>;
using swapargs = std::tuple<steps, STAmount, trates, std::uint32_t>;
jtx::Account const gw{jtx::Account("gw")};
token_iter const end_;
std::optional<STAmount>
getAmt(token_iter const& p, bool* delimited = nullptr)
{
using namespace jtx;
if (p == end_)
return STAmount{};
std::string str = *p;
str = boost::regex_replace(str, boost::regex("^(A|O)[(]"), "");
boost::smatch match;
// XXX(val))?
boost::regex rx("^([^(]+)[(]([^)]+)[)]([)])?$");
if (boost::regex_search(str, match, rx))
{
if (delimited)
*delimited = (match[3] != "");
if (match[1] == "XRP")
return XRP(std::stoll(match[2]));
// drops
else if (match[1] == "XRPA")
return XRPAmount{std::stoll(match[2])};
return amountFromString(gw[match[1]].asset(), match[2]);
}
return std::nullopt;
}
std::optional<std::tuple<std::string, std::uint32_t, bool>>
getRate(token_iter const& p)
{
if (p == end_)
return std::nullopt;
std::string str = *p;
str = boost::regex_replace(str, boost::regex("^T[(]"), "");
// XXX(rate))?
boost::smatch match;
boost::regex rx("^([^(]+)[(]([^)]+)[)]([)])?$");
if (boost::regex_search(str, match, rx))
{
std::string const currency = match[1];
// input is rate * 100, no fraction
std::uint32_t rate = 10'000'000 * std::stoi(match[2].str());
// true if delimited - )
return {{currency, rate, match[3] != "" ? true : false}};
}
return std::nullopt;
}
std::uint32_t
getFee(token_iter const& p)
{
if (p != end_)
{
std::string const s = *p;
return std::stoll(s);
}
return 0;
}
std::optional<std::pair<Amounts, bool>>
getAmounts(token_iter& p)
{
if (p == end_)
return std::nullopt;
std::string const s = *p;
bool const amm = s[0] == 'O' ? false : true;
auto const a1 = getAmt(p++);
if (!a1 || p == end_)
return std::nullopt;
auto const a2 = getAmt(p++);
if (!a2)
return std::nullopt;
return {{{*a1, *a2}, amm}};
}
std::optional<trates>
getTransferRate(token_iter& p)
{
trates rates{};
if (p == end_)
return rates;
std::string str = *p;
if (str[0] != 'T')
return rates;
// T(USD(rate),GBP(rate), ...)
while (p != end_)
{
if (auto const rate = getRate(p++))
{
auto const [currency, trate, delimited] = *rate;
rates[currency] = trate;
if (delimited)
break;
}
else
return std::nullopt;
}
return rates;
}
std::optional<swapargs>
getSwap(token_iter& p)
{
// pairs of amm pool or offer
steps pairs;
// either amm pool or offer
auto isPair = [](auto const& p) {
std::string const s = *p;
return s[0] == 'A' || s[0] == 'O';
};
// get AMM or offer
while (isPair(p))
{
auto const res = getAmounts(p);
if (!res || p == end_)
return std::nullopt;
pairs.push_back(*res);
}
// swap in/out amount
auto const swap = getAmt(p++);
if (!swap)
return std::nullopt;
// optional transfer rate
auto const rate = getTransferRate(p);
if (!rate)
return std::nullopt;
auto const fee = getFee(p);
return {{pairs, *swap, *rate, fee}};
}
std::string
toString(STAmount const& a)
{
std::stringstream str;
str << a.getText() << "/" << to_string(a.issue().currency);
return str.str();
}
STAmount
mulratio(STAmount const& amt, std::uint32_t a, std::uint32_t b, bool round)
{
if (a == b)
return amt;
if (amt.native())
return toSTAmount(mulRatio(amt.xrp(), a, b, round), amt.issue());
return toSTAmount(mulRatio(amt.iou(), a, b, round), amt.issue());
}
void
swapOut(swapargs const& args)
{
auto const vp = std::get<steps>(args);
STAmount sout = std::get<STAmount>(args);
auto const fee = std::get<std::uint32_t>(args);
auto const rates = std::get<trates>(args);
STAmount resultOut = sout;
STAmount resultIn{};
STAmount sin{};
int limitingStep = vp.size();
STAmount limitStepOut{};
auto trate = [&](auto const& amt) {
auto const currency = to_string(amt.issue().currency);
return rates.find(currency) != rates.end() ? rates.at(currency)
: QUALITY_ONE;
};
// swap out reverse
sin = sout;
for (auto it = vp.rbegin(); it != vp.rend(); ++it)
{
sout = mulratio(sin, trate(sin), QUALITY_ONE, true);
auto const [amts, amm] = *it;
// assume no amm limit
if (amm)
{
sin = swapAssetOut(amts, sout, fee);
}
else if (sout <= amts.out)
{
sin = Quality{amts}.ceil_out(amts, sout).in;
}
// limiting step
else
{
sin = amts.in;
limitingStep = vp.rend() - it - 1;
limitStepOut = amts.out;
if (it == vp.rbegin())
resultOut = amts.out;
}
resultIn = sin;
}
sin = limitStepOut;
// swap in if limiting step
for (int i = limitingStep + 1; i < vp.size(); ++i)
{
auto const [amts, amm] = vp[i];
sin = mulratio(sin, QUALITY_ONE, trate(sin), false);
if (amm)
{
sout = swapAssetIn(amts, sin, fee);
}
// assume there is no limiting step in fwd
else
{
sout = Quality{amts}.ceil_in(amts, sin).out;
}
sin = sout;
resultOut = sout;
}
std::cout << "in: " << toString(resultIn)
<< " out: " << toString(resultOut) << std::endl;
}
void
swapIn(swapargs const& args)
{
auto const vp = std::get<steps>(args);
STAmount sin = std::get<STAmount>(args);
auto const fee = std::get<std::uint32_t>(args);
auto const rates = std::get<trates>(args);
STAmount resultIn = sin;
STAmount resultOut{};
STAmount sout{};
int limitingStep = 0;
STAmount limitStepIn{};
auto trate = [&](auto const& amt) {
auto const currency = to_string(amt.issue().currency);
return rates.find(currency) != rates.end() ? rates.at(currency)
: QUALITY_ONE;
};
// Swap in forward
for (auto it = vp.begin(); it != vp.end(); ++it)
{
auto const [amts, amm] = *it;
sin = mulratio(
sin,
QUALITY_ONE,
trate(sin),
false); // out of the next step
// assume no amm limit
if (amm)
{
sout = swapAssetIn(amts, sin, fee);
}
else if (sin <= amts.in)
{
sout = Quality{amts}.ceil_in(amts, sin).out;
}
// limiting step, requested in is greater than the offer
// pay exactly amts.in, which gets amts.out
else
{
sout = amts.out;
limitingStep = it - vp.begin();
limitStepIn = amts.in;
}
sin = sout;
resultOut = sout;
}
sin = limitStepIn;
// swap out if limiting step
for (int i = limitingStep - 1; i >= 0; --i)
{
sout = mulratio(sin, trate(sin), QUALITY_ONE, false);
auto const [amts, amm] = vp[i];
if (amm)
{
sin = swapAssetOut(amts, sout, fee);
}
// assume there is no limiting step
else
{
sin = Quality{amts}.ceil_out(amts, sout).in;
}
resultIn = sin;
}
resultOut = mulratio(resultOut, QUALITY_ONE, trate(resultOut), true);
std::cout << "in: " << toString(resultIn)
<< " out: " << toString(resultOut) << std::endl;
}
void
run() override
{
using namespace jtx;
auto const a = arg();
boost::regex re(",");
token_iter p(a.begin(), a.end(), re, -1);
// Token is denoted as CUR(xxx), where CUR is the currency code
// and xxx is the amount, for instance: XRP(100) or USD(11.5)
// AMM is denoted as A(CUR1(xxx1),CUR2(xxx2)), for instance:
// A(XRP(1000),USD(1000)), the tokens must be in the order
// poolGets/poolPays
// Offer is denoted as O(CUR1(xxx1),CUR2(xxx2)), for instance:
// O(XRP(100),USD(100)), the tokens must be in the order
// takerPays/takerGets
// Transfer rate is denoted as a comma separated list for each
// currency with the transfer rate, for instance:
// T(USD(175),...,EUR(100)).
// the transfer rate is 100 * rate, with no fraction, for instance:
// 1.75 = 1.75 * 100 = 175
// the transfer rate is optional
// AMM trading fee is an integer in {0,1000}, 1000 represents 1%
// the trading fee is optional
auto const exec = [&]() -> bool {
if (p == end_)
return true;
// Swap in to the steps. Execute steps in forward direction first.
// swapin,A(XRP(1000),USD(1000)),O(USD(10),EUR(10)),XRP(11),
// T(USD(125)),1000
// where
// A(...),O(...) are the payment steps, in this case
// consisting of AMM and Offer.
// XRP(11) is the swapIn value. Note the order of tokens in AMM;
// i.e. poolGets/poolPays.
// T(USD(125) is the transfer rate of 1.25%.
// 1000 is AMM trading fee of 1%, the fee is optional.
if (*p == "swapin")
{
if (auto const swap = getSwap(++p); swap)
{
swapIn(*swap);
return true;
}
}
// Swap out of the steps. Execute steps in reverse direction first.
// swapout,A(USD(1000),XRP(1000)),XRP(10),T(USD(100)),100
// where
// A(...) is the payment step, in this case
// consisting of AMM.
// XRP(10) is the swapOut value. Note the order of tokens in AMM:
// i.e. poolGets/poolPays.
// T(USD(100) is the transfer rate of 1%.
// 100 is AMM trading fee of 0.1%.
else if (*p == "swapout")
{
if (auto const swap = getSwap(++p); swap)
{
swapOut(*swap);
return true;
}
}
// Calculate AMM lptokens
// lptokens,USD(1000),XRP(1000)
// where
// USD(...),XRP(...) is the pool composition
else if (*p == "lptokens")
{
if (auto const pool = getAmounts(++p); pool)
{
Account const amm("amm");
auto const LPT = amm["LPT"];
std::cout
<< to_string(
ammLPTokens(pool->first.in, pool->first.out, LPT)
.iou())
<< std::endl;
return true;
}
}
// Change spot price quality - generates AMM offer such that
// when consumed the updated AMM spot price quality is equal
// to the CLOB offer quality
// changespq,A(XRP(1000),USD(1000)),O(XRP(100),USD(99)),10
// where
// A(...) is AMM
// O(...) is CLOB offer
// 10 is AMM trading fee
else if (*p == "changespq")
{
Env env(*this);
if (auto const pool = getAmounts(++p))
{
if (auto const offer = getAmounts(p))
{
auto const fee = getFee(p);
if (auto const ammOffer = changeSpotPriceQuality(
pool->first,
Quality{offer->first},
fee,
env.current()->rules(),
beast::Journal(beast::Journal::getNullSink()));
ammOffer)
std::cout
<< "amm offer: " << toString(ammOffer->in)
<< " " << toString(ammOffer->out)
<< "\nnew pool: "
<< toString(pool->first.in + ammOffer->in)
<< " "
<< toString(pool->first.out - ammOffer->out)
<< std::endl;
else
std::cout << "can't change the pool's SP quality"
<< std::endl;
return true;
}
}
}
return false;
};
bool res = false;
try
{
res = exec();
}
catch (std::exception const& ex)
{
std::cout << ex.what() << std::endl;
}
BEAST_EXPECT(res);
}
};
BEAST_DEFINE_TESTSUITE_MANUAL(AMMCalc, app, xrpl);
} // namespace test
} // namespace xrpl