From a9246c3478dfbba40989ee2ff4ce871a0302d339 Mon Sep 17 00:00:00 2001 From: Gregory Tsipenyuk Date: Sun, 3 Dec 2023 11:21:51 -0500 Subject: [PATCH] CFToken implementation from shawnxie999:cft-set --- Builds/CMake/RippledCore.cmake | 6 + src/ripple/app/tx/impl/InvariantCheck.cpp | 163 +++++++++++++++++++++ src/ripple/app/tx/impl/InvariantCheck.h | 27 +++- src/ripple/app/tx/impl/applySteps.cpp | 12 ++ src/ripple/protocol/Feature.h | 3 +- src/ripple/protocol/Indexes.h | 14 ++ src/ripple/protocol/LedgerFormats.h | 24 +++ src/ripple/protocol/Protocol.h | 3 + src/ripple/protocol/SField.h | 10 ++ src/ripple/protocol/TER.h | 5 +- src/ripple/protocol/TxFlags.h | 31 ++++ src/ripple/protocol/TxFormats.h | 11 ++ src/ripple/protocol/impl/Feature.cpp | 1 + src/ripple/protocol/impl/Indexes.cpp | 22 +++ src/ripple/protocol/impl/LedgerFormats.cpp | 31 ++++ src/ripple/protocol/impl/SField.cpp | 9 ++ src/ripple/protocol/impl/TER.cpp | 1 + src/ripple/protocol/impl/TxFormats.cpp | 33 +++++ src/ripple/protocol/jss.h | 7 + src/test/jtx.h | 1 + src/test/jtx/Env.h | 6 + src/test/jtx/impl/Env.cpp | 8 + 22 files changed, 425 insertions(+), 3 deletions(-) diff --git a/Builds/CMake/RippledCore.cmake b/Builds/CMake/RippledCore.cmake index 269c107ca9e..4ac9dc250eb 100644 --- a/Builds/CMake/RippledCore.cmake +++ b/Builds/CMake/RippledCore.cmake @@ -511,6 +511,10 @@ target_sources (rippled PRIVATE src/ripple/app/tx/impl/AMMWithdraw.cpp src/ripple/app/tx/impl/ApplyContext.cpp src/ripple/app/tx/impl/BookTip.cpp + src/ripple/app/tx/impl/CFTokenIssuanceCreate.cpp + src/ripple/app/tx/impl/CFTokenIssuanceDestroy.cpp + src/ripple/app/tx/impl/CFTokenAuthorize.cpp + src/ripple/app/tx/impl/CFTokenIssuanceSet.cpp src/ripple/app/tx/impl/CancelCheck.cpp src/ripple/app/tx/impl/CancelOffer.cpp src/ripple/app/tx/impl/CashCheck.cpp @@ -782,6 +786,7 @@ if (tests) src/test/app/AMM_test.cpp src/test/app/AMMCalc_test.cpp src/test/app/AMMExtended_test.cpp + src/test/app/CFToken_test.cpp src/test/app/Check_test.cpp src/test/app/Clawback_test.cpp src/test/app/CrossingLimits_test.cpp @@ -930,6 +935,7 @@ if (tests) src/test/jtx/impl/Account.cpp src/test/jtx/impl/AMM.cpp src/test/jtx/impl/AMMTest.cpp + src/test/jtx/impl/cft.cpp src/test/jtx/impl/Env.cpp src/test/jtx/impl/JSONRPCClient.cpp src/test/jtx/impl/TestHelpers.cpp diff --git a/src/ripple/app/tx/impl/InvariantCheck.cpp b/src/ripple/app/tx/impl/InvariantCheck.cpp index c717777f88f..f1948ba1669 100644 --- a/src/ripple/app/tx/impl/InvariantCheck.cpp +++ b/src/ripple/app/tx/impl/InvariantCheck.cpp @@ -392,6 +392,8 @@ LedgerEntryTypesMatch::visitEntry( case ltXCHAIN_OWNED_CLAIM_ID: case ltXCHAIN_OWNED_CREATE_ACCOUNT_CLAIM_ID: case ltDID: + case ltCFTOKEN_ISSUANCE: + case ltCFTOKEN: break; default: invalidTypeAdded_ = true; @@ -799,4 +801,165 @@ ValidClawback::finalize( return true; } +//------------------------------------------------------------------------------ + +void +ValidCFTIssuance::visitEntry( + bool isDelete, + std::shared_ptr const& before, + std::shared_ptr const& after) +{ + if (after && after->getType() == ltCFTOKEN_ISSUANCE) + { + if (isDelete) + cftIssuancesDeleted_++; + else if (!before) + cftIssuancesCreated_++; + } + + if (after && after->getType() == ltCFTOKEN) + { + if (isDelete) + cftokensDeleted_++; + else if (!before) + cftokensCreated_++; + } +} + +bool +ValidCFTIssuance::finalize( + STTx const& tx, + TER const result, + XRPAmount const _fee, + ReadView const& _view, + beast::Journal const& j) +{ + if (tx.getTxnType() == ttCFTOKEN_ISSUANCE_CREATE && result == tesSUCCESS) + { + if (cftIssuancesCreated_ == 0) + { + JLOG(j.fatal()) << "Invariant failed: CFT issuance creation " + "succeeded without creating a CFT issuance"; + } + else if (cftIssuancesDeleted_ != 0) + { + JLOG(j.fatal()) << "Invariant failed: CFT issuance creation " + "succeeded while removing CFT issuances"; + } + else if (cftIssuancesCreated_ > 1) + { + JLOG(j.fatal()) << "Invariant failed: CFT issuance creation " + "succeeded but created multiple issuances"; + } + + return cftIssuancesCreated_ == 1 && cftIssuancesDeleted_ == 0; + } + + if (tx.getTxnType() == ttCFTOKEN_ISSUANCE_DESTROY && result == tesSUCCESS) + { + if (cftIssuancesDeleted_ == 0) + { + JLOG(j.fatal()) << "Invariant failed: CFT issuance deletion " + "succeeded without removing a CFT issuance"; + } + else if (cftIssuancesCreated_ > 0) + { + JLOG(j.fatal()) << "Invariant failed: CFT issuance deletion " + "succeeded while creating CFT issuances"; + } + else if (cftIssuancesDeleted_ > 1) + { + JLOG(j.fatal()) << "Invariant failed: CFT issuance deletion " + "succeeded but deleted multiple issuances"; + } + + return cftIssuancesCreated_ == 0 && cftIssuancesDeleted_ == 1; + } + + if (tx.getTxnType() == ttCFTOKEN_AUTHORIZE && result == tesSUCCESS) + { + bool const submittedByIssuer = tx.isFieldPresent(sfCFTokenHolder); + + if (cftIssuancesCreated_ > 0) + { + JLOG(j.fatal()) << "Invariant failed: CFT authorize " + "succeeded but created CFT issuances"; + return false; + } + else if (cftIssuancesDeleted_ > 0) + { + JLOG(j.fatal()) << "Invariant failed: CFT authorize " + "succeeded but deleted issuances"; + return false; + } + else if ( + submittedByIssuer && (cftokensCreated_ > 0 || cftokensDeleted_ > 0)) + { + JLOG(j.fatal()) + << "Invariant failed: CFT authorize submitted by issuer " + "succeeded but created/deleted cftokens"; + return false; + } + else if ( + !submittedByIssuer && (cftokensCreated_ + cftokensDeleted_ != 1)) + { + // if the holder submitted this tx, then a cftoken must be either + // created or deleted. + JLOG(j.fatal()) + << "Invariant failed: CFT authorize submitted by holder " + "succeeded but created/deleted bad number of cftokens"; + return false; + } + + return true; + } + + if (tx.getTxnType() == ttCFTOKEN_ISSUANCE_SET && result == tesSUCCESS) + { + if (cftIssuancesDeleted_ > 0) + { + JLOG(j.fatal()) << "Invariant failed: CFT issuance set " + "succeeded while removing CFT issuances"; + } + else if (cftIssuancesCreated_ > 0) + { + JLOG(j.fatal()) << "Invariant failed: CFT issuance set " + "succeeded while creating CFT issuances"; + } + else if (cftokensDeleted_ > 0) + { + JLOG(j.fatal()) << "Invariant failed: CFT issuance set " + "succeeded while removing CFTokens"; + } + else if (cftokensCreated_ > 0) + { + JLOG(j.fatal()) << "Invariant failed: CFT issuance set " + "succeeded while creating CFTokens"; + } + + return cftIssuancesCreated_ == 0 && cftIssuancesDeleted_ == 0 && + cftokensCreated_ == 0 && cftokensDeleted_ == 0; + } + + if (cftIssuancesCreated_ != 0) + { + JLOG(j.fatal()) << "Invariant failed: a CFT issuance was created"; + } + else if (cftIssuancesDeleted_ != 0) + { + JLOG(j.fatal()) << "Invariant failed: a CFT issuance was deleted"; + } + else if (cftokensCreated_ != 0) + { + JLOG(j.fatal()) << "Invariant failed: a CFToken was created"; + } + else if (cftokensDeleted_ != 0) + { + JLOG(j.fatal()) << "Invariant failed: a CFToken was deleted"; + } + + return cftIssuancesCreated_ == 0 && cftIssuancesDeleted_ == 0 && + cftokensCreated_ == 0 && cftokensDeleted_ == 0; +} + } // namespace ripple diff --git a/src/ripple/app/tx/impl/InvariantCheck.h b/src/ripple/app/tx/impl/InvariantCheck.h index eb606c2ed3b..de7f17d80a4 100644 --- a/src/ripple/app/tx/impl/InvariantCheck.h +++ b/src/ripple/app/tx/impl/InvariantCheck.h @@ -418,6 +418,30 @@ class ValidClawback beast::Journal const&); }; +class ValidCFTIssuance +{ + std::uint32_t cftIssuancesCreated_ = 0; + std::uint32_t cftIssuancesDeleted_ = 0; + + std::uint32_t cftokensCreated_ = 0; + std::uint32_t cftokensDeleted_ = 0; + +public: + void + visitEntry( + bool, + std::shared_ptr const&, + std::shared_ptr const&); + + bool + finalize( + STTx const&, + TER const, + XRPAmount const, + ReadView const&, + beast::Journal const&); +}; + // additional invariant checks can be declared above and then added to this // tuple using InvariantChecks = std::tuple< @@ -432,7 +456,8 @@ using InvariantChecks = std::tuple< ValidNewAccountRoot, ValidNFTokenPage, NFTokenCountTracking, - ValidClawback>; + ValidClawback, + ValidCFTIssuance>; /** * @brief get a tuple of all invariant checks diff --git a/src/ripple/app/tx/impl/applySteps.cpp b/src/ripple/app/tx/impl/applySteps.cpp index 10e2b0c4524..a41857a3330 100644 --- a/src/ripple/app/tx/impl/applySteps.cpp +++ b/src/ripple/app/tx/impl/applySteps.cpp @@ -25,6 +25,10 @@ #include #include #include +#include +#include +#include +#include #include #include #include @@ -159,6 +163,14 @@ with_txn_type(TxType txnType, F&& f) return f.template operator()(); case ttDID_DELETE: return f.template operator()(); + case ttCFTOKEN_ISSUANCE_CREATE: + return f.template operator()(); + case ttCFTOKEN_ISSUANCE_DESTROY: + return f.template operator()(); + case ttCFTOKEN_AUTHORIZE: + return f.template operator()(); + case ttCFTOKEN_ISSUANCE_SET: + return f.template operator()(); default: throw UnknownTxnType(txnType); } diff --git a/src/ripple/protocol/Feature.h b/src/ripple/protocol/Feature.h index 3bdfcb15c59..7c79c17310b 100644 --- a/src/ripple/protocol/Feature.h +++ b/src/ripple/protocol/Feature.h @@ -74,7 +74,7 @@ namespace detail { // Feature.cpp. Because it's only used to reserve storage, and determine how // large to make the FeatureBitset, it MAY be larger. It MUST NOT be less than // the actual number of amendments. A LogicError on startup will verify this. -static constexpr std::size_t numFeatures = 65; +static constexpr std::size_t numFeatures = 66; /** Amendments that this server supports and the default voting behavior. Whether they are enabled depends on the Rules defined in the validated @@ -352,6 +352,7 @@ extern uint256 const featureXChainBridge; extern uint256 const fixDisallowIncomingV1; extern uint256 const featureDID; extern uint256 const fixFillOrKill; +extern uint256 const featureCFTokensV1; } // namespace ripple diff --git a/src/ripple/protocol/Indexes.h b/src/ripple/protocol/Indexes.h index 9a330b6b4f0..38be866a627 100644 --- a/src/ripple/protocol/Indexes.h +++ b/src/ripple/protocol/Indexes.h @@ -283,6 +283,20 @@ xChainCreateAccountClaimID(STXChainBridge const& bridge, std::uint64_t seq); Keylet did(AccountID const& account) noexcept; +Keylet +cftIssuance(AccountID const& issuer, std::uint32_t seq) noexcept; + +inline Keylet +cftIssuance(uint256 const& issuance) +{ + return {ltCFTOKEN_ISSUANCE, issuance}; +} + +Keylet +cftoken(uint256 const& issuanceID, AccountID const& holder) noexcept; + +Keylet +cft_dir(uint256 const& id) noexcept; } // namespace keylet // Everything below is deprecated and should be removed in favor of keylets: diff --git a/src/ripple/protocol/LedgerFormats.h b/src/ripple/protocol/LedgerFormats.h index db64942790a..1a95cd729ff 100644 --- a/src/ripple/protocol/LedgerFormats.h +++ b/src/ripple/protocol/LedgerFormats.h @@ -192,6 +192,18 @@ enum LedgerEntryType : std::uint16_t */ ltDID = 0x0049, + /** A ledger object representing an individual CFToken asset type, but not + * any balances of that asset itself. + + \sa keylet::cftIssuance + */ + ltCFTOKEN_ISSUANCE = 0x007e, + + /** A ledger object representing an individual CFToken balance. + + \sa keylet::cftoken + */ + ltCFTOKEN = 0x007f, //--------------------------------------------------------------------------- /** A special type, matching any ledger entry type. @@ -303,6 +315,18 @@ enum LedgerSpecificFlags { // ltNFTOKEN_OFFER lsfSellNFToken = 0x00000001, + + // ltCFTOKEN_ISSUANCE + lsfCFTLocked = 0x00000001, // Also used in ltCFTOKEN + lsfCFTCanLock = 0x00000002, + lsfCFTRequireAuth = 0x00000004, + lsfCFTCanEscrow = 0x00000008, + lsfCFTCanTrade = 0x00000010, + lsfCFTCanTransfer = 0x00000020, + lsfCFTCanClawback = 0x00000040, + + // ltCFTOKEN + lsfCFTAuthorized = 0x00000002, }; //------------------------------------------------------------------------------ diff --git a/src/ripple/protocol/Protocol.h b/src/ripple/protocol/Protocol.h index 49642efc4cf..b484fee7b0a 100644 --- a/src/ripple/protocol/Protocol.h +++ b/src/ripple/protocol/Protocol.h @@ -95,6 +95,9 @@ std::size_t constexpr maxDIDAttestationLength = 256; /** The maximum length of a domain */ std::size_t constexpr maxDomainLength = 256; +/** The maximum length of CFTokenMetadata */ +std::size_t constexpr maxCFTokenMetadataLength = 1024; + /** A ledger index. */ using LedgerIndex = std::uint32_t; diff --git a/src/ripple/protocol/SField.h b/src/ripple/protocol/SField.h index 5d7acb12383..c3f68d812a6 100644 --- a/src/ripple/protocol/SField.h +++ b/src/ripple/protocol/SField.h @@ -369,6 +369,7 @@ extern SF_UINT8 const sfWasLockingChainSend; extern SF_UINT8 const sfTickSize; extern SF_UINT8 const sfUNLModifyDisabling; extern SF_UINT8 const sfHookResult; +extern SF_UINT8 const sfAssetScale; // 16-bit integers (common) extern SF_UINT16 const sfLedgerEntryType; @@ -450,6 +451,7 @@ extern SF_UINT64 const sfCookie; extern SF_UINT64 const sfServerVersion; extern SF_UINT64 const sfNFTokenOfferNode; extern SF_UINT64 const sfEmitBurden; +extern SF_UINT64 const sfCFTokenNode; // 64-bit integers (uncommon) extern SF_UINT64 const sfHookOn; @@ -459,6 +461,10 @@ extern SF_UINT64 const sfReferenceCount; extern SF_UINT64 const sfXChainClaimID; extern SF_UINT64 const sfXChainAccountCreateCount; extern SF_UINT64 const sfXChainAccountClaimCount; +extern SF_UINT64 const sfMaximumAmount; +extern SF_UINT64 const sfOutstandingAmount; +extern SF_UINT64 const sfLockedAmount; +extern SF_UINT64 const sfCFTAmount; // 128-bit extern SF_UINT128 const sfEmailHash; @@ -484,6 +490,7 @@ extern SF_UINT256 const sfEmitParentTxnID; extern SF_UINT256 const sfEmitNonce; extern SF_UINT256 const sfEmitHookHash; extern SF_UINT256 const sfAMMID; +extern SF_UINT256 const sfCFTokenIssuanceID; // 256-bit (uncommon) extern SF_UINT256 const sfBookDirectory; @@ -554,6 +561,7 @@ extern SF_VL const sfMemoData; extern SF_VL const sfMemoFormat; extern SF_VL const sfDIDDocument; extern SF_VL const sfData; +extern SF_VL const sfCFTokenMetadata; // variable length (uncommon) extern SF_VL const sfFulfillment; @@ -577,6 +585,7 @@ extern SF_ACCOUNT const sfUnauthorize; extern SF_ACCOUNT const sfRegularKey; extern SF_ACCOUNT const sfNFTokenMinter; extern SF_ACCOUNT const sfEmitCallback; +extern SF_ACCOUNT const sfCFTokenHolder; // account (uncommon) extern SF_ACCOUNT const sfHookAccount; @@ -636,6 +645,7 @@ extern SField const sfXChainClaimProofSig; extern SField const sfXChainCreateAccountProofSig; extern SField const sfXChainClaimAttestationCollectionElement; extern SField const sfXChainCreateAccountAttestationCollectionElement; +extern SField const CFToken; // array of objects (common) // ARRAY/1 is reserved for end of array diff --git a/src/ripple/protocol/TER.h b/src/ripple/protocol/TER.h index 23d4fb3ef00..4e3fb438fe2 100644 --- a/src/ripple/protocol/TER.h +++ b/src/ripple/protocol/TER.h @@ -124,6 +124,7 @@ enum TEMcodes : TERUnderlyingType { temSEQ_AND_TICKET, temBAD_NFTOKEN_TRANSFER_FEE, + temBAD_CFTOKEN_TRANSFER_FEE, temBAD_AMM_TOKENS, @@ -331,7 +332,9 @@ enum TECcodes : TERUnderlyingType { tecXCHAIN_SELF_COMMIT = 184, tecXCHAIN_BAD_PUBLIC_KEY_ACCOUNT_PAIR = 185, tecXCHAIN_CREATE_ACCOUNT_DISABLED = 186, - tecEMPTY_DID = 187 + tecEMPTY_DID = 187, + tecCFTOKEN_EXISTS = 188 + }; //------------------------------------------------------------------------------ diff --git a/src/ripple/protocol/TxFlags.h b/src/ripple/protocol/TxFlags.h index ba2b97562db..8ab53351de2 100644 --- a/src/ripple/protocol/TxFlags.h +++ b/src/ripple/protocol/TxFlags.h @@ -22,6 +22,8 @@ #include +#include + namespace ripple { /** Transaction flags. @@ -130,6 +132,23 @@ constexpr std::uint32_t const tfOnlyXRP = 0x00000002; constexpr std::uint32_t const tfTrustLine = 0x00000004; constexpr std::uint32_t const tfTransferable = 0x00000008; +// CFTokenIssuanceCreate flags: +// NOTE - there is intentionally no flag here for 0x01 because that +// corresponds to lsfCFTLocked, which this transaction cannot mutate. +constexpr std::uint32_t const tfCFTCanLock = lsfCFTCanLock; +constexpr std::uint32_t const tfCFTRequireAuth = lsfCFTRequireAuth; +constexpr std::uint32_t const tfCFTCanEscrow = lsfCFTCanEscrow; +constexpr std::uint32_t const tfCFTCanTrade = lsfCFTCanTrade; +constexpr std::uint32_t const tfCFTCanTransfer = lsfCFTCanTransfer; +constexpr std::uint32_t const tfCFTCanClawback = lsfCFTCanClawback; + +// CFTokenAuthorize flags: +constexpr std::uint32_t const tfCFTUnauthorize = 0x00000001; + +// CFTokenIssuanceSet flags: +constexpr std::uint32_t const tfCFTLock = 0x00000001; +constexpr std::uint32_t const tfCFTUnlock = 0x00000002; + // Prior to fixRemoveNFTokenAutoTrustLine, transfer of an NFToken between // accounts allowed a TrustLine to be added to the issuer of that token // without explicit permission from that issuer. This was enabled by @@ -185,6 +204,18 @@ constexpr std::uint32_t tfDepositMask = ~(tfUniversal | tfDepositSubTx); constexpr std::uint32_t tfClearAccountCreateAmount = 0x00010000; constexpr std::uint32_t tfBridgeModifyMask = ~(tfUniversal | tfClearAccountCreateAmount); +// CFTokenIssuanceCreate flags: +constexpr std::uint32_t const tfCFTokenIssuanceCreateMask = + ~(tfCFTCanLock | tfCFTRequireAuth | tfCFTCanEscrow | tfCFTCanTrade | tfCFTCanTransfer | tfCFTCanClawback | tfUniversal); + +// CFTokenIssuanceDestroy flags: +constexpr std::uint32_t const tfCFTokenIssuanceDestroyMask = ~tfUniversal; + +// CFTokenAuthorize flags: +constexpr std::uint32_t const tfCFTokenAuthorizeMask = ~(tfCFTUnauthorize | tfUniversal); + +// CFTokenIssuanceSet flags: +constexpr std::uint32_t const tfCFTokenIssuanceSetMask = ~(tfCFTLock | tfCFTUnlock | tfUniversal); // clang-format on } // namespace ripple diff --git a/src/ripple/protocol/TxFormats.h b/src/ripple/protocol/TxFormats.h index b12547b0a67..2aac7329f84 100644 --- a/src/ripple/protocol/TxFormats.h +++ b/src/ripple/protocol/TxFormats.h @@ -190,6 +190,17 @@ enum TxType : std::uint16_t /** This transaction type deletes a DID */ ttDID_DELETE = 50, + /** This transaction creates a new CFTokenIssuance object. */ + ttCFTOKEN_ISSUANCE_CREATE = 51, + + /** This transaction destroys an existing CFTokenIssuance object. */ + ttCFTOKEN_ISSUANCE_DESTROY = 52, + + /** This transaction destroys an existing CFTokenIssuance object. */ + ttCFTOKEN_AUTHORIZE = 53, + + /** This transaction sets an existing CFTokenIssuance or CFToken object. */ + ttCFTOKEN_ISSUANCE_SET = 54, /** This system-generated transaction type is used to update the status of the various amendments. diff --git a/src/ripple/protocol/impl/Feature.cpp b/src/ripple/protocol/impl/Feature.cpp index 25033d4336e..917a6c6bdb3 100644 --- a/src/ripple/protocol/impl/Feature.cpp +++ b/src/ripple/protocol/impl/Feature.cpp @@ -459,6 +459,7 @@ REGISTER_FEATURE(XChainBridge, Supported::yes, VoteBehavior::De REGISTER_FIX (fixDisallowIncomingV1, Supported::yes, VoteBehavior::DefaultNo); REGISTER_FEATURE(DID, Supported::yes, VoteBehavior::DefaultNo); REGISTER_FIX(fixFillOrKill, Supported::yes, VoteBehavior::DefaultNo); +REGISTER_FEATURE(CFTokensV1, Supported::yes, VoteBehavior::DefaultNo); // The following amendments are obsolete, but must remain supported // because they could potentially get enabled. diff --git a/src/ripple/protocol/impl/Indexes.cpp b/src/ripple/protocol/impl/Indexes.cpp index 74f6b6492de..9dddb889095 100644 --- a/src/ripple/protocol/impl/Indexes.cpp +++ b/src/ripple/protocol/impl/Indexes.cpp @@ -72,6 +72,9 @@ enum class LedgerNameSpace : std::uint16_t { XCHAIN_CLAIM_ID = 'Q', XCHAIN_CREATE_ACCOUNT_CLAIM_ID = 'K', DID = 'I', + CFTOKEN_ISSUANCE = '~', + CFTOKEN = 't', + CFT_DIR = 'k', // No longer used or supported. Left here to reserve the space // to avoid accidental reuse. @@ -444,6 +447,25 @@ did(AccountID const& account) noexcept return {ltDID, indexHash(LedgerNameSpace::DID, account)}; } +Keylet +cftIssuance(AccountID const& issuer, std::uint32_t seq) noexcept +{ + return { + ltCFTOKEN_ISSUANCE, + indexHash(LedgerNameSpace::CFTOKEN_ISSUANCE, issuer, seq)}; +} + +Keylet +cftoken(uint256 const& issuanceID, AccountID const& holder) noexcept +{ + return {ltCFTOKEN, indexHash(LedgerNameSpace::CFTOKEN, issuanceID, holder)}; +} + +Keylet +cft_dir(uint256 const& id) noexcept +{ + return {ltDIR_NODE, indexHash(LedgerNameSpace::CFT_DIR, id)}; +} } // namespace keylet } // namespace ripple diff --git a/src/ripple/protocol/impl/LedgerFormats.cpp b/src/ripple/protocol/impl/LedgerFormats.cpp index 729ddc1c7bc..1ec96abc004 100644 --- a/src/ripple/protocol/impl/LedgerFormats.cpp +++ b/src/ripple/protocol/impl/LedgerFormats.cpp @@ -74,6 +74,7 @@ LedgerFormats::LedgerFormats() {sfIndexNext, soeOPTIONAL}, {sfIndexPrevious, soeOPTIONAL}, {sfNFTokenID, soeOPTIONAL}, + {sfCFTokenIssuanceID, soeOPTIONAL}, }, commonFields); @@ -339,6 +340,36 @@ LedgerFormats::LedgerFormats() {sfPreviousTxnLgrSeq, soeREQUIRED} }, commonFields); + + add(jss::CFTokenIssuance, + ltCFTOKEN_ISSUANCE, + { + {sfIssuer, soeREQUIRED}, + {sfTransferFee, soeDEFAULT}, + {sfOwnerNode, soeREQUIRED}, + {sfAssetScale, soeDEFAULT}, + {sfMaximumAmount, soeOPTIONAL}, + {sfOutstandingAmount, soeREQUIRED}, + {sfLockedAmount, soeDEFAULT}, + {sfCFTokenMetadata, soeOPTIONAL}, + {sfPreviousTxnID, soeREQUIRED}, + {sfPreviousTxnLgrSeq, soeREQUIRED} + }, + commonFields); + + add(jss::CFToken, + ltCFTOKEN, + { + {sfAccount, soeREQUIRED}, + {sfCFTokenIssuanceID, soeREQUIRED}, + {sfCFTAmount, soeREQUIRED}, + {sfLockedAmount, soeDEFAULT}, + {sfOwnerNode, soeREQUIRED}, + {sfCFTokenNode, soeREQUIRED}, + {sfPreviousTxnID, soeREQUIRED}, + {sfPreviousTxnLgrSeq, soeREQUIRED} + }, + commonFields); // clang-format on } diff --git a/src/ripple/protocol/impl/SField.cpp b/src/ripple/protocol/impl/SField.cpp index 027c8ffb9c5..1db5245ed25 100644 --- a/src/ripple/protocol/impl/SField.cpp +++ b/src/ripple/protocol/impl/SField.cpp @@ -97,6 +97,7 @@ CONSTRUCT_TYPED_SFIELD(sfTickSize, "TickSize", UINT8, CONSTRUCT_TYPED_SFIELD(sfUNLModifyDisabling, "UNLModifyDisabling", UINT8, 17); CONSTRUCT_TYPED_SFIELD(sfHookResult, "HookResult", UINT8, 18); CONSTRUCT_TYPED_SFIELD(sfWasLockingChainSend, "WasLockingChainSend", UINT8, 19); +CONSTRUCT_TYPED_SFIELD(sfAssetScale, "AssetScale", UINT8, 20); // 16-bit integers CONSTRUCT_TYPED_SFIELD(sfLedgerEntryType, "LedgerEntryType", UINT16, 1, SField::sMD_Never); @@ -179,6 +180,7 @@ CONSTRUCT_TYPED_SFIELD(sfCookie, "Cookie", UINT64, CONSTRUCT_TYPED_SFIELD(sfServerVersion, "ServerVersion", UINT64, 11); CONSTRUCT_TYPED_SFIELD(sfNFTokenOfferNode, "NFTokenOfferNode", UINT64, 12); CONSTRUCT_TYPED_SFIELD(sfEmitBurden, "EmitBurden", UINT64, 13); +CONSTRUCT_TYPED_SFIELD(sfCFTokenNode, "CFTokenNode", UINT64, 14); // 64-bit integers (uncommon) CONSTRUCT_TYPED_SFIELD(sfHookOn, "HookOn", UINT64, 16); @@ -188,6 +190,10 @@ CONSTRUCT_TYPED_SFIELD(sfReferenceCount, "ReferenceCount", U CONSTRUCT_TYPED_SFIELD(sfXChainClaimID, "XChainClaimID", UINT64, 20); CONSTRUCT_TYPED_SFIELD(sfXChainAccountCreateCount, "XChainAccountCreateCount", UINT64, 21); CONSTRUCT_TYPED_SFIELD(sfXChainAccountClaimCount, "XChainAccountClaimCount", UINT64, 22); +CONSTRUCT_TYPED_SFIELD(sfMaximumAmount, "MaximumAmount", UINT64, 23); +CONSTRUCT_TYPED_SFIELD(sfOutstandingAmount, "OutstandingAmount", UINT64, 24); +CONSTRUCT_TYPED_SFIELD(sfLockedAmount, "LockedAmount", UINT64, 25); +CONSTRUCT_TYPED_SFIELD(sfCFTAmount, "CFTAmount", UINT64, 26); // 128-bit CONSTRUCT_TYPED_SFIELD(sfEmailHash, "EmailHash", UINT128, 1); @@ -213,6 +219,7 @@ CONSTRUCT_TYPED_SFIELD(sfEmitParentTxnID, "EmitParentTxnID", UINT256, CONSTRUCT_TYPED_SFIELD(sfEmitNonce, "EmitNonce", UINT256, 12); CONSTRUCT_TYPED_SFIELD(sfEmitHookHash, "EmitHookHash", UINT256, 13); CONSTRUCT_TYPED_SFIELD(sfAMMID, "AMMID", UINT256, 14); +CONSTRUCT_TYPED_SFIELD(sfCFTokenIssuanceID, "CFTokenIssuanceID", UINT256, 15); // 256-bit (uncommon) CONSTRUCT_TYPED_SFIELD(sfBookDirectory, "BookDirectory", UINT256, 16); @@ -300,6 +307,7 @@ CONSTRUCT_TYPED_SFIELD(sfHookParameterName, "HookParameterName", VL, CONSTRUCT_TYPED_SFIELD(sfHookParameterValue, "HookParameterValue", VL, 25); CONSTRUCT_TYPED_SFIELD(sfDIDDocument, "DIDDocument", VL, 26); CONSTRUCT_TYPED_SFIELD(sfData, "Data", VL, 27); +CONSTRUCT_TYPED_SFIELD(sfCFTokenMetadata, "CFTokenMetadata", VL, 28); // account CONSTRUCT_TYPED_SFIELD(sfAccount, "Account", ACCOUNT, 1); @@ -312,6 +320,7 @@ CONSTRUCT_TYPED_SFIELD(sfUnauthorize, "Unauthorize", ACCOUNT, CONSTRUCT_TYPED_SFIELD(sfRegularKey, "RegularKey", ACCOUNT, 8); CONSTRUCT_TYPED_SFIELD(sfNFTokenMinter, "NFTokenMinter", ACCOUNT, 9); CONSTRUCT_TYPED_SFIELD(sfEmitCallback, "EmitCallback", ACCOUNT, 10); +CONSTRUCT_TYPED_SFIELD(sfCFTokenHolder, "CFTokenHolder", ACCOUNT, 11); // account (uncommon) CONSTRUCT_TYPED_SFIELD(sfHookAccount, "HookAccount", ACCOUNT, 16); diff --git a/src/ripple/protocol/impl/TER.cpp b/src/ripple/protocol/impl/TER.cpp index 1c2db3feb3b..47af5a8c7d7 100644 --- a/src/ripple/protocol/impl/TER.cpp +++ b/src/ripple/protocol/impl/TER.cpp @@ -111,6 +111,7 @@ transResults() MAKE_ERROR(tecXCHAIN_BAD_PUBLIC_KEY_ACCOUNT_PAIR, "Bad public key account pair in an xchain transaction."), MAKE_ERROR(tecXCHAIN_CREATE_ACCOUNT_DISABLED, "This bridge does not support account creation."), MAKE_ERROR(tecEMPTY_DID, "The DID object did not have a URI or DIDDocument field."), + MAKE_ERROR(tecCFTOKEN_EXISTS, "The account already owns the CFToken object."), MAKE_ERROR(tefALREADY, "The exact transaction was already in this ledger."), MAKE_ERROR(tefBAD_ADD_AUTH, "Not authorized to add account."), diff --git a/src/ripple/protocol/impl/TxFormats.cpp b/src/ripple/protocol/impl/TxFormats.cpp index 7be8ca741e2..957b9f2a5b9 100644 --- a/src/ripple/protocol/impl/TxFormats.cpp +++ b/src/ripple/protocol/impl/TxFormats.cpp @@ -483,6 +483,39 @@ TxFormats::TxFormats() commonFields); add(jss::DIDDelete, ttDID_DELETE, {}, commonFields); + + add(jss::CFTokenIssuanceCreate, + ttCFTOKEN_ISSUANCE_CREATE, + { + {sfAssetScale, soeOPTIONAL}, + {sfTransferFee, soeOPTIONAL}, + {sfMaximumAmount, soeOPTIONAL}, + {sfCFTokenMetadata, soeOPTIONAL}, + }, + commonFields); + + add(jss::CFTokenIssuanceDestroy, + ttCFTOKEN_ISSUANCE_DESTROY, + { + {sfCFTokenIssuanceID, soeREQUIRED}, + }, + commonFields); + + add(jss::CFTokenAuthorize, + ttCFTOKEN_AUTHORIZE, + { + {sfCFTokenIssuanceID, soeREQUIRED}, + {sfCFTokenHolder, soeOPTIONAL}, + }, + commonFields); + + add(jss::CFTokenIssuanceSet, + ttCFTOKEN_ISSUANCE_SET, + { + {sfCFTokenIssuanceID, soeREQUIRED}, + {sfCFTokenHolder, soeOPTIONAL}, + }, + commonFields); } TxFormats const& diff --git a/src/ripple/protocol/jss.h b/src/ripple/protocol/jss.h index 8a701defad8..7ae732a6962 100644 --- a/src/ripple/protocol/jss.h +++ b/src/ripple/protocol/jss.h @@ -63,6 +63,13 @@ JSS(Asset2); // in: AMM Asset2 JSS(AuthAccount); // in: AMM Auction Slot JSS(AuthAccounts); // in: AMM Auction Slot JSS(Bridge); // ledger type. +JSS(CFToken); // ledger type. +JSS(CFTokenIssuance); // ledger type. +JSS(CFTokenIssuanceCreate); // transaction type. +JSS(CFTokenIssuanceDestroy); // transaction type. +JSS(CFTokenAuthorize); // transaction type. +JSS(CFTokenIssuanceSet); // transaction type. +JSS(CFTokenIssuanceID); // in: CFTokenIssuanceDestroy, CFTokenAuthorize JSS(Check); // ledger type. JSS(CheckCancel); // transaction type. JSS(CheckCash); // transaction type. diff --git a/src/test/jtx.h b/src/test/jtx.h index 03bbf154e63..4c8ff226bf5 100644 --- a/src/test/jtx.h +++ b/src/test/jtx.h @@ -32,6 +32,7 @@ #include #include #include +#include #include #include #include diff --git a/src/test/jtx/Env.h b/src/test/jtx/Env.h index 6a55f2f9141..25aa595cdac 100644 --- a/src/test/jtx/Env.h +++ b/src/test/jtx/Env.h @@ -433,6 +433,12 @@ class Env PrettyAmount balance(Account const& account, Issue const& issue) const; + /** Return the number of objects owned by an account. + * Returns 0 if the account does not exist. + */ + std::uint32_t + ownerCount(Account const& account) const; + /** Return an account root. @return empty if the account does not exist. */ diff --git a/src/test/jtx/impl/Env.cpp b/src/test/jtx/impl/Env.cpp index e82183c0001..f6273a70015 100644 --- a/src/test/jtx/impl/Env.cpp +++ b/src/test/jtx/impl/Env.cpp @@ -212,6 +212,14 @@ Env::seq(Account const& account) const return sle->getFieldU32(sfSequence); } +std::uint32_t +Env::ownerCount(Account const& account) const +{ + if (auto const sleAcct = le(account)) + return sleAcct->at(sfOwnerCount); + return 0; +} + std::shared_ptr Env::le(Account const& account) const {