From 2dc0a8c97a8d8cd9fd9f9c4cfdf2462809e277ee Mon Sep 17 00:00:00 2001 From: Gregory Tsipenyuk Date: Sat, 28 Sep 2024 20:35:38 -0400 Subject: [PATCH] Add cross-asset payment unit-tests plus some fixes --- src/libxrpl/protocol/STParsedJSON.cpp | 82 +++++-- src/test/app/MPToken_test.cpp | 308 ++++++++++++++++++++++++++ src/test/jtx/amount.h | 14 +- src/test/jtx/impl/paths.cpp | 9 +- 4 files changed, 378 insertions(+), 35 deletions(-) diff --git a/src/libxrpl/protocol/STParsedJSON.cpp b/src/libxrpl/protocol/STParsedJSON.cpp index 7e6b8ff5975..11faa6bd8d0 100644 --- a/src/libxrpl/protocol/STParsedJSON.cpp +++ b/src/libxrpl/protocol/STParsedJSON.cpp @@ -633,7 +633,7 @@ parseLeaf( json_name + "." + ss.str()); // each element in this path has some combination of - // account, currency, or issuer + // account, asset, or issuer Json::Value pathEl = value[i][j]; @@ -643,20 +643,32 @@ parseLeaf( return ret; } - Json::Value const& account = pathEl["account"]; - Json::Value const& currency = pathEl["currency"]; - Json::Value const& issuer = pathEl["issuer"]; + if (pathEl.isMember(jss::currency) && + pathEl.isMember(jss::mpt_issuance_id)) + { + error = RPC::make_error( + rpcINVALID_PARAMS, "Invalid Asset."); + return ret; + } + + bool const isMPT = + pathEl.isMember(jss::mpt_issuance_id); + auto const assetName = + isMPT ? jss::mpt_issuance_id : jss::currency; + Json::Value const& account = pathEl[jss::account]; + Json::Value const& asset = pathEl[assetName]; + Json::Value const& issuer = pathEl[jss::issuer]; bool hasCurrency = false; AccountID uAccount, uIssuer; - Currency uCurrency; + PathAsset uAsset; if (account) { // human account id if (!account.isString()) { - error = - string_expected(element_name, "account"); + error = string_expected( + element_name, jss::account.c_str()); return ret; } @@ -668,35 +680,57 @@ parseLeaf( parseBase58(account.asString()); if (!a) { - error = - invalid_data(element_name, "account"); + error = invalid_data( + element_name, jss::account.c_str()); return ret; } uAccount = *a; } } - if (currency) + if (asset) { - // human currency - if (!currency.isString()) + // human asset + if (!asset.isString()) { - error = - string_expected(element_name, "currency"); + error = string_expected( + element_name, assetName.c_str()); return ret; } hasCurrency = true; - if (!uCurrency.parseHex(currency.asString())) + if (isMPT) { - if (!to_currency( - uCurrency, currency.asString())) + MPTID u; + if (!u.parseHex(asset.asString())) { - error = - invalid_data(element_name, "currency"); + error = invalid_data( + element_name, assetName.c_str()); return ret; } + uAsset = u; + if (getMPTIssuer(u) == beast::zero) + { + error = invalid_data( + element_name, jss::account.c_str()); + return ret; + } + } + else + { + Currency currency; + if (!currency.parseHex(asset.asString())) + { + if (!to_currency( + currency, asset.asString())) + { + error = invalid_data( + element_name, assetName.c_str()); + return ret; + } + } + uAsset = currency; } } @@ -705,7 +739,8 @@ parseLeaf( // human account id if (!issuer.isString()) { - error = string_expected(element_name, "issuer"); + error = string_expected( + element_name, jss::issuer.c_str()); return ret; } @@ -715,16 +750,15 @@ parseLeaf( parseBase58(issuer.asString()); if (!a) { - error = - invalid_data(element_name, "issuer"); + error = invalid_data( + element_name, jss::issuer.c_str()); return ret; } uIssuer = *a; } } - p.emplace_back( - uAccount, uCurrency, uIssuer, hasCurrency); + p.emplace_back(uAccount, uAsset, uIssuer, hasCurrency); } tail.push_back(p); diff --git a/src/test/app/MPToken_test.cpp b/src/test/app/MPToken_test.cpp index 5d615afb41c..3a1f3cbe69a 100644 --- a/src/test/app/MPToken_test.cpp +++ b/src/test/app/MPToken_test.cpp @@ -18,6 +18,8 @@ //============================================================================== #include +#include +#include #include #include #include @@ -1735,6 +1737,309 @@ class MPToken_test : public beast::unit_test::suite } } + void + testCrossAssetPayment(FeatureBitset features) + { + testcase("Cross Asset Payment"); + using namespace test::jtx; + Account const gw = Account("gw"); + Account const alice = Account("alice"); + Account const carol = Account("carol"); + Account const bob = Account("bob"); + auto const USD = gw["USD"]; + + // MPT/XRP + { + Env env{*this, features}; + MPTTester mpt(env, gw, {.holders = {&alice, &carol, &bob}}); + + mpt.create({.ownerCount = 1, .holderCount = 0}); + auto const MPT = mpt["MPT"]; + + mpt.authorize({.account = &alice}); + mpt.pay(gw, alice, 200); + + mpt.authorize({.account = &carol}); + mpt.pay(gw, carol, 200); + + mpt.authorize({.account = &bob}); + + env(offer(alice, XRP(100), mpt.mpt(101))); + env.close(); + BEAST_EXPECT(expectOffers( + env, alice, 1, {{Amounts{XRP(100), mpt.mpt(101)}}})); + + env(pay(carol, bob, mpt.mpt(101)), + test::jtx::path(~MPT), + sendmax(XRP(100)), + txflags(tfPartialPayment)); + env.close(); + + BEAST_EXPECT(expectOffers(env, alice, 0)); + BEAST_EXPECT(mpt.checkMPTokenOutstandingAmount(400)); + BEAST_EXPECT(mpt.checkMPTokenAmount(alice, 99)); + BEAST_EXPECT(mpt.checkMPTokenAmount(bob, 101)); + } + return; + // MPT/IOU + { + Env env{*this, features}; + + MPTTester mpt(env, gw, {.holders = {&alice, &carol, &bob}}); + + mpt.create({.ownerCount = 1, .holderCount = 0}); + auto const MPT = mpt["MPT"]; + + env(trust(alice, USD(2'000))); + env(pay(gw, alice, USD(1'000))); + env(trust(bob, USD(2'000))); + env(pay(gw, bob, USD(1'000))); + env(trust(carol, USD(2'000))); + env(pay(gw, carol, USD(1'000))); + env.close(); + + mpt.authorize({.account = &alice}); + mpt.pay(gw, alice, 200); + + mpt.authorize({.account = &carol}); + mpt.pay(gw, carol, 200); + + mpt.authorize({.account = &bob}); + + env(offer(alice, USD(100), mpt.mpt(101))); + env.close(); + BEAST_EXPECT(expectOffers( + env, alice, 1, {{Amounts{USD(100), mpt.mpt(101)}}})); + + env(pay(carol, bob, mpt.mpt(101)), + test::jtx::path(~MPT), + sendmax(USD(100)), + txflags(tfPartialPayment)); + env.close(); + + BEAST_EXPECT(expectOffers(env, alice, 0)); + BEAST_EXPECT(env.balance(carol, USD) == USD(900)); + BEAST_EXPECT(mpt.checkMPTokenOutstandingAmount(400)); + BEAST_EXPECT(mpt.checkMPTokenAmount(alice, 99)); + BEAST_EXPECT(mpt.checkMPTokenAmount(bob, 101)); + } + + // IOU/MPT + { + Env env{*this, features}; + + MPTTester mpt(env, gw, {.holders = {&alice, &carol, &bob}}); + + mpt.create({.ownerCount = 1, .holderCount = 0}); + + env(trust(alice, USD(2'000)), txflags(tfClearNoRipple)); + env(pay(gw, alice, USD(1'000))); + env(trust(bob, USD(2'000)), txflags(tfClearNoRipple)); + env.close(); + + mpt.authorize({.account = &alice}); + env(pay(gw, alice, mpt.mpt(200))); + + mpt.authorize({.account = &carol}); + env(pay(gw, carol, mpt.mpt(200))); + + env(offer(alice, mpt.mpt(101), USD(100))); + env.close(); + BEAST_EXPECT(expectOffers( + env, alice, 1, {{Amounts{mpt.mpt(101), USD(100)}}})); + + env(pay(carol, bob, USD(100)), + test::jtx::path(~USD), + sendmax(mpt.mpt(101)), + txflags(tfPartialPayment | tfNoRippleDirect)); + env.close(); + + BEAST_EXPECT(expectOffers(env, alice, 0)); + BEAST_EXPECT(env.balance(alice, USD) == USD(900)); + BEAST_EXPECT(mpt.checkMPTokenAmount(alice, 301)); + BEAST_EXPECT(mpt.checkMPTokenOutstandingAmount(400)); + BEAST_EXPECT(mpt.checkMPTokenAmount(carol, 99)); + BEAST_EXPECT(env.balance(bob, USD) == USD(100)); + } + + // MPT/MPT + { + Env env{*this, features}; + + MPTTester mpt1(env, gw, {.holders = {&alice, &carol, &bob}}); + mpt1.create({.ownerCount = 1, .holderCount = 0}); + auto const MPT1 = mpt1["MPT1"]; + + MPTTester mpt2( + env, gw, {.holders = {&alice, &carol, &bob}, .fund = false}); + mpt2.create({.ownerCount = 2, .holderCount = 0}); + + mpt1.authorize({.account = &alice}); + mpt1.pay(gw, alice, 200); + mpt2.authorize({.account = &alice}); + + mpt2.authorize({.account = &carol}); + mpt2.pay(gw, carol, 200); + + mpt1.authorize({.account = &bob}); + + env(offer(alice, mpt2.mpt(100), mpt1.mpt(101))); + env.close(); + BEAST_EXPECT(expectOffers( + env, alice, 1, {{Amounts{mpt2.mpt(100), mpt1.mpt(101)}}})); + + env(pay(carol, bob, mpt1.mpt(101)), + test::jtx::path(~MPT1), + sendmax(mpt2.mpt(100)), + txflags(tfPartialPayment)); + env.close(); + + BEAST_EXPECT(expectOffers(env, alice, 0)); + BEAST_EXPECT(mpt1.checkMPTokenOutstandingAmount(200)); + BEAST_EXPECT(mpt2.checkMPTokenAmount(alice, 100)); + BEAST_EXPECT(mpt1.checkMPTokenAmount(alice, 99)); + BEAST_EXPECT(mpt1.checkMPTokenAmount(bob, 101)); + } + + // XRP/MPT AMM + { + Env env{*this, features}; + + fund(env, gw, {alice, carol, bob}, XRP(11'000), {USD(20'000)}); + + MPTTester mpt(env, gw, {.fund = false}); + + mpt.create({.ownerCount = 1, .holderCount = 0}); + auto const MPT = mpt["MPT"]; + + mpt.authorize({.account = &alice}); + mpt.authorize({.account = &bob}); + mpt.pay(gw, alice, 10'100); + + AMM amm(env, alice, XRP(10'000), mpt.mpt(10'100)); + + env(pay(carol, bob, mpt.mpt(100)), + test::jtx::path(~MPT), + sendmax(XRP(100)), + txflags(tfPartialPayment | tfNoRippleDirect)); + env.close(); + + BEAST_EXPECT( + amm.expectBalances(XRP(10'100), mpt.mpt(10'000), amm.tokens())); + BEAST_EXPECT(mpt.checkMPTokenAmount(bob, 100)); + } + + // IOU/MPT AMM + { + Env env{*this, features}; + + fund(env, gw, {alice, carol, bob}, XRP(11'000), {USD(20'000)}); + + MPTTester mpt(env, gw, {.fund = false}); + + mpt.create({.ownerCount = 1, .holderCount = 0}); + auto const MPT = mpt["MPT"]; + + mpt.authorize({.account = &alice}); + mpt.authorize({.account = &bob}); + mpt.pay(gw, alice, 10'100); + + AMM amm(env, alice, USD(10'000), mpt.mpt(10'100)); + + env(pay(carol, bob, mpt.mpt(100)), + test::jtx::path(~MPT), + sendmax(USD(100)), + txflags(tfPartialPayment | tfNoRippleDirect)); + env.close(); + + BEAST_EXPECT( + amm.expectBalances(USD(10'100), mpt.mpt(10'000), amm.tokens())); + BEAST_EXPECT(mpt.checkMPTokenAmount(bob, 100)); + } + + // MPT/MPT AMM cross-asset payment + { + Env env{*this, features}; + env.fund(XRP(20'000), gw, alice, carol, bob); + env.close(); + + MPTTester mpt1(env, gw, {.fund = false}); + mpt1.create(); + auto const MPT1 = mpt1["MPT1"]; + mpt1.authorize({.account = &alice}); + mpt1.authorize({.account = &bob}); + mpt1.pay(gw, alice, 10'100); + + MPTTester mpt2(env, gw, {.fund = false}); + mpt2.create(); + mpt2.authorize({.account = &alice}); + mpt2.authorize({.account = &bob}); + mpt2.authorize({.account = &carol}); + mpt2.pay(gw, alice, 10'100); + mpt2.pay(gw, carol, 100); + + AMM amm(env, alice, mpt2.mpt(10'000), mpt1.mpt(10'100)); + + env(pay(carol, bob, mpt1.mpt(100)), + test::jtx::path(~MPT1), + sendmax(mpt2.mpt(100)), + txflags(tfPartialPayment | tfNoRippleDirect)); + env.close(); + + BEAST_EXPECT(amm.expectBalances( + mpt2.mpt(10'100), mpt1.mpt(10'000), amm.tokens())); + BEAST_EXPECT(mpt1.checkMPTokenAmount(bob, 100)); + } + + // Multi-steps with AMM + // IOU/MPT1 MPT1/MPT2 MPT2/IOU IOU/IOU AMM:IOU/MPT MPT/IOU + { + Env env{*this, features}; + auto const USD = gw["USD"]; + auto const EUR = gw["EUR"]; + auto const CRN = gw["CRN"]; + auto const YAN = gw["YAN"]; + + fund( + env, + gw, + {alice, carol, bob}, + XRP(1'000), + {USD(1'000), EUR(1'000), CRN(2'000), YAN(1'000)}); + + auto createMPT = [&]() -> std::pair { + MPTTester mpt(env, gw, {.fund = false}); + mpt.create(); + mpt.authorize({.account = &alice}); + mpt.pay(gw, alice, 2'000); + return {mpt, mpt["MPT"]}; + }; + + auto const [mpt1, MPT1] = createMPT(); + auto const [mpt2, MPT2] = createMPT(); + auto const [mpt3, MPT3] = createMPT(); + + env(offer(alice, EUR(100), mpt1.mpt(101))); + env(offer(alice, mpt1.mpt(101), mpt2.mpt(102))); + env(offer(alice, mpt2.mpt(102), USD(103))); + env(offer(alice, USD(103), CRN(104))); + env.close(); + AMM amm(env, alice, CRN(1'000), mpt3.mpt(1'104)); + env(offer(alice, mpt3.mpt(104), YAN(100))); + + env(pay(carol, bob, YAN(100)), + test::jtx::path(~MPT1, ~MPT2, ~USD, ~CRN, ~MPT3, ~YAN), + sendmax(EUR(100)), + txflags(tfPartialPayment | tfNoRippleDirect)); + env.close(); + + BEAST_EXPECT(env.balance(bob, YAN) == YAN(1'100)); + BEAST_EXPECT( + amm.expectBalances(CRN(1'104), mpt3.mpt(1'000), amm.tokens())); + BEAST_EXPECT(expectOffers(env, alice, 0)); + } + } + public: void run() override @@ -1773,6 +2078,9 @@ class MPToken_test : public beast::unit_test::suite // Test offer crossing testOfferCrossing(all); + + // Test cross asset payment + testCrossAssetPayment(all); } }; diff --git a/src/test/jtx/amount.h b/src/test/jtx/amount.h index 44bfc0f25a6..d43f49927b4 100644 --- a/src/test/jtx/amount.h +++ b/src/test/jtx/amount.h @@ -154,11 +154,9 @@ operator<<(std::ostream& os, PrettyAmount const& amount); // Specifies an order book struct BookSpec { - AccountID account; - ripple::Currency currency; + ripple::Asset asset; - BookSpec(AccountID const& account_, ripple::Currency const& currency_) - : account(account_), currency(currency_) + BookSpec(ripple::Asset const& asset_) : asset(asset_) { } }; @@ -223,7 +221,7 @@ struct XRP_t friend BookSpec operator~(XRP_t const&) { - return BookSpec(xrpAccount(), xrpCurrency()); + return BookSpec(Issue{xrpCurrency(), xrpAccount()}); } }; @@ -350,7 +348,7 @@ class IOU friend BookSpec operator~(IOU const& iou) { - return BookSpec(iou.account.id(), iou.currency); + return BookSpec(Issue{iou.currency, iou.account.id()}); } }; @@ -423,9 +421,7 @@ class MPT friend BookSpec operator~(MPT const& mpt) { - assert(false); - Throw("MPT is not supported"); - return BookSpec{beast::zero, noCurrency()}; + return BookSpec{Asset{mpt}}; } }; diff --git a/src/test/jtx/impl/paths.cpp b/src/test/jtx/impl/paths.cpp index 7ad135553e4..4608a6cde98 100644 --- a/src/test/jtx/impl/paths.cpp +++ b/src/test/jtx/impl/paths.cpp @@ -88,8 +88,13 @@ void path::append_one(BookSpec const& book) { auto& jv = create(); - jv["currency"] = to_string(book.currency); - jv["issuer"] = toBase58(book.account); + if (book.asset.holds()) + jv["mpt_issuance_id"] = to_string(book.asset); + else + { + jv["currency"] = to_string(book.asset.get().currency); + jv["issuer"] = toBase58(book.asset.getIssuer()); + } } void