feat: XLS-68: Sponsor, #5887 continuation (#7350)

Co-authored-by: tequ <git@tequ.dev>
Co-authored-by: yinyiqian1 <yqian@ripple.com>
Co-authored-by: Mayukha Vadari <mvadari@ripple.com>
Co-authored-by: Mayukha Vadari <mvadari@gmail.com>
Co-authored-by: xrplf-ai-reviewer[bot] <266832837+xrplf-ai-reviewer[bot]@users.noreply.github.com>
Co-authored-by: Peter Chen <34582813+PeterChen13579@users.noreply.github.com>
Co-authored-by: Zhiyuan Wang <96991820+Kassaking7@users.noreply.github.com>
Co-authored-by: Ayaz Salikhov <asalikhov@ripple.com>
Co-authored-by: Ayaz Salikhov <mathbunnyru@users.noreply.github.com>
Co-authored-by: Zhiyuan Wang <1830604455@qq.com>
This commit is contained in:
Olek
2026-07-10 17:58:19 -04:00
committed by GitHub
parent c7adb215ed
commit fd2cc6dcb3
170 changed files with 13151 additions and 660 deletions

View File

@@ -9,8 +9,11 @@
#include <test/jtx/owners.h> // IWYU pragma: keep
#include <test/jtx/pay.h>
#include <test/jtx/permissioned_domains.h>
#include <test/jtx/sig.h>
#include <test/jtx/sponsor.h>
#include <test/jtx/ticket.h>
#include <test/jtx/token.h>
#include <test/jtx/trust.h>
#include <test/jtx/txflags.h>
#include <test/jtx/xchain_bridge.h>
@@ -22,6 +25,7 @@
#include <xrpl/json/to_string.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/jss.h>
@@ -200,6 +204,15 @@ public:
resp[jss::result][jss::error_message] ==
"Invalid field 'limit', not unsigned integer.");
}
// test error on sponsored param not a boolean
{
json::Value params;
params[jss::account] = bob.human();
params[jss::sponsored] = "true";
auto resp = env.rpc("json", "account_objects", to_string(params));
BEAST_EXPECT(
resp[jss::result][jss::error_message] == "Invalid field 'sponsored', not boolean.");
}
// test errors on marker
{
Account const gw{"G"};
@@ -605,6 +618,7 @@ public:
BEAST_EXPECT(acctObjsIsSize(acctObjs(gw, jss::amm), 0));
BEAST_EXPECT(acctObjsIsSize(acctObjs(gw, jss::did), 0));
BEAST_EXPECT(acctObjsIsSize(acctObjs(gw, jss::permissioned_domain), 0));
BEAST_EXPECT(acctObjsIsSize(acctObjs(gw, jss::sponsorship), 0));
// we expect invalid field type reported for the following types
BEAST_EXPECT(acctObjsTypeIsInvalid(acctObjs(gw, jss::amendments)));
@@ -926,6 +940,30 @@ public:
BEAST_EXPECT(ticket[sfTicketSequence.jsonName].asUInt() == seq + 1);
}
{
// Create a sponsorship
env(sponsor::set(alice, tfSponsorshipSetRequireSignForFee, 200, XRP(100), drops(10)),
sponsor::SponseeAcc(gw));
env.close();
// Find the sponsorship.
for (auto const& acct : {alice, gw})
{
json::Value const resp = acctObjs(acct, jss::sponsorship);
BEAST_EXPECT(acctObjsIsSize(resp, 1));
auto const& sponsorship = resp[jss::result][jss::account_objects][0u];
BEAST_EXPECT(sponsorship[sfOwner.jsonName] == alice.human());
BEAST_EXPECT(sponsorship[sfSponsee.jsonName] == gw.human());
BEAST_EXPECT(
sponsorship[sfFlags.jsonName].asUInt() == tfSponsorshipSetRequireSignForFee);
BEAST_EXPECT(sponsorship[sfRemainingOwnerCount.jsonName].asUInt() == 200);
BEAST_EXPECT(sponsorship[sfFeeAmount.jsonName].asUInt() == 100000000);
BEAST_EXPECT(sponsorship[sfMaxFee.jsonName].asUInt() == 10);
}
}
{
// See how "deletion_blockers_only" handles gw's directory.
json::Value params;
@@ -940,7 +978,8 @@ public:
jss::NFTokenPage.cStr(),
jss::RippleState.cStr(),
jss::PayChannel.cStr(),
jss::PermissionedDomain.cStr()};
jss::PermissionedDomain.cStr(),
jss::Sponsorship.cStr()};
std::ranges::sort(v);
return v;
}();
@@ -1350,6 +1389,233 @@ public:
}
}
void
testSponsoredFilter()
{
testcase("SponsoredFilter");
using namespace jtx;
Env env(*this, testableAmendments());
Account const alice("alice");
Account const bob("bob");
Account const sponsor1("sponsor1");
Account const gw("gw");
auto const usd = gw["USD"];
env.fund(XRP(10000), alice, bob, sponsor1, gw);
env.close();
// Helper to call account_objects with sponsored filter
auto acctObjsSponsored = [](Env& testEnv,
AccountID const& acct,
bool sponsored,
std::optional<json::StaticString> const& type = std::nullopt) {
json::Value params;
params[jss::account] = to_string(acct);
params[jss::sponsored] = sponsored;
if (type)
params[jss::type] = *type;
params[jss::ledger_index] = "validated";
return testEnv.rpc("json", "account_objects", to_string(params));
};
// Create a trust line for bob (not sponsored)
env(trust(bob, usd(1000)));
env.close();
// sponsored=true should not find any objects for bob (doesn't have any sponsored objects)
{
auto const resp = acctObjsSponsored(env, bob.id(), true);
auto const& objs = resp[jss::result][jss::account_objects];
BEAST_EXPECT(objs.size() == 0);
}
// Now sponsor bob's trust line
auto const trustId = keylet::trustLine(bob, gw, usd.currency);
if (!BEAST_EXPECT(env.le(trustId)))
return;
env(sponsor::transfer(bob, tfSponsorshipCreate, trustId.key),
sponsor::As(sponsor1, spfSponsorReserve),
Sig(sfSponsorSignature, sponsor1));
env.close();
// Verify trust line has sponsor field
{
auto const sle = env.le(trustId);
if (!BEAST_EXPECT(sle))
return;
BEAST_EXPECT(sle->isFieldPresent(sfHighSponsor) || sle->isFieldPresent(sfLowSponsor));
}
// sponsored=true on bob should include the sponsored trust line
{
auto const resp = acctObjsSponsored(env, bob.id(), true);
auto const& objs = resp[jss::result][jss::account_objects];
if (!BEAST_EXPECT(objs.size() == 1))
return;
auto const& obj = objs[0u];
BEAST_EXPECT(obj[sfLedgerEntryType.jsonName] == jss::RippleState);
BEAST_EXPECT(
obj.isMember(sfHighSponsor.jsonName) || obj.isMember(sfLowSponsor.jsonName));
}
// sponsored=false on bob should NOT include the sponsored trust line
{
auto const resp = acctObjsSponsored(env, bob.id(), false);
BEAST_EXPECT(resp[jss::result][jss::account_objects].size() == 0);
}
// A trust line sponsored on either side is classified as sponsored
// for both parties.
{
Env env(*this, testableAmendments());
Account const issuer("issuer");
Account const user("user");
Account const sponsor("sponsor");
auto const usd = issuer["USD"];
env.fund(XRP(10000), issuer, user, sponsor);
env.close();
env(trust(issuer, user["USD"](100)));
env.close();
env(trust(user, usd(100)));
env.close();
auto const trustId = keylet::trustLine(user, issuer, usd.currency);
if (!BEAST_EXPECT(env.le(trustId)))
return;
env(sponsor::transfer(user, tfSponsorshipCreate, trustId.key),
sponsor::As(sponsor, spfSponsorReserve),
Sig(sfSponsorSignature, sponsor));
env.close();
auto const line = env.le(trustId);
if (!BEAST_EXPECT(line))
return;
auto const userIsHigh = line->getFieldAmount(sfHighLimit).getIssuer() == user.id();
auto const& userSponsorField = userIsHigh ? sfHighSponsor : sfLowSponsor;
auto const& issuerSponsorField = userIsHigh ? sfLowSponsor : sfHighSponsor;
BEAST_EXPECT(line->isFieldPresent(userSponsorField));
BEAST_EXPECT(!line->isFieldPresent(issuerSponsorField));
{
auto const resp = acctObjsSponsored(env, user.id(), true, jss::state);
auto const& objs = resp[jss::result][jss::account_objects];
if (BEAST_EXPECT(objs.size() == 1))
BEAST_EXPECT(objs[0u][sfLedgerEntryType.jsonName] == jss::RippleState);
}
{
auto const resp = acctObjsSponsored(env, user.id(), false, jss::state);
auto const& objs = resp[jss::result][jss::account_objects];
BEAST_EXPECT(objs.size() == 0);
}
{
auto const resp = acctObjsSponsored(env, issuer.id(), true, jss::state);
auto const& objs = resp[jss::result][jss::account_objects];
if (BEAST_EXPECT(objs.size() == 1))
BEAST_EXPECT(objs[0u][sfLedgerEntryType.jsonName] == jss::RippleState);
}
{
auto const resp = acctObjsSponsored(env, issuer.id(), false, jss::state);
auto const& objs = resp[jss::result][jss::account_objects];
BEAST_EXPECT(objs.size() == 0);
}
}
// A sponsored Check is classified as sponsored in both the writer's
// and the destination's results.
{
Env env(*this, testableAmendments());
Account const owner("owner");
Account const dest("dest");
Account const sponsor("sponsor");
env.fund(XRP(10000), owner, dest, sponsor);
env.close();
auto const checkSeq = env.seq(owner);
env(check::create(owner, dest, XRP(1)));
env.close();
auto const checkId = keylet::check(owner, checkSeq);
if (!BEAST_EXPECT(env.le(checkId)))
return;
env(sponsor::transfer(owner, tfSponsorshipCreate, checkId.key),
sponsor::As(sponsor, spfSponsorReserve),
Sig(sfSponsorSignature, sponsor));
env.close();
{
auto const sle = env.le(checkId);
if (!BEAST_EXPECT(sle))
return;
BEAST_EXPECT(sle->isFieldPresent(sfSponsor));
}
for (auto const& acct : {owner.id(), dest.id()})
{
{
auto const resp = acctObjsSponsored(env, acct, true, jss::check);
auto const& objs = resp[jss::result][jss::account_objects];
if (BEAST_EXPECT(objs.size() == 1))
BEAST_EXPECT(objs[0u][sfLedgerEntryType.jsonName] == jss::Check);
}
{
auto const resp = acctObjsSponsored(env, acct, false, jss::check);
BEAST_EXPECT(resp[jss::result][jss::account_objects].size() == 0);
}
}
}
// A Sponsorship object is visible to both sides.
{
Env env(*this, testableAmendments());
Account const owner("owner");
Account const sponsee("sponsee");
env.fund(XRP(10000), owner, sponsee);
env.close();
env(sponsor::set(owner, 0, 100, XRP(100)), sponsor::SponseeAcc(sponsee));
env.close();
auto const sponsorshipKeylet = keylet::sponsorship(owner, sponsee);
if (!BEAST_EXPECT(env.le(sponsorshipKeylet)))
return;
{
auto const resp = acctObjsSponsored(env, owner.id(), false, jss::sponsorship);
auto const& objs = resp[jss::result][jss::account_objects];
if (BEAST_EXPECT(objs.size() == 1))
BEAST_EXPECT(objs[0u][sfLedgerEntryType.jsonName] == jss::Sponsorship);
}
{
auto const resp = acctObjsSponsored(env, sponsee.id(), false, jss::sponsorship);
auto const& objs = resp[jss::result][jss::account_objects];
if (BEAST_EXPECT(objs.size() == 1))
BEAST_EXPECT(objs[0u][sfLedgerEntryType.jsonName] == jss::Sponsorship);
}
{
auto const resp = acctObjsSponsored(env, owner.id(), true, jss::sponsorship);
auto const& objs = resp[jss::result][jss::account_objects];
BEAST_EXPECT(objs.size() == 0);
}
{
auto const resp = acctObjsSponsored(env, sponsee.id(), true, jss::sponsorship);
auto const& objs = resp[jss::result][jss::account_objects];
BEAST_EXPECT(objs.size() == 0);
}
}
}
void
run() override
{
@@ -1360,6 +1626,7 @@ public:
testNFTsMarker();
testAccountNFTs();
testAccountObjectMarker();
testSponsoredFilter();
}
};

View File

@@ -15,6 +15,7 @@
#include <test/jtx/pay.h>
#include <test/jtx/regkey.h>
#include <test/jtx/sig.h>
#include <test/jtx/sponsor.h>
#include <test/jtx/ter.h>
#include <test/jtx/ticket.h>
#include <test/jtx/trust.h>
@@ -773,7 +774,7 @@ class AccountTx_test : public beast::unit_test::Suite
// All it takes is a large enough XRP payment to resurrect
// becky's account. Try too small a payment.
env(pay(alice, becky, drops(env.current()->fees().accountReserve(0)) - drops(1)),
env(pay(alice, becky, drops(env.current()->fees().accountReserve(0, 1)) - drops(1)),
Ter(tecNO_DST_INSUF_XRP));
env.close();
@@ -888,6 +889,82 @@ class AccountTx_test : public beast::unit_test::Suite
checkAliceAcctTx(9, jss::Payment);
}
void
testSponsorship()
{
// test all sponsored transactions are in sponsor and sponsee's account
// tx list
testcase("Sponsorship");
using namespace test::jtx;
Env env(*this);
Account const alice("alice");
Account const sponsor("sponsor");
Account const sponsor2("sponsor2");
env.fund(XRP(10000), alice, sponsor, sponsor2);
env.close();
// check the latest sponsorship-related txn is in account tx list
auto const checkTx = [&](Account const& account, json::StaticString txType) {
json::Value params;
params[jss::account] = account.human();
params[jss::limit] = 100;
auto const jv = env.rpc("json", "account_tx", to_string(params))[jss::result];
auto const& tx0(jv[jss::transactions][0u][jss::tx]);
BEAST_EXPECT(tx0[jss::TransactionType] == txType);
std::string const txHash{
env.tx()->getJson(JsonOptions::Values::None)[jss::hash].asString()};
BEAST_EXPECT(tx0[jss::hash] == txHash);
};
// fee sponsorship
env(noop(alice), sponsor::As(sponsor, spfSponsorFee), Sig(sfSponsorSignature, sponsor));
env.close();
checkTx(alice, jss::AccountSet);
checkTx(sponsor, jss::AccountSet);
// set sponsor
env(sponsor::set(sponsor, 0, 100, XRP(100)), sponsor::SponseeAcc(alice), Ter(tesSUCCESS));
env.close();
checkTx(alice, jss::SponsorshipSet);
checkTx(sponsor, jss::SponsorshipSet);
// create an object with sponsor
auto const checkId = keylet::check(alice, env.seq(alice)).key;
env(check::create(alice, sponsor, XRP(1)), sponsor::As(sponsor, spfSponsorReserve));
env.close();
checkTx(alice, jss::CheckCreate);
checkTx(sponsor, jss::CheckCreate);
// transfer object sponsorship
env(sponsor::transfer(alice, tfSponsorshipReassign, checkId),
sponsor::As(sponsor2, spfSponsorReserve),
Sig(sfSponsorSignature, sponsor2));
env.close();
checkTx(alice, jss::SponsorshipTransfer);
checkTx(sponsor, jss::SponsorshipTransfer);
checkTx(sponsor2, jss::SponsorshipTransfer);
// delete the sponsored object
env(check::cancel(alice, checkId),
sponsor::As(sponsor, spfSponsorFee),
Sig(sfSponsorSignature, sponsor));
env.close();
checkTx(alice, jss::CheckCancel);
checkTx(sponsor, jss::CheckCancel);
checkTx(sponsor2, jss::CheckCancel);
// account sponsorship
env(sponsor::transfer(alice, tfSponsorshipCreate),
sponsor::As(sponsor, spfSponsorReserve),
Sig(sfSponsorSignature, sponsor));
env.close();
checkTx(alice, jss::SponsorshipTransfer);
checkTx(sponsor, jss::SponsorshipTransfer);
}
public:
void
run() override
@@ -896,6 +973,7 @@ public:
testContents();
testAccountDelete();
testMPT();
testSponsorship();
}
};
BEAST_DEFINE_TESTSUITE(AccountTx, rpc, xrpl);

View File

@@ -15,6 +15,7 @@
#include <test/jtx/offer.h>
#include <test/jtx/pay.h>
#include <test/jtx/permissioned_domains.h>
#include <test/jtx/sponsor.h>
#include <test/jtx/ticket.h>
#include <test/jtx/token.h>
#include <test/jtx/txflags.h>
@@ -93,6 +94,8 @@ std::vector<std::pair<json::StaticString, FieldType>> gMappings{
{jss::oracle_document_id, FieldType::UInt32Field},
{jss::owner, FieldType::AccountField},
{jss::seq, FieldType::UInt32Field},
{jss::sponsor, FieldType::AccountField},
{jss::sponsee, FieldType::AccountField},
{jss::subject, FieldType::AccountField},
{jss::ticket_seq, FieldType::UInt32Field},
};
@@ -107,7 +110,7 @@ getFieldType(json::StaticString fieldName)
return it->second;
}
Throw<std::runtime_error>("`mappings` is missing field " + std::string(fieldName.cStr()));
Throw<std::runtime_error>("`gMappings` is missing field " + std::string(fieldName.cStr()));
}
std::string
@@ -1884,6 +1887,59 @@ class LedgerEntry_test : public beast::unit_test::Suite
runLedgerEntryTest(env, jss::signer_list);
}
void
testSponsorship()
{
testcase("Sponsorship");
using namespace test::jtx;
Env env{*this};
Account const alice{"alice"};
Account const bob{"bob"};
env.fund(XRP(10000), alice, bob);
env.close();
env(sponsor::set(alice, 0, 100), sponsor::SponseeAcc(bob));
env.close();
std::string const ledgerHash{to_string(env.closed()->header().hash)};
auto const sponsorshipIndex = to_string(keylet::sponsorship(alice.id(), bob.id()).key);
{
// Request by sponsor and sponsee.
json::Value jvParams;
jvParams[jss::sponsorship][jss::sponsor] = alice.human();
jvParams[jss::sponsorship][jss::sponsee] = bob.human();
jvParams[jss::ledger_hash] = ledgerHash;
auto const jrr = env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
BEAST_EXPECT(jrr[jss::node][sfLedgerEntryType.jsonName] == jss::Sponsorship);
BEAST_EXPECT(jrr[jss::node][sfOwner.jsonName] == alice.human());
BEAST_EXPECT(jrr[jss::node][sfSponsee.jsonName] == bob.human());
BEAST_EXPECT(sponsorshipIndex == jrr[jss::node][jss::index].asString());
}
{
// Request by index.
json::Value jvParams;
jvParams[jss::sponsorship] = sponsorshipIndex;
jvParams[jss::ledger_hash] = ledgerHash;
json::Value const jrr =
env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result];
BEAST_EXPECT(jrr[jss::node][sfLedgerEntryType.jsonName] == jss::Sponsorship);
BEAST_EXPECT(jrr[jss::node][sfOwner.jsonName] == alice.human());
BEAST_EXPECT(jrr[jss::node][sfSponsee.jsonName] == bob.human());
BEAST_EXPECT(sponsorshipIndex == jrr[jss::node][jss::index].asString());
}
{
// Check all malformed cases.
runLedgerEntryTest(
env,
jss::sponsorship,
{
{.fieldName = jss::sponsor, .malformedErrorMsg = "malformedSponsor"},
{.fieldName = jss::sponsee, .malformedErrorMsg = "malformedSponsee"},
});
}
}
void
testTicket()
{
@@ -2676,6 +2732,7 @@ public:
testPayChan();
testRippleState();
testSignerList();
testSponsorship();
testTicket();
testDID();
testInvalidOracleLedgerEntry();

View File

@@ -364,6 +364,37 @@ class Simulate_test : public beast::unit_test::Suite
auto const resp = env.rpc("json", "simulate", to_string(params));
BEAST_EXPECT(resp[jss::result][jss::error_message] == "Invalid field 'tx.Signers[0]'.");
}
{
// Non-object SponsorSignature field
json::Value params;
json::Value txJson = json::ValueType::Object;
txJson[jss::TransactionType] = jss::AccountSet;
txJson[jss::Account] = env.master.human();
txJson[sfSponsorSignature] = "";
params[jss::tx_json] = txJson;
auto const resp = env.rpc("json", "simulate", to_string(params));
BEAST_EXPECT(
resp[jss::result][jss::error_message] ==
"Invalid field 'SponsorSignature', not object.");
}
{
// Invalid SponsorSignature.Signers field
json::Value params;
json::Value txJson = json::ValueType::Object;
txJson[jss::TransactionType] = jss::AccountSet;
txJson[jss::Account] = env.master.human();
json::Value sponsorSignature = json::ValueType::Object;
sponsorSignature[sfSigners] = "1";
txJson[sfSponsorSignature] = sponsorSignature;
params[jss::tx_json] = txJson;
auto const resp = env.rpc("json", "simulate", to_string(params));
BEAST_EXPECTS(
resp[jss::result][jss::error_message] ==
"Invalid field 'tx.SponsorSignature.Signers'.",
resp.toStyledString());
}
{
// Invalid transaction
json::Value params;
@@ -561,6 +592,75 @@ class Simulate_test : public beast::unit_test::Suite
// test without autofill
testTx(env, tx, validateOutput);
}
{
// autofill sponsor signature
auto validateOutput = [&](json::Value const& resp, json::Value const& tx) {
auto result = resp[jss::result];
checkBasicReturnValidity(
result, tx, env.seq(env.master), env.current()->fees().base);
BEAST_EXPECT(result[jss::engine_result] == "tesSUCCESS");
BEAST_EXPECT(result[jss::engine_result_code] == 0);
BEAST_EXPECT(
result[jss::engine_result_message] ==
"The simulated transaction would have been applied.");
if (BEAST_EXPECT(result.isMember(jss::meta) || result.isMember(jss::meta_blob)))
{
json::Value const metadata = getJsonMetadata(result);
if (BEAST_EXPECT(metadata.isMember(sfAffectedNodes.jsonName)))
{
BEAST_EXPECT(metadata[sfAffectedNodes.jsonName].size() == 2);
auto node = metadata[sfAffectedNodes.jsonName][0u];
if (BEAST_EXPECT(node.isMember(sfModifiedNode.jsonName)))
{
auto modifiedNode = node[sfModifiedNode];
BEAST_EXPECT(modifiedNode[sfLedgerEntryType] == "AccountRoot");
auto previousFields = modifiedNode[sfPreviousFields];
BEAST_EXPECT(!previousFields.isMember(sfBalance.jsonName));
}
auto node2 = metadata[sfAffectedNodes.jsonName][1u];
if (BEAST_EXPECT(node2.isMember(sfModifiedNode.jsonName)))
{
auto modifiedNode = node2[sfModifiedNode];
BEAST_EXPECT(modifiedNode[sfLedgerEntryType] == "AccountRoot");
auto previousFields = modifiedNode[sfPreviousFields];
BEAST_EXPECT(previousFields.isMember(sfBalance.jsonName));
}
}
BEAST_EXPECT(metadata[sfTransactionIndex.jsonName] == 0);
BEAST_EXPECT(metadata[sfTransactionResult.jsonName] == "tesSUCCESS");
}
};
Account const sponsor("sponsor");
env.fund(XRP(10000), sponsor);
env.close();
json::Value tx;
tx[jss::Account] = env.master.human();
tx[jss::TransactionType] = jss::AccountSet;
tx[sfDomain.jsonName] = kNewDomain;
tx[sfSponsor.jsonName] = sponsor.human();
tx[sfSponsorFlags.jsonName] = spfSponsorFee;
tx[sfSponsorSignature.jsonName] = json::ValueType::Object;
// test with autofill
testTx(env, tx, validateOutput);
tx[sfSponsorSignature.jsonName][sfTxnSignature.jsonName] = "";
tx[sfSponsorSignature.jsonName][sfSigningPubKey.jsonName] = "";
// test without autofill
testTx(env, tx, validateOutput);
}
}
void
@@ -763,6 +863,58 @@ class Simulate_test : public beast::unit_test::Suite
}
}
void
testSuccessfulSponsoredTransactionMultisigned()
{
testcase("Successful sponsored multi-signed transaction");
using namespace jtx;
Env env(*this);
Account const sponsor("sponsor");
Account const signer("signer");
env.fund(XRP(10000), sponsor, signer);
env.close();
env(signers(sponsor, 1, {{signer, 1}}));
env.close();
auto validateOutput = [&](json::Value const& resp, json::Value const& tx) {
auto const result = resp[jss::result];
// Verifies Fee autofill counts nested sponsor-signature signers.
auto const expectedFee = env.current()->fees().base * 2;
checkBasicReturnValidity(result, tx, env.seq(env.master), expectedFee);
BEAST_EXPECT(result[jss::engine_result] == "tesSUCCESS");
BEAST_EXPECT(result[jss::engine_result_code] == 0);
BEAST_EXPECT(
result[jss::engine_result_message] ==
"The simulated transaction would have been applied.");
if (BEAST_EXPECT(result.isMember(jss::meta) || result.isMember(jss::meta_blob)))
{
json::Value const metadata = getJsonMetadata(result);
BEAST_EXPECT(metadata[sfTransactionResult.jsonName] == "tesSUCCESS");
}
};
json::Value tx;
tx[jss::Account] = env.master.human();
tx[jss::TransactionType] = jss::AccountSet;
tx[sfDomain] = "123ABC";
tx[sfSponsor.jsonName] = sponsor.human();
tx[sfSponsorFlags.jsonName] = spfSponsorFee;
tx[sfSponsorSignature.jsonName] = json::ValueType::Object;
tx[sfSponsorSignature.jsonName][sfSigners.jsonName] = json::ValueType::Array;
json::Value signerObj;
signerObj[sfSigner][jss::Account] = signer.human();
tx[sfSponsorSignature.jsonName][sfSigners.jsonName].append(signerObj);
// Leave Fee unset so simulate must autofill it after sponsor signer normalization.
BEAST_EXPECT(!tx.isMember(jss::Fee));
testTx(env, tx, validateOutput, false);
}
void
testTransactionSigningFailure()
{
@@ -1195,6 +1347,7 @@ public:
testTransactionNonTecFailure();
testTransactionTecFailure();
testSuccessfulTransactionMultisigned();
testSuccessfulSponsoredTransactionMultisigned();
testTransactionSigningFailure();
testInvalidSingleAndMultiSigningTransaction();
testMultisignedBadPubKey();