#include #include #include #include #include #include #include #include #include namespace xrpl { namespace test { namespace jtx { static Number number(STAmount const& a) { if (isXRP(a)) return a.xrp(); return a; } IOUAmount AMM::initialTokens() { if (!env_.enabled(fixAMMv1_3)) { auto const product = number(asset1_) * number(asset2_); return (IOUAmount)(product.mantissa() >= 0 ? root2(product) : root2(-product)); } return getLPTokensBalance(); } 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 flags, std::optional seq, std::optional ms, std::optional const& ter, bool close) : env_(env) , creatorAccount_(account) , asset1_(asset1) , asset2_(asset2) , ammID_(keylet::amm(asset1_.issue(), asset2_.issue()).key) , log_(log) , doClose_(close) , lastPurchasePrice_(0) , msig_(ms) , fee_(fee) , ammAccount_(create(tfee, flags, seq, ter)) , lptIssue_(xrpl::ammLPTIssue(asset1_.issue().currency, asset2_.issue().currency, ammAccount_)) , initialLPTokens_(initialTokens()) { } AMM::AMM( Env& env, Account const& account, STAmount const& asset1, STAmount const& asset2, ter const& ter, bool log, bool close) : AMM(env, account, asset1, asset2, log, 0, 0, std::nullopt, std::nullopt, std::nullopt, ter, close) { } AMM::AMM( Env& env, Account const& account, STAmount const& asset1, STAmount const& asset2, CreateArg const& arg) : AMM(env, account, asset1, asset2, arg.log, arg.tfee, arg.fee, arg.flags, arg.seq, arg.ms, arg.err, arg.close) { } [[nodiscard]] AccountID AMM::create( std::uint32_t tfee, std::optional const& flags, std::optional const& seq, std::optional 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[sfFee] = 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 const& account, std::optional const& ledgerIndex, std::optional issue1, std::optional issue2, std::optional const& ammAccount, bool ignoreParams, unsigned apiVersion) const { Json::Value jv; if (account) jv[jss::account] = to_string(*account); if (ledgerIndex) jv[jss::ledger_index] = *ledgerIndex; if (!ignoreParams) { if (issue1 || issue2) { if (issue1) jv[jss::asset] = STIssue(sfAsset, *issue1).getJson(JsonOptions::none); if (issue2) jv[jss::asset2] = STIssue(sfAsset2, *issue2).getJson(JsonOptions::none); } else if (!ammAccount) { jv[jss::asset] = STIssue(sfAsset, asset1_.issue()).getJson(JsonOptions::none); jv[jss::asset2] = STIssue(sfAsset2, asset2_.issue()).getJson(JsonOptions::none); } if (ammAccount) jv[jss::amm_account] = to_string(*ammAccount); } auto jr = (apiVersion == RPC::apiInvalidVersion ? env_.rpc("json", "amm_info", to_string(jv)) : env_.rpc(apiVersion, "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 AMM::balances(Issue const& issue1, Issue const& issue2, std::optional 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 const& account) const { auto const [asset1Balance, asset2Balance, lptAMMBalance] = balances(asset1.issue(), asset2.issue(), account); return asset1 == asset1Balance && asset2 == asset2Balance && lptAMMBalance == STAmount{lpt, lptIssue_}; } IOUAmount AMM::getLPTokensBalance(std::optional 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 timeSlot, IOUAmount expectedPrice) const { return expectAuctionSlot([&](std::uint32_t slotFee, std::optional 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 const& authAccounts) const { return expectAuctionSlot( [&](std::uint32_t, std::optional, 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 const& account, std::optional const& ledger_index, std::optional const& ammAccount) const { auto const jv = ammRpcInfo(account, ledger_index, std::nullopt, std::nullopt, ammAccount); 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> 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 const& account, Json::Value& jv, std::optional> const& assets, std::optional const& seq, std::optional 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 const& account, LPToken tokens, std::optional const& asset1In, std::optional const& flags, std::optional const& ter) { return deposit( account, tokens, asset1In, std::nullopt, std::nullopt, flags, std::nullopt, std::nullopt, std::nullopt, ter); } IOUAmount AMM::deposit( std::optional const& account, STAmount const& asset1In, std::optional const& asset2In, std::optional const& maxEP, std::optional const& flags, std::optional const& ter) { assert(!(asset2In && maxEP)); return deposit( account, std::nullopt, asset1In, asset2In, maxEP, flags, std::nullopt, std::nullopt, std::nullopt, ter); } IOUAmount AMM::deposit( std::optional const& account, std::optional tokens, std::optional const& asset1In, std::optional const& asset2In, std::optional const& maxEP, std::optional const& flags, std::optional> const& assets, std::optional const& seq, std::optional const& tfee, std::optional 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]); if (tfee) jv[jss::TradingFee] = *tfee; 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) == 0u) { 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::deposit(DepositArg const& arg) { return deposit( arg.account, arg.tokens, arg.asset1In, arg.asset2In, arg.maxEP, arg.flags, arg.assets, arg.seq, arg.tfee, arg.err); } IOUAmount AMM::withdraw( std::optional const& account, Json::Value& jv, std::optional const& seq, std::optional> const& assets, std::optional 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 const& account, std::optional const& tokens, std::optional const& asset1Out, std::optional const& flags, std::optional const& ter) { return withdraw( account, tokens, asset1Out, std::nullopt, std::nullopt, flags, std::nullopt, std::nullopt, ter); } IOUAmount AMM::withdraw( std::optional const& account, STAmount const& asset1Out, std::optional const& asset2Out, std::optional const& maxEP, std::optional 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 const& account, std::optional const& tokens, std::optional const& asset1Out, std::optional const& asset2Out, std::optional const& maxEP, std::optional const& flags, std::optional> const& assets, std::optional const& seq, std::optional 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) == 0u) { 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); } IOUAmount AMM::withdraw(WithdrawArg const& arg) { return withdraw( arg.account, arg.tokens, arg.asset1Out, arg.asset2Out, arg.maxEP, arg.flags, arg.assets, arg.seq, arg.err); } void AMM::vote( std::optional const& account, std::uint32_t feeVal, std::optional const& flags, std::optional const& seq, std::optional> const& assets, std::optional 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::vote(VoteArg const& arg) { vote(arg.account, arg.tfee, arg.flags, arg.seq, arg.assets, arg.err); } Json::Value AMM::bid(BidArg const& arg) { if (auto const amm = env_.current()->read(keylet::amm(asset1_.issue(), asset2_.issue()))) { assert( !env_.current()->rules().enabled(fixInnerObjTemplate) || amm->isFieldPresent(sfAuctionSlot)); if (amm->isFieldPresent(sfAuctionSlot)) { auto const& auctionSlot = safe_downcast(amm->peekAtField(sfAuctionSlot)); lastPurchasePrice_ = auctionSlot[sfPrice].iou(); } } bidMin_ = std::nullopt; bidMax_ = std::nullopt; Json::Value jv; jv[jss::Account] = arg.account ? arg.account->human() : creatorAccount_.human(); setTokens(jv, arg.assets); auto getBid = [&](auto const& bid) { if (std::holds_alternative(bid)) { return STAmount{lptIssue_, std::get(bid)}; } if (std::holds_alternative(bid)) { return toSTAmount(std::get(bid), lptIssue_); } return std::get(bid); }; if (arg.bidMin) { STAmount const saTokens = getBid(*arg.bidMin); saTokens.setJson(jv[jss::BidMin]); bidMin_ = saTokens.iou(); } if (arg.bidMax) { STAmount const saTokens = getBid(*arg.bidMax); saTokens.setJson(jv[jss::BidMax]); bidMax_ = saTokens.iou(); } if (!arg.authAccounts.empty()) { Json::Value accounts(Json::arrayValue); for (auto const& account : arg.authAccounts) { Json::Value acct; Json::Value authAcct; acct[jss::Account] = account.human(); authAcct[jss::AuthAccount] = acct; accounts.append(authAcct); } jv[jss::AuthAccounts] = accounts; } if (arg.flags) jv[jss::Flags] = *arg.flags; jv[jss::TransactionType] = jss::AMMBid; if (fee_ != 0) jv[jss::Fee] = std::to_string(fee_); return jv; } void AMM::submit( Json::Value const& jv, std::optional const& seq, std::optional 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); } if (doClose_) env_.close(); } bool AMM::expectAuctionSlot(auto&& cb) const { if (auto const amm = env_.current()->read(keylet::amm(asset1_.issue(), asset2_.issue()))) { assert( !env_.current()->rules().enabled(fixInnerObjTemplate) || amm->isFieldPresent(sfAuctionSlot)); if (amm->isFieldPresent(sfAuctionSlot)) { auto const& auctionSlot = safe_downcast(amm->peekAtField(sfAuctionSlot)); if (auctionSlot.isFieldPresent(sfAccount)) { // This could fail in pre-fixInnerObjTemplate tests // if the submitted transactions recreate one of // the failure scenarios. Access as optional // to avoid the failure. auto const slotFee = auctionSlot[~sfDiscountedFee].value_or(0); auto const slotInterval = ammAuctionTimeSlot( env_.app().getTimeKeeper().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; } void AMM::ammDelete(AccountID const& deleter, std::optional const& ter) { Json::Value jv; jv[jss::Account] = to_string(deleter); setTokens(jv); jv[jss::TransactionType] = jss::AMMDelete; if (fee_ != 0) jv[jss::Fee] = std::to_string(fee_); submit(jv, std::nullopt, ter); } namespace amm { Json::Value trust(AccountID const& account, STAmount const& amount, std::uint32_t flags) { if (isXRP(amount)) Throw("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; return jv; } Json::Value ammClawback( Account const& issuer, Account const& holder, Issue const& asset, Issue const& asset2, std::optional const& amount) { Json::Value jv; jv[jss::TransactionType] = jss::AMMClawback; jv[jss::Account] = issuer.human(); jv[jss::Holder] = holder.human(); jv[jss::Asset] = to_json(asset); jv[jss::Asset2] = to_json(asset2); if (amount) jv[jss::Amount] = amount->getJson(JsonOptions::none); return jv; } } // namespace amm } // namespace jtx } // namespace test } // namespace xrpl