mirror of
https://github.com/XRPLF/rippled.git
synced 2025-12-06 17:27:55 +00:00
Introduce AMM support (XLS-30d): (#4294)
Add AMM functionality: - InstanceCreate - Deposit - Withdraw - Governance - Auctioning - payment engine integration To support this functionality, add: - New RPC method, `amm_info`, to fetch pool and LPT balances - AMM Root Account - trust line for each IOU AMM token - trust line to track Liquidity Provider Tokens (LPT) - `ltAMM` object The `ltAMM` object tracks: - fee votes - auction slot bids - AMM tokens pair - total outstanding tokens balance - `AMMID` to AMM `RootAccountID` mapping Add new classes to facilitate AMM integration into the payment engine. `BookStep` uses these classes to infer if AMM liquidity can be consumed. The AMM formula implementation uses the new Number class added in #4192. IOUAmount and STAmount use Number arithmetic. Add AMM unit tests for all features. AMM requires the following amendments: - featureAMM - fixUniversalNumber - featureFlowCross Notes: - Current trading fee threshold is 1% - AMM currency is generated by: 0x03 + 152 bits of sha256{cur1, cur2} - Current max AMM Offers is 30 --------- Co-authored-by: Howard Hinnant <howard.hinnant@gmail.com>
This commit is contained in:
committed by
GitHub
parent
eeb8b41889
commit
3c9db4b69e
727
src/test/jtx/impl/AMM.cpp
Normal file
727
src/test/jtx/impl/AMM.cpp
Normal file
@@ -0,0 +1,727 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2023 Ripple Labs Inc.
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <test/jtx/AMM.h>
|
||||
|
||||
#include <ripple/app/misc/AMMUtils.h>
|
||||
#include <ripple/protocol/AMMCore.h>
|
||||
#include <ripple/protocol/AmountConversions.h>
|
||||
#include <ripple/protocol/jss.h>
|
||||
#include <ripple/rpc/impl/RPCHelpers.h>
|
||||
#include <test/jtx/Env.h>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
namespace jtx {
|
||||
|
||||
static Number
|
||||
number(STAmount const& a)
|
||||
{
|
||||
if (isXRP(a))
|
||||
return a.xrp();
|
||||
return a;
|
||||
}
|
||||
|
||||
static IOUAmount
|
||||
initialTokens(STAmount const& asset1, STAmount const& asset2)
|
||||
{
|
||||
auto const product = number(asset1) * number(asset2);
|
||||
return (IOUAmount)(
|
||||
product.mantissa() >= 0 ? root2(product) : root2(-product));
|
||||
}
|
||||
|
||||
AMM::AMM(
|
||||
Env& env,
|
||||
Account const& account,
|
||||
STAmount const& asset1,
|
||||
STAmount const& asset2,
|
||||
bool log,
|
||||
std::uint16_t tfee,
|
||||
std::uint32_t fee,
|
||||
std::optional<std::uint32_t> flags,
|
||||
std::optional<jtx::seq> seq,
|
||||
std::optional<jtx::msig> ms,
|
||||
std::optional<ter> const& ter)
|
||||
: env_(env)
|
||||
, creatorAccount_(account)
|
||||
, asset1_(asset1)
|
||||
, asset2_(asset2)
|
||||
, initialLPTokens_(initialTokens(asset1, asset2))
|
||||
, log_(log)
|
||||
, lastPurchasePrice_(0)
|
||||
, bidMin_()
|
||||
, bidMax_()
|
||||
, msig_(ms)
|
||||
, fee_(fee)
|
||||
, ammAccount_(create(tfee, flags, seq, ter))
|
||||
, lptIssue_(ripple::ammLPTIssue(
|
||||
asset1_.issue().currency,
|
||||
asset2_.issue().currency,
|
||||
ammAccount_))
|
||||
{
|
||||
}
|
||||
|
||||
AMM::AMM(
|
||||
Env& env,
|
||||
Account const& account,
|
||||
STAmount const& asset1,
|
||||
STAmount const& asset2,
|
||||
ter const& ter,
|
||||
bool log)
|
||||
: AMM(env,
|
||||
account,
|
||||
asset1,
|
||||
asset2,
|
||||
log,
|
||||
0,
|
||||
0,
|
||||
std::nullopt,
|
||||
std::nullopt,
|
||||
std::nullopt,
|
||||
ter)
|
||||
{
|
||||
}
|
||||
|
||||
[[nodiscard]] AccountID
|
||||
AMM::create(
|
||||
std::uint32_t tfee,
|
||||
std::optional<std::uint32_t> const& flags,
|
||||
std::optional<jtx::seq> const& seq,
|
||||
std::optional<ter> const& ter)
|
||||
{
|
||||
Json::Value jv;
|
||||
jv[jss::Account] = creatorAccount_.human();
|
||||
jv[jss::Amount] = asset1_.getJson(JsonOptions::none);
|
||||
jv[jss::Amount2] = asset2_.getJson(JsonOptions::none);
|
||||
jv[jss::TradingFee] = tfee;
|
||||
jv[jss::TransactionType] = jss::AMMCreate;
|
||||
if (flags)
|
||||
jv[jss::Flags] = *flags;
|
||||
if (fee_ != 0)
|
||||
jv[jss::Fee] = std::to_string(fee_);
|
||||
else
|
||||
jv[jss::Fee] = std::to_string(env_.current()->fees().increment.drops());
|
||||
submit(jv, seq, ter);
|
||||
|
||||
if (!ter || env_.ter() == tesSUCCESS)
|
||||
{
|
||||
if (auto const amm = env_.current()->read(
|
||||
keylet::amm(asset1_.issue(), asset2_.issue())))
|
||||
{
|
||||
return amm->getAccountID(sfAccount);
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
Json::Value
|
||||
AMM::ammRpcInfo(
|
||||
std::optional<AccountID> const& account,
|
||||
std::optional<std::string> const& ledgerIndex,
|
||||
std::optional<std::pair<Issue, Issue>> tokens) const
|
||||
{
|
||||
Json::Value jv;
|
||||
if (account)
|
||||
jv[jss::account] = to_string(*account);
|
||||
if (ledgerIndex)
|
||||
jv[jss::ledger_index] = *ledgerIndex;
|
||||
if (tokens)
|
||||
{
|
||||
jv[jss::asset] =
|
||||
STIssue(sfAsset, tokens->first).getJson(JsonOptions::none);
|
||||
jv[jss::asset2] =
|
||||
STIssue(sfAsset2, tokens->second).getJson(JsonOptions::none);
|
||||
}
|
||||
else
|
||||
{
|
||||
jv[jss::asset] =
|
||||
STIssue(sfAsset, asset1_.issue()).getJson(JsonOptions::none);
|
||||
jv[jss::asset2] =
|
||||
STIssue(sfAsset2, asset2_.issue()).getJson(JsonOptions::none);
|
||||
}
|
||||
auto jr = env_.rpc("json", "amm_info", to_string(jv));
|
||||
if (jr.isObject() && jr.isMember(jss::result) &&
|
||||
jr[jss::result].isMember(jss::status))
|
||||
return jr[jss::result];
|
||||
return Json::nullValue;
|
||||
}
|
||||
|
||||
std::tuple<STAmount, STAmount, STAmount>
|
||||
AMM::balances(
|
||||
Issue const& issue1,
|
||||
Issue const& issue2,
|
||||
std::optional<AccountID> const& account) const
|
||||
{
|
||||
if (auto const amm =
|
||||
env_.current()->read(keylet::amm(asset1_.issue(), asset2_.issue())))
|
||||
{
|
||||
auto const ammAccountID = amm->getAccountID(sfAccount);
|
||||
auto const [asset1Balance, asset2Balance] = ammPoolHolds(
|
||||
*env_.current(),
|
||||
ammAccountID,
|
||||
issue1,
|
||||
issue2,
|
||||
FreezeHandling::fhIGNORE_FREEZE,
|
||||
env_.journal);
|
||||
auto const lptAMMBalance = account
|
||||
? ammLPHolds(*env_.current(), *amm, *account, env_.journal)
|
||||
: amm->getFieldAmount(sfLPTokenBalance);
|
||||
return {asset1Balance, asset2Balance, lptAMMBalance};
|
||||
}
|
||||
return {STAmount{}, STAmount{}, STAmount{}};
|
||||
}
|
||||
|
||||
bool
|
||||
AMM::expectBalances(
|
||||
STAmount const& asset1,
|
||||
STAmount const& asset2,
|
||||
IOUAmount const& lpt,
|
||||
std::optional<AccountID> const& account) const
|
||||
{
|
||||
auto const [asset1Balance, asset2Balance, lptAMMBalance] =
|
||||
balances(asset1.issue(), asset2.issue(), account);
|
||||
return asset1 == asset1Balance && asset2 == asset2Balance &&
|
||||
lptAMMBalance == STAmount{lpt, lptIssue_};
|
||||
return false;
|
||||
}
|
||||
|
||||
IOUAmount
|
||||
AMM::getLPTokensBalance(std::optional<AccountID> const& account) const
|
||||
{
|
||||
if (account)
|
||||
return accountHolds(
|
||||
*env_.current(),
|
||||
*account,
|
||||
lptIssue_,
|
||||
FreezeHandling::fhZERO_IF_FROZEN,
|
||||
env_.journal)
|
||||
.iou();
|
||||
if (auto const amm =
|
||||
env_.current()->read(keylet::amm(asset1_.issue(), asset2_.issue())))
|
||||
return amm->getFieldAmount(sfLPTokenBalance).iou();
|
||||
return IOUAmount{0};
|
||||
}
|
||||
|
||||
bool
|
||||
AMM::expectLPTokens(AccountID const& account, IOUAmount const& expTokens) const
|
||||
{
|
||||
if (auto const amm =
|
||||
env_.current()->read(keylet::amm(asset1_.issue(), asset2_.issue())))
|
||||
{
|
||||
auto const lptAMMBalance =
|
||||
ammLPHolds(*env_.current(), *amm, account, env_.journal);
|
||||
return lptAMMBalance == STAmount{expTokens, lptIssue_};
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool
|
||||
AMM::expectAuctionSlot(
|
||||
std::uint32_t fee,
|
||||
std::optional<std::uint8_t> timeSlot,
|
||||
IOUAmount expectedPrice) const
|
||||
{
|
||||
return expectAuctionSlot([&](std::uint32_t slotFee,
|
||||
std::optional<std::uint8_t> slotInterval,
|
||||
IOUAmount const& slotPrice,
|
||||
auto const&) {
|
||||
return slotFee == fee &&
|
||||
// Auction slot might be expired, in which case slotInterval is
|
||||
// 0
|
||||
((!timeSlot && slotInterval == 0) || slotInterval == timeSlot) &&
|
||||
slotPrice == expectedPrice;
|
||||
});
|
||||
}
|
||||
|
||||
bool
|
||||
AMM::expectAuctionSlot(std::vector<AccountID> const& authAccounts) const
|
||||
{
|
||||
return expectAuctionSlot([&](std::uint32_t,
|
||||
std::optional<std::uint8_t>,
|
||||
IOUAmount const&,
|
||||
STArray const& accounts) {
|
||||
for (auto const& account : accounts)
|
||||
{
|
||||
if (std::find(
|
||||
authAccounts.cbegin(),
|
||||
authAccounts.cend(),
|
||||
account.getAccountID(sfAccount)) == authAccounts.end())
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
bool
|
||||
AMM::expectTradingFee(std::uint16_t fee) const
|
||||
{
|
||||
auto const amm =
|
||||
env_.current()->read(keylet::amm(asset1_.issue(), asset2_.issue()));
|
||||
return amm && (*amm)[sfTradingFee] == fee;
|
||||
}
|
||||
|
||||
bool
|
||||
AMM::ammExists() const
|
||||
{
|
||||
return env_.current()->read(keylet::account(ammAccount_)) != nullptr &&
|
||||
env_.current()->read(keylet::amm(asset1_.issue(), asset2_.issue())) !=
|
||||
nullptr;
|
||||
}
|
||||
|
||||
bool
|
||||
AMM::expectAmmRpcInfo(
|
||||
STAmount const& asset1,
|
||||
STAmount const& asset2,
|
||||
IOUAmount const& balance,
|
||||
std::optional<AccountID> const& account,
|
||||
std::optional<std::string> const& ledger_index) const
|
||||
{
|
||||
auto const jv = ammRpcInfo(account, ledger_index);
|
||||
return expectAmmInfo(asset1, asset2, balance, jv);
|
||||
}
|
||||
|
||||
bool
|
||||
AMM::expectAmmInfo(
|
||||
STAmount const& asset1,
|
||||
STAmount const& asset2,
|
||||
IOUAmount const& balance,
|
||||
Json::Value const& jvres) const
|
||||
{
|
||||
if (!jvres.isMember(jss::amm))
|
||||
return false;
|
||||
auto const& jv = jvres[jss::amm];
|
||||
if (!jv.isMember(jss::amount) || !jv.isMember(jss::amount2) ||
|
||||
!jv.isMember(jss::lp_token))
|
||||
return false;
|
||||
STAmount asset1Info;
|
||||
if (!amountFromJsonNoThrow(asset1Info, jv[jss::amount]))
|
||||
return false;
|
||||
STAmount asset2Info;
|
||||
if (!amountFromJsonNoThrow(asset2Info, jv[jss::amount2]))
|
||||
return false;
|
||||
STAmount lptBalance;
|
||||
if (!amountFromJsonNoThrow(lptBalance, jv[jss::lp_token]))
|
||||
return false;
|
||||
// ammRpcInfo returns unordered assets
|
||||
if (asset1Info.issue() != asset1.issue())
|
||||
std::swap(asset1Info, asset2Info);
|
||||
return asset1 == asset1Info && asset2 == asset2Info &&
|
||||
lptBalance == STAmount{balance, lptIssue_};
|
||||
}
|
||||
|
||||
void
|
||||
AMM::setTokens(
|
||||
Json::Value& jv,
|
||||
std::optional<std::pair<Issue, Issue>> const& assets)
|
||||
{
|
||||
if (assets)
|
||||
{
|
||||
jv[jss::Asset] =
|
||||
STIssue(sfAsset, assets->first).getJson(JsonOptions::none);
|
||||
jv[jss::Asset2] =
|
||||
STIssue(sfAsset, assets->second).getJson(JsonOptions::none);
|
||||
}
|
||||
else
|
||||
{
|
||||
jv[jss::Asset] =
|
||||
STIssue(sfAsset, asset1_.issue()).getJson(JsonOptions::none);
|
||||
jv[jss::Asset2] =
|
||||
STIssue(sfAsset, asset2_.issue()).getJson(JsonOptions::none);
|
||||
}
|
||||
}
|
||||
|
||||
IOUAmount
|
||||
AMM::deposit(
|
||||
std::optional<Account> const& account,
|
||||
Json::Value& jv,
|
||||
std::optional<std::pair<Issue, Issue>> const& assets,
|
||||
std::optional<jtx::seq> const& seq,
|
||||
std::optional<ter> const& ter)
|
||||
{
|
||||
auto const& acct = account ? *account : creatorAccount_;
|
||||
auto const lpTokens = getLPTokensBalance(acct);
|
||||
jv[jss::Account] = acct.human();
|
||||
setTokens(jv, assets);
|
||||
jv[jss::TransactionType] = jss::AMMDeposit;
|
||||
if (fee_ != 0)
|
||||
jv[jss::Fee] = std::to_string(fee_);
|
||||
submit(jv, seq, ter);
|
||||
return getLPTokensBalance(acct) - lpTokens;
|
||||
}
|
||||
|
||||
IOUAmount
|
||||
AMM::deposit(
|
||||
std::optional<Account> const& account,
|
||||
LPToken tokens,
|
||||
std::optional<STAmount> const& asset1In,
|
||||
std::optional<std::uint32_t> const& flags,
|
||||
std::optional<ter> const& ter)
|
||||
{
|
||||
return deposit(
|
||||
account,
|
||||
tokens,
|
||||
asset1In,
|
||||
std::nullopt,
|
||||
std::nullopt,
|
||||
flags,
|
||||
std::nullopt,
|
||||
std::nullopt,
|
||||
ter);
|
||||
}
|
||||
|
||||
IOUAmount
|
||||
AMM::deposit(
|
||||
std::optional<Account> const& account,
|
||||
STAmount const& asset1In,
|
||||
std::optional<STAmount> const& asset2In,
|
||||
std::optional<STAmount> const& maxEP,
|
||||
std::optional<std::uint32_t> const& flags,
|
||||
std::optional<ter> const& ter)
|
||||
{
|
||||
assert(!(asset2In && maxEP));
|
||||
return deposit(
|
||||
account,
|
||||
std::nullopt,
|
||||
asset1In,
|
||||
asset2In,
|
||||
maxEP,
|
||||
flags,
|
||||
std::nullopt,
|
||||
std::nullopt,
|
||||
ter);
|
||||
}
|
||||
|
||||
IOUAmount
|
||||
AMM::deposit(
|
||||
std::optional<Account> const& account,
|
||||
std::optional<LPToken> tokens,
|
||||
std::optional<STAmount> const& asset1In,
|
||||
std::optional<STAmount> const& asset2In,
|
||||
std::optional<STAmount> const& maxEP,
|
||||
std::optional<std::uint32_t> const& flags,
|
||||
std::optional<std::pair<Issue, Issue>> const& assets,
|
||||
std::optional<jtx::seq> const& seq,
|
||||
std::optional<ter> const& ter)
|
||||
{
|
||||
Json::Value jv;
|
||||
if (tokens)
|
||||
tokens->tokens(lptIssue_).setJson(jv[jss::LPTokenOut]);
|
||||
if (asset1In)
|
||||
asset1In->setJson(jv[jss::Amount]);
|
||||
if (asset2In)
|
||||
asset2In->setJson(jv[jss::Amount2]);
|
||||
if (maxEP)
|
||||
maxEP->setJson(jv[jss::EPrice]);
|
||||
std::uint32_t jvflags = 0;
|
||||
if (flags)
|
||||
jvflags = *flags;
|
||||
// If including asset1In and asset2In or tokens as
|
||||
// deposit min amounts then must set the flags
|
||||
// explicitly instead of relying on this logic.
|
||||
if (!(jvflags & tfDepositSubTx))
|
||||
{
|
||||
if (tokens && !asset1In)
|
||||
jvflags |= tfLPToken;
|
||||
else if (tokens && asset1In)
|
||||
jvflags |= tfOneAssetLPToken;
|
||||
else if (asset1In && asset2In)
|
||||
jvflags |= tfTwoAsset;
|
||||
else if (maxEP && asset1In)
|
||||
jvflags |= tfLimitLPToken;
|
||||
else if (asset1In)
|
||||
jvflags |= tfSingleAsset;
|
||||
}
|
||||
jv[jss::Flags] = jvflags;
|
||||
return deposit(account, jv, assets, seq, ter);
|
||||
}
|
||||
|
||||
IOUAmount
|
||||
AMM::withdraw(
|
||||
std::optional<Account> const& account,
|
||||
Json::Value& jv,
|
||||
std::optional<jtx::seq> const& seq,
|
||||
std::optional<std::pair<Issue, Issue>> const& assets,
|
||||
std::optional<ter> const& ter)
|
||||
{
|
||||
auto const& acct = account ? *account : creatorAccount_;
|
||||
auto const lpTokens = getLPTokensBalance(acct);
|
||||
jv[jss::Account] = acct.human();
|
||||
setTokens(jv, assets);
|
||||
jv[jss::TransactionType] = jss::AMMWithdraw;
|
||||
if (fee_ != 0)
|
||||
jv[jss::Fee] = std::to_string(fee_);
|
||||
submit(jv, seq, ter);
|
||||
return lpTokens - getLPTokensBalance(acct);
|
||||
}
|
||||
|
||||
IOUAmount
|
||||
AMM::withdraw(
|
||||
std::optional<Account> const& account,
|
||||
std::optional<LPToken> const& tokens,
|
||||
std::optional<STAmount> const& asset1Out,
|
||||
std::optional<std::uint32_t> const& flags,
|
||||
std::optional<ter> const& ter)
|
||||
{
|
||||
return withdraw(
|
||||
account,
|
||||
tokens,
|
||||
asset1Out,
|
||||
std::nullopt,
|
||||
std::nullopt,
|
||||
flags,
|
||||
std::nullopt,
|
||||
std::nullopt,
|
||||
ter);
|
||||
}
|
||||
|
||||
IOUAmount
|
||||
AMM::withdraw(
|
||||
std::optional<Account> const& account,
|
||||
STAmount const& asset1Out,
|
||||
std::optional<STAmount> const& asset2Out,
|
||||
std::optional<IOUAmount> const& maxEP,
|
||||
std::optional<ter> const& ter)
|
||||
{
|
||||
assert(!(asset2Out && maxEP));
|
||||
return withdraw(
|
||||
account,
|
||||
std::nullopt,
|
||||
asset1Out,
|
||||
asset2Out,
|
||||
maxEP,
|
||||
std::nullopt,
|
||||
std::nullopt,
|
||||
std::nullopt,
|
||||
ter);
|
||||
}
|
||||
|
||||
IOUAmount
|
||||
AMM::withdraw(
|
||||
std::optional<Account> const& account,
|
||||
std::optional<LPToken> const& tokens,
|
||||
std::optional<STAmount> const& asset1Out,
|
||||
std::optional<STAmount> const& asset2Out,
|
||||
std::optional<IOUAmount> const& maxEP,
|
||||
std::optional<std::uint32_t> const& flags,
|
||||
std::optional<std::pair<Issue, Issue>> const& assets,
|
||||
std::optional<jtx::seq> const& seq,
|
||||
std::optional<ter> const& ter)
|
||||
{
|
||||
Json::Value jv;
|
||||
if (tokens)
|
||||
tokens->tokens(lptIssue_).setJson(jv[jss::LPTokenIn]);
|
||||
if (asset1Out)
|
||||
asset1Out->setJson(jv[jss::Amount]);
|
||||
if (asset2Out)
|
||||
asset2Out->setJson(jv[jss::Amount2]);
|
||||
if (maxEP)
|
||||
{
|
||||
STAmount const saMaxEP{*maxEP, lptIssue_};
|
||||
saMaxEP.setJson(jv[jss::EPrice]);
|
||||
}
|
||||
std::uint32_t jvflags = 0;
|
||||
if (flags)
|
||||
jvflags = *flags;
|
||||
if (!(jvflags & tfWithdrawSubTx))
|
||||
{
|
||||
if (tokens && !asset1Out)
|
||||
jvflags |= tfLPToken;
|
||||
else if (asset1Out && asset2Out)
|
||||
jvflags |= tfTwoAsset;
|
||||
else if (tokens && asset1Out)
|
||||
jvflags |= tfOneAssetLPToken;
|
||||
else if (asset1Out && maxEP)
|
||||
jvflags |= tfLimitLPToken;
|
||||
else if (asset1Out)
|
||||
jvflags |= tfSingleAsset;
|
||||
}
|
||||
jv[jss::Flags] = jvflags;
|
||||
return withdraw(account, jv, seq, assets, ter);
|
||||
}
|
||||
|
||||
void
|
||||
AMM::vote(
|
||||
std::optional<Account> const& account,
|
||||
std::uint32_t feeVal,
|
||||
std::optional<std::uint32_t> const& flags,
|
||||
std::optional<jtx::seq> const& seq,
|
||||
std::optional<std::pair<Issue, Issue>> const& assets,
|
||||
std::optional<ter> const& ter)
|
||||
{
|
||||
Json::Value jv;
|
||||
jv[jss::Account] = account ? account->human() : creatorAccount_.human();
|
||||
setTokens(jv, assets);
|
||||
jv[jss::TradingFee] = feeVal;
|
||||
jv[jss::TransactionType] = jss::AMMVote;
|
||||
if (flags)
|
||||
jv[jss::Flags] = *flags;
|
||||
if (fee_ != 0)
|
||||
jv[jss::Fee] = std::to_string(fee_);
|
||||
submit(jv, seq, ter);
|
||||
}
|
||||
|
||||
void
|
||||
AMM::bid(
|
||||
std::optional<Account> const& account,
|
||||
std::optional<std::variant<int, IOUAmount, STAmount>> const& bidMin,
|
||||
std::optional<std::variant<int, IOUAmount, STAmount>> const& bidMax,
|
||||
std::vector<Account> const& authAccounts,
|
||||
std::optional<std::uint32_t> const& flags,
|
||||
std::optional<jtx::seq> const& seq,
|
||||
std::optional<std::pair<Issue, Issue>> const& assets,
|
||||
std::optional<ter> const& ter)
|
||||
{
|
||||
if (auto const amm =
|
||||
env_.current()->read(keylet::amm(asset1_.issue(), asset2_.issue())))
|
||||
{
|
||||
if (amm->isFieldPresent(sfAuctionSlot))
|
||||
{
|
||||
auto const& auctionSlot =
|
||||
static_cast<STObject const&>(amm->peekAtField(sfAuctionSlot));
|
||||
lastPurchasePrice_ = auctionSlot[sfPrice].iou();
|
||||
}
|
||||
}
|
||||
bidMin_ = std::nullopt;
|
||||
bidMax_ = std::nullopt;
|
||||
|
||||
Json::Value jv;
|
||||
jv[jss::Account] = account ? account->human() : creatorAccount_.human();
|
||||
setTokens(jv, assets);
|
||||
auto getBid = [&](auto const& bid) {
|
||||
if (std::holds_alternative<int>(bid))
|
||||
return STAmount{lptIssue_, std::get<int>(bid)};
|
||||
else if (std::holds_alternative<IOUAmount>(bid))
|
||||
return toSTAmount(std::get<IOUAmount>(bid), lptIssue_);
|
||||
else
|
||||
return std::get<STAmount>(bid);
|
||||
};
|
||||
if (bidMin)
|
||||
{
|
||||
STAmount saTokens = getBid(*bidMin);
|
||||
saTokens.setJson(jv[jss::BidMin]);
|
||||
bidMin_ = saTokens.iou();
|
||||
}
|
||||
if (bidMax)
|
||||
{
|
||||
STAmount saTokens = getBid(*bidMax);
|
||||
saTokens.setJson(jv[jss::BidMax]);
|
||||
bidMax_ = saTokens.iou();
|
||||
}
|
||||
if (authAccounts.size() > 0)
|
||||
{
|
||||
Json::Value accounts(Json::arrayValue);
|
||||
for (auto const& account : authAccounts)
|
||||
{
|
||||
Json::Value acct;
|
||||
Json::Value authAcct;
|
||||
acct[jss::Account] = account.human();
|
||||
authAcct[jss::AuthAccount] = acct;
|
||||
accounts.append(authAcct);
|
||||
}
|
||||
jv[jss::AuthAccounts] = accounts;
|
||||
}
|
||||
if (flags)
|
||||
jv[jss::Flags] = *flags;
|
||||
jv[jss::TransactionType] = jss::AMMBid;
|
||||
if (fee_ != 0)
|
||||
jv[jss::Fee] = std::to_string(fee_);
|
||||
submit(jv, seq, ter);
|
||||
}
|
||||
|
||||
void
|
||||
AMM::submit(
|
||||
Json::Value const& jv,
|
||||
std::optional<jtx::seq> const& seq,
|
||||
std::optional<ter> const& ter)
|
||||
{
|
||||
if (log_)
|
||||
std::cout << jv.toStyledString();
|
||||
if (msig_)
|
||||
{
|
||||
if (seq && ter)
|
||||
env_(jv, *msig_, *seq, *ter);
|
||||
else if (seq)
|
||||
env_(jv, *msig_, *seq);
|
||||
else if (ter)
|
||||
env_(jv, *msig_, *ter);
|
||||
else
|
||||
env_(jv, *msig_);
|
||||
}
|
||||
else if (seq && ter)
|
||||
env_(jv, *seq, *ter);
|
||||
else if (seq)
|
||||
env_(jv, *seq);
|
||||
else if (ter)
|
||||
env_(jv, *ter);
|
||||
else
|
||||
env_(jv);
|
||||
env_.close();
|
||||
}
|
||||
|
||||
bool
|
||||
AMM::expectAuctionSlot(auto&& cb) const
|
||||
{
|
||||
if (auto const amm =
|
||||
env_.current()->read(keylet::amm(asset1_.issue(), asset2_.issue()));
|
||||
amm && amm->isFieldPresent(sfAuctionSlot))
|
||||
{
|
||||
auto const& auctionSlot =
|
||||
static_cast<STObject const&>(amm->peekAtField(sfAuctionSlot));
|
||||
if (auctionSlot.isFieldPresent(sfAccount))
|
||||
{
|
||||
auto const slotFee = auctionSlot[sfDiscountedFee];
|
||||
auto const slotInterval = ammAuctionTimeSlot(
|
||||
env_.app().timeKeeper().now().time_since_epoch().count(),
|
||||
auctionSlot);
|
||||
auto const slotPrice = auctionSlot[sfPrice].iou();
|
||||
auto const authAccounts = auctionSlot.getFieldArray(sfAuthAccounts);
|
||||
return cb(slotFee, slotInterval, slotPrice, authAccounts);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
namespace amm {
|
||||
Json::Value
|
||||
trust(AccountID const& account, STAmount const& amount, std::uint32_t flags)
|
||||
{
|
||||
if (isXRP(amount))
|
||||
Throw<std::runtime_error>("trust() requires IOU");
|
||||
Json::Value jv;
|
||||
jv[jss::Account] = to_string(account);
|
||||
jv[jss::LimitAmount] = amount.getJson(JsonOptions::none);
|
||||
jv[jss::TransactionType] = jss::TrustSet;
|
||||
jv[jss::Flags] = flags;
|
||||
return jv;
|
||||
}
|
||||
Json::Value
|
||||
pay(Account const& account, AccountID const& to, STAmount const& amount)
|
||||
{
|
||||
Json::Value jv;
|
||||
jv[jss::Account] = account.human();
|
||||
jv[jss::Amount] = amount.getJson(JsonOptions::none);
|
||||
jv[jss::Destination] = to_string(to);
|
||||
jv[jss::TransactionType] = jss::Payment;
|
||||
jv[jss::Flags] = tfUniversal;
|
||||
return jv;
|
||||
}
|
||||
} // namespace amm
|
||||
} // namespace jtx
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
278
src/test/jtx/impl/AMMTest.cpp
Normal file
278
src/test/jtx/impl/AMMTest.cpp
Normal file
@@ -0,0 +1,278 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2023 Ripple Labs Inc.
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <test/jtx/AMMTest.h>
|
||||
|
||||
#include <ripple/protocol/STParsedJSON.h>
|
||||
#include <ripple/resource/Fees.h>
|
||||
#include <ripple/rpc/RPCHandler.h>
|
||||
#include <ripple/rpc/impl/RPCHelpers.h>
|
||||
#include <test/jtx/AMM.h>
|
||||
#include <test/jtx/Env.h>
|
||||
#include <test/jtx/pay.h>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
namespace jtx {
|
||||
|
||||
void
|
||||
fund(
|
||||
jtx::Env& env,
|
||||
jtx::Account const& gw,
|
||||
std::vector<jtx::Account> const& accounts,
|
||||
std::vector<STAmount> const& amts,
|
||||
Fund how)
|
||||
{
|
||||
fund(env, gw, accounts, XRP(30000), amts, how);
|
||||
}
|
||||
|
||||
void
|
||||
fund(
|
||||
jtx::Env& env,
|
||||
std::vector<jtx::Account> const& accounts,
|
||||
STAmount const& xrp,
|
||||
std::vector<STAmount> const& amts,
|
||||
Fund how)
|
||||
{
|
||||
for (auto const& account : accounts)
|
||||
{
|
||||
if (how == Fund::All || how == Fund::Acct)
|
||||
{
|
||||
env.fund(xrp, account);
|
||||
}
|
||||
}
|
||||
env.close();
|
||||
for (auto const& account : accounts)
|
||||
{
|
||||
for (auto const& amt : amts)
|
||||
{
|
||||
env.trust(amt + amt, account);
|
||||
env(pay(amt.issue().account, account, amt));
|
||||
}
|
||||
}
|
||||
env.close();
|
||||
}
|
||||
|
||||
void
|
||||
fund(
|
||||
jtx::Env& env,
|
||||
jtx::Account const& gw,
|
||||
std::vector<jtx::Account> const& accounts,
|
||||
STAmount const& xrp,
|
||||
std::vector<STAmount> const& amts,
|
||||
Fund how)
|
||||
{
|
||||
if (how == Fund::All || how == Fund::Gw)
|
||||
env.fund(xrp, gw);
|
||||
env.close();
|
||||
fund(env, accounts, xrp, amts, how);
|
||||
}
|
||||
|
||||
AMMTestBase::AMMTestBase()
|
||||
: gw("gateway")
|
||||
, carol("carol")
|
||||
, alice("alice")
|
||||
, bob("bob")
|
||||
, USD(gw["USD"])
|
||||
, EUR(gw["EUR"])
|
||||
, GBP(gw["GBP"])
|
||||
, BTC(gw["BTC"])
|
||||
, BAD(jtx::IOU(gw, badCurrency()))
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
AMMTestBase::testAMM(
|
||||
std::function<void(jtx::AMM&, jtx::Env&)>&& cb,
|
||||
std::optional<std::pair<STAmount, STAmount>> const& pool,
|
||||
std::uint16_t tfee,
|
||||
std::optional<jtx::ter> const& ter,
|
||||
std::optional<FeatureBitset> const& features)
|
||||
{
|
||||
using namespace jtx;
|
||||
auto env = [&]() {
|
||||
if (features)
|
||||
return Env{*this, *features};
|
||||
return Env{*this};
|
||||
}();
|
||||
|
||||
auto const [asset1, asset2] =
|
||||
pool ? *pool : std::make_pair(XRP(10000), USD(10000));
|
||||
auto tofund = [&](STAmount const& a) -> STAmount {
|
||||
if (a.native())
|
||||
{
|
||||
auto const defXRP = XRP(30000);
|
||||
if (a <= defXRP)
|
||||
return defXRP;
|
||||
return a + XRP(1000);
|
||||
}
|
||||
auto const defIOU = STAmount{a.issue(), 30000};
|
||||
if (a <= defIOU)
|
||||
return defIOU;
|
||||
return a + STAmount{a.issue(), 1000};
|
||||
};
|
||||
auto const toFund1 = tofund(asset1);
|
||||
auto const toFund2 = tofund(asset2);
|
||||
BEAST_EXPECT(asset1 <= toFund1 && asset2 <= toFund2);
|
||||
|
||||
if (!asset1.native() && !asset2.native())
|
||||
fund(env, gw, {alice, carol}, {toFund1, toFund2}, Fund::All);
|
||||
else if (asset1.native())
|
||||
fund(env, gw, {alice, carol}, toFund1, {toFund2}, Fund::All);
|
||||
else if (asset2.native())
|
||||
fund(env, gw, {alice, carol}, toFund2, {toFund1}, Fund::All);
|
||||
|
||||
AMM ammAlice(env, alice, asset1, asset2, false, tfee);
|
||||
BEAST_EXPECT(ammAlice.expectBalances(asset1, asset2, ammAlice.tokens()));
|
||||
cb(ammAlice, env);
|
||||
}
|
||||
|
||||
XRPAmount
|
||||
AMMTest::reserve(jtx::Env& env, std::uint32_t count) const
|
||||
{
|
||||
return env.current()->fees().accountReserve(count);
|
||||
}
|
||||
|
||||
XRPAmount
|
||||
AMMTest::ammCrtFee(jtx::Env& env) const
|
||||
{
|
||||
return env.current()->fees().increment;
|
||||
}
|
||||
|
||||
jtx::Env
|
||||
AMMTest::pathTestEnv()
|
||||
{
|
||||
// These tests were originally written with search parameters that are
|
||||
// different from the current defaults. This function creates an env
|
||||
// with the search parameters that the tests were written for.
|
||||
return Env(*this, envconfig([](std::unique_ptr<Config> cfg) {
|
||||
cfg->PATH_SEARCH_OLD = 7;
|
||||
cfg->PATH_SEARCH = 7;
|
||||
cfg->PATH_SEARCH_MAX = 10;
|
||||
return cfg;
|
||||
}));
|
||||
}
|
||||
|
||||
Json::Value
|
||||
AMMTest::find_paths_request(
|
||||
jtx::Env& env,
|
||||
jtx::Account const& src,
|
||||
jtx::Account const& dst,
|
||||
STAmount const& saDstAmount,
|
||||
std::optional<STAmount> const& saSendMax,
|
||||
std::optional<Currency> const& saSrcCurrency)
|
||||
{
|
||||
using namespace jtx;
|
||||
|
||||
auto& app = env.app();
|
||||
Resource::Charge loadType = Resource::feeReferenceRPC;
|
||||
Resource::Consumer c;
|
||||
|
||||
RPC::JsonContext context{
|
||||
{env.journal,
|
||||
app,
|
||||
loadType,
|
||||
app.getOPs(),
|
||||
app.getLedgerMaster(),
|
||||
c,
|
||||
Role::USER,
|
||||
{},
|
||||
{},
|
||||
RPC::apiVersionIfUnspecified},
|
||||
{},
|
||||
{}};
|
||||
|
||||
Json::Value params = Json::objectValue;
|
||||
params[jss::command] = "ripple_path_find";
|
||||
params[jss::source_account] = toBase58(src);
|
||||
params[jss::destination_account] = toBase58(dst);
|
||||
params[jss::destination_amount] = saDstAmount.getJson(JsonOptions::none);
|
||||
if (saSendMax)
|
||||
params[jss::send_max] = saSendMax->getJson(JsonOptions::none);
|
||||
if (saSrcCurrency)
|
||||
{
|
||||
auto& sc = params[jss::source_currencies] = Json::arrayValue;
|
||||
Json::Value j = Json::objectValue;
|
||||
j[jss::currency] = to_string(saSrcCurrency.value());
|
||||
sc.append(j);
|
||||
}
|
||||
|
||||
Json::Value result;
|
||||
gate g;
|
||||
app.getJobQueue().postCoro(jtCLIENT, "RPC-Client", [&](auto const& coro) {
|
||||
context.params = std::move(params);
|
||||
context.coro = coro;
|
||||
RPC::doCommand(context, result);
|
||||
g.signal();
|
||||
});
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
BEAST_EXPECT(g.wait_for(5s));
|
||||
BEAST_EXPECT(!result.isMember(jss::error));
|
||||
return result;
|
||||
}
|
||||
|
||||
std::tuple<STPathSet, STAmount, STAmount>
|
||||
AMMTest::find_paths(
|
||||
jtx::Env& env,
|
||||
jtx::Account const& src,
|
||||
jtx::Account const& dst,
|
||||
STAmount const& saDstAmount,
|
||||
std::optional<STAmount> const& saSendMax,
|
||||
std::optional<Currency> const& saSrcCurrency)
|
||||
{
|
||||
Json::Value result = find_paths_request(
|
||||
env, src, dst, saDstAmount, saSendMax, saSrcCurrency);
|
||||
BEAST_EXPECT(!result.isMember(jss::error));
|
||||
|
||||
STAmount da;
|
||||
if (result.isMember(jss::destination_amount))
|
||||
da = amountFromJson(sfGeneric, result[jss::destination_amount]);
|
||||
|
||||
STAmount sa;
|
||||
STPathSet paths;
|
||||
if (result.isMember(jss::alternatives))
|
||||
{
|
||||
auto const& alts = result[jss::alternatives];
|
||||
if (alts.size() > 0)
|
||||
{
|
||||
auto const& path = alts[0u];
|
||||
|
||||
if (path.isMember(jss::source_amount))
|
||||
sa = amountFromJson(sfGeneric, path[jss::source_amount]);
|
||||
|
||||
if (path.isMember(jss::destination_amount))
|
||||
da = amountFromJson(sfGeneric, path[jss::destination_amount]);
|
||||
|
||||
if (path.isMember(jss::paths_computed))
|
||||
{
|
||||
Json::Value p;
|
||||
p["Paths"] = path[jss::paths_computed];
|
||||
STParsedJSONObject po("generic", p);
|
||||
paths = po.object->getFieldPathSet(sfPaths);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return std::make_tuple(std::move(paths), std::move(sa), std::move(da));
|
||||
}
|
||||
|
||||
} // namespace jtx
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
388
src/test/jtx/impl/TestHelpers.cpp
Normal file
388
src/test/jtx/impl/TestHelpers.cpp
Normal file
@@ -0,0 +1,388 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2023 Ripple Labs Inc.
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <test/jtx/TestHelpers.h>
|
||||
|
||||
#include <ripple/protocol/TxFlags.h>
|
||||
#include <test/jtx/offer.h>
|
||||
#include <test/jtx/owners.h>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
namespace jtx {
|
||||
|
||||
// Functions used in debugging
|
||||
Json::Value
|
||||
getAccountOffers(Env& env, AccountID const& acct, bool current)
|
||||
{
|
||||
Json::Value jv;
|
||||
jv[jss::account] = to_string(acct);
|
||||
return env.rpc("json", "account_offers", to_string(jv))[jss::result];
|
||||
}
|
||||
|
||||
Json::Value
|
||||
getAccountLines(Env& env, AccountID const& acctId)
|
||||
{
|
||||
Json::Value jv;
|
||||
jv[jss::account] = to_string(acctId);
|
||||
return env.rpc("json", "account_lines", to_string(jv))[jss::result];
|
||||
}
|
||||
|
||||
bool
|
||||
checkArraySize(Json::Value const& val, unsigned int size)
|
||||
{
|
||||
return val.isArray() && val.size() == size;
|
||||
}
|
||||
|
||||
/* Path finding */
|
||||
/******************************************************************************/
|
||||
void
|
||||
stpath_append_one(STPath& st, Account const& account)
|
||||
{
|
||||
st.push_back(STPathElement({account.id(), std::nullopt, std::nullopt}));
|
||||
}
|
||||
|
||||
void
|
||||
stpath_append_one(STPath& st, STPathElement const& pe)
|
||||
{
|
||||
st.push_back(pe);
|
||||
}
|
||||
|
||||
bool
|
||||
equal(STAmount const& sa1, STAmount const& sa2)
|
||||
{
|
||||
return sa1 == sa2 && sa1.issue().account == sa2.issue().account;
|
||||
}
|
||||
|
||||
// Issue path element
|
||||
STPathElement
|
||||
IPE(Issue const& iss)
|
||||
{
|
||||
return STPathElement(
|
||||
STPathElement::typeCurrency | STPathElement::typeIssuer,
|
||||
xrpAccount(),
|
||||
iss.currency,
|
||||
iss.account);
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
XRPAmount
|
||||
txfee(Env const& env, std::uint16_t n)
|
||||
{
|
||||
return env.current()->fees().base * n;
|
||||
}
|
||||
|
||||
PrettyAmount
|
||||
xrpMinusFee(Env const& env, std::int64_t xrpAmount)
|
||||
{
|
||||
auto feeDrops = env.current()->fees().base;
|
||||
return drops(dropsPerXRP * xrpAmount - feeDrops);
|
||||
};
|
||||
|
||||
[[nodiscard]] bool
|
||||
expectLine(
|
||||
Env& env,
|
||||
AccountID const& account,
|
||||
STAmount const& value,
|
||||
bool defaultLimits)
|
||||
{
|
||||
if (auto const sle = env.le(keylet::line(account, value.issue())))
|
||||
{
|
||||
Issue const issue = value.issue();
|
||||
bool const accountLow = account < issue.account;
|
||||
|
||||
bool expectDefaultTrustLine = true;
|
||||
if (defaultLimits)
|
||||
{
|
||||
STAmount low{issue};
|
||||
STAmount high{issue};
|
||||
|
||||
low.setIssuer(accountLow ? account : issue.account);
|
||||
high.setIssuer(accountLow ? issue.account : account);
|
||||
|
||||
expectDefaultTrustLine = sle->getFieldAmount(sfLowLimit) == low &&
|
||||
sle->getFieldAmount(sfHighLimit) == high;
|
||||
}
|
||||
|
||||
auto amount = sle->getFieldAmount(sfBalance);
|
||||
amount.setIssuer(value.issue().account);
|
||||
if (!accountLow)
|
||||
amount.negate();
|
||||
return amount == value && expectDefaultTrustLine;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
expectLine(Env& env, AccountID const& account, None const& value)
|
||||
{
|
||||
return !env.le(keylet::line(account, value.issue));
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
expectOffers(
|
||||
Env& env,
|
||||
AccountID const& account,
|
||||
std::uint16_t size,
|
||||
std::vector<Amounts> const& toMatch)
|
||||
{
|
||||
std::uint16_t cnt = 0;
|
||||
std::uint16_t matched = 0;
|
||||
forEachItem(
|
||||
*env.current(), account, [&](std::shared_ptr<SLE const> const& sle) {
|
||||
if (!sle)
|
||||
return false;
|
||||
if (sle->getType() == ltOFFER)
|
||||
{
|
||||
++cnt;
|
||||
if (std::find_if(
|
||||
toMatch.begin(), toMatch.end(), [&](auto const& a) {
|
||||
return a.in == sle->getFieldAmount(sfTakerPays) &&
|
||||
a.out == sle->getFieldAmount(sfTakerGets);
|
||||
}) != toMatch.end())
|
||||
++matched;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
return size == cnt && matched == toMatch.size();
|
||||
}
|
||||
|
||||
Json::Value
|
||||
ledgerEntryRoot(Env& env, Account const& acct)
|
||||
{
|
||||
Json::Value jvParams;
|
||||
jvParams[jss::ledger_index] = "current";
|
||||
jvParams[jss::account_root] = acct.human();
|
||||
return env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
}
|
||||
|
||||
Json::Value
|
||||
ledgerEntryState(
|
||||
Env& env,
|
||||
Account const& acct_a,
|
||||
Account const& acct_b,
|
||||
std::string const& currency)
|
||||
{
|
||||
Json::Value jvParams;
|
||||
jvParams[jss::ledger_index] = "current";
|
||||
jvParams[jss::ripple_state][jss::currency] = currency;
|
||||
jvParams[jss::ripple_state][jss::accounts] = Json::arrayValue;
|
||||
jvParams[jss::ripple_state][jss::accounts].append(acct_a.human());
|
||||
jvParams[jss::ripple_state][jss::accounts].append(acct_b.human());
|
||||
return env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
}
|
||||
|
||||
Json::Value
|
||||
accountBalance(Env& env, Account const& acct)
|
||||
{
|
||||
auto const jrr = ledgerEntryRoot(env, acct);
|
||||
return jrr[jss::node][sfBalance.fieldName];
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
expectLedgerEntryRoot(
|
||||
Env& env,
|
||||
Account const& acct,
|
||||
STAmount const& expectedValue)
|
||||
{
|
||||
return accountBalance(env, acct) == to_string(expectedValue.xrp());
|
||||
}
|
||||
|
||||
/* Escrow */
|
||||
/******************************************************************************/
|
||||
|
||||
Json::Value
|
||||
escrow(AccountID const& account, AccountID const& to, STAmount const& amount)
|
||||
{
|
||||
Json::Value jv;
|
||||
jv[jss::TransactionType] = jss::EscrowCreate;
|
||||
jv[jss::Flags] = tfUniversal;
|
||||
jv[jss::Account] = to_string(account);
|
||||
jv[jss::Destination] = to_string(to);
|
||||
jv[jss::Amount] = amount.getJson(JsonOptions::none);
|
||||
return jv;
|
||||
}
|
||||
|
||||
Json::Value
|
||||
finish(AccountID const& account, AccountID const& from, std::uint32_t seq)
|
||||
{
|
||||
Json::Value jv;
|
||||
jv[jss::TransactionType] = jss::EscrowFinish;
|
||||
jv[jss::Flags] = tfUniversal;
|
||||
jv[jss::Account] = to_string(account);
|
||||
jv[sfOwner.jsonName] = to_string(from);
|
||||
jv[sfOfferSequence.jsonName] = seq;
|
||||
return jv;
|
||||
}
|
||||
|
||||
Json::Value
|
||||
cancel(AccountID const& account, Account const& from, std::uint32_t seq)
|
||||
{
|
||||
Json::Value jv;
|
||||
jv[jss::TransactionType] = jss::EscrowCancel;
|
||||
jv[jss::Flags] = tfUniversal;
|
||||
jv[jss::Account] = to_string(account);
|
||||
jv[sfOwner.jsonName] = from.human();
|
||||
jv[sfOfferSequence.jsonName] = seq;
|
||||
return jv;
|
||||
}
|
||||
|
||||
/* Payment Channel */
|
||||
/******************************************************************************/
|
||||
Json::Value
|
||||
create(
|
||||
AccountID const& account,
|
||||
AccountID const& to,
|
||||
STAmount const& amount,
|
||||
NetClock::duration const& settleDelay,
|
||||
PublicKey const& pk,
|
||||
std::optional<NetClock::time_point> const& cancelAfter,
|
||||
std::optional<std::uint32_t> const& dstTag)
|
||||
{
|
||||
Json::Value jv;
|
||||
jv[jss::TransactionType] = jss::PaymentChannelCreate;
|
||||
jv[jss::Flags] = tfUniversal;
|
||||
jv[jss::Account] = to_string(account);
|
||||
jv[jss::Destination] = to_string(to);
|
||||
jv[jss::Amount] = amount.getJson(JsonOptions::none);
|
||||
jv[jss::SettleDelay] = settleDelay.count();
|
||||
jv[sfPublicKey.fieldName] = strHex(pk.slice());
|
||||
if (cancelAfter)
|
||||
jv[sfCancelAfter.fieldName] = cancelAfter->time_since_epoch().count();
|
||||
if (dstTag)
|
||||
jv[sfDestinationTag.fieldName] = *dstTag;
|
||||
return jv;
|
||||
}
|
||||
|
||||
Json::Value
|
||||
fund(
|
||||
AccountID const& account,
|
||||
uint256 const& channel,
|
||||
STAmount const& amount,
|
||||
std::optional<NetClock::time_point> const& expiration)
|
||||
{
|
||||
Json::Value jv;
|
||||
jv[jss::TransactionType] = jss::PaymentChannelFund;
|
||||
jv[jss::Flags] = tfUniversal;
|
||||
jv[jss::Account] = to_string(account);
|
||||
jv[sfChannel.fieldName] = to_string(channel);
|
||||
jv[jss::Amount] = amount.getJson(JsonOptions::none);
|
||||
if (expiration)
|
||||
jv[sfExpiration.fieldName] = expiration->time_since_epoch().count();
|
||||
return jv;
|
||||
}
|
||||
|
||||
Json::Value
|
||||
claim(
|
||||
AccountID const& account,
|
||||
uint256 const& channel,
|
||||
std::optional<STAmount> const& balance,
|
||||
std::optional<STAmount> const& amount,
|
||||
std::optional<Slice> const& signature,
|
||||
std::optional<PublicKey> const& pk)
|
||||
{
|
||||
Json::Value jv;
|
||||
jv[jss::TransactionType] = jss::PaymentChannelClaim;
|
||||
jv[jss::Flags] = tfUniversal;
|
||||
jv[jss::Account] = to_string(account);
|
||||
jv["Channel"] = to_string(channel);
|
||||
if (amount)
|
||||
jv[jss::Amount] = amount->getJson(JsonOptions::none);
|
||||
if (balance)
|
||||
jv["Balance"] = balance->getJson(JsonOptions::none);
|
||||
if (signature)
|
||||
jv["Signature"] = strHex(*signature);
|
||||
if (pk)
|
||||
jv["PublicKey"] = strHex(pk->slice());
|
||||
return jv;
|
||||
}
|
||||
|
||||
uint256
|
||||
channel(
|
||||
AccountID const& account,
|
||||
AccountID const& dst,
|
||||
std::uint32_t seqProxyValue)
|
||||
{
|
||||
auto const k = keylet::payChan(account, dst, seqProxyValue);
|
||||
return k.key;
|
||||
}
|
||||
|
||||
STAmount
|
||||
channelBalance(ReadView const& view, uint256 const& chan)
|
||||
{
|
||||
auto const slep = view.read({ltPAYCHAN, chan});
|
||||
if (!slep)
|
||||
return XRPAmount{-1};
|
||||
return (*slep)[sfBalance];
|
||||
}
|
||||
|
||||
bool
|
||||
channelExists(ReadView const& view, uint256 const& chan)
|
||||
{
|
||||
auto const slep = view.read({ltPAYCHAN, chan});
|
||||
return bool(slep);
|
||||
}
|
||||
|
||||
/* Crossing Limits */
|
||||
/******************************************************************************/
|
||||
|
||||
void
|
||||
n_offers(
|
||||
Env& env,
|
||||
std::size_t n,
|
||||
Account const& account,
|
||||
STAmount const& in,
|
||||
STAmount const& out)
|
||||
{
|
||||
auto const ownerCount = env.le(account)->getFieldU32(sfOwnerCount);
|
||||
for (std::size_t i = 0; i < n; i++)
|
||||
{
|
||||
env(offer(account, in, out));
|
||||
env.close();
|
||||
}
|
||||
env.require(owners(account, ownerCount + n));
|
||||
}
|
||||
|
||||
/* Pay Strand */
|
||||
/***************************************************************/
|
||||
|
||||
// Currency path element
|
||||
STPathElement
|
||||
cpe(Currency const& c)
|
||||
{
|
||||
return STPathElement(
|
||||
STPathElement::typeCurrency, xrpAccount(), c, xrpAccount());
|
||||
};
|
||||
|
||||
// All path element
|
||||
STPathElement
|
||||
allpe(AccountID const& a, Issue const& iss)
|
||||
{
|
||||
return STPathElement(
|
||||
STPathElement::typeAccount | STPathElement::typeCurrency |
|
||||
STPathElement::typeIssuer,
|
||||
a,
|
||||
iss.currency,
|
||||
iss.account);
|
||||
};
|
||||
|
||||
} // namespace jtx
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
@@ -65,9 +65,15 @@ path::create()
|
||||
|
||||
void
|
||||
path::append_one(Account const& account)
|
||||
{
|
||||
append_one(account.id());
|
||||
}
|
||||
|
||||
void
|
||||
path::append_one(AccountID const& account)
|
||||
{
|
||||
auto& jv = create();
|
||||
jv["account"] = toBase58(account.id());
|
||||
jv["account"] = toBase58(account);
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
@@ -26,17 +26,22 @@ namespace test {
|
||||
namespace jtx {
|
||||
|
||||
Json::Value
|
||||
pay(Account const& account, Account const& to, AnyAmount amount)
|
||||
pay(AccountID const& account, AccountID const& to, AnyAmount amount)
|
||||
{
|
||||
amount.to(to);
|
||||
Json::Value jv;
|
||||
jv[jss::Account] = account.human();
|
||||
jv[jss::Account] = to_string(account);
|
||||
jv[jss::Amount] = amount.value.getJson(JsonOptions::none);
|
||||
jv[jss::Destination] = to.human();
|
||||
jv[jss::Destination] = to_string(to);
|
||||
jv[jss::TransactionType] = jss::Payment;
|
||||
jv[jss::Flags] = tfUniversal;
|
||||
return jv;
|
||||
}
|
||||
Json::Value
|
||||
pay(Account const& account, Account const& to, AnyAmount amount)
|
||||
{
|
||||
return pay(account.id(), to.id(), amount);
|
||||
}
|
||||
|
||||
} // namespace jtx
|
||||
} // namespace test
|
||||
|
||||
Reference in New Issue
Block a user