From abd8620c97ff304b3af6c089dd78405e4bdd835c Mon Sep 17 00:00:00 2001 From: tequ Date: Fri, 1 Aug 2025 13:23:41 +0900 Subject: [PATCH] Add SponsorTransfer --- .../xrpl/protocol/detail/transactions.macro | 5 + src/test/app/Sponsor_test.cpp | 137 +++++++++ src/test/jtx/impl/sponsor.cpp | 18 +- src/test/jtx/sponsor.h | 9 +- src/xrpld/app/tx/detail/SponsorTransfer.cpp | 285 ++++++++++++++++++ src/xrpld/app/tx/detail/SponsorTransfer.h | 48 +++ src/xrpld/app/tx/detail/applySteps.cpp | 1 + 7 files changed, 485 insertions(+), 18 deletions(-) create mode 100644 src/xrpld/app/tx/detail/SponsorTransfer.cpp create mode 100644 src/xrpld/app/tx/detail/SponsorTransfer.h diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index 89e9a16df5..fc3429e0ed 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -525,6 +525,11 @@ TRANSACTION(ttBATCH, 71, Batch, Delegation::notDelegatable, ({ {sfBatchSigners, soeOPTIONAL}, })) +/** This transaction transfer sponsor */ +TRANSACTION(ttSPONSOR_TRANSFER, 72, SponsorTransfer, Delegation::notDelegatable, ({ + {sfLedgerIndex, soeOPTIONAL}, +})) + /** This system-generated transaction type is used to update the status of the various amendments. For details, see: https://xrpl.org/amendments.html diff --git a/src/test/app/Sponsor_test.cpp b/src/test/app/Sponsor_test.cpp index 55d2a0137c..1578388b6e 100644 --- a/src/test/app/Sponsor_test.cpp +++ b/src/test/app/Sponsor_test.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include @@ -46,6 +47,141 @@ public: sponsor::as(sponsor), sponsor::sig(sponsor), ter(temDISABLED)); + + env(sponsor::transfer(alice), ter(temDISABLED)); + } + + void + testTransferSponsor() + { + testcase("Transfer Sponsor"); + using namespace test::jtx; + + { + // sponsor account + Env env{*this, testable_amendments()}; + Account const alice("alice"); + Account const sponsor1("sponsor1"); + Account const sponsor2("sponsor2"); + env.fund(XRP(10000), alice, sponsor1, sponsor2); + + env(sponsor::transfer(alice), + sponsor::as(sponsor1, tfSponsorReserve), + sponsor::sig(sponsor1)); + env.close(); + + env.require(sponsored_owners(alice, 0)); + env.require(sponsored_owners(sponsor1, 0)); + env.require(sponsoring_owners(alice, 0)); + env.require(sponsoring_owners(sponsor1, 0)); + env.require(sponsoring_account_count(alice, 0)); + env.require(sponsoring_account_count(sponsor1, 1)); + auto const sle1 = env.le(keylet::account(alice)); + BEAST_EXPECT(sle1->isFieldPresent(sfSponsorAccount)); + BEAST_EXPECT(sle1->getAccountID(sfSponsorAccount) == sponsor1.id()); + + // transfer sponsor + env(sponsor::transfer(alice), + sponsor::as(sponsor2, tfSponsorReserve), + sponsor::sig(sponsor2)); + env.close(); + + env.require(sponsored_owners(alice, 0)); + env.require(sponsored_owners(sponsor1, 0)); + env.require(sponsored_owners(sponsor2, 0)); + env.require(sponsoring_owners(alice, 0)); + env.require(sponsoring_owners(sponsor1, 0)); + env.require(sponsoring_owners(sponsor2, 0)); + env.require(sponsoring_account_count(alice, 0)); + env.require(sponsoring_account_count(sponsor1, 0)); + env.require(sponsoring_account_count(sponsor2, 1)); + auto const sle2 = env.le(keylet::account(alice)); + BEAST_EXPECT(sle2->isFieldPresent(sfSponsorAccount)); + BEAST_EXPECT(sle2->getAccountID(sfSponsorAccount) == sponsor2.id()); + + // dissolve sponsor + env(sponsor::transfer(alice)); + env.close(); + + env.require(sponsored_owners(alice, 0)); + env.require(sponsored_owners(sponsor1, 0)); + env.require(sponsored_owners(sponsor2, 0)); + env.require(sponsoring_owners(alice, 0)); + env.require(sponsoring_owners(sponsor1, 0)); + env.require(sponsoring_owners(sponsor2, 0)); + env.require(sponsoring_account_count(alice, 0)); + env.require(sponsoring_account_count(sponsor1, 0)); + env.require(sponsoring_account_count(sponsor2, 0)); + auto const sle3 = env.le(keylet::account(alice)); + BEAST_EXPECT(!sle3->isFieldPresent(sfSponsorAccount)); + } + { + // sponsor object + Env env{*this, testable_amendments()}; + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor1("sponsor1"); + Account const sponsor2("sponsor2"); + env.fund(XRP(10000), alice, bob, sponsor1, sponsor2); + + auto const seq = env.seq(alice); + env(check::create(alice, bob, XRP(1))); + env.close(); + + auto const checkId = keylet::check(alice, seq).key; + BEAST_EXPECT(env.le(keylet::unchecked(checkId)) != nullptr); + + env(sponsor::transfer(alice, checkId), + sponsor::as(sponsor1, tfSponsorReserve), + sponsor::sig(sponsor1)); + env.close(); + + env.require(owners(alice, 1)); + env.require(sponsored_owners(alice, 1)); + env.require(sponsored_owners(sponsor1, 0)); + env.require(sponsoring_owners(alice, 0)); + env.require(sponsoring_owners(sponsor1, 1)); + env.require(sponsoring_account_count(alice, 0)); + env.require(sponsoring_account_count(sponsor1, 0)); + auto const sle1 = env.le(keylet::unchecked(checkId)); + BEAST_EXPECT(sle1->isFieldPresent(sfSponsorAccount)); + BEAST_EXPECT(sle1->getAccountID(sfSponsorAccount) == sponsor1.id()); + + // transfer sponsor + env(sponsor::transfer(alice, checkId), + sponsor::as(sponsor2, tfSponsorReserve), + sponsor::sig(sponsor2)); + env.close(); + + env.require(sponsored_owners(alice, 1)); + env.require(sponsored_owners(sponsor1, 0)); + env.require(sponsored_owners(sponsor2, 0)); + env.require(sponsoring_owners(alice, 0)); + env.require(sponsoring_owners(sponsor1, 0)); + env.require(sponsoring_owners(sponsor2, 1)); + env.require(sponsoring_account_count(alice, 0)); + env.require(sponsoring_account_count(sponsor1, 0)); + env.require(sponsoring_account_count(sponsor2, 0)); + auto const sle2 = env.le(keylet::unchecked(checkId)); + BEAST_EXPECT(sle2->isFieldPresent(sfSponsorAccount)); + BEAST_EXPECT(sle2->getAccountID(sfSponsorAccount) == sponsor2.id()); + + // dissolve sponsor + env(sponsor::transfer(alice, checkId)); + env.close(); + + env.require(sponsored_owners(alice, 0)); + env.require(sponsored_owners(sponsor1, 0)); + env.require(sponsored_owners(sponsor2, 0)); + env.require(sponsoring_owners(alice, 0)); + env.require(sponsoring_owners(sponsor1, 0)); + env.require(sponsoring_owners(sponsor2, 0)); + env.require(sponsoring_account_count(alice, 0)); + env.require(sponsoring_account_count(sponsor1, 0)); + env.require(sponsoring_account_count(sponsor2, 0)); + auto const sle3 = env.le(keylet::unchecked(checkId)); + BEAST_EXPECT(!sle3->isFieldPresent(sfSponsorAccount)); + } } void @@ -455,6 +591,7 @@ public: run() override { testDisabled(); + testTransferSponsor(); testSponsorFee(); testSponsorAccount(); testSponsorReserve(); diff --git a/src/test/jtx/impl/sponsor.cpp b/src/test/jtx/impl/sponsor.cpp index 2db16a80db..385395f8e5 100644 --- a/src/test/jtx/impl/sponsor.cpp +++ b/src/test/jtx/impl/sponsor.cpp @@ -30,21 +30,13 @@ namespace jtx { namespace sponsor { Json::Value -transferAccount(jtx::Account const& account) +transfer(jtx::Account const& account, std::optional const& index) { Json::Value jv; - jv[jss::TransactionType] = "SponsorTransfer"; - jv[sfSponsor.jsonName] = account.human(); - - return jv; -} - -Json::Value -transferObject(uint256 const& id) -{ - Json::Value jv; - jv[jss::TransactionType] = "SponsorTransfer"; - jv[sfLedgerIndex.jsonName] = to_string(id); + jv[jss::TransactionType] = jss::SponsorTransfer; + jv[jss::Account] = account.human(); + if (index) + jv[sfLedgerIndex.jsonName] = to_string(*index); return jv; } diff --git a/src/test/jtx/sponsor.h b/src/test/jtx/sponsor.h index ec0c813158..e09ccd26bd 100644 --- a/src/test/jtx/sponsor.h +++ b/src/test/jtx/sponsor.h @@ -30,11 +30,10 @@ namespace jtx { namespace sponsor { -// Json::Value -// transferAccount(jtx::Account const& account); - -// Json::Value -// transferObject(uint256 const& id); +Json::Value +transfer( + jtx::Account const& account, + std::optional const& index = std::nullopt); struct as { diff --git a/src/xrpld/app/tx/detail/SponsorTransfer.cpp b/src/xrpld/app/tx/detail/SponsorTransfer.cpp new file mode 100644 index 0000000000..72c05eaef7 --- /dev/null +++ b/src/xrpld/app/tx/detail/SponsorTransfer.cpp @@ -0,0 +1,285 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2025 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace ripple { + +NotTEC +SponsorTransfer::preflight(PreflightContext const& ctx) +{ + if (!ctx.rules.enabled(featureSponsor)) + return temDISABLED; + + if (auto const ter = preflight1(ctx)) + return ter; + + if (ctx.tx.getFlags() & tfUniversalMask) + return temINVALID_FLAG; + + return preflight2(ctx); +} + +template +inline std::optional +getLedgerEntryOwner( + ReadView const& view, + T const& sle, + AccountID const& account) +{ + switch (sle->getType()) + { + case ltNFTOKEN_OFFER: + case ltORACLE: + case ltPERMISSIONED_DOMAIN: + case ltVAULT: + return sle->getAccountID(sfOwner); + case ltCHECK: + case ltDID: + case ltTICKET: + case ltOFFER: + case ltXCHAIN_OWNED_CLAIM_ID: + case ltXCHAIN_OWNED_CREATE_ACCOUNT_CLAIM_ID: + case ltESCROW: + case ltPAYCHAN: + case ltMPTOKEN: + case ltDELEGATE: + case ltBRIDGE: + case ltDEPOSIT_PREAUTH: + return sle->getAccountID(sfAccount); + case ltMPTOKEN_ISSUANCE: + return sle->getAccountID(sfIssuer); + case ltSIGNER_LIST: { + auto const signerList = view.read(keylet::signers(account)); + if (!signerList) + return std::nullopt; + if (signerList->getFieldH256(sfLedgerIndex) == + sle->getFieldH256(sfLedgerIndex)) + return account; + return std::nullopt; + } + case ltCREDENTIAL: { + if (sle->getFlags() & lsfAccepted) + return sle->getAccountID(sfSubject); + return sle->getAccountID(sfIssuer); + } + // case ltNFTOKEN_PAGE: + // case ltRIPPLE_STATE: + case ltACCOUNT_ROOT: { + // AccountRoot is not supported for object sponsorship + return std::nullopt; + } + case ltNEGATIVE_UNL: + case ltDIR_NODE: + case ltAMENDMENTS: + case ltLEDGER_HASHES: + case ltFEE_SETTINGS: + case ltAMM: + return std::nullopt; + default: + return std::nullopt; + }; +} + +TER +SponsorTransfer::preclaim(PreclaimContext const& ctx) +{ + auto const index = ctx.tx[~sfLedgerIndex]; + + if (index) + { + auto const sle = ctx.view.read(keylet::unchecked(*index)); + if (!sle) + return tecNO_ENTRY; + + auto const owner = + getLedgerEntryOwner(ctx.view, sle, ctx.tx[sfAccount]); + if (!owner) + return tecNO_PERMISSION; + + if (sle->isFieldPresent(sfSponsorAccount)) + { + auto const sponsor = sle->getAccountID(sfSponsorAccount); + if (sponsor == owner) + return tecNO_PERMISSION; + + // TODO: check reserve + } + else + { + // TODO: check reserve + } + } + else + { + auto const accSle = ctx.view.read(keylet::account(ctx.tx[sfAccount])); + if (!accSle) + return tecINTERNAL; + + if (accSle->isFieldPresent(sfSponsorAccount)) + { + // TODO: check reserve + } + else + { + // TODO: check reserve + } + } + + return tesSUCCESS; +} + +TER +SponsorTransfer::doApply() +{ + auto const& tx = ctx_.tx; + + auto const index = tx[~sfLedgerIndex]; + + auto const accSle = view().peek(keylet::account(account_)); + if (!accSle) + return tefINTERNAL; // LCOV_EXCL_LINE + + if (index) + { + // transfer object sponsor + auto const objSle = view().peek(keylet::unchecked(*index)); + if (!objSle) + return tefINTERNAL; // LCOV_EXCL_LINE + + auto const owner = getLedgerEntryOwner(view(), objSle, account_); + if (!owner) + return tefINTERNAL; // LCOV_EXCL_LINE + + auto const ownerSle = view().peek(keylet::account(*owner)); + if (!ownerSle) + return tefINTERNAL; // LCOV_EXCL_LINE + + if (tx.isFieldPresent(sfSponsor)) + { + auto const sponsorObj = tx.getFieldObject(sfSponsor); + auto const oldSponsor = objSle->getAccountID(sfSponsorAccount); + auto const newSponsor = sponsorObj[sfAccount]; + // decrement old sponsoring count if exists + if (auto const oldSponsorSle = + view().peek(keylet::account(oldSponsor))) + { + oldSponsorSle->setFieldU32( + sfSponsoringOwnerCount, + oldSponsorSle->getFieldU32(sfSponsoringOwnerCount) - 1); + view().update(oldSponsorSle); + } + else + { + // update owner's sponsored count + ownerSle->setFieldU32( + sfSponsoredOwnerCount, + ownerSle->getFieldU32(sfSponsoredOwnerCount) + 1); + view().update(ownerSle); + } + // increment new sponsoring count + auto const newSponsorSle = view().peek(keylet::account(newSponsor)); + newSponsorSle->setFieldU32( + sfSponsoringOwnerCount, + newSponsorSle->getFieldU32(sfSponsoringOwnerCount) + 1); + view().update(newSponsorSle); + + objSle->setAccountID(sfSponsorAccount, newSponsor); + view().update(objSle); + } + else + { + // dissolve object sponsor + auto const oldSponsor = objSle->getAccountID(sfSponsorAccount); + // decrement sponsored count + accSle->setFieldU32( + sfSponsoredOwnerCount, + accSle->getFieldU32(sfSponsoredOwnerCount) - 1); + view().update(accSle); + // decrement old sponsoring count + if (auto const oldSponsorSle = + view().peek(keylet::account(oldSponsor))) + { + oldSponsorSle->setFieldU32( + sfSponsoringOwnerCount, + oldSponsorSle->getFieldU32(sfSponsoringOwnerCount) - 1); + view().update(oldSponsorSle); + } + + // remove sponsor from object + objSle->makeFieldAbsent(sfSponsorAccount); + view().update(objSle); + } + } + else + { + // Transfer Account sponsor + if (tx.isFieldPresent(sfSponsor)) + { + // transfer account sponsor + auto const sponsorObj = tx.getFieldObject(sfSponsor); + // increment new sponsoring count + auto const newSponsor = sponsorObj[sfAccount]; + auto const newSponsorSle = view().peek(keylet::account(newSponsor)); + newSponsorSle->setFieldU32( + sfSponsoringAccountCount, + newSponsorSle->getFieldU32(sfSponsoringAccountCount) + 1); + view().update(newSponsorSle); + // decrement old sponsoring count + if (accSle->isFieldPresent(sfSponsorAccount)) + { + auto const oldSponsor = accSle->getAccountID(sfSponsorAccount); + auto const oldSponsorSle = + view().peek(keylet::account(oldSponsor)); + oldSponsorSle->setFieldU32( + sfSponsoringAccountCount, + oldSponsorSle->getFieldU32(sfSponsoringAccountCount) - 1); + view().update(oldSponsorSle); + } + accSle->setAccountID(sfSponsorAccount, newSponsor); + view().update(accSle); + } + else + { + // dissolve account sponsor + auto const oldSponsor = accSle->getAccountID(sfSponsorAccount); + accSle->makeFieldAbsent(sfSponsorAccount); + // decrement account sponsoring count + auto const oldSponsorSle = view().peek(keylet::account(oldSponsor)); + oldSponsorSle->setFieldU32( + sfSponsoringAccountCount, + oldSponsorSle->getFieldU32(sfSponsoringAccountCount) - 1); + view().update(oldSponsorSle); + } + } + + return tesSUCCESS; +} + +} // namespace ripple diff --git a/src/xrpld/app/tx/detail/SponsorTransfer.h b/src/xrpld/app/tx/detail/SponsorTransfer.h new file mode 100644 index 0000000000..68f596bcd0 --- /dev/null +++ b/src/xrpld/app/tx/detail/SponsorTransfer.h @@ -0,0 +1,48 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2025 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef RIPPLE_TX_SPONSORTRANSFER_H_INCLUDED +#define RIPPLE_TX_SPONSORTRANSFER_H_INCLUDED + +#include + +namespace ripple { + +class SponsorTransfer : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; + + explicit SponsorTransfer(ApplyContext& ctx) : Transactor(ctx) + { + } + + static NotTEC + preflight(PreflightContext const& ctx); + + static TER + preclaim(PreclaimContext const& ctx); + + TER + doApply() override; +}; + +} // namespace ripple + +#endif diff --git a/src/xrpld/app/tx/detail/applySteps.cpp b/src/xrpld/app/tx/detail/applySteps.cpp index 34259ebef0..f47d47ebef 100644 --- a/src/xrpld/app/tx/detail/applySteps.cpp +++ b/src/xrpld/app/tx/detail/applySteps.cpp @@ -62,6 +62,7 @@ #include #include #include +#include #include #include #include