Add PermissionDelegation feature (#5354)

This change implements the account permission delegation described in XLS-75d, see https://github.com/XRPLF/XRPL-Standards/pull/257.

* Introduces transaction-level and granular permissions that can be delegated to other accounts.
* Adds `DelegateSet` transaction to grant specified permissions to another account.
* Adds `ltDelegate` ledger object to maintain the permission list for delegating/delegated account pair.
* Adds an optional `Delegate` field in common fields, allowing a delegated account to send transactions on behalf of the delegating account within the granted permission scope. The `Account` field remains the delegating account; the `Delegate` field specifies the delegated account. The transaction is signed by the delegated account.
This commit is contained in:
yinyiqian1
2025-05-08 06:14:02 -04:00
committed by GitHub
parent 9ec2d7f8ff
commit 2db2791805
49 changed files with 2976 additions and 91 deletions

View File

@@ -120,7 +120,7 @@ enum error_code_i {
rpcSRC_ACT_MALFORMED = 65,
rpcSRC_ACT_MISSING = 66,
rpcSRC_ACT_NOT_FOUND = 67,
// unused 68,
rpcDELEGATE_ACT_NOT_FOUND = 68,
rpcSRC_CUR_MALFORMED = 69,
rpcSRC_ISR_MALFORMED = 70,
rpcSTREAM_MALFORMED = 71,

View File

@@ -279,6 +279,10 @@ amm(Asset const& issue1, Asset const& issue2) noexcept;
Keylet
amm(uint256 const& amm) noexcept;
/** A keylet for Delegate object */
Keylet
delegate(AccountID const& account, AccountID const& authorizedAccount) noexcept;
Keylet
bridge(STXChainBridge const& bridge, STXChainBridge::ChainType chainType);

View File

@@ -0,0 +1,97 @@
//------------------------------------------------------------------------------
/*
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_PROTOCOL_PERMISSION_H_INCLUDED
#define RIPPLE_PROTOCOL_PERMISSION_H_INCLUDED
#include <xrpl/protocol/TxFormats.h>
#include <optional>
#include <string>
#include <unordered_map>
#include <unordered_set>
namespace ripple {
/**
* We have both transaction type permissions and granular type permissions.
* Since we will reuse the TransactionFormats to parse the Transaction
* Permissions, only the GranularPermissionType is defined here. To prevent
* conflicts with TxType, the GranularPermissionType is always set to a value
* greater than the maximum value of uint16.
*/
enum GranularPermissionType : std::uint32_t {
#pragma push_macro("PERMISSION")
#undef PERMISSION
#define PERMISSION(type, txType, value) type = value,
#include <xrpl/protocol/detail/permissions.macro>
#undef PERMISSION
#pragma pop_macro("PERMISSION")
};
enum Delegation { delegatable, notDelegatable };
class Permission
{
private:
Permission();
std::unordered_map<std::uint16_t, Delegation> delegatableTx_;
std::unordered_map<std::string, GranularPermissionType>
granularPermissionMap_;
std::unordered_map<GranularPermissionType, std::string> granularNameMap_;
std::unordered_map<GranularPermissionType, TxType> granularTxTypeMap_;
public:
static Permission const&
getInstance();
Permission(const Permission&) = delete;
Permission&
operator=(const Permission&) = delete;
std::optional<std::uint32_t>
getGranularValue(std::string const& name) const;
std::optional<std::string>
getGranularName(GranularPermissionType const& value) const;
std::optional<TxType>
getGranularTxType(GranularPermissionType const& gpType) const;
bool
isDelegatable(std::uint32_t const& permissionValue) const;
// for tx level permission, permission value is equal to tx type plus one
uint32_t
txToPermissionType(const TxType& type) const;
// tx type value is permission value minus one
TxType
permissionToTxType(uint32_t const& value) const;
};
} // namespace ripple
#endif

View File

@@ -155,6 +155,10 @@ std::size_t constexpr maxPriceScale = 20;
*/
std::size_t constexpr maxTrim = 25;
/** The maximum number of delegate permissions an account can grant
*/
std::size_t constexpr permissionMaxSize = 10;
} // namespace ripple
#endif

View File

@@ -120,6 +120,13 @@ constexpr std::uint32_t tfTrustSetMask =
~(tfUniversal | tfSetfAuth | tfSetNoRipple | tfClearNoRipple | tfSetFreeze |
tfClearFreeze | tfSetDeepFreeze | tfClearDeepFreeze);
// valid flags for granular permission
constexpr std::uint32_t tfTrustSetGranularMask = tfSetfAuth | tfSetFreeze | tfClearFreeze;
// bits representing supportedGranularMask are set to 0 and the bits
// representing other flags are set to 1 in tfPermissionMask.
constexpr std::uint32_t tfTrustSetPermissionMask = (~tfTrustSetMask) & (~tfTrustSetGranularMask);
// EnableAmendment flags:
constexpr std::uint32_t tfGotMajority = 0x00010000;
constexpr std::uint32_t tfLostMajority = 0x00020000;
@@ -155,6 +162,8 @@ constexpr std::uint32_t const tfMPTokenAuthorizeMask = ~(tfUniversal | tfMPTUna
constexpr std::uint32_t const tfMPTLock = 0x00000001;
constexpr std::uint32_t const tfMPTUnlock = 0x00000002;
constexpr std::uint32_t const tfMPTokenIssuanceSetMask = ~(tfUniversal | tfMPTLock | tfMPTUnlock);
constexpr std::uint32_t const tfMPTokenIssuanceSetGranularMask = tfMPTLock | tfMPTUnlock;
constexpr std::uint32_t const tfMPTokenIssuanceSetPermissionMask = (~tfMPTokenIssuanceSetMask) & (~tfMPTokenIssuanceSetGranularMask);
// MPTokenIssuanceDestroy flags:
constexpr std::uint32_t const tfMPTokenIssuanceDestroyMask = ~tfUniversal;

View File

@@ -59,7 +59,7 @@ enum TxType : std::uint16_t
#pragma push_macro("TRANSACTION")
#undef TRANSACTION
#define TRANSACTION(tag, value, name, fields) tag = value,
#define TRANSACTION(tag, value, name, delegatable, fields) tag = value,
#include <xrpl/protocol/detail/transactions.macro>

View File

@@ -32,6 +32,7 @@
// If you add an amendment here, then do not forget to increment `numFeatures`
// in include/xrpl/protocol/Feature.h.
XRPL_FEATURE(PermissionDelegation, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FIX (PayChanCancelAfter, Supported::yes, VoteBehavior::DefaultNo)
// Check flags in Credential transactions
XRPL_FIX (InvalidTxFlags, Supported::yes, VoteBehavior::DefaultNo)

View File

@@ -460,6 +460,18 @@ LEDGER_ENTRY(ltPERMISSIONED_DOMAIN, 0x0082, PermissionedDomain, permissioned_dom
{sfPreviousTxnLgrSeq, soeREQUIRED},
}))
/** A ledger object representing permissions an account has delegated to another account.
\sa keylet::delegate
*/
LEDGER_ENTRY(ltDELEGATE, 0x0083, Delegate, delegate, ({
{sfAccount, soeREQUIRED},
{sfAuthorize, soeREQUIRED},
{sfPermissions, soeREQUIRED},
{sfOwnerNode, soeREQUIRED},
{sfPreviousTxnID, soeREQUIRED},
{sfPreviousTxnLgrSeq, soeREQUIRED},
}))
#undef EXPAND
#undef LEDGER_ENTRY_DUPLICATE

View File

@@ -0,0 +1,68 @@
//------------------------------------------------------------------------------
/*
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.
*/
//==============================================================================
#if !defined(PERMISSION)
#error "undefined macro: PERMISSION"
#endif
/**
* PERMISSION(name, type, txType, value)
*
* This macro defines a permission:
* name: the name of the permission.
* type: the GranularPermissionType enum.
* txType: the corresponding TxType for this permission.
* value: the uint32 numeric value for the enum type.
*/
/** This permission grants the delegated account the ability to authorize a trustline. */
PERMISSION(TrustlineAuthorize, ttTRUST_SET, 65537)
/** This permission grants the delegated account the ability to freeze a trustline. */
PERMISSION(TrustlineFreeze, ttTRUST_SET, 65538)
/** This permission grants the delegated account the ability to unfreeze a trustline. */
PERMISSION(TrustlineUnfreeze, ttTRUST_SET, 65539)
/** This permission grants the delegated account the ability to set Domain. */
PERMISSION(AccountDomainSet, ttACCOUNT_SET, 65540)
/** This permission grants the delegated account the ability to set EmailHashSet. */
PERMISSION(AccountEmailHashSet, ttACCOUNT_SET, 65541)
/** This permission grants the delegated account the ability to set MessageKey. */
PERMISSION(AccountMessageKeySet, ttACCOUNT_SET, 65542)
/** This permission grants the delegated account the ability to set TransferRate. */
PERMISSION(AccountTransferRateSet, ttACCOUNT_SET, 65543)
/** This permission grants the delegated account the ability to set TickSize. */
PERMISSION(AccountTickSizeSet, ttACCOUNT_SET, 65544)
/** This permission grants the delegated account the ability to mint payment, which means sending a payment for a currency where the sending account is the issuer. */
PERMISSION(PaymentMint, ttPAYMENT, 65545)
/** This permission grants the delegated account the ability to burn payment, which means sending a payment for a currency where the destination account is the issuer */
PERMISSION(PaymentBurn, ttPAYMENT, 65546)
/** This permission grants the delegated account the ability to lock MPToken. */
PERMISSION(MPTokenIssuanceLock, ttMPTOKEN_ISSUANCE_SET, 65547)
/** This permission grants the delegated account the ability to unlock MPToken. */
PERMISSION(MPTokenIssuanceUnlock, ttMPTOKEN_ISSUANCE_SET, 65548)

View File

@@ -112,6 +112,7 @@ TYPED_SFIELD(sfEmitGeneration, UINT32, 46)
TYPED_SFIELD(sfVoteWeight, UINT32, 48)
TYPED_SFIELD(sfFirstNFTokenSequence, UINT32, 50)
TYPED_SFIELD(sfOracleDocumentID, UINT32, 51)
TYPED_SFIELD(sfPermissionValue, UINT32, 52)
// 64-bit integers (common)
TYPED_SFIELD(sfIndexNext, UINT64, 1)
@@ -278,6 +279,7 @@ TYPED_SFIELD(sfRegularKey, ACCOUNT, 8)
TYPED_SFIELD(sfNFTokenMinter, ACCOUNT, 9)
TYPED_SFIELD(sfEmitCallback, ACCOUNT, 10)
TYPED_SFIELD(sfHolder, ACCOUNT, 11)
TYPED_SFIELD(sfDelegate, ACCOUNT, 12)
// account (uncommon)
TYPED_SFIELD(sfHookAccount, ACCOUNT, 16)
@@ -327,6 +329,7 @@ UNTYPED_SFIELD(sfSignerEntry, OBJECT, 11)
UNTYPED_SFIELD(sfNFToken, OBJECT, 12)
UNTYPED_SFIELD(sfEmitDetails, OBJECT, 13)
UNTYPED_SFIELD(sfHook, OBJECT, 14)
UNTYPED_SFIELD(sfPermission, OBJECT, 15)
// inner object (uncommon)
UNTYPED_SFIELD(sfSigner, OBJECT, 16)
@@ -377,3 +380,4 @@ UNTYPED_SFIELD(sfAuthAccounts, ARRAY, 25)
UNTYPED_SFIELD(sfAuthorizeCredentials, ARRAY, 26)
UNTYPED_SFIELD(sfUnauthorizeCredentials, ARRAY, 27)
UNTYPED_SFIELD(sfAcceptedCredentials, ARRAY, 28)
UNTYPED_SFIELD(sfPermissions, ARRAY, 29)

View File

@@ -22,14 +22,14 @@
#endif
/**
* TRANSACTION(tag, value, name, fields)
* TRANSACTION(tag, value, name, delegatable, fields)
*
* You must define a transactor class in the `ripple` namespace named `name`,
* and include its header in `src/xrpld/app/tx/detail/applySteps.cpp`.
*/
/** This transaction type executes a payment. */
TRANSACTION(ttPAYMENT, 0, Payment, ({
TRANSACTION(ttPAYMENT, 0, Payment, Delegation::delegatable, ({
{sfDestination, soeREQUIRED},
{sfAmount, soeREQUIRED, soeMPTSupported},
{sfSendMax, soeOPTIONAL, soeMPTSupported},
@@ -41,7 +41,7 @@ TRANSACTION(ttPAYMENT, 0, Payment, ({
}))
/** This transaction type creates an escrow object. */
TRANSACTION(ttESCROW_CREATE, 1, EscrowCreate, ({
TRANSACTION(ttESCROW_CREATE, 1, EscrowCreate, Delegation::delegatable, ({
{sfDestination, soeREQUIRED},
{sfAmount, soeREQUIRED},
{sfCondition, soeOPTIONAL},
@@ -51,7 +51,7 @@ TRANSACTION(ttESCROW_CREATE, 1, EscrowCreate, ({
}))
/** This transaction type completes an existing escrow. */
TRANSACTION(ttESCROW_FINISH, 2, EscrowFinish, ({
TRANSACTION(ttESCROW_FINISH, 2, EscrowFinish, Delegation::delegatable, ({
{sfOwner, soeREQUIRED},
{sfOfferSequence, soeREQUIRED},
{sfFulfillment, soeOPTIONAL},
@@ -61,7 +61,7 @@ TRANSACTION(ttESCROW_FINISH, 2, EscrowFinish, ({
/** This transaction type adjusts various account settings. */
TRANSACTION(ttACCOUNT_SET, 3, AccountSet, ({
TRANSACTION(ttACCOUNT_SET, 3, AccountSet, Delegation::notDelegatable, ({
{sfEmailHash, soeOPTIONAL},
{sfWalletLocator, soeOPTIONAL},
{sfWalletSize, soeOPTIONAL},
@@ -75,20 +75,20 @@ TRANSACTION(ttACCOUNT_SET, 3, AccountSet, ({
}))
/** This transaction type cancels an existing escrow. */
TRANSACTION(ttESCROW_CANCEL, 4, EscrowCancel, ({
TRANSACTION(ttESCROW_CANCEL, 4, EscrowCancel, Delegation::delegatable, ({
{sfOwner, soeREQUIRED},
{sfOfferSequence, soeREQUIRED},
}))
/** This transaction type sets or clears an account's "regular key". */
TRANSACTION(ttREGULAR_KEY_SET, 5, SetRegularKey, ({
TRANSACTION(ttREGULAR_KEY_SET, 5, SetRegularKey, Delegation::notDelegatable, ({
{sfRegularKey, soeOPTIONAL},
}))
// 6 deprecated
/** This transaction type creates an offer to trade one asset for another. */
TRANSACTION(ttOFFER_CREATE, 7, OfferCreate, ({
TRANSACTION(ttOFFER_CREATE, 7, OfferCreate, Delegation::delegatable, ({
{sfTakerPays, soeREQUIRED},
{sfTakerGets, soeREQUIRED},
{sfExpiration, soeOPTIONAL},
@@ -96,14 +96,14 @@ TRANSACTION(ttOFFER_CREATE, 7, OfferCreate, ({
}))
/** This transaction type cancels existing offers to trade one asset for another. */
TRANSACTION(ttOFFER_CANCEL, 8, OfferCancel, ({
TRANSACTION(ttOFFER_CANCEL, 8, OfferCancel, Delegation::delegatable, ({
{sfOfferSequence, soeREQUIRED},
}))
// 9 deprecated
/** This transaction type creates a new set of tickets. */
TRANSACTION(ttTICKET_CREATE, 10, TicketCreate, ({
TRANSACTION(ttTICKET_CREATE, 10, TicketCreate, Delegation::delegatable, ({
{sfTicketCount, soeREQUIRED},
}))
@@ -112,13 +112,13 @@ TRANSACTION(ttTICKET_CREATE, 10, TicketCreate, ({
/** This transaction type modifies the signer list associated with an account. */
// The SignerEntries are optional because a SignerList is deleted by
// setting the SignerQuorum to zero and omitting SignerEntries.
TRANSACTION(ttSIGNER_LIST_SET, 12, SignerListSet, ({
TRANSACTION(ttSIGNER_LIST_SET, 12, SignerListSet, Delegation::notDelegatable, ({
{sfSignerQuorum, soeREQUIRED},
{sfSignerEntries, soeOPTIONAL},
}))
/** This transaction type creates a new unidirectional XRP payment channel. */
TRANSACTION(ttPAYCHAN_CREATE, 13, PaymentChannelCreate, ({
TRANSACTION(ttPAYCHAN_CREATE, 13, PaymentChannelCreate, Delegation::delegatable, ({
{sfDestination, soeREQUIRED},
{sfAmount, soeREQUIRED},
{sfSettleDelay, soeREQUIRED},
@@ -128,14 +128,14 @@ TRANSACTION(ttPAYCHAN_CREATE, 13, PaymentChannelCreate, ({
}))
/** This transaction type funds an existing unidirectional XRP payment channel. */
TRANSACTION(ttPAYCHAN_FUND, 14, PaymentChannelFund, ({
TRANSACTION(ttPAYCHAN_FUND, 14, PaymentChannelFund, Delegation::delegatable, ({
{sfChannel, soeREQUIRED},
{sfAmount, soeREQUIRED},
{sfExpiration, soeOPTIONAL},
}))
/** This transaction type submits a claim against an existing unidirectional payment channel. */
TRANSACTION(ttPAYCHAN_CLAIM, 15, PaymentChannelClaim, ({
TRANSACTION(ttPAYCHAN_CLAIM, 15, PaymentChannelClaim, Delegation::delegatable, ({
{sfChannel, soeREQUIRED},
{sfAmount, soeOPTIONAL},
{sfBalance, soeOPTIONAL},
@@ -145,7 +145,7 @@ TRANSACTION(ttPAYCHAN_CLAIM, 15, PaymentChannelClaim, ({
}))
/** This transaction type creates a new check. */
TRANSACTION(ttCHECK_CREATE, 16, CheckCreate, ({
TRANSACTION(ttCHECK_CREATE, 16, CheckCreate, Delegation::delegatable, ({
{sfDestination, soeREQUIRED},
{sfSendMax, soeREQUIRED},
{sfExpiration, soeOPTIONAL},
@@ -154,19 +154,19 @@ TRANSACTION(ttCHECK_CREATE, 16, CheckCreate, ({
}))
/** This transaction type cashes an existing check. */
TRANSACTION(ttCHECK_CASH, 17, CheckCash, ({
TRANSACTION(ttCHECK_CASH, 17, CheckCash, Delegation::delegatable, ({
{sfCheckID, soeREQUIRED},
{sfAmount, soeOPTIONAL},
{sfDeliverMin, soeOPTIONAL},
}))
/** This transaction type cancels an existing check. */
TRANSACTION(ttCHECK_CANCEL, 18, CheckCancel, ({
TRANSACTION(ttCHECK_CANCEL, 18, CheckCancel, Delegation::delegatable, ({
{sfCheckID, soeREQUIRED},
}))
/** This transaction type grants or revokes authorization to transfer funds. */
TRANSACTION(ttDEPOSIT_PREAUTH, 19, DepositPreauth, ({
TRANSACTION(ttDEPOSIT_PREAUTH, 19, DepositPreauth, Delegation::delegatable, ({
{sfAuthorize, soeOPTIONAL},
{sfUnauthorize, soeOPTIONAL},
{sfAuthorizeCredentials, soeOPTIONAL},
@@ -174,14 +174,14 @@ TRANSACTION(ttDEPOSIT_PREAUTH, 19, DepositPreauth, ({
}))
/** This transaction type modifies a trustline between two accounts. */
TRANSACTION(ttTRUST_SET, 20, TrustSet, ({
TRANSACTION(ttTRUST_SET, 20, TrustSet, Delegation::delegatable, ({
{sfLimitAmount, soeOPTIONAL},
{sfQualityIn, soeOPTIONAL},
{sfQualityOut, soeOPTIONAL},
}))
/** This transaction type deletes an existing account. */
TRANSACTION(ttACCOUNT_DELETE, 21, AccountDelete, ({
TRANSACTION(ttACCOUNT_DELETE, 21, AccountDelete, Delegation::notDelegatable, ({
{sfDestination, soeREQUIRED},
{sfDestinationTag, soeOPTIONAL},
{sfCredentialIDs, soeOPTIONAL},
@@ -190,7 +190,7 @@ TRANSACTION(ttACCOUNT_DELETE, 21, AccountDelete, ({
// 22 reserved
/** This transaction mints a new NFT. */
TRANSACTION(ttNFTOKEN_MINT, 25, NFTokenMint, ({
TRANSACTION(ttNFTOKEN_MINT, 25, NFTokenMint, Delegation::delegatable, ({
{sfNFTokenTaxon, soeREQUIRED},
{sfTransferFee, soeOPTIONAL},
{sfIssuer, soeOPTIONAL},
@@ -201,13 +201,13 @@ TRANSACTION(ttNFTOKEN_MINT, 25, NFTokenMint, ({
}))
/** This transaction burns (i.e. destroys) an existing NFT. */
TRANSACTION(ttNFTOKEN_BURN, 26, NFTokenBurn, ({
TRANSACTION(ttNFTOKEN_BURN, 26, NFTokenBurn, Delegation::delegatable, ({
{sfNFTokenID, soeREQUIRED},
{sfOwner, soeOPTIONAL},
}))
/** This transaction creates a new offer to buy or sell an NFT. */
TRANSACTION(ttNFTOKEN_CREATE_OFFER, 27, NFTokenCreateOffer, ({
TRANSACTION(ttNFTOKEN_CREATE_OFFER, 27, NFTokenCreateOffer, Delegation::delegatable, ({
{sfNFTokenID, soeREQUIRED},
{sfAmount, soeREQUIRED},
{sfDestination, soeOPTIONAL},
@@ -216,25 +216,25 @@ TRANSACTION(ttNFTOKEN_CREATE_OFFER, 27, NFTokenCreateOffer, ({
}))
/** This transaction cancels an existing offer to buy or sell an existing NFT. */
TRANSACTION(ttNFTOKEN_CANCEL_OFFER, 28, NFTokenCancelOffer, ({
TRANSACTION(ttNFTOKEN_CANCEL_OFFER, 28, NFTokenCancelOffer, Delegation::delegatable, ({
{sfNFTokenOffers, soeREQUIRED},
}))
/** This transaction accepts an existing offer to buy or sell an existing NFT. */
TRANSACTION(ttNFTOKEN_ACCEPT_OFFER, 29, NFTokenAcceptOffer, ({
TRANSACTION(ttNFTOKEN_ACCEPT_OFFER, 29, NFTokenAcceptOffer, Delegation::delegatable, ({
{sfNFTokenBuyOffer, soeOPTIONAL},
{sfNFTokenSellOffer, soeOPTIONAL},
{sfNFTokenBrokerFee, soeOPTIONAL},
}))
/** This transaction claws back issued tokens. */
TRANSACTION(ttCLAWBACK, 30, Clawback, ({
TRANSACTION(ttCLAWBACK, 30, Clawback, Delegation::delegatable, ({
{sfAmount, soeREQUIRED, soeMPTSupported},
{sfHolder, soeOPTIONAL},
}))
/** This transaction claws back tokens from an AMM pool. */
TRANSACTION(ttAMM_CLAWBACK, 31, AMMClawback, ({
TRANSACTION(ttAMM_CLAWBACK, 31, AMMClawback, Delegation::delegatable, ({
{sfHolder, soeREQUIRED},
{sfAsset, soeREQUIRED},
{sfAsset2, soeREQUIRED},
@@ -242,14 +242,14 @@ TRANSACTION(ttAMM_CLAWBACK, 31, AMMClawback, ({
}))
/** This transaction type creates an AMM instance */
TRANSACTION(ttAMM_CREATE, 35, AMMCreate, ({
TRANSACTION(ttAMM_CREATE, 35, AMMCreate, Delegation::delegatable, ({
{sfAmount, soeREQUIRED},
{sfAmount2, soeREQUIRED},
{sfTradingFee, soeREQUIRED},
}))
/** This transaction type deposits into an AMM instance */
TRANSACTION(ttAMM_DEPOSIT, 36, AMMDeposit, ({
TRANSACTION(ttAMM_DEPOSIT, 36, AMMDeposit, Delegation::delegatable, ({
{sfAsset, soeREQUIRED},
{sfAsset2, soeREQUIRED},
{sfAmount, soeOPTIONAL},
@@ -260,7 +260,7 @@ TRANSACTION(ttAMM_DEPOSIT, 36, AMMDeposit, ({
}))
/** This transaction type withdraws from an AMM instance */
TRANSACTION(ttAMM_WITHDRAW, 37, AMMWithdraw, ({
TRANSACTION(ttAMM_WITHDRAW, 37, AMMWithdraw, Delegation::delegatable, ({
{sfAsset, soeREQUIRED},
{sfAsset2, soeREQUIRED},
{sfAmount, soeOPTIONAL},
@@ -270,14 +270,14 @@ TRANSACTION(ttAMM_WITHDRAW, 37, AMMWithdraw, ({
}))
/** This transaction type votes for the trading fee */
TRANSACTION(ttAMM_VOTE, 38, AMMVote, ({
TRANSACTION(ttAMM_VOTE, 38, AMMVote, Delegation::delegatable, ({
{sfAsset, soeREQUIRED},
{sfAsset2, soeREQUIRED},
{sfTradingFee, soeREQUIRED},
}))
/** This transaction type bids for the auction slot */
TRANSACTION(ttAMM_BID, 39, AMMBid, ({
TRANSACTION(ttAMM_BID, 39, AMMBid, Delegation::delegatable, ({
{sfAsset, soeREQUIRED},
{sfAsset2, soeREQUIRED},
{sfBidMin, soeOPTIONAL},
@@ -286,20 +286,20 @@ TRANSACTION(ttAMM_BID, 39, AMMBid, ({
}))
/** This transaction type deletes AMM in the empty state */
TRANSACTION(ttAMM_DELETE, 40, AMMDelete, ({
TRANSACTION(ttAMM_DELETE, 40, AMMDelete, Delegation::delegatable, ({
{sfAsset, soeREQUIRED},
{sfAsset2, soeREQUIRED},
}))
/** This transactions creates a crosschain sequence number */
TRANSACTION(ttXCHAIN_CREATE_CLAIM_ID, 41, XChainCreateClaimID, ({
TRANSACTION(ttXCHAIN_CREATE_CLAIM_ID, 41, XChainCreateClaimID, Delegation::delegatable, ({
{sfXChainBridge, soeREQUIRED},
{sfSignatureReward, soeREQUIRED},
{sfOtherChainSource, soeREQUIRED},
}))
/** This transactions initiates a crosschain transaction */
TRANSACTION(ttXCHAIN_COMMIT, 42, XChainCommit, ({
TRANSACTION(ttXCHAIN_COMMIT, 42, XChainCommit, Delegation::delegatable, ({
{sfXChainBridge, soeREQUIRED},
{sfXChainClaimID, soeREQUIRED},
{sfAmount, soeREQUIRED},
@@ -307,7 +307,7 @@ TRANSACTION(ttXCHAIN_COMMIT, 42, XChainCommit, ({
}))
/** This transaction completes a crosschain transaction */
TRANSACTION(ttXCHAIN_CLAIM, 43, XChainClaim, ({
TRANSACTION(ttXCHAIN_CLAIM, 43, XChainClaim, Delegation::delegatable, ({
{sfXChainBridge, soeREQUIRED},
{sfXChainClaimID, soeREQUIRED},
{sfDestination, soeREQUIRED},
@@ -316,7 +316,7 @@ TRANSACTION(ttXCHAIN_CLAIM, 43, XChainClaim, ({
}))
/** This transaction initiates a crosschain account create transaction */
TRANSACTION(ttXCHAIN_ACCOUNT_CREATE_COMMIT, 44, XChainAccountCreateCommit, ({
TRANSACTION(ttXCHAIN_ACCOUNT_CREATE_COMMIT, 44, XChainAccountCreateCommit, Delegation::delegatable, ({
{sfXChainBridge, soeREQUIRED},
{sfDestination, soeREQUIRED},
{sfAmount, soeREQUIRED},
@@ -324,7 +324,7 @@ TRANSACTION(ttXCHAIN_ACCOUNT_CREATE_COMMIT, 44, XChainAccountCreateCommit, ({
}))
/** This transaction adds an attestation to a claim */
TRANSACTION(ttXCHAIN_ADD_CLAIM_ATTESTATION, 45, XChainAddClaimAttestation, ({
TRANSACTION(ttXCHAIN_ADD_CLAIM_ATTESTATION, 45, XChainAddClaimAttestation, Delegation::delegatable, ({
{sfXChainBridge, soeREQUIRED},
{sfAttestationSignerAccount, soeREQUIRED},
@@ -340,7 +340,7 @@ TRANSACTION(ttXCHAIN_ADD_CLAIM_ATTESTATION, 45, XChainAddClaimAttestation, ({
}))
/** This transaction adds an attestation to an account */
TRANSACTION(ttXCHAIN_ADD_ACCOUNT_CREATE_ATTESTATION, 46, XChainAddAccountCreateAttestation, ({
TRANSACTION(ttXCHAIN_ADD_ACCOUNT_CREATE_ATTESTATION, 46, XChainAddAccountCreateAttestation, Delegation::delegatable, ({
{sfXChainBridge, soeREQUIRED},
{sfAttestationSignerAccount, soeREQUIRED},
@@ -357,31 +357,31 @@ TRANSACTION(ttXCHAIN_ADD_ACCOUNT_CREATE_ATTESTATION, 46, XChainAddAccountCreateA
}))
/** This transaction modifies a sidechain */
TRANSACTION(ttXCHAIN_MODIFY_BRIDGE, 47, XChainModifyBridge, ({
TRANSACTION(ttXCHAIN_MODIFY_BRIDGE, 47, XChainModifyBridge, Delegation::delegatable, ({
{sfXChainBridge, soeREQUIRED},
{sfSignatureReward, soeOPTIONAL},
{sfMinAccountCreateAmount, soeOPTIONAL},
}))
/** This transactions creates a sidechain */
TRANSACTION(ttXCHAIN_CREATE_BRIDGE, 48, XChainCreateBridge, ({
TRANSACTION(ttXCHAIN_CREATE_BRIDGE, 48, XChainCreateBridge, Delegation::delegatable, ({
{sfXChainBridge, soeREQUIRED},
{sfSignatureReward, soeREQUIRED},
{sfMinAccountCreateAmount, soeOPTIONAL},
}))
/** This transaction type creates or updates a DID */
TRANSACTION(ttDID_SET, 49, DIDSet, ({
TRANSACTION(ttDID_SET, 49, DIDSet, Delegation::delegatable, ({
{sfDIDDocument, soeOPTIONAL},
{sfURI, soeOPTIONAL},
{sfData, soeOPTIONAL},
}))
/** This transaction type deletes a DID */
TRANSACTION(ttDID_DELETE, 50, DIDDelete, ({}))
TRANSACTION(ttDID_DELETE, 50, DIDDelete, Delegation::delegatable, ({}))
/** This transaction type creates an Oracle instance */
TRANSACTION(ttORACLE_SET, 51, OracleSet, ({
TRANSACTION(ttORACLE_SET, 51, OracleSet, Delegation::delegatable, ({
{sfOracleDocumentID, soeREQUIRED},
{sfProvider, soeOPTIONAL},
{sfURI, soeOPTIONAL},
@@ -391,18 +391,18 @@ TRANSACTION(ttORACLE_SET, 51, OracleSet, ({
}))
/** This transaction type deletes an Oracle instance */
TRANSACTION(ttORACLE_DELETE, 52, OracleDelete, ({
TRANSACTION(ttORACLE_DELETE, 52, OracleDelete, Delegation::delegatable, ({
{sfOracleDocumentID, soeREQUIRED},
}))
/** This transaction type fixes a problem in the ledger state */
TRANSACTION(ttLEDGER_STATE_FIX, 53, LedgerStateFix, ({
TRANSACTION(ttLEDGER_STATE_FIX, 53, LedgerStateFix, Delegation::notDelegatable, ({
{sfLedgerFixType, soeREQUIRED},
{sfOwner, soeOPTIONAL},
}))
/** This transaction type creates a MPTokensIssuance instance */
TRANSACTION(ttMPTOKEN_ISSUANCE_CREATE, 54, MPTokenIssuanceCreate, ({
TRANSACTION(ttMPTOKEN_ISSUANCE_CREATE, 54, MPTokenIssuanceCreate, Delegation::delegatable, ({
{sfAssetScale, soeOPTIONAL},
{sfTransferFee, soeOPTIONAL},
{sfMaximumAmount, soeOPTIONAL},
@@ -410,24 +410,24 @@ TRANSACTION(ttMPTOKEN_ISSUANCE_CREATE, 54, MPTokenIssuanceCreate, ({
}))
/** This transaction type destroys a MPTokensIssuance instance */
TRANSACTION(ttMPTOKEN_ISSUANCE_DESTROY, 55, MPTokenIssuanceDestroy, ({
TRANSACTION(ttMPTOKEN_ISSUANCE_DESTROY, 55, MPTokenIssuanceDestroy, Delegation::delegatable, ({
{sfMPTokenIssuanceID, soeREQUIRED},
}))
/** This transaction type sets flags on a MPTokensIssuance or MPToken instance */
TRANSACTION(ttMPTOKEN_ISSUANCE_SET, 56, MPTokenIssuanceSet, ({
TRANSACTION(ttMPTOKEN_ISSUANCE_SET, 56, MPTokenIssuanceSet, Delegation::delegatable, ({
{sfMPTokenIssuanceID, soeREQUIRED},
{sfHolder, soeOPTIONAL},
}))
/** This transaction type authorizes a MPToken instance */
TRANSACTION(ttMPTOKEN_AUTHORIZE, 57, MPTokenAuthorize, ({
TRANSACTION(ttMPTOKEN_AUTHORIZE, 57, MPTokenAuthorize, Delegation::delegatable, ({
{sfMPTokenIssuanceID, soeREQUIRED},
{sfHolder, soeOPTIONAL},
}))
/** This transaction type create an Credential instance */
TRANSACTION(ttCREDENTIAL_CREATE, 58, CredentialCreate, ({
TRANSACTION(ttCREDENTIAL_CREATE, 58, CredentialCreate, Delegation::delegatable, ({
{sfSubject, soeREQUIRED},
{sfCredentialType, soeREQUIRED},
{sfExpiration, soeOPTIONAL},
@@ -435,41 +435,47 @@ TRANSACTION(ttCREDENTIAL_CREATE, 58, CredentialCreate, ({
}))
/** This transaction type accept an Credential object */
TRANSACTION(ttCREDENTIAL_ACCEPT, 59, CredentialAccept, ({
TRANSACTION(ttCREDENTIAL_ACCEPT, 59, CredentialAccept, Delegation::delegatable, ({
{sfIssuer, soeREQUIRED},
{sfCredentialType, soeREQUIRED},
}))
/** This transaction type delete an Credential object */
TRANSACTION(ttCREDENTIAL_DELETE, 60, CredentialDelete, ({
TRANSACTION(ttCREDENTIAL_DELETE, 60, CredentialDelete, Delegation::delegatable, ({
{sfSubject, soeOPTIONAL},
{sfIssuer, soeOPTIONAL},
{sfCredentialType, soeREQUIRED},
}))
/** This transaction type modify a NFToken */
TRANSACTION(ttNFTOKEN_MODIFY, 61, NFTokenModify, ({
TRANSACTION(ttNFTOKEN_MODIFY, 61, NFTokenModify, Delegation::delegatable, ({
{sfNFTokenID, soeREQUIRED},
{sfOwner, soeOPTIONAL},
{sfURI, soeOPTIONAL},
}))
/** This transaction type creates or modifies a Permissioned Domain */
TRANSACTION(ttPERMISSIONED_DOMAIN_SET, 62, PermissionedDomainSet, ({
TRANSACTION(ttPERMISSIONED_DOMAIN_SET, 62, PermissionedDomainSet, Delegation::delegatable, ({
{sfDomainID, soeOPTIONAL},
{sfAcceptedCredentials, soeREQUIRED},
}))
/** This transaction type deletes a Permissioned Domain */
TRANSACTION(ttPERMISSIONED_DOMAIN_DELETE, 63, PermissionedDomainDelete, ({
TRANSACTION(ttPERMISSIONED_DOMAIN_DELETE, 63, PermissionedDomainDelete, Delegation::delegatable, ({
{sfDomainID, soeREQUIRED},
}))
/** This transaction type delegates authorized account specified permissions */
TRANSACTION(ttDELEGATE_SET, 64, DelegateSet, Delegation::notDelegatable, ({
{sfAuthorize, soeREQUIRED},
{sfPermissions, soeREQUIRED},
}))
/** This system-generated transaction type is used to update the status of the various amendments.
For details, see: https://xrpl.org/amendments.html
*/
TRANSACTION(ttAMENDMENT, 100, EnableAmendment, ({
TRANSACTION(ttAMENDMENT, 100, EnableAmendment, Delegation::notDelegatable, ({
{sfLedgerSequence, soeREQUIRED},
{sfAmendment, soeREQUIRED},
}))
@@ -477,7 +483,7 @@ TRANSACTION(ttAMENDMENT, 100, EnableAmendment, ({
/** This system-generated transaction type is used to update the network's fee settings.
For details, see: https://xrpl.org/fee-voting.html
*/
TRANSACTION(ttFEE, 101, SetFee, ({
TRANSACTION(ttFEE, 101, SetFee, Delegation::notDelegatable, ({
{sfLedgerSequence, soeOPTIONAL},
// Old version uses raw numbers
{sfBaseFee, soeOPTIONAL},
@@ -494,7 +500,7 @@ TRANSACTION(ttFEE, 101, SetFee, ({
For details, see: https://xrpl.org/negative-unl.html
*/
TRANSACTION(ttUNL_MODIFY, 102, UNLModify, ({
TRANSACTION(ttUNL_MODIFY, 102, UNLModify, Delegation::notDelegatable, ({
{sfUNLModifyDisabling, soeREQUIRED},
{sfLedgerSequence, soeREQUIRED},
{sfUNLModifyValidator, soeREQUIRED},

View File

@@ -145,6 +145,7 @@ JSS(attestations);
JSS(attestation_reward_account);
JSS(auction_slot); // out: amm_info
JSS(authorized); // out: AccountLines
JSS(authorize); // out: delegate
JSS(authorized_credentials); // in: ledger_entry DepositPreauth
JSS(auth_accounts); // out: amm_info
JSS(auth_change); // out: AccountInfo
@@ -699,7 +700,7 @@ JSS(write_load); // out: GetCounts
#pragma push_macro("TRANSACTION")
#undef TRANSACTION
#define TRANSACTION(tag, value, name, fields) JSS(name);
#define TRANSACTION(tag, value, name, delegatable, fields) JSS(name);
#include <xrpl/protocol/detail/transactions.macro>

View File

@@ -107,6 +107,7 @@ constexpr static ErrorInfo unorderedErrorInfos[]{
{rpcSRC_ACT_MALFORMED, "srcActMalformed", "Source account is malformed.", 400},
{rpcSRC_ACT_MISSING, "srcActMissing", "Source account not provided.", 400},
{rpcSRC_ACT_NOT_FOUND, "srcActNotFound", "Source account not found.", 404},
{rpcDELEGATE_ACT_NOT_FOUND, "delegateActNotFound", "Delegate account not found.", 404},
{rpcSRC_CUR_MALFORMED, "srcCurMalformed", "Source currency is malformed.", 400},
{rpcSRC_ISR_MALFORMED, "srcIsrMalformed", "Source issuer is malformed.", 400},
{rpcSTREAM_MALFORMED, "malformedStream", "Stream malformed.", 400},

View File

@@ -94,6 +94,7 @@ enum class LedgerNameSpace : std::uint16_t {
MPTOKEN = 't',
CREDENTIAL = 'D',
PERMISSIONED_DOMAIN = 'm',
DELEGATE = 'E',
// No longer used or supported. Left here to reserve the space
// to avoid accidental reuse.
@@ -452,6 +453,14 @@ amm(uint256 const& id) noexcept
return {ltAMM, id};
}
Keylet
delegate(AccountID const& account, AccountID const& authorizedAccount) noexcept
{
return {
ltDELEGATE,
indexHash(LedgerNameSpace::DELEGATE, account, authorizedAccount)};
}
Keylet
bridge(STXChainBridge const& bridge, STXChainBridge::ChainType chainType)
{

View File

@@ -154,6 +154,10 @@ InnerObjectFormats::InnerObjectFormats()
{sfIssuer, soeREQUIRED},
{sfCredentialType, soeREQUIRED},
});
add(sfPermission.jsonName.c_str(),
sfPermission.getCode(),
{{sfPermissionValue, soeREQUIRED}});
}
InnerObjectFormats const&

View File

@@ -0,0 +1,148 @@
//------------------------------------------------------------------------------
/*
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 <xrpl/beast/utility/instrumentation.h>
#include <xrpl/protocol/Permissions.h>
#include <xrpl/protocol/jss.h>
namespace ripple {
Permission::Permission()
{
delegatableTx_ = {
#pragma push_macro("TRANSACTION")
#undef TRANSACTION
#define TRANSACTION(tag, value, name, delegatable, fields) {value, delegatable},
#include <xrpl/protocol/detail/transactions.macro>
#undef TRANSACTION
#pragma pop_macro("TRANSACTION")
};
granularPermissionMap_ = {
#pragma push_macro("PERMISSION")
#undef PERMISSION
#define PERMISSION(type, txType, value) {#type, type},
#include <xrpl/protocol/detail/permissions.macro>
#undef PERMISSION
#pragma pop_macro("PERMISSION")
};
granularNameMap_ = {
#pragma push_macro("PERMISSION")
#undef PERMISSION
#define PERMISSION(type, txType, value) {type, #type},
#include <xrpl/protocol/detail/permissions.macro>
#undef PERMISSION
#pragma pop_macro("PERMISSION")
};
granularTxTypeMap_ = {
#pragma push_macro("PERMISSION")
#undef PERMISSION
#define PERMISSION(type, txType, value) {type, txType},
#include <xrpl/protocol/detail/permissions.macro>
#undef PERMISSION
#pragma pop_macro("PERMISSION")
};
for ([[maybe_unused]] auto const& permission : granularPermissionMap_)
XRPL_ASSERT(
permission.second > UINT16_MAX,
"ripple::Permission::granularPermissionMap_ : granular permission "
"value must not exceed the maximum uint16_t value.");
}
Permission const&
Permission::getInstance()
{
static Permission const instance;
return instance;
}
std::optional<std::uint32_t>
Permission::getGranularValue(std::string const& name) const
{
auto const it = granularPermissionMap_.find(name);
if (it != granularPermissionMap_.end())
return static_cast<uint32_t>(it->second);
return std::nullopt;
}
std::optional<std::string>
Permission::getGranularName(GranularPermissionType const& value) const
{
auto const it = granularNameMap_.find(value);
if (it != granularNameMap_.end())
return it->second;
return std::nullopt;
}
std::optional<TxType>
Permission::getGranularTxType(GranularPermissionType const& gpType) const
{
auto const it = granularTxTypeMap_.find(gpType);
if (it != granularTxTypeMap_.end())
return it->second;
return std::nullopt;
}
bool
Permission::isDelegatable(std::uint32_t const& permissionValue) const
{
auto const granularPermission =
getGranularName(static_cast<GranularPermissionType>(permissionValue));
if (granularPermission)
// granular permissions are always allowed to be delegated
return true;
auto const it = delegatableTx_.find(permissionValue - 1);
if (it != delegatableTx_.end() && it->second == Delegation::notDelegatable)
return false;
return true;
}
uint32_t
Permission::txToPermissionType(TxType const& type) const
{
return static_cast<uint32_t>(type) + 1;
}
TxType
Permission::permissionToTxType(uint32_t const& value) const
{
return static_cast<TxType>(value - 1);
}
} // namespace ripple

View File

@@ -22,6 +22,7 @@
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/json/json_value.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/Permissions.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STBase.h>
#include <xrpl/protocol/STInteger.h>
@@ -177,6 +178,27 @@ template <>
Json::Value
STUInt32::getJson(JsonOptions) const
{
if (getFName() == sfPermissionValue)
{
auto const permissionValue =
static_cast<GranularPermissionType>(value_);
auto const granular =
Permission::getInstance().getGranularName(permissionValue);
if (granular)
{
return *granular;
}
else
{
auto const txType =
Permission::getInstance().permissionToTxType(value_);
auto item = TxFormats::getInstance().findByType(txType);
if (item != nullptr)
return item->getName();
}
}
return value_;
}

View File

@@ -27,6 +27,7 @@
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/ErrorCodes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/Permissions.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAccount.h>
#include <xrpl/protocol/STAmount.h>
@@ -373,10 +374,35 @@ parseLeaf(
{
if (value.isString())
{
ret = detail::make_stvar<STUInt32>(
field,
beast::lexicalCastThrow<std::uint32_t>(
value.asString()));
if (field == sfPermissionValue)
{
std::string const strValue = value.asString();
auto const granularPermission =
Permission::getInstance().getGranularValue(
strValue);
if (granularPermission)
{
ret = detail::make_stvar<STUInt32>(
field, *granularPermission);
}
else
{
auto const& txType =
TxFormats::getInstance().findTypeByName(
strValue);
ret = detail::make_stvar<STUInt32>(
field,
Permission::getInstance().txToPermissionType(
txType));
}
}
else
{
ret = detail::make_stvar<STUInt32>(
field,
beast::lexicalCastThrow<std::uint32_t>(
value.asString()));
}
}
else if (value.isInt())
{

View File

@@ -46,6 +46,7 @@ TxFormats::TxFormats()
{sfTxnSignature, soeOPTIONAL},
{sfSigners, soeOPTIONAL}, // submit_multisigned
{sfNetworkID, soeOPTIONAL},
{sfDelegate, soeOPTIONAL},
};
#pragma push_macro("UNWRAP")
@@ -54,7 +55,7 @@ TxFormats::TxFormats()
#undef TRANSACTION
#define UNWRAP(...) __VA_ARGS__
#define TRANSACTION(tag, value, name, fields) \
#define TRANSACTION(tag, value, name, delegatable, fields) \
add(jss::name, tag, UNWRAP fields, commonFields);
#include <xrpl/protocol/detail/transactions.macro>

File diff suppressed because it is too large Load Diff

62
src/test/jtx/delegate.h Normal file
View File

@@ -0,0 +1,62 @@
//------------------------------------------------------------------------------
/*
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.
*/
//==============================================================================
#pragma once
#include <test/jtx/Account.h>
#include <test/jtx/Env.h>
namespace ripple {
namespace test {
namespace jtx {
namespace delegate {
Json::Value
set(jtx::Account const& account,
jtx::Account const& authorize,
std::vector<std::string> const& permissions);
Json::Value
entry(
jtx::Env& env,
jtx::Account const& account,
jtx::Account const& authorize);
struct as
{
private:
jtx::Account delegate_;
public:
explicit as(jtx::Account const& account) : delegate_(account)
{
}
void
operator()(jtx::Env&, jtx::JTx& jtx) const
{
jtx.jv[sfDelegate.jsonName] = delegate_.human();
}
};
} // namespace delegate
} // namespace jtx
} // namespace test
} // namespace ripple

View File

@@ -84,6 +84,18 @@ private:
case asfAllowTrustLineClawback:
mask_ |= lsfAllowTrustLineClawback;
break;
case asfDisallowIncomingCheck:
mask_ |= lsfDisallowIncomingCheck;
break;
case asfDisallowIncomingNFTokenOffer:
mask_ |= lsfDisallowIncomingNFTokenOffer;
break;
case asfDisallowIncomingPayChan:
mask_ |= lsfDisallowIncomingPayChan;
break;
case asfDisallowIncomingTrustline:
mask_ |= lsfDisallowIncomingTrustline;
break;
default:
Throw<std::runtime_error>("unknown flag");
}

View File

@@ -465,7 +465,9 @@ Env::autofill_sig(JTx& jt)
return jt.signer(*this, jt);
if (!jt.fill_sig)
return;
auto const account = lookup(jv[jss::Account].asString());
auto const account = jv.isMember(sfDelegate.jsonName)
? lookup(jv[sfDelegate.jsonName].asString())
: lookup(jv[jss::Account].asString());
if (!app().checkSigs())
{
jv[jss::SigningPubKey] = strHex(account.pk().slice());

View File

@@ -0,0 +1,67 @@
//------------------------------------------------------------------------------
/*
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 <test/jtx/delegate.h>
#include <xrpl/protocol/jss.h>
namespace ripple {
namespace test {
namespace jtx {
namespace delegate {
Json::Value
set(jtx::Account const& account,
jtx::Account const& authorize,
std::vector<std::string> const& permissions)
{
Json::Value jv;
jv[jss::TransactionType] = jss::DelegateSet;
jv[jss::Account] = account.human();
jv[sfAuthorize.jsonName] = authorize.human();
Json::Value permissionsJson(Json::arrayValue);
for (auto const& permission : permissions)
{
Json::Value permissionValue;
permissionValue[sfPermissionValue.jsonName] = permission;
Json::Value permissionObj;
permissionObj[sfPermission.jsonName] = permissionValue;
permissionsJson.append(permissionObj);
}
jv[sfPermissions.jsonName] = permissionsJson;
return jv;
}
Json::Value
entry(jtx::Env& env, jtx::Account const& account, jtx::Account const& authorize)
{
Json::Value jvParams;
jvParams[jss::ledger_index] = jss::validated;
jvParams[jss::delegate][jss::account] = account.human();
jvParams[jss::delegate][jss::authorize] = authorize.human();
return env.rpc("json", "ledger_entry", to_string(jvParams));
}
} // namespace delegate
} // namespace jtx
} // namespace test
} // namespace ripple

View File

@@ -233,6 +233,8 @@ MPTTester::set(MPTSet const& arg)
}
if (arg.holder)
jv[sfHolder] = arg.holder->human();
if (arg.delegate)
jv[sfDelegate] = arg.delegate->human();
if (submit(arg, jv) == tesSUCCESS && arg.flags.value_or(0))
{
auto require = [&](std::optional<Account> const& holder,

View File

@@ -136,6 +136,7 @@ struct MPTSet
std::optional<std::uint32_t> ownerCount = std::nullopt;
std::optional<std::uint32_t> holderCount = std::nullopt;
std::optional<std::uint32_t> flags = std::nullopt;
std::optional<Account> delegate = std::nullopt;
std::optional<TER> err = std::nullopt;
};

View File

@@ -2042,6 +2042,78 @@ static constexpr TxnTestData txnTestArray[] = {
"Cannot specify differing 'Amount' and 'DeliverMax'",
"Cannot specify differing 'Amount' and 'DeliverMax'"}}},
{"Minimal delegated transaction.",
__LINE__,
R"({
"command": "doesnt_matter",
"secret": "a",
"tx_json": {
"Account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
"Amount": "1000000000",
"Destination": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA",
"TransactionType": "Payment",
"Delegate": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA"
}
})",
{{"",
"",
"Missing field 'account'.",
"Missing field 'tx_json.Sequence'."}}},
{"Delegate not well formed.",
__LINE__,
R"({
"command": "doesnt_matter",
"secret": "a",
"tx_json": {
"Account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
"Amount": "1000000000",
"Destination": "rJrxi4Wxev4bnAGVNP9YCdKPdAoKfAmcsi",
"TransactionType": "Payment",
"Delegate": "NotAnAccount"
}
})",
{{"Invalid field 'tx_json.Delegate'.",
"Invalid field 'tx_json.Delegate'.",
"Missing field 'account'.",
"Missing field 'tx_json.Sequence'."}}},
{"Delegate not in ledger.",
__LINE__,
R"({
"command": "doesnt_matter",
"secret": "a",
"tx_json": {
"Account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
"Amount": "1000000000",
"Destination": "rJrxi4Wxev4bnAGVNP9YCdKPdAoKfAmcsi",
"TransactionType": "Payment",
"Delegate": "rDg53Haik2475DJx8bjMDSDPj4VX7htaMd"
}
})",
{{"Delegate account not found.",
"Delegate account not found.",
"Missing field 'account'.",
"Missing field 'tx_json.Sequence'."}}},
{"Delegate and secret not match.",
__LINE__,
R"({
"command": "doesnt_matter",
"secret": "aa",
"tx_json": {
"Account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
"Amount": "1000000000",
"Destination": "rJrxi4Wxev4bnAGVNP9YCdKPdAoKfAmcsi",
"TransactionType": "Payment",
"Delegate": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA"
}
})",
{{"Secret does not match account.",
"Secret does not match account.",
"Missing field 'account'.",
"Missing field 'tx_json.Sequence'."}}},
};
class JSONRPC_test : public beast::unit_test::suite

View File

@@ -20,6 +20,7 @@
#include <test/jtx.h>
#include <test/jtx/Oracle.h>
#include <test/jtx/attester.h>
#include <test/jtx/delegate.h>
#include <test/jtx/multisign.h>
#include <test/jtx/xchain_bridge.h>
@@ -439,6 +440,116 @@ class LedgerEntry_test : public beast::unit_test::suite
}
}
void
testLedgerEntryDelegate()
{
testcase("ledger_entry Delegate");
using namespace test::jtx;
Env env{*this};
Account const alice{"alice"};
Account const bob{"bob"};
env.fund(XRP(10000), alice, bob);
env.close();
env(delegate::set(alice, bob, {"Payment", "CheckCreate"}));
env.close();
std::string const ledgerHash{to_string(env.closed()->info().hash)};
std::string delegateIndex;
{
// Request by account and authorize
Json::Value jvParams;
jvParams[jss::delegate][jss::account] = alice.human();
jvParams[jss::delegate][jss::authorize] = bob.human();
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::Delegate);
BEAST_EXPECT(jrr[jss::node][sfAccount.jsonName] == alice.human());
BEAST_EXPECT(jrr[jss::node][sfAuthorize.jsonName] == bob.human());
delegateIndex = jrr[jss::node][jss::index].asString();
}
{
// Request by index.
Json::Value jvParams;
jvParams[jss::delegate] = delegateIndex;
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::Delegate);
BEAST_EXPECT(jrr[jss::node][sfAccount.jsonName] == alice.human());
BEAST_EXPECT(jrr[jss::node][sfAuthorize.jsonName] == bob.human());
}
{
// Malformed request: delegate neither object nor string.
Json::Value jvParams;
jvParams[jss::delegate] = 5;
jvParams[jss::ledger_hash] = ledgerHash;
Json::Value const jrr = env.rpc(
"json", "ledger_entry", to_string(jvParams))[jss::result];
checkErrorValue(jrr, "malformedRequest", "");
}
{
// Malformed request: delegate not hex string.
Json::Value jvParams;
jvParams[jss::delegate] = "0123456789ABCDEFG";
jvParams[jss::ledger_hash] = ledgerHash;
Json::Value const jrr = env.rpc(
"json", "ledger_entry", to_string(jvParams))[jss::result];
checkErrorValue(jrr, "malformedRequest", "");
}
{
// Malformed request: account not a string
Json::Value jvParams;
jvParams[jss::delegate][jss::account] = 5;
jvParams[jss::delegate][jss::authorize] = bob.human();
jvParams[jss::ledger_hash] = ledgerHash;
Json::Value const jrr = env.rpc(
"json", "ledger_entry", to_string(jvParams))[jss::result];
checkErrorValue(jrr, "malformedAddress", "");
}
{
// Malformed request: authorize not a string
Json::Value jvParams;
jvParams[jss::delegate][jss::account] = alice.human();
jvParams[jss::delegate][jss::authorize] = 5;
jvParams[jss::ledger_hash] = ledgerHash;
Json::Value const jrr = env.rpc(
"json", "ledger_entry", to_string(jvParams))[jss::result];
checkErrorValue(jrr, "malformedAddress", "");
}
{
// this lambda function is used test malformed account and authroize
auto testMalformedAccount =
[&](std::optional<std::string> const& account,
std::optional<std::string> const& authorize,
std::string const& error) {
Json::Value jvParams;
jvParams[jss::ledger_hash] = ledgerHash;
if (account)
jvParams[jss::delegate][jss::account] = *account;
if (authorize)
jvParams[jss::delegate][jss::authorize] = *authorize;
auto const jrr = env.rpc(
"json",
"ledger_entry",
to_string(jvParams))[jss::result];
checkErrorValue(jrr, error, "");
};
// missing account
testMalformedAccount(std::nullopt, bob.human(), "malformedRequest");
// missing authorize
testMalformedAccount(
alice.human(), std::nullopt, "malformedRequest");
// malformed account
testMalformedAccount("-", bob.human(), "malformedAddress");
// malformed authorize
testMalformedAccount(alice.human(), "-", "malformedAddress");
}
}
void
testLedgerEntryDepositPreauth()
{
@@ -2266,6 +2377,7 @@ public:
testLedgerEntryAccountRoot();
testLedgerEntryCheck();
testLedgerEntryCredentials();
testLedgerEntryDelegate();
testLedgerEntryDepositPreauth();
testLedgerEntryDepositPreauthCred();
testLedgerEntryDirectory();

View File

@@ -20,6 +20,7 @@
#include <test/jtx.h>
#include <test/jtx/Oracle.h>
#include <test/jtx/attester.h>
#include <test/jtx/delegate.h>
#include <test/jtx/multisign.h>
#include <test/jtx/xchain_bridge.h>

View File

@@ -17,8 +17,8 @@
*/
//==============================================================================
#ifndef RIPPLE_APP_MISC_AMMUTILS_H_INLCUDED
#define RIPPLE_APP_MISC_AMMUTILS_H_INLCUDED
#ifndef RIPPLE_APP_MISC_AMMUTILS_H_INCLUDED
#define RIPPLE_APP_MISC_AMMUTILS_H_INCLUDED
#include <xrpld/ledger/View.h>
@@ -127,4 +127,4 @@ isOnlyLiquidityProvider(
} // namespace ripple
#endif // RIPPLE_APP_MISC_AMMUTILS_H_INLCUDED
#endif // RIPPLE_APP_MISC_AMMUTILS_H_INCLUDED

View File

@@ -0,0 +1,56 @@
//------------------------------------------------------------------------------
/*
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_APP_MISC_DELEGATEUTILS_H_INCLUDED
#define RIPPLE_APP_MISC_DELEGATEUTILS_H_INCLUDED
#include <xrpl/protocol/Permissions.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
namespace ripple {
/**
* Check if the delegate account has permission to execute the transaction.
* @param delegate The delegate account.
* @param tx The transaction that the delegate account intends to execute.
* @return tesSUCCESS if the transaction is allowed, tecNO_PERMISSION if not.
*/
TER
checkTxPermission(std::shared_ptr<SLE const> const& delegate, STTx const& tx);
/**
* Load the granular permissions granted to the delegate account for the
* specified transaction type
* @param delegate The delegate account.
* @param type Used to determine which granted granular permissions to load,
* based on the transaction type.
* @param granularPermissions Granted granular permissions tied to the
* transaction type.
*/
void
loadGranularPermission(
std::shared_ptr<SLE const> const& delegate,
TxType const& type,
std::unordered_set<GranularPermissionType>& granularPermissions);
} // namespace ripple
#endif // RIPPLE_APP_MISC_DELEGATEUTILS_H_INCLUDED

View File

@@ -0,0 +1,66 @@
//------------------------------------------------------------------------------
/*
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/misc/DelegateUtils.h>
#include <xrpl/protocol/STArray.h>
namespace ripple {
TER
checkTxPermission(std::shared_ptr<SLE const> const& delegate, STTx const& tx)
{
if (!delegate)
return tecNO_PERMISSION; // LCOV_EXCL_LINE
auto const permissionArray = delegate->getFieldArray(sfPermissions);
auto const txPermission = tx.getTxnType() + 1;
for (auto const& permission : permissionArray)
{
auto const permissionValue = permission[sfPermissionValue];
if (permissionValue == txPermission)
return tesSUCCESS;
}
return tecNO_PERMISSION;
}
void
loadGranularPermission(
std::shared_ptr<SLE const> const& delegate,
TxType const& txType,
std::unordered_set<GranularPermissionType>& granularPermissions)
{
if (!delegate)
return; // LCOV_EXCL_LINE
auto const permissionArray = delegate->getFieldArray(sfPermissions);
for (auto const& permission : permissionArray)
{
auto const permissionValue = permission[sfPermissionValue];
auto const granularValue =
static_cast<GranularPermissionType>(permissionValue);
auto const& type =
Permission::getInstance().getGranularTxType(granularValue);
if (type && *type == txType)
granularPermissions.insert(granularValue);
}
}
} // namespace ripple

View File

@@ -0,0 +1,162 @@
//------------------------------------------------------------------------------
/*
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/DelegateSet.h>
#include <xrpld/ledger/View.h>
#include <xrpl/basics/Log.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/st.h>
namespace ripple {
NotTEC
DelegateSet::preflight(PreflightContext const& ctx)
{
if (!ctx.rules.enabled(featurePermissionDelegation))
return temDISABLED;
if (auto const ret = preflight1(ctx); !isTesSuccess(ret))
return ret;
auto const& permissions = ctx.tx.getFieldArray(sfPermissions);
if (permissions.size() > permissionMaxSize)
return temARRAY_TOO_LARGE;
// can not authorize self
if (ctx.tx[sfAccount] == ctx.tx[sfAuthorize])
return temMALFORMED;
std::unordered_set<std::uint32_t> permissionSet;
for (auto const& permission : permissions)
{
if (!permissionSet.insert(permission[sfPermissionValue]).second)
return temMALFORMED;
}
return preflight2(ctx);
}
TER
DelegateSet::preclaim(PreclaimContext const& ctx)
{
if (!ctx.view.exists(keylet::account(ctx.tx[sfAccount])))
return terNO_ACCOUNT; // LCOV_EXCL_LINE
if (!ctx.view.exists(keylet::account(ctx.tx[sfAuthorize])))
return terNO_ACCOUNT;
auto const& permissions = ctx.tx.getFieldArray(sfPermissions);
for (auto const& permission : permissions)
{
auto const permissionValue = permission[sfPermissionValue];
if (!Permission::getInstance().isDelegatable(permissionValue))
return tecNO_PERMISSION;
}
return tesSUCCESS;
}
TER
DelegateSet::doApply()
{
auto const sleOwner = ctx_.view().peek(keylet::account(account_));
if (!sleOwner)
return tefINTERNAL; // LCOV_EXCL_LINE
auto const& authAccount = ctx_.tx[sfAuthorize];
auto const delegateKey = keylet::delegate(account_, authAccount);
auto sle = ctx_.view().peek(delegateKey);
if (sle)
{
auto const& permissions = ctx_.tx.getFieldArray(sfPermissions);
if (permissions.empty())
// if permissions array is empty, delete the ledger object.
return deleteDelegate(view(), sle, account_, j_);
sle->setFieldArray(sfPermissions, permissions);
ctx_.view().update(sle);
return tesSUCCESS;
}
STAmount const reserve{ctx_.view().fees().accountReserve(
sleOwner->getFieldU32(sfOwnerCount) + 1)};
if (mPriorBalance < reserve)
return tecINSUFFICIENT_RESERVE;
auto const& permissions = ctx_.tx.getFieldArray(sfPermissions);
if (!permissions.empty())
{
sle = std::make_shared<SLE>(delegateKey);
sle->setAccountID(sfAccount, account_);
sle->setAccountID(sfAuthorize, authAccount);
sle->setFieldArray(sfPermissions, permissions);
auto const page = ctx_.view().dirInsert(
keylet::ownerDir(account_),
delegateKey,
describeOwnerDir(account_));
if (!page)
return tecDIR_FULL; // LCOV_EXCL_LINE
(*sle)[sfOwnerNode] = *page;
ctx_.view().insert(sle);
adjustOwnerCount(ctx_.view(), sleOwner, 1, ctx_.journal);
}
return tesSUCCESS;
}
TER
DelegateSet::deleteDelegate(
ApplyView& view,
std::shared_ptr<SLE> const& sle,
AccountID const& account,
beast::Journal j)
{
if (!sle)
return tecINTERNAL; // LCOV_EXCL_LINE
if (!view.dirRemove(
keylet::ownerDir(account), (*sle)[sfOwnerNode], sle->key(), false))
{
// LCOV_EXCL_START
JLOG(j.fatal()) << "Unable to delete Delegate from owner.";
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
auto const sleOwner = view.peek(keylet::account(account));
if (!sleOwner)
return tecINTERNAL; // LCOV_EXCL_LINE
adjustOwnerCount(view, sleOwner, -1, j);
view.erase(sle);
return tesSUCCESS;
}
} // namespace ripple

View File

@@ -0,0 +1,56 @@
//------------------------------------------------------------------------------
/*
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_DELEGATESET_H_INCLUDED
#define RIPPLE_TX_DELEGATESET_H_INCLUDED
#include <xrpld/app/tx/detail/Transactor.h>
namespace ripple {
class DelegateSet : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit DelegateSet(ApplyContext& ctx) : Transactor(ctx)
{
}
static NotTEC
preflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
// Interface used by DeleteAccount
static TER
deleteDelegate(
ApplyView& view,
std::shared_ptr<SLE> const& sle,
AccountID const& account,
beast::Journal j);
};
} // namespace ripple
#endif

View File

@@ -19,6 +19,7 @@
#include <xrpld/app/misc/CredentialHelpers.h>
#include <xrpld/app/tx/detail/DID.h>
#include <xrpld/app/tx/detail/DelegateSet.h>
#include <xrpld/app/tx/detail/DeleteAccount.h>
#include <xrpld/app/tx/detail/DeleteOracle.h>
#include <xrpld/app/tx/detail/DepositPreauth.h>
@@ -180,6 +181,18 @@ removeCredentialFromLedger(
return credentials::deleteSLE(view, sleDel, j);
}
TER
removeDelegateFromLedger(
Application& app,
ApplyView& view,
AccountID const& account,
uint256 const& delIndex,
std::shared_ptr<SLE> const& sleDel,
beast::Journal j)
{
return DelegateSet::deleteDelegate(view, sleDel, account, j);
}
// Return nullptr if the LedgerEntryType represents an obligation that can't
// be deleted. Otherwise return the pointer to the function that can delete
// the non-obligation
@@ -204,6 +217,8 @@ nonObligationDeleter(LedgerEntryType t)
return removeOracleFromLedger;
case ltCREDENTIAL:
return removeCredentialFromLedger;
case ltDELEGATE:
return removeDelegateFromLedger;
default:
return nullptr;
}

View File

@@ -387,6 +387,7 @@ AccountRootsDeletedClean::finalize(
view.rules().enabled(featureInvariantsV1_1);
auto const objectExists = [&view, enforce, &j](auto const& keylet) {
(void)enforce;
if (auto const sle = view.read(keylet))
{
// Finding the object is bad
@@ -463,6 +464,7 @@ LedgerEntryTypesMatch::visitEntry(
switch (after->getType())
{
case ltACCOUNT_ROOT:
case ltDELEGATE:
case ltDIR_NODE:
case ltRIPPLE_STATE:
case ltTICKET:

View File

@@ -17,6 +17,7 @@
*/
//==============================================================================
#include <xrpld/app/misc/DelegateUtils.h>
#include <xrpld/app/tx/detail/MPTokenIssuanceSet.h>
#include <xrpl/protocol/Feature.h>
@@ -50,6 +51,43 @@ MPTokenIssuanceSet::preflight(PreflightContext const& ctx)
return preflight2(ctx);
}
TER
MPTokenIssuanceSet::checkPermission(ReadView const& view, STTx const& tx)
{
auto const delegate = tx[~sfDelegate];
if (!delegate)
return tesSUCCESS;
auto const delegateKey = keylet::delegate(tx[sfAccount], *delegate);
auto const sle = view.read(delegateKey);
if (!sle)
return tecNO_PERMISSION;
if (checkTxPermission(sle, tx) == tesSUCCESS)
return tesSUCCESS;
auto const txFlags = tx.getFlags();
// this is added in case more flags will be added for MPTokenIssuanceSet
// in the future. Currently unreachable.
if (txFlags & tfMPTokenIssuanceSetPermissionMask)
return tecNO_PERMISSION; // LCOV_EXCL_LINE
std::unordered_set<GranularPermissionType> granularPermissions;
loadGranularPermission(sle, ttMPTOKEN_ISSUANCE_SET, granularPermissions);
if (txFlags & tfMPTLock &&
!granularPermissions.contains(MPTokenIssuanceLock))
return tecNO_PERMISSION;
if (txFlags & tfMPTUnlock &&
!granularPermissions.contains(MPTokenIssuanceUnlock))
return tecNO_PERMISSION;
return tesSUCCESS;
}
TER
MPTokenIssuanceSet::preclaim(PreclaimContext const& ctx)
{

View File

@@ -36,6 +36,9 @@ public:
static NotTEC
preflight(PreflightContext const& ctx);
static TER
checkPermission(ReadView const& view, STTx const& tx);
static TER
preclaim(PreclaimContext const& ctx);

View File

@@ -18,6 +18,7 @@
//==============================================================================
#include <xrpld/app/misc/CredentialHelpers.h>
#include <xrpld/app/misc/DelegateUtils.h>
#include <xrpld/app/paths/RippleCalc.h>
#include <xrpld/app/tx/detail/Payment.h>
#include <xrpld/ledger/View.h>
@@ -238,6 +239,39 @@ Payment::preflight(PreflightContext const& ctx)
return preflight2(ctx);
}
TER
Payment::checkPermission(ReadView const& view, STTx const& tx)
{
auto const delegate = tx[~sfDelegate];
if (!delegate)
return tesSUCCESS;
auto const delegateKey = keylet::delegate(tx[sfAccount], *delegate);
auto const sle = view.read(delegateKey);
if (!sle)
return tecNO_PERMISSION;
if (checkTxPermission(sle, tx) == tesSUCCESS)
return tesSUCCESS;
std::unordered_set<GranularPermissionType> granularPermissions;
loadGranularPermission(sle, ttPAYMENT, granularPermissions);
auto const& dstAmount = tx.getFieldAmount(sfAmount);
auto const& amountIssue = dstAmount.issue();
if (granularPermissions.contains(PaymentMint) && !isXRP(amountIssue) &&
amountIssue.account == tx[sfAccount])
return tesSUCCESS;
if (granularPermissions.contains(PaymentBurn) && !isXRP(amountIssue) &&
amountIssue.account == tx[sfDestination])
return tesSUCCESS;
return tecNO_PERMISSION;
}
TER
Payment::preclaim(PreclaimContext const& ctx)
{

View File

@@ -45,6 +45,9 @@ public:
static NotTEC
preflight(PreflightContext const& ctx);
static TER
checkPermission(ReadView const& view, STTx const& tx);
static TER
preclaim(PreclaimContext const& ctx);

View File

@@ -17,6 +17,7 @@
*/
//==============================================================================
#include <xrpld/app/misc/DelegateUtils.h>
#include <xrpld/app/tx/detail/SetAccount.h>
#include <xrpld/core/Config.h>
#include <xrpld/ledger/View.h>
@@ -188,6 +189,61 @@ SetAccount::preflight(PreflightContext const& ctx)
return preflight2(ctx);
}
TER
SetAccount::checkPermission(ReadView const& view, STTx const& tx)
{
// SetAccount is prohibited to be granted on a transaction level,
// but some granular permissions are allowed.
auto const delegate = tx[~sfDelegate];
if (!delegate)
return tesSUCCESS;
auto const delegateKey = keylet::delegate(tx[sfAccount], *delegate);
auto const sle = view.read(delegateKey);
if (!sle)
return tecNO_PERMISSION;
std::unordered_set<GranularPermissionType> granularPermissions;
loadGranularPermission(sle, ttACCOUNT_SET, granularPermissions);
auto const uSetFlag = tx.getFieldU32(sfSetFlag);
auto const uClearFlag = tx.getFieldU32(sfClearFlag);
auto const uTxFlags = tx.getFlags();
// We don't support any flag based granular permission under
// AccountSet transaction. If any delegated account is trying to
// update the flag on behalf of another account, it is not
// authorized.
if (uSetFlag != 0 || uClearFlag != 0 || uTxFlags != tfFullyCanonicalSig)
return tecNO_PERMISSION;
if (tx.isFieldPresent(sfEmailHash) &&
!granularPermissions.contains(AccountEmailHashSet))
return tecNO_PERMISSION;
if (tx.isFieldPresent(sfWalletLocator) ||
tx.isFieldPresent(sfNFTokenMinter))
return tecNO_PERMISSION;
if (tx.isFieldPresent(sfMessageKey) &&
!granularPermissions.contains(AccountMessageKeySet))
return tecNO_PERMISSION;
if (tx.isFieldPresent(sfDomain) &&
!granularPermissions.contains(AccountDomainSet))
return tecNO_PERMISSION;
if (tx.isFieldPresent(sfTransferRate) &&
!granularPermissions.contains(AccountTransferRateSet))
return tecNO_PERMISSION;
if (tx.isFieldPresent(sfTickSize) &&
!granularPermissions.contains(AccountTickSizeSet))
return tecNO_PERMISSION;
return tesSUCCESS;
}
TER
SetAccount::preclaim(PreclaimContext const& ctx)
{

View File

@@ -41,6 +41,9 @@ public:
static NotTEC
preflight(PreflightContext const& ctx);
static TER
checkPermission(ReadView const& view, STTx const& tx);
static TER
preclaim(PreclaimContext const& ctx);

View File

@@ -17,6 +17,7 @@
*/
//==============================================================================
#include <xrpld/app/misc/DelegateUtils.h>
#include <xrpld/app/tx/detail/SetTrust.h>
#include <xrpld/ledger/View.h>
@@ -127,6 +128,69 @@ SetTrust::preflight(PreflightContext const& ctx)
return preflight2(ctx);
}
TER
SetTrust::checkPermission(ReadView const& view, STTx const& tx)
{
auto const delegate = tx[~sfDelegate];
if (!delegate)
return tesSUCCESS;
auto const delegateKey = keylet::delegate(tx[sfAccount], *delegate);
auto const sle = view.read(delegateKey);
if (!sle)
return tecNO_PERMISSION;
if (checkTxPermission(sle, tx) == tesSUCCESS)
return tesSUCCESS;
std::uint32_t const txFlags = tx.getFlags();
// Currently we only support TrustlineAuthorize, TrustlineFreeze and
// TrustlineUnfreeze granular permission. Setting other flags returns
// error.
if (txFlags & tfTrustSetPermissionMask)
return tecNO_PERMISSION;
if (tx.isFieldPresent(sfQualityIn) || tx.isFieldPresent(sfQualityOut))
return tecNO_PERMISSION;
auto const saLimitAmount = tx.getFieldAmount(sfLimitAmount);
auto const sleRippleState = view.read(keylet::line(
tx[sfAccount], saLimitAmount.getIssuer(), saLimitAmount.getCurrency()));
// if the trustline does not exist, granular permissions are
// not allowed to create trustline
if (!sleRippleState)
return tecNO_PERMISSION;
std::unordered_set<GranularPermissionType> granularPermissions;
loadGranularPermission(sle, ttTRUST_SET, granularPermissions);
if (txFlags & tfSetfAuth &&
!granularPermissions.contains(TrustlineAuthorize))
return tecNO_PERMISSION;
if (txFlags & tfSetFreeze && !granularPermissions.contains(TrustlineFreeze))
return tecNO_PERMISSION;
if (txFlags & tfClearFreeze &&
!granularPermissions.contains(TrustlineUnfreeze))
return tecNO_PERMISSION;
// updating LimitAmount is not allowed only with granular permissions,
// unless there's a new granular permission for this in the future.
auto const curLimit = tx[sfAccount] > saLimitAmount.getIssuer()
? sleRippleState->getFieldAmount(sfHighLimit)
: sleRippleState->getFieldAmount(sfLowLimit);
STAmount saLimitAllow = saLimitAmount;
saLimitAllow.setIssuer(tx[sfAccount]);
if (curLimit != saLimitAllow)
return tecNO_PERMISSION;
return tesSUCCESS;
}
TER
SetTrust::preclaim(PreclaimContext const& ctx)
{

View File

@@ -38,6 +38,9 @@ public:
static NotTEC
preflight(PreflightContext const& ctx);
static TER
checkPermission(ReadView const& view, STTx const& tx);
static TER
preclaim(PreclaimContext const& ctx);

View File

@@ -19,6 +19,7 @@
#include <xrpld/app/main/Application.h>
#include <xrpld/app/misc/CredentialHelpers.h>
#include <xrpld/app/misc/DelegateUtils.h>
#include <xrpld/app/misc/LoadFeeTrack.h>
#include <xrpld/app/tx/apply.h>
#include <xrpld/app/tx/detail/NFTokenUtils.h>
@@ -89,6 +90,15 @@ preflight1(PreflightContext const& ctx)
return temMALFORMED;
}
if (ctx.tx.isFieldPresent(sfDelegate))
{
if (!ctx.rules.enabled(featurePermissionDelegation))
return temDISABLED;
if (ctx.tx[sfDelegate] == ctx.tx[sfAccount])
return temBAD_SIGNER;
}
auto const ret = preflight0(ctx);
if (!isTesSuccess(ret))
return ret;
@@ -190,6 +200,22 @@ Transactor::Transactor(ApplyContext& ctx)
{
}
TER
Transactor::checkPermission(ReadView const& view, STTx const& tx)
{
auto const delegate = tx[~sfDelegate];
if (!delegate)
return tesSUCCESS;
auto const delegateKey = keylet::delegate(tx[sfAccount], *delegate);
auto const sle = view.read(delegateKey);
if (!sle)
return tecNO_PERMISSION;
return checkTxPermission(sle, tx);
}
XRPAmount
Transactor::calculateBaseFee(ReadView const& view, STTx const& tx)
{
@@ -246,7 +272,9 @@ Transactor::checkFee(PreclaimContext const& ctx, XRPAmount baseFee)
if (feePaid == beast::zero)
return tesSUCCESS;
auto const id = ctx.tx.getAccountID(sfAccount);
auto const id = ctx.tx.isFieldPresent(sfDelegate)
? ctx.tx.getAccountID(sfDelegate)
: ctx.tx.getAccountID(sfAccount);
auto const sle = ctx.view.read(keylet::account(id));
if (!sle)
return terNO_ACCOUNT;
@@ -276,17 +304,32 @@ Transactor::payFee()
{
auto const feePaid = ctx_.tx[sfFee].xrp();
auto const sle = view().peek(keylet::account(account_));
if (!sle)
return tefINTERNAL;
if (ctx_.tx.isFieldPresent(sfDelegate))
{
// Delegated transactions are paid by the delegated account.
auto const delegate = ctx_.tx.getAccountID(sfDelegate);
auto const delegatedSle = view().peek(keylet::account(delegate));
if (!delegatedSle)
return tefINTERNAL; // LCOV_EXCL_LINE
// Deduct the fee, so it's not available during the transaction.
// Will only write the account back if the transaction succeeds.
delegatedSle->setFieldAmount(
sfBalance, delegatedSle->getFieldAmount(sfBalance) - feePaid);
view().update(delegatedSle);
}
else
{
auto const sle = view().peek(keylet::account(account_));
if (!sle)
return tefINTERNAL; // LCOV_EXCL_LINE
mSourceBalance -= feePaid;
sle->setFieldAmount(sfBalance, mSourceBalance);
// Deduct the fee, so it's not available during the transaction.
// Will only write the account back if the transaction succeeds.
// VFALCO Should we call view().rawDestroyXRP() here as well?
mSourceBalance -= feePaid;
sle->setFieldAmount(sfBalance, mSourceBalance);
// VFALCO Should we call view().rawDestroyXRP() here as well?
}
return tesSUCCESS;
}
@@ -542,7 +585,9 @@ Transactor::checkSingleSign(PreclaimContext const& ctx)
}
// Look up the account.
auto const idAccount = ctx.tx.getAccountID(sfAccount);
auto const idAccount = ctx.tx.isFieldPresent(sfDelegate)
? ctx.tx.getAccountID(sfDelegate)
: ctx.tx.getAccountID(sfAccount);
auto const sleAccount = ctx.view.read(keylet::account(idAccount));
if (!sleAccount)
return terNO_ACCOUNT;
@@ -612,7 +657,9 @@ Transactor::checkSingleSign(PreclaimContext const& ctx)
NotTEC
Transactor::checkMultiSign(PreclaimContext const& ctx)
{
auto const id = ctx.tx.getAccountID(sfAccount);
auto const id = ctx.tx.isFieldPresent(sfDelegate)
? ctx.tx.getAccountID(sfDelegate)
: ctx.tx.getAccountID(sfAccount);
// Get mTxnAccountID's SignerList and Quorum.
std::shared_ptr<STLedgerEntry const> sleAccountSigners =
ctx.view.read(keylet::signers(id));
@@ -870,15 +917,22 @@ Transactor::reset(XRPAmount fee)
// is missing then we can't very well charge it a fee, can we?
return {tefINTERNAL, beast::zero};
auto const balance = txnAcct->getFieldAmount(sfBalance).xrp();
auto const payerSle = ctx_.tx.isFieldPresent(sfDelegate)
? view().peek(keylet::account(ctx_.tx.getAccountID(sfDelegate)))
: txnAcct;
if (!payerSle)
return {tefINTERNAL, beast::zero}; // LCOV_EXCL_LINE
auto const balance = payerSle->getFieldAmount(sfBalance).xrp();
// balance should have already been checked in checkFee / preFlight.
XRPL_ASSERT(
balance != beast::zero && (!view().open() || balance >= fee),
"ripple::Transactor::reset : valid balance");
// We retry/reject the transaction if the account balance is zero or we're
// applying against an open ledger and the balance is less than the fee
// We retry/reject the transaction if the account balance is zero or
// we're applying against an open ledger and the balance is less than
// the fee
if (fee > balance)
fee = balance;
@@ -888,13 +942,17 @@ Transactor::reset(XRPAmount fee)
// If for some reason we are unable to consume the ticket or sequence
// then the ledger is corrupted. Rather than make things worse we
// reject the transaction.
txnAcct->setFieldAmount(sfBalance, balance - fee);
payerSle->setFieldAmount(sfBalance, balance - fee);
TER const ter{consumeSeqProxy(txnAcct)};
XRPL_ASSERT(
isTesSuccess(ter), "ripple::Transactor::reset : result is tesSUCCESS");
if (isTesSuccess(ter))
{
view().update(txnAcct);
if (payerSle != txnAcct)
view().update(payerSle);
}
return {ter, fee};
}

View File

@@ -24,6 +24,7 @@
#include <xrpld/app/tx/detail/ApplyContext.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/protocol/Permissions.h>
#include <xrpl/protocol/XRPAmount.h>
namespace ripple {
@@ -149,6 +150,9 @@ public:
// after checkSeq/Fee/Sign.
return tesSUCCESS;
}
static TER
checkPermission(ReadView const& view, STTx const& tx);
/////////////////////////////////////////////////////
// Interface used by DeleteAccount

View File

@@ -36,6 +36,7 @@
#include <xrpld/app/tx/detail/CreateTicket.h>
#include <xrpld/app/tx/detail/Credentials.h>
#include <xrpld/app/tx/detail/DID.h>
#include <xrpld/app/tx/detail/DelegateSet.h>
#include <xrpld/app/tx/detail/DeleteAccount.h>
#include <xrpld/app/tx/detail/DeleteOracle.h>
#include <xrpld/app/tx/detail/DepositPreauth.h>
@@ -89,8 +90,8 @@ with_txn_type(TxType txnType, F&& f)
#pragma push_macro("TRANSACTION")
#undef TRANSACTION
#define TRANSACTION(tag, value, name, fields) \
case tag: \
#define TRANSACTION(tag, value, name, delegatable, fields) \
case tag: \
return f.template operator()<name>();
#include <xrpl/protocol/detail/transactions.macro>
@@ -193,6 +194,11 @@ invoke_preclaim(PreclaimContext const& ctx)
result = T::checkFee(ctx, calculateBaseFee(ctx.view, ctx.tx));
if (result != tesSUCCESS)
return result;
result = T::checkPermission(ctx.view, ctx.tx);
if (result != tesSUCCESS)
return result;

View File

@@ -531,10 +531,40 @@ transactionPreProcessImpl(
if (!signingArgs.isMultiSigning())
{
// Make sure the account and secret belong together.
auto const err = acctMatchesPubKey(sle, srcAddressID, pk);
if (tx_json.isMember(sfDelegate.jsonName))
{
// Delegated transaction
auto const delegateJson = tx_json[sfDelegate.jsonName];
auto const ptrDelegatedAddressID = delegateJson.isString()
? parseBase58<AccountID>(delegateJson.asString())
: std::nullopt;
if (err != rpcSUCCESS)
return rpcError(err);
if (!ptrDelegatedAddressID)
{
return RPC::make_error(
rpcSRC_ACT_MALFORMED,
RPC::invalid_field_message("tx_json.Delegate"));
}
auto delegatedAddressID = *ptrDelegatedAddressID;
auto delegatedSle = app.openLedger().current()->read(
keylet::account(delegatedAddressID));
if (!delegatedSle)
return rpcError(rpcDELEGATE_ACT_NOT_FOUND);
auto const err =
acctMatchesPubKey(delegatedSle, delegatedAddressID, pk);
if (err != rpcSUCCESS)
return rpcError(err);
}
else
{
auto const err = acctMatchesPubKey(sle, srcAddressID, pk);
if (err != rpcSUCCESS)
return rpcError(err);
}
}
}

View File

@@ -230,6 +230,46 @@ parseAuthorizeCredentials(Json::Value const& jv)
return arr;
}
static std::optional<uint256>
parseDelegate(Json::Value const& params, Json::Value& jvResult)
{
if (!params.isObject())
{
uint256 uNodeIndex;
if (!params.isString() || !uNodeIndex.parseHex(params.asString()))
{
jvResult[jss::error] = "malformedRequest";
return std::nullopt;
}
return uNodeIndex;
}
if (!params.isMember(jss::account) || !params.isMember(jss::authorize))
{
jvResult[jss::error] = "malformedRequest";
return std::nullopt;
}
if (!params[jss::account].isString() || !params[jss::authorize].isString())
{
jvResult[jss::error] = "malformedAddress";
return std::nullopt;
}
auto const account =
parseBase58<AccountID>(params[jss::account].asString());
if (!account)
{
jvResult[jss::error] = "malformedAddress";
return std::nullopt;
}
auto const authorize =
parseBase58<AccountID>(params[jss::authorize].asString());
if (!authorize)
{
jvResult[jss::error] = "malformedAddress";
return std::nullopt;
}
return keylet::delegate(*account, *authorize).key;
}
static std::optional<uint256>
parseDepositPreauth(Json::Value const& dp, Json::Value& jvResult)
{
@@ -884,6 +924,7 @@ doLedgerEntry(RPC::JsonContext& context)
{jss::bridge, parseBridge, ltBRIDGE},
{jss::check, parseCheck, ltCHECK},
{jss::credential, parseCredential, ltCREDENTIAL},
{jss::delegate, parseDelegate, ltDELEGATE},
{jss::deposit_preauth, parseDepositPreauth, ltDEPOSIT_PREAUTH},
{jss::did, parseDID, ltDID},
{jss::directory, parseDirectory, ltDIR_NODE},