#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace xrpl::test::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 account, STAmount asset1, STAmount 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_(std::move(account)) , asset1_(std::move(asset1)) , asset2_(std::move(asset2)) , ammID_(keylet::amm(asset1_.asset(), asset2_.asset()).key) , log_(log) , doClose_(close) , lastPurchasePrice_(0) , msig_(std::move(ms)) , fee_(fee) , ammAccount_(create(tfee, flags, seq, ter)) , lptIssue_(xrpl::ammLPTIssue(asset1_.asset(), asset2_.asset(), 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) { } json::Value AMM::createJv( AccountID const& account, STAmount const& asset1, STAmount const& asset2, std::uint16_t const& tfee) { json::Value jv; jv[jss::Account] = to_string(account); jv[jss::Amount] = asset1.getJson(JsonOptions::Values::None); jv[jss::Amount2] = asset2.getJson(JsonOptions::Values::None); jv[jss::TradingFee] = tfee; jv[jss::TransactionType] = jss::AMMCreate; return jv; } [[nodiscard]] AccountID AMM::create( std::uint32_t tfee, std::optional const& flags, std::optional const& seq, std::optional const& ter) { json::Value jv = createJv(creatorAccount_, asset1_, asset2_, tfee); 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_.asset(), asset2_.asset()))) { return amm->getAccountID(sfAccount); } } return {}; } json::Value AMM::ammRpcInfo( std::optional const& account, std::optional const& ledgerIndex, std::optional const& asset1, std::optional const& asset2, std::optional const& ammAccount, bool ignoreParams, unsigned apiVersion) const { auto const toJson = [](AccountID const& a) { return json::Value{to_string(a)}; }; return ammRpcInfo( account.transform(toJson), ledgerIndex, asset1, asset2, ammAccount.transform(toJson), ignoreParams, apiVersion); } json::Value AMM::ammRpcInfo( std::optional const& account, std::optional const& ledgerIndex, std::optional const& asset1, std::optional const& asset2, std::optional const& ammAccount, bool ignoreParams, unsigned apiVersion) const { json::Value jv; if (account) jv[jss::account] = *account; if (ledgerIndex) jv[jss::ledger_index] = *ledgerIndex; if (!ignoreParams) { if (asset1 || asset2) { if (asset1) jv[jss::asset] = STIssue(sfAsset, *asset1).getJson(JsonOptions::Values::None); if (asset2) jv[jss::asset2] = STIssue(sfAsset2, *asset2).getJson(JsonOptions::Values::None); } else if (!ammAccount) { jv[jss::asset] = STIssue(sfAsset, asset1_.asset()).getJson(JsonOptions::Values::None); jv[jss::asset2] = STIssue(sfAsset2, asset2_.asset()).getJson(JsonOptions::Values::None); } if (ammAccount) jv[jss::amm_account] = *ammAccount; } auto jr = (apiVersion == RPC::kApiInvalidVersion ? 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::ValueType::Null; } std::tuple AMM::balances(Asset const& asset1, Asset const& asset2, std::optional const& account) const { if (auto const amm = env_.current()->read(keylet::amm(asset1_.asset(), asset2_.asset()))) { auto const ammAccountID = amm->getAccountID(sfAccount); auto const [asset1Balance, asset2Balance] = ammPoolHolds( *env_.current(), ammAccountID, asset1, asset2, FreezeHandling::IgnoreFreeze, AuthHandling::IgnoreAuth, 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.asset(), asset2.asset(), 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::ZeroIfFrozen, env_.journal) .iou(); } if (auto const amm = env_.current()->read(keylet::amm(asset1_.asset(), asset2_.asset()))) 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_.asset(), asset2_.asset()))) { 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) { return std::ranges::all_of(accounts, [&](auto const& account) { return std::ranges::find(authAccounts, account.getAccountID(sfAccount)) != authAccounts.end(); }); }); } bool AMM::expectTradingFee(std::uint16_t fee) const { auto const amm = env_.current()->read(keylet::amm(asset1_.asset(), asset2_.asset())); return amm && (*amm)[sfTradingFee] == fee; } bool AMM::ammExists() const { return env_.current()->read(keylet::account(ammAccount_)) != nullptr && env_.current()->read(keylet::amm(asset1_.asset(), asset2_.asset())) != nullptr; } bool AMM::expectAmmRpcInfo( STAmount const& asset1, STAmount const& asset2, IOUAmount const& balance, std::optional const& account, std::optional const& ledgerIndex, std::optional const& ammAccount) const { auto const jv = ammRpcInfo(account, ledgerIndex, 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.asset() != asset1.asset()) 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::Values::None); jv[jss::Asset2] = STIssue(sfAsset, assets->second).getJson(JsonOptions::Values::None); } else { jv[jss::Asset] = STIssue(sfAsset, asset1_.asset()).getJson(JsonOptions::Values::None); jv[jss::Asset2] = STIssue(sfAsset, asset2_.asset()).getJson(JsonOptions::Values::None); } } json::Value AMM::depositJv(DepositArg const& arg) { json::Value jv; if (!arg.account || !arg.assets) Throw("AMM::depositJv: account or assets not set"); jv[jss::Account] = arg.account->human(); jv[jss::Asset] = STIssue(sfAsset, arg.assets->first).getJson(JsonOptions::Values::None); jv[jss::Asset2] = STIssue(sfAsset, arg.assets->second).getJson(JsonOptions::Values::None); if (arg.tokens) arg.tokens->tokens().setJson(jv[jss::LPTokenOut]); if (arg.asset1In) arg.asset1In->setJson(jv[jss::Amount]); if (arg.asset2In) arg.asset2In->setJson(jv[jss::Amount2]); if (arg.maxEP) arg.maxEP->setJson(jv[jss::EPrice]); if (arg.tfee) jv[jss::TradingFee] = *arg.tfee; std::uint32_t flags = 0; if (arg.flags) flags = *arg.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 ((flags & tfDepositSubTx) == 0u) { if (arg.tokens && !arg.asset1In) { flags |= tfLPToken; } else if (arg.tokens && arg.asset1In) { flags |= tfOneAssetLPToken; } else if (arg.asset1In && arg.asset2In) { flags |= tfTwoAsset; } else if (arg.maxEP && arg.asset1In) { flags |= tfLimitLPToken; } else if (arg.asset1In) { flags |= tfSingleAsset; } } jv[jss::Flags] = flags; jv[jss::TransactionType] = jss::AMMDeposit; return jv; } 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) { if (asset2In && maxEP) Throw("Invalid options: asset2In and 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) { auto const acct = account ? account : creatorAccount_; auto const lpTokens = getLPTokensBalance(acct); json::Value jv = depositJv( {.account = acct, .tokens = tokens ? tokens->tokens(lptIssue_) : tokens, .asset1In = asset1In, .asset2In = asset2In, .maxEP = maxEP, .flags = flags, .assets = assets ? assets : std::make_pair(asset1_.asset(), asset2_.asset()), .tfee = tfee}); if (fee_ != 0) jv[jss::Fee] = std::to_string(fee_); submit(jv, seq, ter); return getLPTokensBalance(acct) - lpTokens; } 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); } json::Value AMM::withdrawJv(WithdrawArg const& arg) { json::Value jv; if (!arg.account || !arg.assets) Throw("AMM::withdrawJv: account or assets not set"); jv[jss::Account] = arg.account->human(); jv[jss::Asset] = STIssue(sfAsset, arg.assets->first).getJson(JsonOptions::Values::None); jv[jss::Asset2] = STIssue(sfAsset, arg.assets->second).getJson(JsonOptions::Values::None); if (arg.tokens) arg.tokens->tokens().setJson(jv[jss::LPTokenIn]); if (arg.asset1Out) arg.asset1Out->setJson(jv[jss::Amount]); if (arg.asset2Out) arg.asset2Out->setJson(jv[jss::Amount2]); if (arg.maxEP) arg.maxEP->tokens().setJson(jv[jss::EPrice]); std::uint32_t flags = 0; if (arg.flags) flags = *arg.flags; if ((flags & tfWithdrawSubTx) == 0u) { if (arg.tokens && !arg.asset1Out) { flags |= tfLPToken; } else if (arg.asset1Out && arg.asset2Out) { flags |= tfTwoAsset; } else if (arg.tokens && arg.asset1Out) { flags |= tfOneAssetLPToken; } else if (arg.asset1Out && arg.maxEP) { flags |= tfLimitLPToken; } else if (arg.asset1Out) { flags |= tfSingleAsset; } } jv[jss::Flags] = flags; jv[jss::TransactionType] = jss::AMMWithdraw; return jv; } 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) { if (asset2Out && maxEP) Throw("Invalid options: asset2Out and 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) { auto const acct = account ? account : creatorAccount_; auto const lpTokens = getLPTokensBalance(acct); json::Value jv = withdrawJv({ .account = acct, .tokens = tokens ? tokens->tokens(lptIssue_) : tokens, .asset1Out = asset1Out, .asset2Out = asset2Out, .maxEP = maxEP ? maxEP->tokens(lptIssue_) : maxEP, .flags = flags, .assets = assets ? assets : std::make_pair(asset1_.asset(), asset2_.asset()), }); if (fee_ != 0) jv[jss::Fee] = std::to_string(fee_); submit(jv, seq, ter); return lpTokens - getLPTokensBalance(acct); } 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); } json::Value AMM::voteJv(VoteArg const& arg) { json::Value jv; if (!arg.account || !arg.assets) Throw("AMM::withdrawJv: account or assets not set"); jv[jss::Account] = arg.account->human(); jv[jss::Asset] = STIssue(sfAsset, arg.assets->first).getJson(JsonOptions::Values::None); jv[jss::Asset2] = STIssue(sfAsset, arg.assets->second).getJson(JsonOptions::Values::None); jv[jss::TradingFee] = arg.tfee; if (arg.flags) jv[jss::Flags] = *arg.flags; jv[jss::TransactionType] = jss::AMMVote; return jv; } 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 = voteJv({ .account = account ? account : creatorAccount_, .tfee = feeVal, .flags = flags, .assets = assets ? assets : std::make_pair(asset1_.asset(), asset2_.asset()), }); 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_.asset(), asset2_.asset()))) { if (!amm->isFieldPresent(sfAuctionSlot)) Throw("AMM::Bid"); if (amm->isFieldPresent(sfAuctionSlot)) { auto const& auctionSlot = safeDowncast(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::ValueType::Array); 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::clawback(ClawbackArg const& arg) { auto const& [asset, asset2] = [&]() { if (arg.assets) return *arg.assets; return std::make_pair(asset1_.asset(), asset2_.asset()); }(); auto jv = amm::ammClawback(arg.issuer, arg.holder, asset, asset2, arg.amount); if (arg.flags) jv[jss::Flags] = *arg.flags; if (fee_ != 0) jv[jss::Fee] = std::to_string(fee_); submit(jv, std::nullopt, arg.err); } 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_.asset(), asset2_.asset()))) { if (!amm->isFieldPresent(sfAuctionSlot)) Throw("AMM::expectAuctionSlot"); if (amm->isFieldPresent(sfAuctionSlot)) { auto const& auctionSlot = safeDowncast(amm->peekAtField(sfAuctionSlot)); if (auctionSlot.isFieldPresent(sfAccount)) { 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; } json::Value AMM::deleteJv(AccountID const& account, Asset const& asset1, Asset const& asset2) { json::Value jv; jv[jss::Account] = to_string(account); jv[jss::Asset] = STIssue(sfAsset, asset1).getJson(JsonOptions::Values::None); jv[jss::Asset2] = STIssue(sfAsset, asset2).getJson(JsonOptions::Values::None); jv[jss::TransactionType] = jss::AMMDelete; return jv; } void AMM::ammDelete(AccountID const& account, std::optional const& ter) { json::Value jv = deleteJv(account, asset1_.asset(), asset2_.asset()); if (fee_ != 0) jv[jss::Fee] = std::to_string(fee_); submit(jv, std::nullopt, ter); } namespace amm { json::Value ammClawback( Account const& issuer, Account const& holder, Asset const& asset, Asset 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] = toJson(asset); jv[jss::Asset2] = toJson(asset2); if (amount) jv[jss::Amount] = amount->getJson(JsonOptions::Values::None); return jv; } } // namespace amm } // namespace xrpl::test::jtx