Files
rippled/src/test/app/TrustAndBalance_test.cpp
Bart 1d42c4f6de refactor: Remove unnecessary copyright notices already covered by LICENSE.md (#5929)
Per XLS-0095, we are taking steps to rename ripple(d) to xrpl(d).

This change specifically removes all copyright notices referencing Ripple, XRPLF, and certain affiliated contributors upon mutual agreement, so the notice in the LICENSE.md file applies throughout. Copyright notices referencing external contributions remain as-is. Duplicate verbiage is also removed.
2025-11-04 08:33:42 +00:00

473 lines
15 KiB
C++

#include <test/jtx.h>
#include <test/jtx/WSClient.h>
#include <xrpl/beast/unit_test.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/jss.h>
namespace ripple {
class TrustAndBalance_test : public beast::unit_test::suite
{
void
testPayNonexistent(FeatureBitset features)
{
testcase("Payment to Nonexistent Account");
using namespace test::jtx;
Env env{*this, features};
env(pay(env.master, "alice", XRP(1)), ter(tecNO_DST_INSUF_XRP));
env.close();
}
void
testTrustNonexistent()
{
testcase("Trust Nonexistent Account");
using namespace test::jtx;
Env env{*this};
Account alice{"alice"};
env(trust(env.master, alice["USD"](100)), ter(tecNO_DST));
}
void
testCreditLimit()
{
testcase("Credit Limit");
using namespace test::jtx;
Env env{*this};
Account gw{"gateway"};
Account alice{"alice"};
Account bob{"bob"};
env.fund(XRP(10000), gw, alice, bob);
env.close();
// credit limit doesn't exist yet - verify ledger_entry
// reflects this
auto jrr = ledgerEntryState(env, gw, alice, "USD");
BEAST_EXPECT(jrr[jss::error] == "entryNotFound");
// now create a credit limit
env(trust(alice, gw["USD"](800)));
jrr = ledgerEntryState(env, gw, alice, "USD");
BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "0");
BEAST_EXPECT(
jrr[jss::node][sfHighLimit.fieldName][jss::value] == "800");
BEAST_EXPECT(
jrr[jss::node][sfHighLimit.fieldName][jss::issuer] ==
alice.human());
BEAST_EXPECT(
jrr[jss::node][sfHighLimit.fieldName][jss::currency] == "USD");
BEAST_EXPECT(jrr[jss::node][sfLowLimit.fieldName][jss::value] == "0");
BEAST_EXPECT(
jrr[jss::node][sfLowLimit.fieldName][jss::issuer] == gw.human());
BEAST_EXPECT(
jrr[jss::node][sfLowLimit.fieldName][jss::currency] == "USD");
// modify the credit limit
env(trust(alice, gw["USD"](700)));
jrr = ledgerEntryState(env, gw, alice, "USD");
BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "0");
BEAST_EXPECT(
jrr[jss::node][sfHighLimit.fieldName][jss::value] == "700");
BEAST_EXPECT(
jrr[jss::node][sfHighLimit.fieldName][jss::issuer] ==
alice.human());
BEAST_EXPECT(
jrr[jss::node][sfHighLimit.fieldName][jss::currency] == "USD");
BEAST_EXPECT(jrr[jss::node][sfLowLimit.fieldName][jss::value] == "0");
BEAST_EXPECT(
jrr[jss::node][sfLowLimit.fieldName][jss::issuer] == gw.human());
BEAST_EXPECT(
jrr[jss::node][sfLowLimit.fieldName][jss::currency] == "USD");
// set negative limit - expect failure
env(trust(alice, gw["USD"](-1)), ter(temBAD_LIMIT));
// set zero limit
env(trust(alice, gw["USD"](0)));
// ensure line is deleted
jrr = ledgerEntryState(env, gw, alice, "USD");
BEAST_EXPECT(jrr[jss::error] == "entryNotFound");
// TODO Check in both owner books.
// set another credit limit
env(trust(alice, bob["USD"](600)));
// set limit on other side
env(trust(bob, alice["USD"](500)));
// check the ledger state for the trust line
jrr = ledgerEntryState(env, alice, bob, "USD");
BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "0");
BEAST_EXPECT(
jrr[jss::node][sfHighLimit.fieldName][jss::value] == "500");
BEAST_EXPECT(
jrr[jss::node][sfHighLimit.fieldName][jss::issuer] == bob.human());
BEAST_EXPECT(
jrr[jss::node][sfHighLimit.fieldName][jss::currency] == "USD");
BEAST_EXPECT(jrr[jss::node][sfLowLimit.fieldName][jss::value] == "600");
BEAST_EXPECT(
jrr[jss::node][sfLowLimit.fieldName][jss::issuer] == alice.human());
BEAST_EXPECT(
jrr[jss::node][sfLowLimit.fieldName][jss::currency] == "USD");
}
void
testDirectRipple(FeatureBitset features)
{
testcase("Direct Payment, Ripple");
using namespace test::jtx;
Env env{*this, features};
Account alice{"alice"};
Account bob{"bob"};
env.fund(XRP(10000), alice, bob);
env.close();
env(trust(alice, bob["USD"](600)));
env(trust(bob, alice["USD"](700)));
// alice sends bob partial with alice as issuer
env(pay(alice, bob, alice["USD"](24)));
env.require(balance(bob, alice["USD"](24)));
// alice sends bob more with bob as issuer
env(pay(alice, bob, bob["USD"](33)));
env.require(balance(bob, alice["USD"](57)));
// bob sends back more than sent
env(pay(bob, alice, bob["USD"](90)));
env.require(balance(bob, alice["USD"](-33)));
// alice sends to her limit
env(pay(alice, bob, bob["USD"](733)));
env.require(balance(bob, alice["USD"](700)));
// bob sends to his limit
env(pay(bob, alice, bob["USD"](1300)));
env.require(balance(bob, alice["USD"](-600)));
// bob sends past limit
env(pay(bob, alice, bob["USD"](1)), ter(tecPATH_DRY));
env.require(balance(bob, alice["USD"](-600)));
}
void
testWithTransferFee(bool subscribe, bool with_rate, FeatureBitset features)
{
testcase(
std::string("Direct Payment: ") +
(with_rate ? "With " : "Without ") + " Xfer Fee, " +
(subscribe ? "With " : "Without ") + " Subscribe");
using namespace test::jtx;
Env env{*this, features};
auto wsc = test::makeWSClient(env.app().config());
Account gw{"gateway"};
Account alice{"alice"};
Account bob{"bob"};
env.fund(XRP(10000), gw, alice, bob);
env.close();
env(trust(alice, gw["AUD"](100)));
env(trust(bob, gw["AUD"](100)));
env(pay(gw, alice, alice["AUD"](1)));
env.close();
env.require(balance(alice, gw["AUD"](1)));
// alice sends bob 1 AUD
env(pay(alice, bob, gw["AUD"](1)));
env.close();
env.require(balance(alice, gw["AUD"](0)));
env.require(balance(bob, gw["AUD"](1)));
env.require(balance(gw, bob["AUD"](-1)));
if (with_rate)
{
// set a transfer rate
env(rate(gw, 1.1));
env.close();
// bob sends alice 0.5 AUD with a max to spend
env(pay(bob, alice, gw["AUD"](0.5)), sendmax(gw["AUD"](0.55)));
}
else
{
// bob sends alice 0.5 AUD
env(pay(bob, alice, gw["AUD"](0.5)));
}
env.require(balance(alice, gw["AUD"](0.5)));
env.require(balance(bob, gw["AUD"](with_rate ? 0.45 : 0.5)));
env.require(balance(gw, bob["AUD"](with_rate ? -0.45 : -0.5)));
if (subscribe)
{
Json::Value jvs;
jvs[jss::accounts] = Json::arrayValue;
jvs[jss::accounts].append(gw.human());
jvs[jss::streams] = Json::arrayValue;
jvs[jss::streams].append("transactions");
jvs[jss::streams].append("ledger");
auto jv = wsc->invoke("subscribe", jvs);
BEAST_EXPECT(jv[jss::status] == "success");
env.close();
using namespace std::chrono_literals;
BEAST_EXPECT(wsc->findMsg(5s, [](auto const& jval) {
auto const& t = jval[jss::transaction];
return t[jss::TransactionType] == jss::Payment;
}));
BEAST_EXPECT(wsc->findMsg(5s, [](auto const& jval) {
return jval[jss::type] == "ledgerClosed";
}));
BEAST_EXPECT(
wsc->invoke("unsubscribe", jv)[jss::status] == "success");
}
}
void
testWithPath(FeatureBitset features)
{
testcase("Payments With Paths and Fees");
using namespace test::jtx;
Env env{*this, features};
Account gw{"gateway"};
Account alice{"alice"};
Account bob{"bob"};
env.fund(XRP(10000), gw, alice, bob);
env.close();
// set a transfer rate
env(rate(gw, 1.1));
env(trust(alice, gw["AUD"](100)));
env(trust(bob, gw["AUD"](100)));
env(pay(gw, alice, alice["AUD"](4.4)));
env.require(balance(alice, gw["AUD"](4.4)));
// alice sends gw issues to bob with a max spend that allows for the
// xfer rate
env(pay(alice, bob, gw["AUD"](1)), sendmax(gw["AUD"](1.1)));
env.require(balance(alice, gw["AUD"](3.3)));
env.require(balance(bob, gw["AUD"](1)));
// alice sends bob issues to bob with a max spend
env(pay(alice, bob, bob["AUD"](1)), sendmax(gw["AUD"](1.1)));
env.require(balance(alice, gw["AUD"](2.2)));
env.require(balance(bob, gw["AUD"](2)));
// alice sends gw issues to bob with a max spend
env(pay(alice, bob, gw["AUD"](1)), sendmax(alice["AUD"](1.1)));
env.require(balance(alice, gw["AUD"](1.1)));
env.require(balance(bob, gw["AUD"](3)));
// alice sends bob issues to bob with a max spend in alice issues.
// expect fail since gw is not involved
env(pay(alice, bob, bob["AUD"](1)),
sendmax(alice["AUD"](1.1)),
ter(tecPATH_DRY));
env.require(balance(alice, gw["AUD"](1.1)));
env.require(balance(bob, gw["AUD"](3)));
}
void
testIndirect(FeatureBitset features)
{
testcase("Indirect Payment");
using namespace test::jtx;
Env env{*this, features};
Account gw{"gateway"};
Account alice{"alice"};
Account bob{"bob"};
env.fund(XRP(10000), gw, alice, bob);
env.close();
env(trust(alice, gw["USD"](600)));
env(trust(bob, gw["USD"](700)));
env(pay(gw, alice, alice["USD"](70)));
env(pay(gw, bob, bob["USD"](50)));
env.require(balance(alice, gw["USD"](70)));
env.require(balance(bob, gw["USD"](50)));
// alice sends more than has to issuer: 100 out of 70
env(pay(alice, gw, gw["USD"](100)), ter(tecPATH_PARTIAL));
// alice sends more than has to bob: 100 out of 70
env(pay(alice, bob, gw["USD"](100)), ter(tecPATH_PARTIAL));
env.close();
env.require(balance(alice, gw["USD"](70)));
env.require(balance(bob, gw["USD"](50)));
// send with an account path
env(pay(alice, bob, gw["USD"](5)), test::jtx::path(gw));
env.require(balance(alice, gw["USD"](65)));
env.require(balance(bob, gw["USD"](55)));
}
void
testIndirectMultiPath(bool with_rate, FeatureBitset features)
{
testcase(
std::string("Indirect Payment, Multi Path, ") +
(with_rate ? "With " : "Without ") + " Xfer Fee, ");
using namespace test::jtx;
Env env{*this, features};
Account gw{"gateway"};
Account amazon{"amazon"};
Account alice{"alice"};
Account bob{"bob"};
Account carol{"carol"};
env.fund(XRP(10000), gw, amazon, alice, bob, carol);
env.close();
env(trust(amazon, gw["USD"](2000)));
env(trust(bob, alice["USD"](600)));
env(trust(bob, gw["USD"](1000)));
env(trust(carol, alice["USD"](700)));
env(trust(carol, gw["USD"](1000)));
if (with_rate)
env(rate(gw, 1.1));
env(pay(gw, bob, bob["USD"](100)));
env(pay(gw, carol, carol["USD"](100)));
env.close();
// alice pays amazon via multiple paths
if (with_rate)
env(pay(alice, amazon, gw["USD"](150)),
sendmax(alice["USD"](200)),
test::jtx::path(bob),
test::jtx::path(carol));
else
env(pay(alice, amazon, gw["USD"](150)),
test::jtx::path(bob),
test::jtx::path(carol));
if (with_rate)
{
env.require(balance(
alice,
STAmount(
carol["USD"].issue(),
6500000000000000ull,
-14,
true,
STAmount::unchecked{})));
env.require(balance(carol, gw["USD"](35)));
}
else
{
env.require(balance(alice, carol["USD"](-50)));
env.require(balance(carol, gw["USD"](50)));
}
env.require(balance(alice, bob["USD"](-100)));
env.require(balance(amazon, gw["USD"](150)));
env.require(balance(bob, gw["USD"](0)));
}
void
testInvoiceID(FeatureBitset features)
{
testcase("Set Invoice ID on Payment");
using namespace test::jtx;
Env env{*this, features};
Account alice{"alice"};
auto wsc = test::makeWSClient(env.app().config());
env.fund(XRP(10000), alice);
env.close();
Json::Value jvs;
jvs[jss::accounts] = Json::arrayValue;
jvs[jss::accounts].append(env.master.human());
jvs[jss::streams] = Json::arrayValue;
jvs[jss::streams].append("transactions");
BEAST_EXPECT(wsc->invoke("subscribe", jvs)[jss::status] == "success");
char const* invoiceid =
"243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89";
Json::Value jv;
auto tx = env.jt(
pay(env.master, alice, XRP(10000)),
json(sfInvoiceID.fieldName, invoiceid));
jv[jss::tx_blob] = strHex(tx.stx->getSerializer().slice());
auto jrr = wsc->invoke("submit", jv)[jss::result];
BEAST_EXPECT(jrr[jss::status] == "success");
BEAST_EXPECT(jrr[jss::tx_json][sfInvoiceID.fieldName] == invoiceid);
env.close();
using namespace std::chrono_literals;
BEAST_EXPECT(wsc->findMsg(2s, [invoiceid](auto const& jval) {
auto const& t = jval[jss::transaction];
return t[jss::TransactionType] == jss::Payment &&
t[sfInvoiceID.fieldName] == invoiceid;
}));
BEAST_EXPECT(wsc->invoke("unsubscribe", jv)[jss::status] == "success");
}
public:
void
run() override
{
testTrustNonexistent();
testCreditLimit();
auto testWithFeatures = [this](FeatureBitset features) {
testPayNonexistent(features);
testDirectRipple(features);
testWithTransferFee(false, false, features);
testWithTransferFee(false, true, features);
testWithTransferFee(true, false, features);
testWithTransferFee(true, true, features);
testWithPath(features);
testIndirect(features);
testIndirectMultiPath(true, features);
testIndirectMultiPath(false, features);
testInvoiceID(features);
};
using namespace test::jtx;
auto const sa = testable_amendments();
testWithFeatures(sa - featurePermissionedDEX);
testWithFeatures(sa);
}
};
BEAST_DEFINE_TESTSUITE(TrustAndBalance, app, ripple);
} // namespace ripple