Add SponsorTransfer

This commit is contained in:
tequ
2025-08-01 13:23:41 +09:00
parent 6e01f233e4
commit abd8620c97
7 changed files with 485 additions and 18 deletions

View File

@@ -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

View File

@@ -22,6 +22,7 @@
#include <test/jtx/amount.h>
#include <test/jtx/check.h>
#include <test/jtx/did.h>
#include <test/jtx/owners.h>
#include <test/jtx/sponsor.h>
#include <xrpl/protocol/Feature.h>
@@ -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();

View File

@@ -30,21 +30,13 @@ namespace jtx {
namespace sponsor {
Json::Value
transferAccount(jtx::Account const& account)
transfer(jtx::Account const& account, std::optional<uint256> 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;
}

View File

@@ -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<uint256> const& index = std::nullopt);
struct as
{

View File

@@ -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 <xrpld/app/tx/detail/SponsorTransfer.h>
#include <xrpld/ledger/ReadView.h>
#include <xrpld/ledger/View.h>
#include <xrpl/protocol/Asset.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STNumber.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
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 <typename T>
inline std::optional<AccountID>
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

View File

@@ -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 <xrpld/app/tx/detail/Transactor.h>
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

View File

@@ -62,6 +62,7 @@
#include <xrpld/app/tx/detail/SetRegularKey.h>
#include <xrpld/app/tx/detail/SetSignerList.h>
#include <xrpld/app/tx/detail/SetTrust.h>
#include <xrpld/app/tx/detail/SponsorTransfer.h>
#include <xrpld/app/tx/detail/VaultClawback.h>
#include <xrpld/app/tx/detail/VaultCreate.h>
#include <xrpld/app/tx/detail/VaultDelete.h>