initial implementation of token holders

This commit is contained in:
Mayukha Vadari
2026-03-22 01:14:13 -07:00
parent 370a420aa3
commit e89e83c802
12 changed files with 448 additions and 72 deletions

View File

@@ -0,0 +1,96 @@
#pragma once
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
#include <xrpl/ledger/helpers/TokenHolderBase.h>
namespace xrpl {
class MPToken : public virtual TokenHolderBase
{
public:
MPToken(ReadView const& view, MPTokenIssuance const& issuance, AccountID const& holder)
: ReadOnlySLE(view.read(keylet::mptoken(issuance.getMptID(), holder)), view)
, TokenHolderBase(
view,
view.read(keylet::mptoken(issuance.getMptID(), holder)),
issuance,
holder)
, issuance_(issuance)
{
}
MPTokenIssuance const&
getIssuance() const
{
return issuance_;
}
protected:
MPTokenIssuance const& issuance_;
};
class WritableMPToken : public virtual WritableTokenHolderBase, public virtual MPToken
{
public:
WritableMPToken(ApplyView& view, WritableMPTokenIssuance& issuance, AccountID const& holder)
: ReadOnlySLE(view.peek(keylet::mptoken(issuance.getMptID(), holder)), view)
, TokenHolderBase(
view,
view.peek(keylet::mptoken(issuance.getMptID(), holder)),
issuance,
holder)
, WritableSLE(view.peek(keylet::mptoken(issuance.getMptID(), holder)), view)
, WritableTokenHolderBase(
view,
view.peek(keylet::mptoken(issuance.getMptID(), holder)),
issuance,
holder)
, MPToken(view, issuance, holder)
, writableIssuance_(issuance)
{
}
// Resolve ambiguity: use writable operator-> for non-const, read-only for const
using WritableSLE::operator->;
using MPToken::operator->;
using WritableSLE::operator*;
using MPToken::operator*;
WritableMPTokenIssuance&
getWritableIssuance()
{
return writableIssuance_;
}
static TER
createMPToken(
ApplyView& view,
WritableMPTokenIssuance& issuance,
AccountID const& account,
std::uint32_t const flags)
{
WritableMPToken mptoken(view, issuance, account);
auto const ownerNode =
view.dirInsert(keylet::ownerDir(account), mptoken.key(), describeOwnerDir(account));
if (!ownerNode)
return tecDIR_FULL; // LCOV_EXCL_LINE
mptoken.newSLE();
(*mptoken)[sfAccount] = account;
(*mptoken)[sfMPTokenIssuanceID] = issuance.getMptID();
(*mptoken)[sfFlags] = flags;
(*mptoken)[sfOwnerNode] = *ownerNode;
mptoken.insert();
return tesSUCCESS;
}
protected:
WritableMPTokenIssuance& writableIssuance_;
};
} // namespace xrpl

View File

@@ -0,0 +1,146 @@
#pragma once
#include <xrpl/basics/Expected.h>
#include <xrpl/ledger/helpers/RippleStateHelpers.h>
#include <xrpl/ledger/helpers/TokenHolderBase.h>
namespace xrpl {
class RippleState : public virtual TokenHolderBase
{
public:
RippleState(ReadView const& view, IOUToken const& token, AccountID const& holder)
: ReadOnlySLE(view.read(keylet::line(holder, token.getIssuer(), token.getCurrency())), view)
, TokenHolderBase(
view,
view.read(keylet::line(holder, token.getIssuer(), token.getCurrency())),
token,
holder)
, iouToken_(token)
{
}
/** Constructor with explicit SLE (for when SLE is already available) */
RippleState(
ReadView const& view,
std::shared_ptr<SLE const> sle,
IOUToken const& token,
AccountID const& holder)
: ReadOnlySLE(sle, view), TokenHolderBase(view, sle, token, holder), iouToken_(token)
{
}
IOUToken const&
getIOUToken() const
{
return iouToken_;
}
protected:
IOUToken const& iouToken_;
};
class WritableRippleState : public virtual WritableTokenHolderBase, public virtual RippleState
{
public:
WritableRippleState(ApplyView& view, WritableIOUToken& token, AccountID const& holder)
: ReadOnlySLE(view.peek(keylet::line(holder, token.getIssuer(), token.getCurrency())), view)
, TokenHolderBase(
view,
view.peek(keylet::line(holder, token.getIssuer(), token.getCurrency())),
token,
holder)
, WritableSLE(view.peek(keylet::line(holder, token.getIssuer(), token.getCurrency())), view)
, WritableTokenHolderBase(
view,
view.peek(keylet::line(holder, token.getIssuer(), token.getCurrency())),
token,
holder)
, RippleState(view, token, holder)
, writableIOUToken_(token)
{
}
/** Constructor with explicit SLE (for creation or when SLE is already available) */
WritableRippleState(
ApplyView& view,
std::shared_ptr<SLE> sle,
WritableIOUToken& token,
AccountID const& holder)
: ReadOnlySLE(sle, view)
, TokenHolderBase(view, sle, token, holder)
, WritableSLE(sle, view)
, WritableTokenHolderBase(view, sle, token, holder)
, RippleState(view, sle, token, holder)
, writableIOUToken_(token)
{
}
// Resolve ambiguity: use writable operator-> for non-const, read-only for const
using WritableSLE::operator->;
using RippleState::operator->;
using WritableSLE::operator*;
using RippleState::operator*;
WritableIOUToken&
getWritableIOUToken()
{
return writableIOUToken_;
}
static Expected<std::unique_ptr<WritableRippleState>, TER>
createHolding(
ApplyView& view,
WritableIOUToken& token,
AccountID const& accountID,
beast::Journal journal)
{
auto const ter = token.addEmptyHolding(accountID, XRPAmount{0}, journal);
if (ter != tesSUCCESS)
return Unexpected(ter);
return std::make_unique<WritableRippleState>(view, token, accountID);
}
//--------------------------------------------------------------------------
//
// Trust line operations
//
//--------------------------------------------------------------------------
/** Create a trust line
This can set an initial balance.
*/
[[nodiscard]] static TER
trustCreate(
ApplyView& view,
bool const bSrcHigh,
AccountID const& uSrcAccountID,
AccountID const& uDstAccountID,
uint256 const& uIndex, // ripple state entry
WritableAccountRoot& wrappedAcct, // the account being set.
bool const bAuth, // authorize account.
bool const bNoRipple, // others cannot ripple through
bool const bFreeze, // funds cannot leave
bool bDeepFreeze, // can neither receive nor send funds
STAmount const& saBalance, // balance of account being set.
// Issuer should be noAccount()
STAmount const& saLimit, // limit for account being set.
// Issuer should be the account being set.
std::uint32_t uQualityIn,
std::uint32_t uQualityOut,
beast::Journal j);
[[nodiscard]] static TER
trustDelete(
ApplyView& view,
std::shared_ptr<SLE> const& sleRippleState,
AccountID const& uLowAccountID,
AccountID const& uHighAccountID,
beast::Journal j);
protected:
WritableIOUToken& writableIOUToken_;
};
} // namespace xrpl

View File

@@ -216,44 +216,6 @@ creditBalance(
Currency const& currency);
/** @} */
//------------------------------------------------------------------------------
//
// Trust line operations
//
//------------------------------------------------------------------------------
/** Create a trust line
This can set an initial balance.
*/
[[nodiscard]] TER
trustCreate(
ApplyView& view,
bool const bSrcHigh,
AccountID const& uSrcAccountID,
AccountID const& uDstAccountID,
uint256 const& uIndex, // ripple state entry
WritableAccountRoot& wrappedAcct, // the account being set.
bool const bAuth, // authorize account.
bool const bNoRipple, // others cannot ripple through
bool const bFreeze, // funds cannot leave
bool bDeepFreeze, // can neither receive nor send funds
STAmount const& saBalance, // balance of account being set.
// Issuer should be noAccount()
STAmount const& saLimit, // limit for account being set.
// Issuer should be the account being set.
std::uint32_t uQualityIn,
std::uint32_t uQualityOut,
beast::Journal j);
[[nodiscard]] TER
trustDelete(
ApplyView& view,
std::shared_ptr<SLE> const& sleRippleState,
AccountID const& uLowAccountID,
AccountID const& uHighAccountID,
beast::Journal j);
//------------------------------------------------------------------------------
//
// IOU issuance/redemption

View File

@@ -0,0 +1,155 @@
#pragma once
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/ledger/helpers/WrappedSLEBase.h>
#include <xrpl/protocol/STLedgerEntry.h>
namespace xrpl {
class TokenHolderBase : public virtual ReadOnlySLE
{
public:
TokenHolderBase(
ReadView const& view,
std::shared_ptr<SLE const> sle,
TokenBase const& token,
AccountID const& holder)
: ReadOnlySLE(sle, view), token_(token), holder_(holder), holderAccount_(holder, view)
{
}
/** Constructor with explicit keylet (for when SLE lookup is needed) */
TokenHolderBase(
ReadView const& view,
Keylet const& key,
TokenBase const& token,
AccountID const& holder)
: ReadOnlySLE(key, view), token_(token), holder_(holder), holderAccount_(holder, view)
{
}
TokenHolderBase() = delete;
AccountID const&
getHolder() const
{
return holder_;
}
TokenBase const&
getToken() const
{
return token_;
}
[[nodiscard]] bool
isFrozen(int depth = 0) const
{
return token_.isFrozen(holder_, depth);
}
[[nodiscard]] bool
isDeepFrozen(int depth = 0) const
{
return token_.isDeepFrozen(holder_, depth);
}
[[nodiscard]] TER
checkFrozen() const
{
return token_.checkFrozen(holder_);
}
[[nodiscard]] TER
checkDeepFrozen() const
{
return token_.checkDeepFrozen(holder_);
}
STAmount
accountHolds(
FreezeHandling zeroIfFrozen,
beast::Journal j,
SpendableHandling includeFullBalance = shSIMPLE_BALANCE) const
{
return token_.accountHolds(holder_, zeroIfFrozen, j, includeFullBalance);
}
[[nodiscard]] STAmount
accountHolds(
FreezeHandling zeroIfFrozen,
AuthHandling zeroIfUnauthorized,
beast::Journal j,
SpendableHandling includeFullBalance = shSIMPLE_BALANCE) const
{
return token_.accountHolds(
holder_, zeroIfFrozen, zeroIfUnauthorized, j, includeFullBalance);
}
[[nodiscard]] TER
requireAuth(AuthType authType = AuthType::Legacy, int depth = 0) const
{
return token_.requireAuth(holder_, authType, depth);
}
[[nodiscard]] TER
canTransfer(AccountID const& to) const
{
return token_.canTransfer(holder_, to);
}
[[nodiscard]] TER
canTransfer(TokenHolderBase const& to) const
{
return token_.canTransfer(holder_, to.getHolder());
}
protected:
TokenBase const& token_;
AccountID const holder_;
AccountRoot holderAccount_;
};
class WritableTokenHolderBase : public virtual TokenHolderBase, public virtual WritableSLE
{
public:
WritableTokenHolderBase(
ApplyView& view,
std::shared_ptr<SLE> sle,
WritableTokenBase& token,
AccountID const& holder)
: ReadOnlySLE(sle, view)
, TokenHolderBase(view, sle, token, holder)
, WritableSLE(sle, view)
, writableToken_(token)
, writableHolderAccount_(holder, view)
{
}
/** Constructor with explicit keylet (for creation or lookup by key) */
WritableTokenHolderBase(
ApplyView& view,
Keylet const& key,
WritableTokenBase& token,
AccountID const& holder)
: ReadOnlySLE(key, view)
, TokenHolderBase(view, key, token, holder)
, WritableSLE(key, view)
, writableToken_(token)
, writableHolderAccount_(holder, view)
{
}
WritableTokenBase&
getWritableToken()
{
return writableToken_;
}
protected:
WritableTokenBase& writableToken_;
WritableAccountRoot writableHolderAccount_;
};
} // namespace xrpl

View File

@@ -60,6 +60,12 @@ public:
return readView_;
}
Keylet const&
key() const
{
return key_;
}
STLedgerEntry const*
operator->() const
{
@@ -80,12 +86,21 @@ protected:
/** Constructor for read-only context (ReadView) */
explicit ReadOnlySLE(std::shared_ptr<SLE const> sle, ReadView const& view)
: sle_(std::move(sle)), readView_(view)
: sle_(std::move(sle))
, readView_(view)
, key_(sle_ ? Keylet(sle_->getType(), sle_->key()) : Keylet(ltANY, uint256{}))
{
}
/** Constructor for read-only context (ReadView) with explicit keylet */
explicit ReadOnlySLE(Keylet const& key, ReadView const& view)
: sle_(view.read(key)), readView_(view), key_(key)
{
}
std::shared_ptr<SLE const> sle_; // Always valid (const view)
ReadView const& readView_; // Always valid
Keylet key_; // Keylet for this entry
};
/**
@@ -192,8 +207,8 @@ protected:
{
}
ApplyView& applyView_; // ApplyView for write contexts (first for init order)
Keylet const key_;
ApplyView& applyView_; // ApplyView for write contexts (first for init order)
Keylet key_; // Keylet for this entry
std::shared_ptr<SLE> mutableSle_; // Mutable SLE for write contexts
};

View File

@@ -6,6 +6,7 @@
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
#include <xrpl/ledger/helpers/RippleState.h>
#include <xrpl/protocol/AmountConversions.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
@@ -301,7 +302,7 @@ IOUToken::transferRate() const
//------------------------------------------------------------------------------
TER
trustCreate(
WritableRippleState::trustCreate(
ApplyView& view,
bool const bSrcHigh,
AccountID const& uSrcAccountID,
@@ -415,7 +416,7 @@ trustCreate(
}
TER
trustDelete(
WritableRippleState::trustDelete(
ApplyView& view,
std::shared_ptr<SLE> const& sleRippleState,
AccountID const& uLowAccountID,
@@ -551,7 +552,7 @@ issueIOU(
state->setFieldAmount(sfBalance, final_balance);
if (must_delete)
{
return trustDelete(
return WritableRippleState::trustDelete(
view,
state,
bSenderHigh ? account : issue.account,
@@ -578,7 +579,7 @@ issueIOU(
bool noRipple = (receiverAccount->getFlags() & lsfDefaultRipple) == 0;
return trustCreate(
return WritableRippleState::trustCreate(
view,
bSenderHigh,
issue.account,
@@ -644,7 +645,7 @@ redeemIOU(
if (must_delete)
{
return trustDelete(
return WritableRippleState::trustDelete(
applyView,
state,
bSenderHigh ? issue.account : account,
@@ -792,7 +793,7 @@ WritableIOUToken::addEmptyHolding(
if (priorBalance < readView_.fees().accountReserve(ownerCount + 1))
return tecNO_LINE_INSUF_RESERVE;
return trustCreate(
return WritableRippleState::trustCreate(
applyView_,
high,
srcId,
@@ -865,7 +866,7 @@ WritableIOUToken::removeEmptyHolding(AccountID const& accountID, beast::Journal
line->clearFlag(lsfHighReserve);
}
return trustDelete(
return WritableRippleState::trustDelete(
applyView_,
line,
line->at(sfLowLimit)->getIssuer(),
@@ -906,7 +907,8 @@ deleteAMMTrustLine(
if (ammAccountID && (low != *ammAccountID && high != *ammAccountID))
return terNO_AMM;
if (auto const ter = trustDelete(view, sleState, low, high, j); !isTesSuccess(ter))
if (auto const ter = WritableRippleState::trustDelete(view, sleState, low, high, j);
!isTesSuccess(ter))
{
JLOG(j.error()) << "deleteAMMTrustLine: failed to delete the trustline.";
return ter;

View File

@@ -4,6 +4,7 @@
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
#include <xrpl/ledger/helpers/RippleState.h>
#include <xrpl/ledger/helpers/RippleStateHelpers.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
@@ -385,7 +386,7 @@ rippleCreditIOU(
if (bDelete)
{
return trustDelete(
return WritableRippleState::trustDelete(
view,
sleRippleState,
bSenderHigh ? uReceiverID : uSenderID,
@@ -413,7 +414,7 @@ rippleCreditIOU(
bool const noRipple = (wrappedAccount->getFlags() & lsfDefaultRipple) == 0;
return trustCreate(
return WritableRippleState::trustCreate(
view,
bSenderHigh,
uSenderID,

View File

@@ -2,6 +2,7 @@
#include <xrpl/basics/scope.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/RippleState.h>
#include <xrpl/ledger/helpers/RippleStateHelpers.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/protocol/Feature.h>
@@ -335,7 +336,7 @@ CheckCash::doApply()
initialBalance.setIssuer(noAccount());
// clang-format off
if (TER const ter = trustCreate(
if (TER const ter = WritableRippleState::trustCreate(
psb, // payment sandbox
destLow, // is dest low?
issuer, // source

View File

@@ -5,6 +5,7 @@
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
#include <xrpl/ledger/helpers/RippleState.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/MPTAmount.h>
@@ -76,7 +77,7 @@ escrowUnlockApplyHelper<Issue>(
initialBalance.setIssuer(noAccount());
// clang-format off
if (TER const ter = trustCreate(
if (TER const ter = WritableRippleState::trustCreate(
view, // payment sandbox
recvLow, // is dest low?
issuer, // source

View File

@@ -1,5 +1,6 @@
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/MPToken.h>
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/TxFlags.h>
@@ -28,6 +29,8 @@ MPTokenAuthorize::preclaim(PreclaimContext const& ctx)
{
auto const accountID = ctx.tx[sfAccount];
auto const holderID = ctx.tx[~sfHolder];
MPTokenIssuance const mptIssuance(ctx.view, ctx.tx[sfMPTokenIssuanceID]);
MPToken const mpt(ctx.view, mptIssuance, accountID);
// if non-issuer account submits this tx, then they are trying either:
// 1. Unauthorize/delete MPToken
@@ -37,9 +40,6 @@ MPTokenAuthorize::preclaim(PreclaimContext const& ctx)
// `holderID` is NOT used
if (!holderID)
{
std::shared_ptr<SLE const> sleMpt =
ctx.view.read(keylet::mptoken(ctx.tx[sfMPTokenIssuanceID], accountID));
// There is an edge case where all holders have zero balance, issuance
// is legally destroyed, then outstanding MPT(s) are deleted afterwards.
// Thus, there is no need to check for the existence of the issuance if
@@ -49,36 +49,31 @@ MPTokenAuthorize::preclaim(PreclaimContext const& ctx)
// if holder wants to delete/unauthorize a mpt
if (ctx.tx.getFlags() & tfMPTUnauthorize)
{
if (!sleMpt)
if (!mpt)
return tecOBJECT_NOT_FOUND;
if ((*sleMpt)[sfMPTAmount] != 0)
if ((*mpt)[sfMPTAmount] != 0)
{
auto const sleMptIssuance =
ctx.view.read(keylet::mptIssuance(ctx.tx[sfMPTokenIssuanceID]));
if (!sleMptIssuance)
if (!mptIssuance)
return tefINTERNAL; // LCOV_EXCL_LINE
return tecHAS_OBLIGATIONS;
}
if ((*sleMpt)[~sfLockedAmount].value_or(0) != 0)
if ((*mpt)[~sfLockedAmount].value_or(0) != 0)
{
auto const sleMptIssuance =
ctx.view.read(keylet::mptIssuance(ctx.tx[sfMPTokenIssuanceID]));
if (!sleMptIssuance)
if (!mptIssuance)
return tefINTERNAL; // LCOV_EXCL_LINE
return tecHAS_OBLIGATIONS;
}
if (ctx.view.rules().enabled(featureSingleAssetVault) && sleMpt->isFlag(lsfMPTLocked))
if (ctx.view.rules().enabled(featureSingleAssetVault) && mpt->isFlag(lsfMPTLocked))
return tecNO_PERMISSION;
return tesSUCCESS;
}
// Now test when the holder wants to hold/create/authorize a new MPT
MPTokenIssuance const mptIssuance(ctx.view, MPTIssue{ctx.tx[sfMPTokenIssuanceID]});
if (!mptIssuance)
return tecOBJECT_NOT_FOUND;
@@ -87,7 +82,7 @@ MPTokenAuthorize::preclaim(PreclaimContext const& ctx)
return tecNO_PERMISSION;
// if holder wants to use and create a mpt
if (sleMpt)
if (mpt)
return tecDUPLICATE;
return tesSUCCESS;
@@ -96,7 +91,6 @@ MPTokenAuthorize::preclaim(PreclaimContext const& ctx)
if (AccountRoot const acctHolder(*holderID, ctx.view); !acctHolder)
return tecNO_DST;
MPTokenIssuance const mptIssuance(ctx.view, MPTIssue{ctx.tx[sfMPTokenIssuanceID]});
if (!mptIssuance)
return tecOBJECT_NOT_FOUND;

View File

@@ -1,6 +1,7 @@
#include <xrpl/basics/Log.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/RippleState.h>
#include <xrpl/protocol/AMMCore.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
@@ -581,7 +582,8 @@ TrustSet::doApply()
{
// Delete.
terResult = trustDelete(view(), sleRippleState, uLowAccountID, uHighAccountID, viewJ);
terResult = WritableRippleState::trustDelete(
view(), sleRippleState, uLowAccountID, uHighAccountID, viewJ);
}
// Reserve is not scaled by load.
else if (bReserveIncrease && preFeeBalance_ < reserveCreate)
@@ -632,7 +634,7 @@ TrustSet::doApply()
JLOG(j_.trace()) << "doTrustSet: Creating ripple line: " << to_string(k.key);
// Create a new ripple line.
terResult = trustCreate(
terResult = WritableRippleState::trustCreate(
view(),
bHigh,
accountID_,

View File

@@ -4,6 +4,7 @@
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/helpers/RippleState.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/InnerObjectFormats.h>
@@ -404,7 +405,7 @@ class Invariants_test : public beast::unit_test::suite
STAmount const lowLimit = line->at(sfLowLimit);
STAmount const highLimit = line->at(sfHighLimit);
BEAST_EXPECT(
trustDelete(
WritableRippleState::trustDelete(
ac.view(),
line,
lowLimit.getIssuer(),