mirror of
https://github.com/XRPLF/rippled.git
synced 2026-07-30 10:30:22 +00:00
Merge branch 'mvadari/sponsor/apply-view-context' into mvadari/sponsor/reserve-context
This commit is contained in:
@@ -201,7 +201,7 @@ canWithdraw(ReadView const& view, STTx const& tx);
|
||||
|
||||
[[nodiscard]] TER
|
||||
doWithdraw(
|
||||
ApplyViewContext& ctx,
|
||||
ApplyViewContext ctx,
|
||||
AccountID const& senderAcct,
|
||||
AccountID const& dstAcct,
|
||||
AccountID const& sourceAcct,
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace xrpl {
|
||||
template <ValidIssueType T>
|
||||
TER
|
||||
escrowUnlockApplyHelper(
|
||||
ApplyViewContext& ctx,
|
||||
ApplyViewContext ctx,
|
||||
Rate lockedRate,
|
||||
SLE::ref sleDest,
|
||||
STAmount const& xrpBalance,
|
||||
@@ -31,7 +31,7 @@ escrowUnlockApplyHelper(
|
||||
template <>
|
||||
inline TER
|
||||
escrowUnlockApplyHelper<Issue>(
|
||||
ApplyViewContext& ctx,
|
||||
ApplyViewContext ctx,
|
||||
Rate lockedRate,
|
||||
SLE::ref sleDest,
|
||||
STAmount const& xrpBalance,
|
||||
@@ -168,7 +168,7 @@ escrowUnlockApplyHelper<Issue>(
|
||||
template <>
|
||||
inline TER
|
||||
escrowUnlockApplyHelper<MPTIssue>(
|
||||
ApplyViewContext& ctx,
|
||||
ApplyViewContext ctx,
|
||||
Rate lockedRate,
|
||||
SLE::ref sleDest,
|
||||
STAmount const& xrpBalance,
|
||||
|
||||
@@ -71,7 +71,7 @@ canAddHolding(ReadView const& view, MPTIssue const& mptIssue);
|
||||
|
||||
[[nodiscard]] TER
|
||||
authorizeMPToken(
|
||||
ApplyViewContext& ctx,
|
||||
ApplyViewContext ctx,
|
||||
XRPAmount const& priorBalance,
|
||||
MPTID const& mptIssuanceID,
|
||||
AccountID const& account,
|
||||
@@ -102,7 +102,7 @@ requireAuth(
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
enforceMPTokenAuthorization(
|
||||
ApplyViewContext& ctx,
|
||||
ApplyViewContext ctx,
|
||||
MPTID const& mptIssuanceID,
|
||||
AccountID const& account,
|
||||
XRPAmount const& priorBalance,
|
||||
@@ -188,7 +188,7 @@ canMPTTradeAndTransfer(
|
||||
|
||||
[[nodiscard]] TER
|
||||
addEmptyHolding(
|
||||
ApplyViewContext& ctx,
|
||||
ApplyViewContext ctx,
|
||||
AccountID const& accountID,
|
||||
XRPAmount priorBalance,
|
||||
MPTIssue const& mptIssue,
|
||||
@@ -196,7 +196,7 @@ addEmptyHolding(
|
||||
|
||||
[[nodiscard]] TER
|
||||
removeEmptyHolding(
|
||||
ApplyViewContext& ctx,
|
||||
ApplyViewContext ctx,
|
||||
AccountID const& accountID,
|
||||
MPTIssue const& mptIssue,
|
||||
beast::Journal journal);
|
||||
|
||||
@@ -229,7 +229,7 @@ canTransfer(ReadView const& view, Issue const& issue, AccountID const& from, Acc
|
||||
/// canAddHolding() in preflight with the same View and Asset
|
||||
[[nodiscard]] TER
|
||||
addEmptyHolding(
|
||||
ApplyViewContext& ctx,
|
||||
ApplyViewContext ctx,
|
||||
AccountID const& accountID,
|
||||
XRPAmount priorBalance,
|
||||
Issue const& issue,
|
||||
@@ -237,7 +237,7 @@ addEmptyHolding(
|
||||
|
||||
[[nodiscard]] TER
|
||||
removeEmptyHolding(
|
||||
ApplyViewContext& ctx,
|
||||
ApplyViewContext ctx,
|
||||
AccountID const& accountID,
|
||||
Issue const& issue,
|
||||
beast::Journal journal);
|
||||
|
||||
@@ -24,14 +24,6 @@ isReserveSponsored(STTx const& tx)
|
||||
return (tx.getFieldU32(sfSponsorFlags) & spfSponsorReserve) != 0u;
|
||||
}
|
||||
|
||||
inline bool
|
||||
isSponsorReserveCoSigning(STTx const& tx)
|
||||
{
|
||||
if (!tx.isFieldPresent(sfSponsorSignature))
|
||||
return false;
|
||||
return isReserveSponsored(tx);
|
||||
}
|
||||
|
||||
inline std::optional<AccountID>
|
||||
getTxReserveSponsorAccountID(STTx const& tx)
|
||||
{
|
||||
@@ -43,7 +35,7 @@ getTxReserveSponsorAccountID(STTx const& tx)
|
||||
}
|
||||
|
||||
inline std::expected<SLE::pointer, TER>
|
||||
getTxReserveSponsor(ApplyViewContext& ctx)
|
||||
getTxReserveSponsor(ApplyViewContext ctx)
|
||||
{
|
||||
auto const sponsorID = getTxReserveSponsorAccountID(ctx.tx);
|
||||
if (sponsorID)
|
||||
|
||||
@@ -230,7 +230,7 @@ canAddHolding(ReadView const& view, Asset const& asset);
|
||||
|
||||
[[nodiscard]] TER
|
||||
addEmptyHolding(
|
||||
ApplyViewContext& ctx,
|
||||
ApplyViewContext ctx,
|
||||
AccountID const& accountID,
|
||||
XRPAmount priorBalance,
|
||||
Asset const& asset,
|
||||
@@ -238,7 +238,7 @@ addEmptyHolding(
|
||||
|
||||
[[nodiscard]] TER
|
||||
removeEmptyHolding(
|
||||
ApplyViewContext& ctx,
|
||||
ApplyViewContext ctx,
|
||||
AccountID const& accountID,
|
||||
Asset const& asset,
|
||||
beast::Journal journal);
|
||||
|
||||
@@ -112,11 +112,11 @@ public:
|
||||
TER
|
||||
checkInvariants(TER const result, XRPAmount const fee);
|
||||
|
||||
ApplyViewContext&
|
||||
ApplyViewContext
|
||||
getApplyViewContext()
|
||||
{
|
||||
XRPL_ASSERT(view_.has_value() && (&viewCtx_.view == &*view_), "Previous view discarded");
|
||||
return viewCtx_;
|
||||
XRPL_ASSERT(view_.has_value(), "Previous view exists");
|
||||
return {.view = *view_, .tx = tx};
|
||||
}
|
||||
|
||||
private:
|
||||
@@ -133,11 +133,6 @@ private:
|
||||
|
||||
// The ID of the batch transaction we are executing under, if seated.
|
||||
std::optional<uint256 const> parentBatchId_;
|
||||
|
||||
// A temporary helper object that passes around ApplyContext info
|
||||
// Only necessary (for now) because the ApplyContext can't be passed into helpers due to
|
||||
// levelization
|
||||
ApplyViewContext viewCtx_;
|
||||
};
|
||||
|
||||
} // namespace xrpl
|
||||
|
||||
@@ -63,7 +63,7 @@ public:
|
||||
beast::Journal const& j) override;
|
||||
|
||||
static std::expected<MPTID, TER>
|
||||
create(ApplyViewContext& ctx, beast::Journal journal, MPTCreateArgs const& args);
|
||||
create(ApplyViewContext ctx, beast::Journal journal, MPTCreateArgs const& args);
|
||||
};
|
||||
|
||||
} // namespace xrpl
|
||||
|
||||
@@ -432,7 +432,7 @@ canWithdraw(ReadView const& view, STTx const& tx)
|
||||
|
||||
TER
|
||||
doWithdraw(
|
||||
ApplyViewContext& ctx,
|
||||
ApplyViewContext ctx,
|
||||
AccountID const& senderAcct,
|
||||
AccountID const& dstAcct,
|
||||
AccountID const& sourceAcct,
|
||||
|
||||
@@ -369,14 +369,14 @@ checkInsufficientReserve(
|
||||
{
|
||||
if (sponsorSle)
|
||||
{
|
||||
auto const isCoSigning = isSponsorReserveCoSigning(tx);
|
||||
|
||||
auto const sle = view.read(
|
||||
keylet::sponsorship(
|
||||
sponsorSle->getAccountID(sfAccount), accSle->getAccountID(sfAccount)));
|
||||
|
||||
// prefunded sponsor should have a sponsorship entry
|
||||
if (!isCoSigning && !sle)
|
||||
// A reserve-sponsored tx must carry a sponsor signature
|
||||
// (cosigning path) and/or have a pre-existing sponsorship SLE
|
||||
// (prefunded path). Absence of both is an internal invariant break.
|
||||
if (isReserveSponsored(tx) && !sle && !tx.isFieldPresent(sfSponsorSignature))
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
if (sle)
|
||||
|
||||
@@ -125,7 +125,7 @@ canAddHolding(ReadView const& view, MPTIssue const& mptIssue)
|
||||
|
||||
[[nodiscard]] TER
|
||||
addEmptyHolding(
|
||||
ApplyViewContext& ctx,
|
||||
ApplyViewContext ctx,
|
||||
AccountID const& accountID,
|
||||
XRPAmount priorBalance,
|
||||
MPTIssue const& mptIssue,
|
||||
@@ -148,7 +148,7 @@ addEmptyHolding(
|
||||
|
||||
[[nodiscard]] TER
|
||||
authorizeMPToken(
|
||||
ApplyViewContext& ctx,
|
||||
ApplyViewContext ctx,
|
||||
XRPAmount const& priorBalance,
|
||||
MPTID const& mptIssuanceID,
|
||||
AccountID const& account,
|
||||
@@ -193,17 +193,16 @@ authorizeMPToken(
|
||||
// - create the MPToken object for the holder
|
||||
|
||||
auto const isSponsored = ctx.reserveContext.isSponsored();
|
||||
auto const isSponsoredAndPreFunded = isSponsored && !isSponsorReserveCoSigning(ctx.tx);
|
||||
|
||||
// The reserve that is required to create the MPToken. Note
|
||||
// that although the reserve increases with every item
|
||||
// an account owns, in the case of MPTokens we only
|
||||
// *enforce* a reserve if the user owns more than two
|
||||
// items. This is similar to the reserve requirements of trust lines.
|
||||
// If PreFunded Sponsor, it must be checked whether sufficient
|
||||
// ReserveCount exists.
|
||||
if (ownerCount(view, isSponsored ? ctx.reserveContext.sponsorSle : sleAcct, journal) >= 2 ||
|
||||
isSponsoredAndPreFunded)
|
||||
// The "free-tier" shortcut (ownerCount < 2) does not apply once a sponsor is on
|
||||
// the tx — the sponsor must always cover the reserve (via balance or prefunded
|
||||
// budget), so this check always runs for sponsored transactions.
|
||||
if (isSponsored || ownerCount(view, sleAcct, journal) >= 2)
|
||||
{
|
||||
if (auto const ret = checkInsufficientReserve(
|
||||
view,
|
||||
@@ -285,7 +284,7 @@ authorizeMPToken(
|
||||
|
||||
[[nodiscard]] TER
|
||||
removeEmptyHolding(
|
||||
ApplyViewContext& ctx,
|
||||
ApplyViewContext ctx,
|
||||
AccountID const& accountID,
|
||||
MPTIssue const& mptIssue,
|
||||
beast::Journal journal)
|
||||
@@ -417,7 +416,7 @@ requireAuth(
|
||||
|
||||
[[nodiscard]] TER
|
||||
enforceMPTokenAuthorization(
|
||||
ApplyViewContext& ctx,
|
||||
ApplyViewContext ctx,
|
||||
MPTID const& mptIssuanceID,
|
||||
AccountID const& account,
|
||||
XRPAmount const& priorBalance, // for MPToken authorization
|
||||
|
||||
@@ -633,7 +633,7 @@ canTransfer(ReadView const& view, Issue const& issue, AccountID const& from, Acc
|
||||
|
||||
TER
|
||||
addEmptyHolding(
|
||||
ApplyViewContext& ctx,
|
||||
ApplyViewContext ctx,
|
||||
AccountID const& accountID,
|
||||
XRPAmount priorBalance,
|
||||
Issue const& issue,
|
||||
@@ -699,7 +699,7 @@ addEmptyHolding(
|
||||
|
||||
TER
|
||||
removeEmptyHolding(
|
||||
ApplyViewContext& ctx,
|
||||
ApplyViewContext ctx,
|
||||
AccountID const& accountID,
|
||||
Issue const& issue,
|
||||
beast::Journal journal)
|
||||
|
||||
@@ -473,7 +473,7 @@ canAddHolding(ReadView const& view, Asset const& asset)
|
||||
|
||||
TER
|
||||
addEmptyHolding(
|
||||
ApplyViewContext& ctx,
|
||||
ApplyViewContext ctx,
|
||||
AccountID const& accountID,
|
||||
XRPAmount priorBalance,
|
||||
Asset const& asset,
|
||||
@@ -488,7 +488,7 @@ addEmptyHolding(
|
||||
|
||||
TER
|
||||
removeEmptyHolding(
|
||||
ApplyViewContext& ctx,
|
||||
ApplyViewContext ctx,
|
||||
AccountID const& accountID,
|
||||
Asset const& asset,
|
||||
beast::Journal journal)
|
||||
|
||||
@@ -40,13 +40,12 @@ ApplyContext::ApplyContext(
|
||||
, journal(journal)
|
||||
, base_(base)
|
||||
, flags_(flags)
|
||||
, view_{std::in_place, &base_, flags_}
|
||||
, parentBatchId_(parentBatchId)
|
||||
, viewCtx_{.view = *view_, .tx = tx, .reserveContext = ReserveContext::makeFromTx(*view_, tx)}
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
parentBatchId.has_value() == ((flags_ & TapBatch) == TapBatch),
|
||||
"Parent Batch ID should be set if batch apply flag is set");
|
||||
view_.emplace(&base_, flags_);
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/scope.h>
|
||||
#include <xrpl/core/ServiceRegistry.h>
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/PaymentSandbox.h>
|
||||
#include <xrpl/ledger/View.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
@@ -389,8 +388,7 @@ CheckCash::doApply()
|
||||
STAmount const flowDeliver{
|
||||
optDeliverMin ? maxDeliverMin() : ctx_.tx.getFieldAmount(sfAmount)};
|
||||
|
||||
ApplyViewContext ctx(psb, ctx_.tx);
|
||||
auto const sponsorSle = getTxReserveSponsor(ctx);
|
||||
auto const sponsorSle = getTxReserveSponsor({.view = psb, .tx = ctx_.tx});
|
||||
if (!sponsorSle)
|
||||
return sponsorSle.error(); // LCOV_EXCL_LINE
|
||||
|
||||
|
||||
@@ -104,7 +104,7 @@ MPTokenIssuanceCreate::preflight(PreflightContext const& ctx)
|
||||
|
||||
std::expected<MPTID, TER>
|
||||
MPTokenIssuanceCreate::create(
|
||||
ApplyViewContext& ctx,
|
||||
ApplyViewContext ctx,
|
||||
beast::Journal journal,
|
||||
MPTCreateArgs const& args)
|
||||
{
|
||||
|
||||
@@ -334,9 +334,9 @@ TrustSet::doApply()
|
||||
|
||||
std::uint32_t const uOwnerCount = ownerCount(view(), *sponsorSle ? *sponsorSle : sle, j_);
|
||||
|
||||
bool const isSponsoredAndPreFunded = *sponsorSle && !isSponsorReserveCoSigning(ctx_.tx);
|
||||
// If PreFunded Sponsor, it must be checked whether sufficient
|
||||
// ReserveCount exists.
|
||||
// The "free-tier" shortcut (ownerCount < 2) only applies when there is no sponsor.
|
||||
// With any sponsor on the tx, the sponsor must cover the reserve (via balance or
|
||||
// prefunded budget), so the reserve check always runs.
|
||||
bool const freeTrustLine = uOwnerCount < 2 && !*sponsorSle;
|
||||
|
||||
std::uint32_t const uQualityIn(bQualityIn ? ctx_.tx.getFieldU32(sfQualityIn) : 0);
|
||||
@@ -540,7 +540,7 @@ TrustSet::doApply()
|
||||
// calling adjustOwnerCount().
|
||||
if (auto const ret = checkInsufficientReserve(
|
||||
view(), ctx_.tx, sleLowAccount, preFeeBalance_, *sponsorSle, 1, 0, j_);
|
||||
isSponsoredAndPreFunded && !isTesSuccess(ret))
|
||||
*sponsorSle && !isTesSuccess(ret))
|
||||
return tecINSUF_RESERVE_LINE;
|
||||
|
||||
// Set reserve for low account.
|
||||
@@ -569,7 +569,7 @@ TrustSet::doApply()
|
||||
// calling adjustOwnerCount().
|
||||
if (auto const ret = checkInsufficientReserve(
|
||||
view(), ctx_.tx, sleHighAccount, preFeeBalance_, *sponsorSle, 1, 0, j_);
|
||||
isSponsoredAndPreFunded && !isTesSuccess(ret))
|
||||
*sponsorSle && !isTesSuccess(ret))
|
||||
return tecINSUF_RESERVE_LINE;
|
||||
|
||||
// Set reserve for high account.
|
||||
|
||||
@@ -746,6 +746,47 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testSponsoredFreeTierReserve()
|
||||
{
|
||||
testcase("Sponsored Free-Tier Reserve");
|
||||
using namespace test::jtx;
|
||||
Account const alice("alice");
|
||||
Account const issuer("issuer");
|
||||
Account const sponsor("sponsor");
|
||||
|
||||
// Trust lines and MPTokens normally skip the reserve check when the
|
||||
// holder's ownerCount < 2 (the "free-tier" / first-two-items shortcut). When the
|
||||
// tx is sponsored, that shortcut must not apply — the sponsor must
|
||||
// still cover the reserve.
|
||||
Env env{*this, testableAmendments()};
|
||||
env.fund(XRP(10000), alice, issuer);
|
||||
// Sponsor is funded just below the reserve required to cover a single
|
||||
// sponsored item.
|
||||
env.fund(reserve(env, 1) - drops(1), sponsor);
|
||||
env.close();
|
||||
BEAST_EXPECT(ownerCount(env, alice) == 0);
|
||||
|
||||
MPTTester mptt(env, issuer, {.fund = false});
|
||||
mptt.create();
|
||||
|
||||
// Free-tier trust line cosigned by an undercapitalized sponsor must
|
||||
// fail — the holder's free-first-two-items shortcut does not let the
|
||||
// sponsor skip the reserve check.
|
||||
env(trust(alice, issuer["USD"](100)),
|
||||
sponsor::As(sponsor, spfSponsorReserve),
|
||||
Sig(sfSponsorSignature, sponsor),
|
||||
Ter(tecNO_LINE_INSUF_RESERVE));
|
||||
env.close();
|
||||
|
||||
// Free-tier MPTokenAuthorize must also fail for the same reason.
|
||||
env(MPTTester::authorizeJV({.account = alice, .id = mptt.issuanceID()}),
|
||||
sponsor::As(sponsor, spfSponsorReserve),
|
||||
Sig(sfSponsorSignature, sponsor),
|
||||
Ter(tecINSUFFICIENT_RESERVE));
|
||||
env.close();
|
||||
}
|
||||
|
||||
void
|
||||
testTransferSponsor()
|
||||
{
|
||||
@@ -2821,10 +2862,11 @@ public:
|
||||
env(noop(sponsor), ticket::Use(ticketSeq));
|
||||
env.close();
|
||||
|
||||
// pass (free mptoken)
|
||||
// pass (free-tier mptoken for the holder, but the sponsor is still
|
||||
// charged a reserve increment regardless of the ownerCount < 2 shortcut).
|
||||
if (cosigning)
|
||||
{
|
||||
adjustAccountXRPBalance(env, sponsor, reserve(env, 2) - drops(1));
|
||||
adjustAccountXRPBalance(env, sponsor, reserve(env, 2));
|
||||
env(jv,
|
||||
sponsor::As(sponsor, spfSponsorReserve),
|
||||
Sig(sfSponsorSignature, sponsor),
|
||||
@@ -3723,6 +3765,7 @@ protected:
|
||||
testSimpleSponsorshipSet();
|
||||
|
||||
testPreFundAndCosign();
|
||||
testSponsoredFreeTierReserve();
|
||||
|
||||
testTransferSponsor();
|
||||
testSponsorFee();
|
||||
|
||||
@@ -1398,17 +1398,17 @@ public:
|
||||
env.close();
|
||||
|
||||
// Helper to call account_objects with sponsored filter
|
||||
auto acctObjsSponsored = [&env](
|
||||
AccountID const& acct,
|
||||
bool sponsored,
|
||||
std::optional<json::StaticString> const& type = std::nullopt) {
|
||||
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 env.rpc("json", "account_objects", to_string(params));
|
||||
return testEnv.rpc("json", "account_objects", to_string(params));
|
||||
};
|
||||
|
||||
// Create a sponsorship (alice sponsors bob)
|
||||
@@ -1421,14 +1421,15 @@ public:
|
||||
|
||||
// sponsored=true should not find any objects for bob (doesn't have any sponsored objects)
|
||||
{
|
||||
auto const resp = acctObjsSponsored(bob.id(), true);
|
||||
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::line(bob, gw, usd.currency);
|
||||
BEAST_EXPECT(env.le(trustId));
|
||||
if (!BEAST_EXPECT(env.le(trustId)))
|
||||
return;
|
||||
|
||||
env(sponsor::transfer(bob, tfSponsorshipCreate, trustId.key),
|
||||
sponsor::As(sponsor1, spfSponsorReserve),
|
||||
@@ -1438,31 +1439,27 @@ public:
|
||||
// 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(bob.id(), true);
|
||||
auto const resp = acctObjsSponsored(env, bob.id(), true);
|
||||
auto const& objs = resp[jss::result][jss::account_objects];
|
||||
bool foundTrustLine = false;
|
||||
BEAST_EXPECT(objs.size() == 1);
|
||||
for (auto const& obj : objs)
|
||||
{
|
||||
if (obj[sfLedgerEntryType.jsonName] == jss::RippleState)
|
||||
{
|
||||
BEAST_EXPECT(
|
||||
obj.isMember(sfHighSponsor.jsonName) ||
|
||||
obj.isMember(sfLowSponsor.jsonName));
|
||||
foundTrustLine = true;
|
||||
}
|
||||
}
|
||||
BEAST_EXPECT(foundTrustLine);
|
||||
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(bob.id(), false);
|
||||
auto const resp = acctObjsSponsored(env, bob.id(), false);
|
||||
auto const& objs = resp[jss::result][jss::account_objects];
|
||||
bool foundSponsoredTrustLine = false;
|
||||
for (auto const& obj : objs)
|
||||
@@ -1476,6 +1473,112 @@ public:
|
||||
BEAST_EXPECT(!foundSponsoredTrustLine);
|
||||
}
|
||||
|
||||
// Only the queried side of a shared trust line should determine
|
||||
// sponsorship classification.
|
||||
{
|
||||
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::line(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];
|
||||
BEAST_EXPECT(objs.size() == 0);
|
||||
}
|
||||
{
|
||||
auto const resp = acctObjsSponsored(env, issuer.id(), false, 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);
|
||||
}
|
||||
}
|
||||
|
||||
// A Sponsorship object is visible to both sides, but its reserve side
|
||||
// belongs only to sfOwner.
|
||||
{
|
||||
Env env(*this, testableAmendments());
|
||||
Account const owner("owner");
|
||||
Account const sponsee("sponsee");
|
||||
Account const sponsor("sponsor");
|
||||
|
||||
env.fund(XRP(10000), owner, sponsee, sponsor);
|
||||
env.close();
|
||||
|
||||
env(sponsor::set_reserve(sponsor, 0, 100), sponsor::SponseeAcc(owner));
|
||||
env.close();
|
||||
|
||||
env(sponsor::set(owner, 0, 100, XRP(100)),
|
||||
sponsor::SponseeAcc(sponsee),
|
||||
sponsor::As(sponsor, spfSponsorReserve),
|
||||
Sig(sfSponsorSignature, sponsor));
|
||||
env.close();
|
||||
|
||||
auto const sponsorship = env.le(keylet::sponsorship(owner, sponsee));
|
||||
if (!BEAST_EXPECT(sponsorship))
|
||||
return;
|
||||
BEAST_EXPECT(sponsorship->isFieldPresent(sfSponsor));
|
||||
|
||||
{
|
||||
auto const resp = acctObjsSponsored(env, owner.id(), true, 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(), true, jss::sponsorship);
|
||||
auto const& objs = resp[jss::result][jss::account_objects];
|
||||
BEAST_EXPECT(objs.size() == 0);
|
||||
}
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// NFT page sponsored filter
|
||||
{
|
||||
// Mint an NFT for bob (creates NFT page)
|
||||
@@ -1483,7 +1586,8 @@ public:
|
||||
env.close();
|
||||
|
||||
auto const nftPageKeylet = keylet::nftpageMax(bob);
|
||||
BEAST_EXPECT(env.le(nftPageKeylet));
|
||||
if (!BEAST_EXPECT(env.le(nftPageKeylet)))
|
||||
return;
|
||||
|
||||
// Sponsor the NFT page
|
||||
env(sponsor::transfer(bob, tfSponsorshipCreate, nftPageKeylet.key),
|
||||
@@ -1492,13 +1596,16 @@ public:
|
||||
env.close();
|
||||
|
||||
// Verify NFT page has sponsor field
|
||||
BEAST_EXPECT(env.le(nftPageKeylet)->isFieldPresent(sfSponsor));
|
||||
auto const nftPage = env.le(nftPageKeylet);
|
||||
if (!BEAST_EXPECT(nftPage))
|
||||
return;
|
||||
BEAST_EXPECT(nftPage->isFieldPresent(sfSponsor));
|
||||
|
||||
// sponsored=true should include the sponsored NFT page
|
||||
// sponsored=false should NOT include the sponsored NFT page
|
||||
for (auto const sponsored : {true, false})
|
||||
{
|
||||
auto const resp = acctObjsSponsored(bob.id(), sponsored);
|
||||
auto const resp = acctObjsSponsored(env, bob.id(), sponsored);
|
||||
auto const& objs = resp[jss::result][jss::account_objects];
|
||||
bool foundNFTPage = false;
|
||||
for (auto const& obj : objs)
|
||||
|
||||
@@ -364,6 +364,20 @@ 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 transaction
|
||||
json::Value params;
|
||||
@@ -817,6 +831,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()
|
||||
{
|
||||
@@ -1249,6 +1315,7 @@ public:
|
||||
testTransactionNonTecFailure();
|
||||
testTransactionTecFailure();
|
||||
testSuccessfulTransactionMultisigned();
|
||||
testSuccessfulSponsoredTransactionMultisigned();
|
||||
testTransactionSigningFailure();
|
||||
testInvalidSingleAndMultiSigningTransaction();
|
||||
testMultisignedBadPubKey();
|
||||
|
||||
@@ -33,7 +33,8 @@ namespace xrpl {
|
||||
@param dirIndex Begin gathering account objects from this directory.
|
||||
@param entryIndex Begin gathering objects from this directory node.
|
||||
@param limit Maximum number of objects to find.
|
||||
@param sponsored Whether to filter by sponsored objects.
|
||||
@param hasSponsoredFilter Whether to filter by sponsored objects.
|
||||
@param sponsored Whether filtered objects should be sponsored.
|
||||
@param jvResult A JSON result that holds the request objects.
|
||||
*/
|
||||
bool
|
||||
@@ -44,7 +45,8 @@ getAccountObjects(
|
||||
uint256 dirIndex,
|
||||
uint256 entryIndex,
|
||||
std::uint32_t const limit,
|
||||
std::optional<bool> const sponsored,
|
||||
bool const hasSponsoredFilter,
|
||||
bool const sponsored,
|
||||
json::Value& jvResult)
|
||||
{
|
||||
// check if dirIndex is valid
|
||||
@@ -103,12 +105,12 @@ getAccountObjects(
|
||||
while (currentPage)
|
||||
{
|
||||
bool canAppendNFT = true;
|
||||
if (sponsored.has_value())
|
||||
if (hasSponsoredFilter)
|
||||
{
|
||||
std::optional<AccountID> const nftSponsor = currentPage->isFieldPresent(sfSponsor)
|
||||
? currentPage->getAccountID(sfSponsor)
|
||||
: std::optional<AccountID>(std::nullopt);
|
||||
if (!sponsoredMatchesFilter(sponsored.value(), nftSponsor))
|
||||
if (!sponsoredMatchesFilter(sponsored, nftSponsor))
|
||||
canAppendNFT = false;
|
||||
}
|
||||
if (canAppendNFT)
|
||||
@@ -204,21 +206,33 @@ getAccountObjects(
|
||||
!typeMatchesFilter(typeFilter.value(), sleNode->getType()))
|
||||
canAppend = false;
|
||||
|
||||
auto const getSponsor = [&sleNode]() -> std::optional<AccountID> {
|
||||
if (sleNode->isFieldPresent(sfSponsor))
|
||||
return sleNode->getAccountID(sfSponsor);
|
||||
auto const getSponsor = [&account, &sleNode]() -> std::optional<AccountID> {
|
||||
if (sleNode->getType() == ltRIPPLE_STATE)
|
||||
{
|
||||
if (sleNode->isFieldPresent(sfHighSponsor))
|
||||
if (sleNode->isFlag(lsfHighReserve) &&
|
||||
sleNode->getFieldAmount(sfHighLimit).getIssuer() == account &&
|
||||
sleNode->isFieldPresent(sfHighSponsor))
|
||||
return sleNode->getAccountID(sfHighSponsor);
|
||||
if (sleNode->isFieldPresent(sfLowSponsor))
|
||||
if (sleNode->isFlag(lsfLowReserve) &&
|
||||
sleNode->getFieldAmount(sfLowLimit).getIssuer() == account &&
|
||||
sleNode->isFieldPresent(sfLowSponsor))
|
||||
return sleNode->getAccountID(sfLowSponsor);
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
if (sleNode->getType() == ltSPONSORSHIP &&
|
||||
sleNode->getAccountID(sfOwner) != account)
|
||||
return std::nullopt;
|
||||
|
||||
if (sleNode->isFieldPresent(sfSponsor))
|
||||
return sleNode->getAccountID(sfSponsor);
|
||||
|
||||
return std::nullopt;
|
||||
};
|
||||
std::optional<AccountID> const sponsor = getSponsor();
|
||||
|
||||
if (sponsored.has_value() && !sponsoredMatchesFilter(sponsored.value(), sponsor))
|
||||
if (hasSponsoredFilter && !sponsoredMatchesFilter(sponsored, sponsor))
|
||||
canAppend = false;
|
||||
|
||||
if (canAppend)
|
||||
@@ -369,8 +383,9 @@ doAccountObjects(RPC::JsonContext& context)
|
||||
return RPC::invalidFieldError(jss::marker);
|
||||
}
|
||||
|
||||
std::optional<bool> sponsored;
|
||||
if (params.isMember(jss::sponsored))
|
||||
bool const hasSponsoredFilter = params.isMember(jss::sponsored);
|
||||
bool sponsored = false;
|
||||
if (hasSponsoredFilter)
|
||||
{
|
||||
auto const& sponsoredJv = params[jss::sponsored];
|
||||
if (!sponsoredJv.isBool())
|
||||
@@ -380,7 +395,15 @@ doAccountObjects(RPC::JsonContext& context)
|
||||
}
|
||||
|
||||
if (!getAccountObjects(
|
||||
*ledger, accountID, typeFilter, dirIndex, entryIndex, limit, sponsored, result))
|
||||
*ledger,
|
||||
accountID,
|
||||
typeFilter,
|
||||
dirIndex,
|
||||
entryIndex,
|
||||
limit,
|
||||
hasSponsoredFilter,
|
||||
sponsored,
|
||||
result))
|
||||
return RPC::invalidFieldError(jss::marker);
|
||||
|
||||
result[jss::account] = toBase58(accountID);
|
||||
|
||||
@@ -131,29 +131,16 @@ autofillSignature(json::Value& sigObject)
|
||||
static std::optional<json::Value>
|
||||
autofillTx(json::Value& txJson, RPC::JsonContext& context)
|
||||
{
|
||||
if (!txJson.isMember(jss::Fee))
|
||||
{
|
||||
// autofill Fee
|
||||
// Must happen after all the other autofills happen
|
||||
// Error handling/messaging works better that way
|
||||
auto feeOrError = RPC::getCurrentNetworkFee(
|
||||
context.role,
|
||||
context.app.config(),
|
||||
context.app.getFeeTrack(),
|
||||
context.app.getTxQ(),
|
||||
context.app,
|
||||
txJson);
|
||||
if (feeOrError.isMember(jss::error))
|
||||
return feeOrError;
|
||||
txJson[jss::Fee] = feeOrError;
|
||||
}
|
||||
|
||||
if (auto error = autofillSignature(txJson))
|
||||
return error;
|
||||
|
||||
if (txJson.isMember(sfSponsorSignature.jsonName))
|
||||
{
|
||||
if (auto error = autofillSignature(txJson[sfSponsorSignature.jsonName]))
|
||||
auto& sponsorSignature = txJson[sfSponsorSignature.jsonName];
|
||||
if (!sponsorSignature.isObject())
|
||||
return RPC::objectFieldError(sfSponsorSignature.jsonName);
|
||||
|
||||
if (auto const error = autofillSignature(sponsorSignature))
|
||||
return error;
|
||||
}
|
||||
|
||||
@@ -172,6 +159,22 @@ autofillTx(json::Value& txJson, RPC::JsonContext& context)
|
||||
txJson[jss::NetworkID] = to_string(networkId);
|
||||
}
|
||||
|
||||
if (!txJson.isMember(jss::Fee))
|
||||
{
|
||||
// Autofill Fee after normalizing nested signer fields so the fee
|
||||
// estimator sees the full transaction shape.
|
||||
auto feeOrError = RPC::getCurrentNetworkFee(
|
||||
context.role,
|
||||
context.app.config(),
|
||||
context.app.getFeeTrack(),
|
||||
context.app.getTxQ(),
|
||||
context.app,
|
||||
txJson);
|
||||
if (feeOrError.isMember(jss::error))
|
||||
return feeOrError;
|
||||
txJson[jss::Fee] = feeOrError;
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user