test: Add tests to raise coverage of AMM (#4971)

---------

Co-authored-by: Howard Hinnant <howard.hinnant@gmail.com>
Co-authored-by: Mark Travis <mtravis@ripple.com>
Co-authored-by: Bronek Kozicki <brok@incorrekt.com>
Co-authored-by: Mayukha Vadari <mvadari@gmail.com>
Co-authored-by: Chenna Keshava <ckeshavabs@gmail.com>
This commit is contained in:
John Freeman
2024-04-18 14:25:22 -05:00
committed by tequ
parent 632c2982a4
commit 415ef13480
15 changed files with 582 additions and 287 deletions

View File

@@ -60,9 +60,12 @@ ammHolds(
*optIssue2,
std::make_optional(std::make_pair(issue1, issue2))))
{
// This error can only be hit if the AMM is corrupted
// LCOV_EXCL_START
JLOG(j.debug()) << "ammHolds: Invalid optIssue1 or optIssue2 "
<< *optIssue1 << " " << *optIssue2;
return std::nullopt;
// LCOV_EXCL_STOP
}
return std::make_optional(std::make_pair(*optIssue1, *optIssue2));
}
@@ -74,9 +77,12 @@ ammHolds(
return std::make_optional(std::make_pair(issue1, issue2));
else if (checkIssue == issue2)
return std::make_optional(std::make_pair(issue2, issue1));
// Unreachable unless AMM corrupted.
// LCOV_EXCL_START
JLOG(j.debug())
<< "ammHolds: Invalid " << label << " " << checkIssue;
return std::nullopt;
// LCOV_EXCL_STOP
};
if (optIssue1)
{
@@ -84,7 +90,8 @@ ammHolds(
}
else if (optIssue2)
{
return singleIssue(*optIssue2, "optIssue2");
// Cannot have Amount2 without Amount.
return singleIssue(*optIssue2, "optIssue2"); // LCOV_EXCL_LINE
}
return std::make_optional(std::make_pair(issue1, issue2));
}();
@@ -210,19 +217,23 @@ deleteAMMTrustLines(
// Should only have the trustlines
if (nodeType != LedgerEntryType::ltRIPPLE_STATE)
{
// LCOV_EXCL_START
JLOG(j.error())
<< "deleteAMMTrustLines: deleting non-trustline "
<< nodeType;
return {tecINTERNAL, SkipEntry::No};
// LCOV_EXCL_STOP
}
// Trustlines must have zero balance
if (sleItem->getFieldAmount(sfBalance) != beast::zero)
{
// LCOV_EXCL_START
JLOG(j.error())
<< "deleteAMMTrustLines: deleting trustline with "
"non-zero balance.";
return {tecINTERNAL, SkipEntry::No};
// LCOV_EXCL_STOP
}
return {
@@ -243,18 +254,22 @@ deleteAMMAccount(
auto ammSle = sb.peek(keylet::amm(asset, asset2));
if (!ammSle)
{
// LCOV_EXCL_START
JLOG(j.error()) << "deleteAMMAccount: AMM object does not exist "
<< asset << " " << asset2;
return tecINTERNAL;
// LCOV_EXCL_STOP
}
auto const ammAccountID = (*ammSle)[sfAccount];
auto sleAMMRoot = sb.peek(keylet::account(ammAccountID));
if (!sleAMMRoot)
{
// LCOV_EXCL_START
JLOG(j.error()) << "deleteAMMAccount: AMM account does not exist "
<< to_string(ammAccountID);
return tecINTERNAL;
// LCOV_EXCL_STOP
}
if (auto const ter =
@@ -266,14 +281,18 @@ deleteAMMAccount(
if (!sb.dirRemove(
ownerDirKeylet, (*ammSle)[sfOwnerNode], ammSle->key(), false))
{
// LCOV_EXCL_START
JLOG(j.error()) << "deleteAMMAccount: failed to remove dir link";
return tecINTERNAL;
// LCOV_EXCL_STOP
}
if (sb.exists(ownerDirKeylet) && !sb.emptyDirDelete(ownerDirKeylet))
{
// LCOV_EXCL_START
JLOG(j.error()) << "deleteAMMAccount: cannot delete root dir node of "
<< toBase58(ammAccountID);
return tecINTERNAL;
// LCOV_EXCL_STOP
}
sb.erase(ammSle);
@@ -322,11 +341,11 @@ initializeFeeAuctionVote(
if (tfee != 0)
ammSle->setFieldU16(sfTradingFee, tfee);
else if (ammSle->isFieldPresent(sfTradingFee))
ammSle->makeFieldAbsent(sfTradingFee);
ammSle->makeFieldAbsent(sfTradingFee); // LCOV_EXCL_LINE
if (auto const dfee = tfee / AUCTION_SLOT_DISCOUNTED_FEE_FRACTION)
auctionSlot.setFieldU16(sfDiscountedFee, dfee);
else if (auctionSlot.isFieldPresent(sfDiscountedFee))
auctionSlot.makeFieldAbsent(sfDiscountedFee);
auctionSlot.makeFieldAbsent(sfDiscountedFee); // LCOV_EXCL_LINE
}
} // namespace ripple

View File

@@ -74,9 +74,6 @@ public:
Issue const&
issueIn() const;
Issue const&
issueOut() const;
AccountID const&
owner() const;

View File

@@ -44,13 +44,6 @@ AMMOffer<TIn, TOut>::issueIn() const
return ammLiquidity_.issueIn();
}
template <typename TIn, typename TOut>
Issue const&
AMMOffer<TIn, TOut>::issueOut() const
{
return ammLiquidity_.issueOut();
}
template <typename TIn, typename TOut>
AccountID const&
AMMOffer<TIn, TOut>::owner() const

View File

@@ -186,7 +186,7 @@ AMMDeposit::preclaim(PreclaimContext const& ctx)
FreezeHandling::fhIGNORE_FREEZE,
ctx.j);
if (!expected)
return expected.error();
return expected.error(); // LCOV_EXCL_LINE
auto const [amountBalance, amount2Balance, lptAMMBalance] = *expected;
if (ctx.tx.getFlags() & tfTwoAssetIfEmpty)
{
@@ -194,8 +194,10 @@ AMMDeposit::preclaim(PreclaimContext const& ctx)
return tecAMM_NOT_EMPTY;
if (amountBalance != beast::zero || amount2Balance != beast::zero)
{
// LCOV_EXCL_START
JLOG(ctx.j.debug()) << "AMM Deposit: tokens balance is not zero.";
return tecINTERNAL;
// LCOV_EXCL_STOP
}
}
else
@@ -205,9 +207,11 @@ AMMDeposit::preclaim(PreclaimContext const& ctx)
if (amountBalance <= beast::zero || amount2Balance <= beast::zero ||
lptAMMBalance < beast::zero)
{
// LCOV_EXCL_START
JLOG(ctx.j.debug())
<< "AMM Deposit: reserves or tokens balance is zero.";
return tecINTERNAL;
// LCOV_EXCL_STOP
}
}
@@ -254,10 +258,12 @@ AMMDeposit::preclaim(PreclaimContext const& ctx)
if (auto const ter =
requireAuth(ctx.view, amount->issue(), accountID))
{
// LCOV_EXCL_START
JLOG(ctx.j.debug())
<< "AMM Deposit: account is not authorized, "
<< amount->issue();
return ter;
// LCOV_EXCL_STOP
}
// AMM account or currency frozen
if (isFrozen(ctx.view, ammAccountID, amount->issue()))
@@ -339,7 +345,7 @@ AMMDeposit::applyGuts(Sandbox& sb)
auto const lpTokensDeposit = ctx_.tx[~sfLPTokenOut];
auto ammSle = sb.peek(keylet::amm(ctx_.tx[sfAsset], ctx_.tx[sfAsset2]));
if (!ammSle)
return {tecINTERNAL, false};
return {tecINTERNAL, false}; // LCOV_EXCL_LINE
auto const ammAccountID = (*ammSle)[sfAccount];
auto const expected = ammHolds(
@@ -350,7 +356,7 @@ AMMDeposit::applyGuts(Sandbox& sb)
FreezeHandling::fhZERO_IF_FROZEN,
ctx_.journal);
if (!expected)
return {expected.error(), false};
return {expected.error(), false}; // LCOV_EXCL_LINE
auto const [amountBalance, amount2Balance, lptAMMBalance] = *expected;
auto const tfee = (lptAMMBalance == beast::zero)
? ctx_.tx[~sfTradingFee].value_or(0)
@@ -421,8 +427,10 @@ AMMDeposit::applyGuts(Sandbox& sb)
lptAMMBalance.issue(),
tfee);
// should not happen.
// LCOV_EXCL_START
JLOG(j_.error()) << "AMM Deposit: invalid options.";
return std::make_pair(tecINTERNAL, STAmount{});
// LCOV_EXCL_STOP
}();
if (result == tesSUCCESS)
@@ -621,10 +629,12 @@ AMMDeposit::equalDepositTokens(
}
catch (std::exception const& e)
{
// LCOV_EXCL_START
JLOG(j_.error()) << "AMMDeposit::equalDepositTokens exception "
<< e.what();
return {tecINTERNAL, STAmount{}};
// LCOV_EXCL_STOP
}
return {tecINTERNAL, STAmount{}};
}
/** Proportional deposit of pool assets with the constraints on the maximum

View File

@@ -201,7 +201,7 @@ AMMWithdraw::preclaim(PreclaimContext const& ctx)
{
JLOG(ctx.j.debug())
<< "AMM Withdraw: reserves or tokens balance is zero.";
return tecINTERNAL;
return tecINTERNAL; // LCOV_EXCL_LINE
}
auto const ammAccountID = ammSle->getAccountID(sfAccount);
@@ -300,11 +300,11 @@ AMMWithdraw::applyGuts(Sandbox& sb)
auto const ePrice = ctx_.tx[~sfEPrice];
auto ammSle = sb.peek(keylet::amm(ctx_.tx[sfAsset], ctx_.tx[sfAsset2]));
if (!ammSle)
return {tecINTERNAL, false};
return {tecINTERNAL, false}; // LCOV_EXCL_LINE
auto const ammAccountID = (*ammSle)[sfAccount];
auto const accountSle = sb.read(keylet::account(ammAccountID));
if (!accountSle)
return {tecINTERNAL, false};
return {tecINTERNAL, false}; // LCOV_EXCL_LINE
auto const lpTokens =
ammLPHolds(ctx_.view(), *ammSle, ctx_.tx[sfAccount], ctx_.journal);
auto const lpTokensWithdraw =
@@ -372,8 +372,10 @@ AMMWithdraw::applyGuts(Sandbox& sb)
*lpTokensWithdraw,
tfee);
// should not happen.
// LCOV_EXCL_START
JLOG(j_.error()) << "AMM Withdraw: invalid options.";
return std::make_pair(tecINTERNAL, STAmount{});
// LCOV_EXCL_STOP
}();
if (result != tesSUCCESS)
@@ -431,7 +433,7 @@ AMMWithdraw::withdraw(
auto const ammSle =
ctx_.view().read(keylet::amm(ctx_.tx[sfAsset], ctx_.tx[sfAsset2]));
if (!ammSle)
return {tecINTERNAL, STAmount{}};
return {tecINTERNAL, STAmount{}}; // LCOV_EXCL_LINE
auto const lpTokens = ammLPHolds(view, *ammSle, account_, ctx_.journal);
auto const expected = ammHolds(
view,
@@ -612,12 +614,14 @@ AMMWithdraw::equalWithdrawTokens(
lpTokensWithdraw,
tfee);
}
// LCOV_EXCL_START
catch (std::exception const& e)
{
JLOG(j_.error()) << "AMMWithdraw::equalWithdrawTokens exception "
<< e.what();
}
return {tecINTERNAL, STAmount{}};
// LCOV_EXCL_STOP
}
/** All assets withdrawal with the constraints on the maximum amount

View File

@@ -56,9 +56,6 @@ public:
SerializedTypeID
getSType() const override;
std::string
getText() const override;
Json::Value getJson(JsonOptions) const override;
void
@@ -71,9 +68,6 @@ public:
isDefault() const override;
private:
static std::unique_ptr<STIssue>
construct(SerialIter&, SField const& name);
STBase*
copy(std::size_t n, void* buf) const override;
STBase*

View File

@@ -65,12 +65,6 @@ STIssue::getSType() const
return STI_ISSUE;
}
std::string
STIssue::getText() const
{
return issue_.getText();
}
Json::Value STIssue::getJson(JsonOptions) const
{
return to_json(issue_);
@@ -97,12 +91,6 @@ STIssue::isDefault() const
return issue_ == xrpIssue();
}
std::unique_ptr<STIssue>
STIssue::construct(SerialIter& sit, SField const& name)
{
return std::make_unique<STIssue>(sit, name);
}
STBase*
STIssue::copy(std::size_t n, void* buf) const
{

View File

@@ -42,6 +42,9 @@
namespace ripple {
namespace test {
/**
* Tests of AMM that use offers too.
*/
struct AMMExtended_test : public jtx::AMMTest
{
private:
@@ -3622,6 +3625,8 @@ private:
int const signerListOwners{features[featureMultiSignReserve] ? 2 : 5};
env.require(owners(alice, signerListOwners + 0));
const msig ms{becky, bogie};
// Multisign all AMM transactions
AMM ammAlice(
env,
@@ -3633,7 +3638,7 @@ private:
ammCrtFee(env).drops(),
std::nullopt,
std::nullopt,
msig(becky, bogie),
ms,
ter(tesSUCCESS));
BEAST_EXPECT(ammAlice.expectBalances(
XRP(10'000), USD(10'000), ammAlice.tokens()));
@@ -3649,7 +3654,8 @@ private:
ammAlice.vote({}, 1'000);
BEAST_EXPECT(ammAlice.expectTradingFee(1'000));
ammAlice.bid(alice, 100);
env(ammAlice.bid({.account = alice, .bidMin = 100}), ms);
env.close();
BEAST_EXPECT(ammAlice.expectAuctionSlot(100, 0, IOUAmount{4'000}));
// 4000 tokens burnt
BEAST_EXPECT(ammAlice.expectBalances(

View File

@@ -19,6 +19,7 @@
#include <ripple/app/misc/AMMHelpers.h>
#include <ripple/app/paths/AMMContext.h>
#include <ripple/app/paths/AMMOffer.h>
#include <ripple/app/tx/impl/AMMBid.h>
#include <ripple/protocol/AMMCore.h>
#include <ripple/protocol/STParsedJSON.h>
#include <ripple/resource/Fees.h>
@@ -37,6 +38,10 @@
namespace ripple {
namespace test {
/**
* Basic tests of AMM that do not use offers.
* Tests incorporating offers are in `AMMExtended_test`.
*/
struct AMM_test : public jtx::AMMTest
{
private:
@@ -81,10 +86,8 @@ private:
env.fund(XRP(30'000), gw, alice);
env.close();
env(fset(gw, asfRequireAuth));
env.close();
env.trust(USD(30'000), alice);
env.close();
env(trust(gw, alice["USD"](30'000)), txflags(tfSetfAuth));
env(trust(alice, gw["USD"](30'000), 0));
env(trust(gw, alice["USD"](0), tfSetfAuth));
env.close();
env(pay(gw, alice, USD(10'000)));
env.close();
@@ -117,6 +120,14 @@ private:
},
std::nullopt,
1'000);
// Make sure asset comparison works.
BEAST_EXPECT(
STIssue(sfAsset, STAmount(XRP(2'000)).issue()) ==
STIssue(sfAsset, STAmount(XRP(2'000)).issue()));
BEAST_EXPECT(
STIssue(sfAsset, STAmount(XRP(2'000)).issue()) !=
STIssue(sfAsset, STAmount({USD(2'000)}).issue()));
}
void
@@ -599,7 +610,12 @@ private:
USD(100),
STAmount{USD, 1, -1},
std::nullopt},
};
{tfTwoAssetIfEmpty | tfLPToken,
std::nullopt,
XRP(100),
USD(100),
STAmount{USD, 1, -1},
std::nullopt}};
for (auto const& it : invalidOptions)
{
ammAlice.deposit(
@@ -615,6 +631,19 @@ private:
ter(temMALFORMED));
}
{
// bad preflight1
Json::Value jv = Json::objectValue;
jv[jss::Account] = alice.human();
jv[jss::TransactionType] = jss::AMMDeposit;
jv[jss::Asset] =
STIssue(sfAsset, XRP).getJson(JsonOptions::none);
jv[jss::Asset2] =
STIssue(sfAsset, USD).getJson(JsonOptions::none);
jv[jss::Fee] = "-1";
env(jv, ter(temBAD_FEE));
}
// Invalid tokens
ammAlice.deposit(
alice, 0, std::nullopt, std::nullopt, ter(temBAD_AMM_TOKENS));
@@ -625,6 +654,33 @@ private:
std::nullopt,
ter(temBAD_AMM_TOKENS));
{
Json::Value jv = Json::objectValue;
jv[jss::Account] = alice.human();
jv[jss::TransactionType] = jss::AMMDeposit;
jv[jss::Asset] =
STIssue(sfAsset, XRP).getJson(JsonOptions::none);
jv[jss::Asset2] =
STIssue(sfAsset, USD).getJson(JsonOptions::none);
jv[jss::LPTokenOut] =
USD(100).value().getJson(JsonOptions::none);
jv[jss::Flags] = tfLPToken;
env(jv, ter(temBAD_AMM_TOKENS));
}
// Invalid trading fee
ammAlice.deposit(
carol,
std::nullopt,
XRP(200),
USD(200),
std::nullopt,
tfTwoAssetIfEmpty,
std::nullopt,
std::nullopt,
10'000,
ter(temBAD_FEE));
// Invalid tokens - bogus currency
{
auto const iss1 = Issue{Currency(0xabc), gw.id()};
@@ -821,6 +877,13 @@ private:
ter(tecFROZEN));
ammAlice.deposit(
carol, 1'000'000, std::nullopt, std::nullopt, ter(tecFROZEN));
ammAlice.deposit(
carol,
XRP(100),
USD(100),
std::nullopt,
std::nullopt,
ter(tecFROZEN));
});
// Individually frozen (AMM) account
@@ -858,6 +921,50 @@ private:
ter(tecFROZEN));
});
// Individually frozen (AMM) account with IOU/IOU AMM
testAMM(
[&](AMM& ammAlice, Env& env) {
env(trust(gw, carol["USD"](0), tfSetFreeze));
env(trust(gw, carol["BTC"](0), tfSetFreeze));
env.close();
ammAlice.deposit(
carol,
1'000'000,
std::nullopt,
std::nullopt,
ter(tecFROZEN));
ammAlice.deposit(
carol,
USD(100),
std::nullopt,
std::nullopt,
std::nullopt,
ter(tecFROZEN));
env(trust(gw, carol["USD"](0), tfClearFreeze));
// Individually frozen AMM
env(trust(
gw,
STAmount{
Issue{gw["USD"].currency, ammAlice.ammAccount()}, 0},
tfSetFreeze));
env.close();
// Cannot deposit non-frozen token
ammAlice.deposit(
carol,
1'000'000,
std::nullopt,
std::nullopt,
ter(tecFROZEN));
ammAlice.deposit(
carol,
USD(100),
BTC(0.01),
std::nullopt,
std::nullopt,
ter(tecFROZEN));
},
{{USD(20'000), BTC(0.5)}});
// Insufficient XRP balance
testAMM([&](AMM& ammAlice, Env& env) {
env.fund(XRP(1'000), bob);
@@ -947,6 +1054,15 @@ private:
std::nullopt,
std::nullopt,
ter(tecINSUF_RESERVE_LINE));
env(offer(carol, XRP(100), USD(103)));
ammAlice.deposit(
carol,
USD(100),
std::nullopt,
std::nullopt,
std::nullopt,
ter(tecINSUF_RESERVE_LINE));
}
// Insufficient reserve, IOU/IOU
@@ -1359,6 +1475,48 @@ private:
using namespace jtx;
testAMM(
[&](AMM& ammAlice, Env& env) {
WithdrawArg args{
.asset1Out = XRP(100),
.err = ter(tecAMM_BALANCE),
};
ammAlice.withdraw(args);
},
{{XRP(99), USD(99)}});
testAMM(
[&](AMM& ammAlice, Env& env) {
WithdrawArg args{
.asset1Out = USD(100),
.err = ter(tecAMM_BALANCE),
};
ammAlice.withdraw(args);
},
{{XRP(99), USD(99)}});
{
Env env{*this};
env.fund(XRP(30'000), gw, alice, bob);
env.close();
env(fset(gw, asfRequireAuth));
env(trust(alice, gw["USD"](30'000), 0));
env(trust(gw, alice["USD"](0), tfSetfAuth));
// Bob trusts Gateway to owe him USD...
env(trust(bob, gw["USD"](30'000), 0));
// ...but Gateway does not authorize Bob to hold its USD.
env.close();
env(pay(gw, alice, USD(10'000)));
env.close();
AMM ammAlice(env, alice, XRP(10'000), USD(10'000));
WithdrawArg args{
.account = bob,
.asset1Out = USD(100),
.err = ter(tecNO_AUTH),
};
ammAlice.withdraw(args);
}
testAMM([&](AMM& ammAlice, Env& env) {
// Invalid flags
ammAlice.withdraw(
@@ -2294,142 +2452,156 @@ private:
using namespace jtx;
using namespace std::chrono;
// burn all the LPTokens through a AMMBid transaction
{
Env env(*this);
fund(env, gw, {alice}, XRP(2'000), {USD(2'000)});
AMM amm(env, gw, XRP(1'000), USD(1'000), false, 1'000);
// auction slot is owned by the creator of the AMM i.e. gw
BEAST_EXPECT(amm.expectAuctionSlot(100, 0, IOUAmount{0}));
// gw attempts to burn all her LPTokens through a bid transaction
// this transaction fails because AMMBid transaction can not burn
// all the outstanding LPTokens
env(amm.bid({
.account = gw,
.bidMin = 1'000'000,
}),
ter(tecAMM_INVALID_TOKENS));
}
// burn all the LPTokens through a AMMBid transaction
{
Env env(*this);
fund(env, gw, {alice}, XRP(2'000), {USD(2'000)});
AMM amm(env, gw, XRP(1'000), USD(1'000), false, 1'000);
// auction slot is owned by the creator of the AMM i.e. gw
BEAST_EXPECT(amm.expectAuctionSlot(100, 0, IOUAmount{0}));
// gw burns all but one of its LPTokens through a bid transaction
// this transaction suceeds because the bid price is less than
// the total outstanding LPToken balance
env(amm.bid({
.account = gw,
.bidMin = STAmount{amm.lptIssue(), UINT64_C(999'999)},
}),
ter(tesSUCCESS));
env.close();
// gw must own the auction slot
BEAST_EXPECT(amm.expectAuctionSlot(100, 0, IOUAmount{999'999}));
// 999'999 tokens are burned, only 1 LPToken is owned by gw
BEAST_EXPECT(
amm.expectBalances(XRP(1'000), USD(1'000), IOUAmount{1}));
// gw owns only 1 LPToken in its balance
BEAST_EXPECT(Number{amm.getLPTokensBalance(gw)} == 1);
// gw attempts to burn the last of its LPTokens in an AMMBid
// transaction. This transaction fails because it would burn all
// the remaining LPTokens
env(amm.bid({
.account = gw,
.bidMin = 1,
}),
ter(tecAMM_INVALID_TOKENS));
}
testAMM([&](AMM& ammAlice, Env& env) {
// Invalid flags
ammAlice.bid(
carol,
0,
std::nullopt,
{},
tfWithdrawAll,
std::nullopt,
std::nullopt,
env(ammAlice.bid({
.account = carol,
.bidMin = 0,
.flags = tfWithdrawAll,
}),
ter(temINVALID_FLAG));
ammAlice.deposit(carol, 1'000'000);
// Invalid Bid price <= 0
for (auto bid : {0, -100})
{
ammAlice.bid(
carol,
bid,
std::nullopt,
{},
std::nullopt,
std::nullopt,
std::nullopt,
env(ammAlice.bid({
.account = carol,
.bidMin = bid,
}),
ter(temBAD_AMOUNT));
ammAlice.bid(
carol,
std::nullopt,
bid,
{},
std::nullopt,
std::nullopt,
std::nullopt,
env(ammAlice.bid({
.account = carol,
.bidMax = bid,
}),
ter(temBAD_AMOUNT));
}
// Invlaid Min/Max combination
ammAlice.bid(
carol,
200,
100,
{},
std::nullopt,
std::nullopt,
std::nullopt,
env(ammAlice.bid({
.account = carol,
.bidMin = 200,
.bidMax = 100,
}),
ter(tecAMM_INVALID_TOKENS));
// Invalid Account
Account bad("bad");
env.memoize(bad);
ammAlice.bid(
bad,
std::nullopt,
100,
{},
std::nullopt,
env(ammAlice.bid({
.account = bad,
.bidMax = 100,
}),
seq(1),
std::nullopt,
ter(terNO_ACCOUNT));
// Account is not LP
Account const dan("dan");
env.fund(XRP(1'000), dan);
ammAlice.bid(
dan,
100,
std::nullopt,
{},
std::nullopt,
std::nullopt,
std::nullopt,
env(ammAlice.bid({
.account = dan,
.bidMin = 100,
}),
ter(tecAMM_INVALID_TOKENS));
ammAlice.bid(
dan,
std::nullopt,
std::nullopt,
{},
std::nullopt,
std::nullopt,
std::nullopt,
env(ammAlice.bid({
.account = dan,
}),
ter(tecAMM_INVALID_TOKENS));
// Auth account is invalid.
ammAlice.bid(
carol,
100,
std::nullopt,
{bob},
std::nullopt,
std::nullopt,
std::nullopt,
env(ammAlice.bid({
.account = carol,
.bidMin = 100,
.authAccounts = {bob},
}),
ter(terNO_ACCOUNT));
// Invalid Assets
ammAlice.bid(
alice,
std::nullopt,
100,
{},
std::nullopt,
std::nullopt,
{{USD, GBP}},
env(ammAlice.bid({
.account = alice,
.bidMax = 100,
.assets = {{USD, GBP}},
}),
ter(terNO_AMM));
// Invalid Min/Max issue
ammAlice.bid(
alice,
std::nullopt,
STAmount{USD, 100},
{},
std::nullopt,
std::nullopt,
std::nullopt,
env(ammAlice.bid({
.account = alice,
.bidMax = STAmount{USD, 100},
}),
ter(temBAD_AMM_TOKENS));
ammAlice.bid(
alice,
STAmount{USD, 100},
std::nullopt,
{},
std::nullopt,
std::nullopt,
std::nullopt,
env(ammAlice.bid({
.account = alice,
.bidMin = STAmount{USD, 100},
}),
ter(temBAD_AMM_TOKENS));
});
// Invalid AMM
testAMM([&](AMM& ammAlice, Env& env) {
ammAlice.withdrawAll(alice);
ammAlice.bid(
alice,
std::nullopt,
100,
{},
std::nullopt,
std::nullopt,
std::nullopt,
env(ammAlice.bid({
.account = alice,
.bidMax = 100,
}),
ter(terNO_AMM));
});
@@ -2442,14 +2614,11 @@ private:
env.fund(XRP(1'000), bob, ed, bill, scott, james);
env.close();
ammAlice.deposit(carol, 1'000'000);
ammAlice.bid(
carol,
100,
std::nullopt,
{bob, ed, bill, scott, james},
std::nullopt,
std::nullopt,
std::nullopt,
env(ammAlice.bid({
.account = carol,
.bidMin = 100,
.authAccounts = {bob, ed, bill, scott, james},
}),
ter(temMALFORMED));
});
@@ -2458,35 +2627,25 @@ private:
fund(env, gw, {bob}, XRP(1'000), {USD(100)}, Fund::Acct);
ammAlice.deposit(carol, 1'000'000);
ammAlice.deposit(bob, 10);
ammAlice.bid(
carol,
1'000'001,
std::nullopt,
{},
std::nullopt,
std::nullopt,
std::nullopt,
env(ammAlice.bid({
.account = carol,
.bidMin = 1'000'001,
}),
ter(tecAMM_INVALID_TOKENS));
ammAlice.bid(
carol,
std::nullopt,
1'000'001,
{},
std::nullopt,
std::nullopt,
std::nullopt,
env(ammAlice.bid({
.account = carol,
.bidMax = 1'000'001,
}),
ter(tecAMM_INVALID_TOKENS));
ammAlice.bid(carol, 1'000);
env(ammAlice.bid({
.account = carol,
.bidMin = 1'000,
}));
BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{1'000}));
// Slot purchase price is more than 1000 but bob only has 10 tokens
ammAlice.bid(
bob,
std::nullopt,
std::nullopt,
{},
std::nullopt,
std::nullopt,
std::nullopt,
env(ammAlice.bid({
.account = bob,
}),
ter(tecAMM_INVALID_TOKENS));
});
@@ -2500,17 +2659,13 @@ private:
env.trust(STAmount{lpIssue, 50}, bob);
env(pay(gw, alice, STAmount{lpIssue, 100}));
env(pay(gw, bob, STAmount{lpIssue, 50}));
amm.bid(alice, 100);
env(amm.bid({.account = alice, .bidMin = 100}));
// Alice doesn't have any more tokens, but
// she still owns the slot.
amm.bid(
bob,
std::nullopt,
50,
{},
std::nullopt,
std::nullopt,
std::nullopt,
env(amm.bid({
.account = bob,
.bidMax = 50,
}),
ter(tecAMM_FAILED));
}
}
@@ -2527,7 +2682,7 @@ private:
// Bid 110 tokens. Pay bidMin.
testAMM([&](AMM& ammAlice, Env& env) {
ammAlice.deposit(carol, 1'000'000);
ammAlice.bid(carol, 110);
env(ammAlice.bid({.account = carol, .bidMin = 110}));
BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{110}));
// 110 tokens are burned.
BEAST_EXPECT(ammAlice.expectBalances(
@@ -2538,12 +2693,12 @@ private:
testAMM([&](AMM& ammAlice, Env& env) {
ammAlice.deposit(carol, 1'000'000);
// Bid exactly 110. Pay 110 because the pay price is < 110.
ammAlice.bid(carol, 110, 110);
env(ammAlice.bid({.account = carol, .bidMin = 110, .bidMax = 110}));
BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{110}));
BEAST_EXPECT(ammAlice.expectBalances(
XRP(11'000), USD(11'000), IOUAmount{10'999'890}));
// Bid exactly 180-200. Pay 180 because the pay price is < 180.
ammAlice.bid(alice, 180, 200);
env(ammAlice.bid({.account = alice, .bidMin = 180, .bidMax = 200}));
BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{180}));
BEAST_EXPECT(ammAlice.expectBalances(
XRP(11'000), USD(11'000), IOUAmount{10'999'814'5, -1}));
@@ -2553,43 +2708,36 @@ private:
testAMM([&](AMM& ammAlice, Env& env) {
ammAlice.deposit(carol, 1'000'000);
// Bid, pay bidMin.
ammAlice.bid(carol, 110);
env(ammAlice.bid({.account = carol, .bidMin = 110}));
BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{110}));
fund(env, gw, {bob}, {USD(10'000)}, Fund::Acct);
ammAlice.deposit(bob, 1'000'000);
// Bid, pay the computed price.
ammAlice.bid(bob);
env(ammAlice.bid({.account = bob}));
BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount(1155, -1)));
// Bid bidMax fails because the computed price is higher.
ammAlice.bid(
carol,
std::nullopt,
120,
{},
std::nullopt,
std::nullopt,
std::nullopt,
env(ammAlice.bid({
.account = carol,
.bidMax = 120,
}),
ter(tecAMM_FAILED));
// Bid MaxSlotPrice succeeds - pay computed price
ammAlice.bid(carol, std::nullopt, 600);
env(ammAlice.bid({.account = carol, .bidMax = 600}));
BEAST_EXPECT(
ammAlice.expectAuctionSlot(0, 0, IOUAmount{121'275, -3}));
// Bid Min/MaxSlotPrice fails because the computed price is not in
// range
ammAlice.bid(
carol,
10,
100,
{},
std::nullopt,
std::nullopt,
std::nullopt,
env(ammAlice.bid({
.account = carol,
.bidMin = 10,
.bidMax = 100,
}),
ter(tecAMM_FAILED));
// Bid Min/MaxSlotPrice succeeds - pay computed price
ammAlice.bid(carol, 100, 600);
env(ammAlice.bid({.account = carol, .bidMin = 100, .bidMax = 600}));
BEAST_EXPECT(
ammAlice.expectAuctionSlot(0, 0, IOUAmount{127'33875, -5}));
});
@@ -2604,23 +2752,23 @@ private:
XRP(12'000), USD(12'000), IOUAmount{12'000'000, 0}));
// Initial state. Pay bidMin.
ammAlice.bid(carol, 110);
env(ammAlice.bid({.account = carol, .bidMin = 110})).close();
BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{110}));
// 1st Interval after close, price for 0th interval.
ammAlice.bid(bob);
env(ammAlice.bid({.account = bob}));
env.close(seconds(AUCTION_SLOT_INTERVAL_DURATION + 1));
BEAST_EXPECT(
ammAlice.expectAuctionSlot(0, 1, IOUAmount{1'155, -1}));
// 10th Interval after close, price for 1st interval.
ammAlice.bid(carol);
env(ammAlice.bid({.account = carol}));
env.close(seconds(10 * AUCTION_SLOT_INTERVAL_DURATION + 1));
BEAST_EXPECT(
ammAlice.expectAuctionSlot(0, 10, IOUAmount{121'275, -3}));
// 20th Interval (expired) after close, price for 10th interval.
ammAlice.bid(bob);
env(ammAlice.bid({.account = bob}));
env.close(seconds(
AUCTION_SLOT_TIME_INTERVALS * AUCTION_SLOT_INTERVAL_DURATION +
1));
@@ -2628,7 +2776,7 @@ private:
0, std::nullopt, IOUAmount{127'33875, -5}));
// 0 Interval.
ammAlice.bid(carol, 110);
env(ammAlice.bid({.account = carol, .bidMin = 110})).close();
BEAST_EXPECT(
ammAlice.expectAuctionSlot(0, std::nullopt, IOUAmount{110}));
// ~321.09 tokens burnt on bidding fees.
@@ -2650,7 +2798,11 @@ private:
ammAlice.deposit(carol, 500'000);
ammAlice.deposit(dan, 500'000);
auto ammTokens = ammAlice.getLPTokensBalance();
ammAlice.bid(carol, 120, std::nullopt, {bob, ed});
env(ammAlice.bid({
.account = carol,
.bidMin = 120,
.authAccounts = {bob, ed},
}));
auto const slotPrice = IOUAmount{5'200};
ammTokens -= slotPrice;
BEAST_EXPECT(ammAlice.expectAuctionSlot(100, 0, slotPrice));
@@ -2760,10 +2912,10 @@ private:
1'000);
// Bid tiny amount
testAMM([&](AMM& ammAlice, Env&) {
testAMM([&](AMM& ammAlice, Env& env) {
// Bid a tiny amount
auto const tiny = Number{STAmount::cMinValue, STAmount::cMinOffset};
ammAlice.bid(alice, IOUAmount{tiny});
env(ammAlice.bid({.account = alice, .bidMin = IOUAmount{tiny}}));
// Auction slot purchase price is equal to the tiny amount
// since the minSlotPrice is 0 with no trading fee.
BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{tiny}));
@@ -2771,8 +2923,10 @@ private:
BEAST_EXPECT(ammAlice.expectBalances(
XRP(10'000), USD(10'000), ammAlice.tokens()));
// Bid the tiny amount
ammAlice.bid(
alice, IOUAmount{STAmount::cMinValue, STAmount::cMinOffset});
env(ammAlice.bid({
.account = alice,
.bidMin = IOUAmount{STAmount::cMinValue, STAmount::cMinOffset},
}));
// Pay slightly higher price
BEAST_EXPECT(ammAlice.expectAuctionSlot(
0, 0, IOUAmount{tiny * Number{105, -2}}));
@@ -2783,14 +2937,22 @@ private:
// Reset auth account
testAMM([&](AMM& ammAlice, Env& env) {
ammAlice.bid(alice, IOUAmount{100}, std::nullopt, {carol});
env(ammAlice.bid({
.account = alice,
.bidMin = IOUAmount{100},
.authAccounts = {carol},
}));
BEAST_EXPECT(ammAlice.expectAuctionSlot({carol}));
ammAlice.bid(alice, IOUAmount{100});
env(ammAlice.bid({.account = alice, .bidMin = IOUAmount{100}}));
BEAST_EXPECT(ammAlice.expectAuctionSlot({}));
Account bob("bob");
Account dan("dan");
fund(env, {bob, dan}, XRP(1'000));
ammAlice.bid(alice, IOUAmount{100}, std::nullopt, {bob, dan});
env(ammAlice.bid({
.account = alice,
.bidMin = IOUAmount{100},
.authAccounts = {bob, dan},
}));
BEAST_EXPECT(ammAlice.expectAuctionSlot({bob, dan}));
});
@@ -2805,7 +2967,7 @@ private:
env(pay(gw, alice, STAmount{lpIssue, 500}));
env(pay(gw, bob, STAmount{lpIssue, 50}));
// Alice doesn't have anymore lp tokens
amm.bid(alice, 500);
env(amm.bid({.account = alice, .bidMin = 500}));
BEAST_EXPECT(amm.expectAuctionSlot(100, 0, IOUAmount{500}));
BEAST_EXPECT(expectLine(env, alice, STAmount{lpIssue, 0}));
// But trades with the discounted fee since she still owns the slot.
@@ -2822,6 +2984,57 @@ private:
STAmount{USD, UINT64_C(1'010'10090898081), -11},
IOUAmount{1'004'487'562112089, -9}));
}
// preflight tests
{
Env env(*this);
fund(env, gw, {alice, bob}, XRP(2'000), {USD(2'000)});
AMM amm(env, gw, XRP(1'000), USD(1'010), false, 1'000);
Json::Value tx = amm.bid({.account = alice, .bidMin = 500});
{
auto jtx = env.jt(tx, seq(1), fee(10));
env.app().config().features.erase(featureAMM);
PreflightContext pfctx(
env.app(),
*jtx.stx,
env.current()->rules(),
tapNONE,
env.journal);
auto pf = AMMBid::preflight(pfctx);
BEAST_EXPECT(pf == temDISABLED);
env.app().config().features.insert(featureAMM);
}
{
auto jtx = env.jt(tx, seq(1), fee(10));
jtx.jv["TxnSignature"] = "deadbeef";
jtx.stx = env.ust(jtx);
PreflightContext pfctx(
env.app(),
*jtx.stx,
env.current()->rules(),
tapNONE,
env.journal);
auto pf = AMMBid::preflight(pfctx);
BEAST_EXPECT(pf != tesSUCCESS);
}
{
auto jtx = env.jt(tx, seq(1), fee(10));
jtx.jv["Asset2"]["currency"] = "XAH";
jtx.jv["Asset2"].removeMember("issuer");
jtx.stx = env.ust(jtx);
PreflightContext pfctx(
env.app(),
*jtx.stx,
env.current()->rules(),
tapNONE,
env.journal);
auto pf = AMMBid::preflight(pfctx);
BEAST_EXPECT(pf == temBAD_AMM_TOKENS);
}
}
}
void
@@ -3601,7 +3814,7 @@ private:
ammAlice.vote(carol, 0);
BEAST_EXPECT(ammAlice.expectTradingFee(0));
// Carol bids
ammAlice.bid(carol, 100);
env(ammAlice.bid({.account = carol, .bidMin = 100}));
BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{4'999'900}));
BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{100}));
BEAST_EXPECT(accountBalance(env, carol) == "22499999960");
@@ -3693,6 +3906,15 @@ private:
Env env{*this, feature};
fund(env, gw, {alice}, {USD(1'000)}, Fund::All);
AMM amm(env, alice, XRP(1'000), USD(1'000), ter(temDISABLED));
env(amm.bid({.bidMax = 1000}), ter(temMALFORMED));
env(amm.bid({}), ter(temDISABLED));
amm.vote(VoteArg{.tfee = 100, .err = ter(temDISABLED)});
amm.withdraw(WithdrawArg{.tokens = 100, .err = ter(temMALFORMED)});
amm.withdraw(WithdrawArg{.err = ter(temDISABLED)});
amm.deposit(
DepositArg{.asset1In = USD(100), .err = ter(temDISABLED)});
amm.ammDelete(alice, ter(temDISABLED));
}
}
@@ -4312,14 +4534,10 @@ private:
// Bid,Vote,Deposit,Withdraw,SetTrust failing with
// tecAMM_EMPTY. Deposit succeeds with tfTwoAssetIfEmpty option.
amm.bid(
alice,
1000,
std::nullopt,
{},
std::nullopt,
std::nullopt,
std::nullopt,
env(amm.bid({
.account = alice,
.bidMin = 1000,
}),
ter(tecAMM_EMPTY));
amm.vote(
std::nullopt,
@@ -4391,6 +4609,9 @@ private:
amm.ammDelete(alice);
BEAST_EXPECT(!amm.ammExists());
BEAST_EXPECT(!env.le(keylet::ownerDir(amm.ammAccount())));
// Try redundant delete
amm.ammDelete(alice, ter(terNO_AMM));
}
}
@@ -4889,6 +5110,65 @@ private:
false);
}
void
testMalformed()
{
using namespace jtx;
testAMM([&](AMM& ammAlice, Env& env) {
WithdrawArg args{
.flags = tfSingleAsset,
.err = ter(temMALFORMED),
};
ammAlice.withdraw(args);
});
testAMM([&](AMM& ammAlice, Env& env) {
WithdrawArg args{
.flags = tfOneAssetLPToken,
.err = ter(temMALFORMED),
};
ammAlice.withdraw(args);
});
testAMM([&](AMM& ammAlice, Env& env) {
WithdrawArg args{
.flags = tfLimitLPToken,
.err = ter(temMALFORMED),
};
ammAlice.withdraw(args);
});
testAMM([&](AMM& ammAlice, Env& env) {
WithdrawArg args{
.asset1Out = XRP(100),
.asset2Out = XRP(100),
.err = ter(temBAD_AMM_TOKENS),
};
ammAlice.withdraw(args);
});
testAMM([&](AMM& ammAlice, Env& env) {
WithdrawArg args{
.asset1Out = XRP(100),
.asset2Out = BAD(100),
.err = ter(temBAD_CURRENCY),
};
ammAlice.withdraw(args);
});
testAMM([&](AMM& ammAlice, Env& env) {
Json::Value jv;
jv[jss::TransactionType] = jss::AMMWithdraw;
jv[jss::Flags] = tfLimitLPToken;
jv[jss::Account] = alice.human();
ammAlice.setTokens(jv);
XRP(100).value().setJson(jv[jss::Amount]);
USD(100).value().setJson(jv[jss::EPrice]);
env(jv, ter(temBAD_AMM_TOKENS));
});
}
void
testFixOverflowOffer()
{
@@ -5164,7 +5444,7 @@ private:
}
void
testCore()
testAll()
{
testInvalidInstance();
testInstanceCreate();
@@ -5190,13 +5470,14 @@ private:
testAMMID();
testSelection();
testFixDefaultInnerObj();
testMalformed();
testFixOverflowOffer();
}
void
run() override
{
testCore();
testAll();
}
};

View File

@@ -113,9 +113,7 @@ struct BidArg
std::optional<std::variant<int, IOUAmount, STAmount>> bidMax = std::nullopt;
std::vector<Account> authAccounts = {};
std::optional<std::uint32_t> flags = std::nullopt;
std::optional<jtx::seq> seq = std::nullopt;
std::optional<std::pair<Issue, Issue>> assets = std::nullopt;
std::optional<ter> err = std::nullopt;
};
/** Convenience class to test AMM functionality.
@@ -317,19 +315,7 @@ public:
void
vote(VoteArg const& arg);
void
bid(std::optional<Account> const& account,
std::optional<std::variant<int, IOUAmount, STAmount>> const& bidMin =
std::nullopt,
std::optional<std::variant<int, IOUAmount, STAmount>> const& bidMax =
std::nullopt,
std::vector<Account> const& authAccounts = {},
std::optional<std::uint32_t> const& flags = std::nullopt,
std::optional<jtx::seq> const& seq = std::nullopt,
std::optional<std::pair<Issue, Issue>> const& assets = std::nullopt,
std::optional<ter> const& ter = std::nullopt);
void
Json::Value
bid(BidArg const& arg);
AccountID const&
@@ -391,12 +377,12 @@ public:
return ammID_;
}
private:
void
setTokens(
Json::Value& jv,
std::optional<std::pair<Issue, Issue>> const& assets = std::nullopt);
private:
AccountID
create(
std::uint32_t tfee = 0,

View File

@@ -538,17 +538,18 @@ public:
/** Apply funclets and submit. */
/** @{ */
template <class JsonValue, class... FN>
void
Env&
apply(JsonValue&& jv, FN const&... fN)
{
submit(jt(std::forward<JsonValue>(jv), fN...));
return *this;
}
template <class JsonValue, class... FN>
void
Env&
operator()(JsonValue&& jv, FN const&... fN)
{
apply(std::forward<JsonValue>(jv), fN...);
return apply(std::forward<JsonValue>(jv), fN...);
}
/** Apply funclets and submit. */
@@ -698,6 +699,13 @@ public:
}
/** @} */
/** Create a STTx from a JTx without sanitizing
Use to inject bogus values into test transactions by first
editing the JSON.
*/
std::shared_ptr<STTx const>
ust(JTx const& jt);
protected:
int trace_ = 0;
TestStopwatch stopwatch_;

View File

@@ -656,16 +656,8 @@ AMM::vote(VoteArg const& arg)
return vote(arg.account, arg.tfee, arg.flags, arg.seq, arg.assets, arg.err);
}
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)
Json::Value
AMM::bid(BidArg const& arg)
{
if (auto const amm =
env_.current()->read(keylet::amm(asset1_.issue(), asset2_.issue())))
@@ -684,8 +676,9 @@ AMM::bid(
bidMax_ = std::nullopt;
Json::Value jv;
jv[jss::Account] = account ? account->human() : creatorAccount_.human();
setTokens(jv, assets);
jv[jss::Account] =
arg.account ? arg.account->human() : creatorAccount_.human();
setTokens(jv, arg.assets);
auto getBid = [&](auto const& bid) {
if (std::holds_alternative<int>(bid))
return STAmount{lptIssue_, std::get<int>(bid)};
@@ -694,22 +687,22 @@ AMM::bid(
else
return std::get<STAmount>(bid);
};
if (bidMin)
if (arg.bidMin)
{
STAmount saTokens = getBid(*bidMin);
STAmount saTokens = getBid(*arg.bidMin);
saTokens.setJson(jv[jss::BidMin]);
bidMin_ = saTokens.iou();
}
if (bidMax)
if (arg.bidMax)
{
STAmount saTokens = getBid(*bidMax);
STAmount saTokens = getBid(*arg.bidMax);
saTokens.setJson(jv[jss::BidMax]);
bidMax_ = saTokens.iou();
}
if (authAccounts.size() > 0)
if (arg.authAccounts.size() > 0)
{
Json::Value accounts(Json::arrayValue);
for (auto const& account : authAccounts)
for (auto const& account : arg.authAccounts)
{
Json::Value acct;
Json::Value authAcct;
@@ -719,26 +712,12 @@ AMM::bid(
}
jv[jss::AuthAccounts] = accounts;
}
if (flags)
jv[jss::Flags] = *flags;
if (arg.flags)
jv[jss::Flags] = *arg.flags;
jv[jss::TransactionType] = jss::AMMBid;
if (fee_ != 0)
jv[jss::Fee] = std::to_string(fee_);
submit(jv, seq, ter);
}
void
AMM::bid(BidArg const& arg)
{
return bid(
arg.account,
arg.bidMin,
arg.bidMax,
arg.authAccounts,
arg.flags,
arg.seq,
arg.assets,
arg.err);
return jv;
}
void

View File

@@ -514,6 +514,32 @@ Env::st(JTx const& jt)
return nullptr;
}
std::shared_ptr<STTx const>
Env::ust(JTx const& jt)
{
// The parse must succeed, since we
// generated the JSON ourselves.
std::optional<STObject> obj;
try
{
obj = jtx::parse(jt.jv);
}
catch (jtx::parse_error const&)
{
test.log << "Exception: parse_error\n" << pretty(jt.jv) << std::endl;
Rethrow();
}
try
{
return std::make_shared<STTx const>(std::move(*obj));
}
catch (std::exception const&)
{
}
return nullptr;
}
Json::Value
Env::do_rpc(
unsigned apiVersion,

View File

@@ -20,6 +20,7 @@
#ifndef RIPPLE_TEST_JTX_MULTISIGN_H_INCLUDED
#define RIPPLE_TEST_JTX_MULTISIGN_H_INCLUDED
#include <concepts>
#include <cstdint>
#include <optional>
#include <test/jtx/Account.h>
@@ -101,7 +102,9 @@ public:
msig(std::vector<Reg> signers_);
template <class AccountType, class... Accounts>
explicit msig(AccountType&& a0, Accounts&&... aN)
requires std::convertible_to<AccountType, Reg> explicit msig(
AccountType&& a0,
Accounts&&... aN)
: msig{std::vector<Reg>{
std::forward<AccountType>(a0),
std::forward<Accounts>(aN)...}}

View File

@@ -130,7 +130,8 @@ public:
Account ed("ed");
Account bill("bill");
env.fund(XRP(1000), bob, ed, bill);
ammAlice.bid(alice, 100, std::nullopt, {carol, bob, ed, bill});
env(ammAlice.bid(
{.bidMin = 100, .authAccounts = {carol, bob, ed, bill}}));
BEAST_EXPECT(ammAlice.expectAmmRpcInfo(
XRP(80000),
USD(80000),