From 89af8fe5005676ac569b52c8861700230dda6a82 Mon Sep 17 00:00:00 2001 From: Sergey Kuznetsov Date: Fri, 31 Jan 2025 15:30:34 +0000 Subject: [PATCH] feat: Permissioned domains (#1841) Fixes #1833. --- src/data/AmendmentCenter.hpp | 1 - src/rpc/Errors.cpp | 1 + src/rpc/handlers/LedgerEntry.cpp | 9 ++ src/rpc/handlers/LedgerEntry.hpp | 18 ++++ src/util/LedgerUtils.hpp | 1 + tests/common/util/TestObject.cpp | 24 +++++ tests/common/util/TestObject.hpp | 10 ++ tests/unit/rpc/handlers/LedgerEntryTests.cpp | 98 ++++++++++++++++++-- tests/unit/util/LedgerUtilsTests.cpp | 7 +- 9 files changed, 159 insertions(+), 10 deletions(-) diff --git a/src/data/AmendmentCenter.hpp b/src/data/AmendmentCenter.hpp index 698f57978..777ba340b 100644 --- a/src/data/AmendmentCenter.hpp +++ b/src/data/AmendmentCenter.hpp @@ -133,7 +133,6 @@ struct Amendments { REGISTER(AMMClawback); REGISTER(Credentials); REGISTER(DynamicNFT); - // TODO: Add PermissionedDomains related RPC changes REGISTER(PermissionedDomains); // Obsolete but supported by libxrpl diff --git a/src/rpc/Errors.cpp b/src/rpc/Errors.cpp index 01e5305fb..2609fc881 100644 --- a/src/rpc/Errors.cpp +++ b/src/rpc/Errors.cpp @@ -89,6 +89,7 @@ getErrorInfo(ClioError code) {.code = ClioError::RpcMalformedAuthorizedCredentials, .error = "malformedAuthorizedCredentials", .message = "Malformed authorized credentials."}, + // special system errors {.code = ClioError::RpcInvalidApiVersion, .error = JS(invalid_API_version), .message = "Invalid API version."}, {.code = ClioError::RpcCommandIsMissing, diff --git a/src/rpc/handlers/LedgerEntry.cpp b/src/rpc/handlers/LedgerEntry.cpp index 36ff61560..c424135aa 100644 --- a/src/rpc/handlers/LedgerEntry.cpp +++ b/src/rpc/handlers/LedgerEntry.cpp @@ -179,6 +179,12 @@ LedgerEntryHandler::process(LedgerEntryHandler::Input input, Context const& ctx) ripple::uint192{std::string_view(boost::json::value_to(input.mptoken->at(JS(mpt_issuance_id)))) }; key = ripple::keylet::mptoken(mptIssuanceID, *holder).key; + } else if (input.permissionedDomain) { + auto const account = ripple::parseBase58( + boost::json::value_to(input.permissionedDomain->at(JS(account))) + ); + auto const seq = input.permissionedDomain->at(JS(seq)).as_int64(); + key = ripple::keylet::permissionedDomain(*account, seq).key; } else { // Must specify 1 of the following fields to indicate what type if (ctx.apiVersion == 1) @@ -313,6 +319,7 @@ tag_invoke(boost::json::value_to_tag, boost::json::va {JS(oracle), ripple::ltORACLE}, {JS(credential), ripple::ltCREDENTIAL}, {JS(mptoken), ripple::ltMPTOKEN}, + {JS(permissioned_domain), ripple::ltPERMISSIONED_DOMAIN} }; auto const parseBridgeFromJson = [](boost::json::value const& bridgeJson) { @@ -399,6 +406,8 @@ tag_invoke(boost::json::value_to_tag, boost::json::va input.credential = parseCredentialFromJson(jv.at(JS(credential))); } else if (jsonObject.contains(JS(mptoken))) { input.mptoken = jv.at(JS(mptoken)).as_object(); + } else if (jsonObject.contains(JS(permissioned_domain))) { + input.permissionedDomain = jv.at(JS(permissioned_domain)).as_object(); } if (jsonObject.contains("include_deleted")) diff --git a/src/rpc/handlers/LedgerEntry.hpp b/src/rpc/handlers/LedgerEntry.hpp index a6ab89ee2..9851ad746 100644 --- a/src/rpc/handlers/LedgerEntry.hpp +++ b/src/rpc/handlers/LedgerEntry.hpp @@ -103,6 +103,7 @@ class LedgerEntryHandler { std::optional ticket; std::optional amm; std::optional mptoken; + std::optional permissionedDomain; std::optional bridge; std::optional bridgeAccount; std::optional chainClaimId; @@ -374,6 +375,23 @@ class LedgerEntryHandler { }, }, }}, + {JS(permissioned_domain), + meta::WithCustomError{ + validation::Type{}, Status(ClioError::RpcMalformedRequest) + }, + meta::IfType{kMALFORMED_REQUEST_HEX_STRING_VALIDATOR}, + meta::IfType{meta::Section{ + {JS(seq), + meta::WithCustomError{validation::Required{}, Status(ClioError::RpcMalformedRequest)}, + meta::WithCustomError{validation::Type{}, Status(ClioError::RpcMalformedRequest)}}, + { + JS(account), + meta::WithCustomError{validation::Required{}, Status(ClioError::RpcMalformedRequest)}, + meta::WithCustomError{ + validation::CustomValidators::accountBase58Validator, Status(ClioError::RpcMalformedAddress) + }, + }, + }}}, {JS(ledger), check::Deprecated{}}, {"include_deleted", validation::Type{}}, }; diff --git a/src/util/LedgerUtils.hpp b/src/util/LedgerUtils.hpp index 3ada20a64..2c1916e0d 100644 --- a/src/util/LedgerUtils.hpp +++ b/src/util/LedgerUtils.hpp @@ -117,6 +117,7 @@ class LedgerTypes { LedgerTypeAttribute::chainLedgerType(JS(nunl), ripple::ltNEGATIVE_UNL), LedgerTypeAttribute::deletionBlockerLedgerType(JS(mpt_issuance), ripple::ltMPTOKEN_ISSUANCE), LedgerTypeAttribute::deletionBlockerLedgerType(JS(mptoken), ripple::ltMPTOKEN), + LedgerTypeAttribute::deletionBlockerLedgerType(JS(permissioned_domain), ripple::ltPERMISSIONED_DOMAIN), }; public: diff --git a/tests/common/util/TestObject.cpp b/tests/common/util/TestObject.cpp index b8ad157c0..47e9e57ce 100644 --- a/tests/common/util/TestObject.cpp +++ b/tests/common/util/TestObject.cpp @@ -1485,6 +1485,30 @@ createMpTokenObject(std::string_view accountId, ripple::uint192 issuanceID, std: return mptoken; } +ripple::STObject +createPermissionedDomainObject( + std::string_view accountId, + std::string_view ledgerIndex, + ripple::LedgerIndex seq, + uint64_t ownerNode, + ripple::uint256 previousTxId, + uint32_t previousTxSeq +) +{ + ripple::STObject object(ripple::sfLedgerEntry); + object.setFieldH256(ripple::sfLedgerIndex, ripple::uint256(ledgerIndex)); + object.setAccountID(ripple::sfOwner, getAccountIdWithString(accountId)); + object.setFieldU32(ripple::sfSequence, seq); + object.setFieldArray(ripple::sfAcceptedCredentials, ripple::STArray{}); + object.setFieldU64(ripple::sfOwnerNode, ownerNode); + object.setFieldH256(ripple::sfPreviousTxnID, previousTxId); + object.setFieldU32(ripple::sfPreviousTxnLgrSeq, previousTxSeq); + object.setFieldU32(ripple::sfFlags, 0); + object.setFieldU16(ripple::sfLedgerEntryType, ripple::ltPERMISSIONED_DOMAIN); + + return object; +} + ripple::STObject createOraclePriceData( uint64_t assetPrice, diff --git a/tests/common/util/TestObject.hpp b/tests/common/util/TestObject.hpp index 7ad745d7a..1bed19bf3 100644 --- a/tests/common/util/TestObject.hpp +++ b/tests/common/util/TestObject.hpp @@ -453,6 +453,16 @@ createMptIssuanceObject(std::string_view accountId, std::uint32_t seq, std::stri [[nodiscard]] ripple::STObject createMpTokenObject(std::string_view accountId, ripple::uint192 issuanceID, std::uint64_t mptAmount = 1); +[[nodiscard]] ripple::STObject +createPermissionedDomainObject( + std::string_view accountId, + std::string_view ledgerIndex, + ripple::LedgerIndex seq, + uint64_t ownerNode, + ripple::uint256 previousTxId, + uint32_t previousTxSeq +); + [[nodiscard]] ripple::STObject createOraclePriceData( uint64_t assetPrice, diff --git a/tests/unit/rpc/handlers/LedgerEntryTests.cpp b/tests/unit/rpc/handlers/LedgerEntryTests.cpp index f6ff78132..4f25a4f9a 100644 --- a/tests/unit/rpc/handlers/LedgerEntryTests.cpp +++ b/tests/unit/rpc/handlers/LedgerEntryTests.cpp @@ -2136,7 +2136,62 @@ generateTestValuesForParametersTest() ), .expectedError = "malformedRequest", .expectedErrorMessage = "Malformed request." - } + }, + ParamTestCaseBundle{ + .testName = "InvalidPermissionedDomain_NotObject", + .testJson = R"json({"permissioned_domain": []})json", + .expectedError = "malformedRequest", + .expectedErrorMessage = "Malformed request.", + }, + ParamTestCaseBundle{ + .testName = "InvalidPermissionedDomain_InvalidString", + .testJson = R"json({"permissioned_domain": "invalid_string"})json", + .expectedError = "malformedRequest", + .expectedErrorMessage = "Malformed request.", + }, + ParamTestCaseBundle{ + .testName = "InvalidPermissionedDomain_EmptyObject", + .testJson = R"json({"permissioned_domain": {}})json", + .expectedError = "malformedRequest", + .expectedErrorMessage = "Malformed request.", + }, + ParamTestCaseBundle{ + .testName = "InvalidPermissionedDomain_BadAccount", + .testJson = R"json({"permissioned_domain": {"account": "1234", "seq": 1234}})json", + .expectedError = "malformedAddress", + .expectedErrorMessage = "Malformed address.", + }, + ParamTestCaseBundle{ + .testName = "InvalidPermissionedDomain_MissingSeq", + .testJson = fmt::format( + R"json({{ + "permissioned_domain": {{ "account": "{}" }} + }})json", + kACCOUNT + ), + .expectedError = "malformedRequest", + .expectedErrorMessage = "Malformed request.", + }, + ParamTestCaseBundle{ + .testName = "InvalidPermissionedDomain_SeqIsNotUint", + .testJson = fmt::format( + R"json({{ + "permissioned_domain": {{ "account": "{}", "seq": -1 }} + }})json", + kACCOUNT + ), + .expectedError = "malformedRequest", + .expectedErrorMessage = "Malformed request.", + }, + ParamTestCaseBundle{ + .testName = "InvalidPermissionedDomain_BothAccountAndSeqAreInvalid", + .testJson = + R"json({ + "permissioned_domain": { "account": "", "seq": -1 } + })json", + .expectedError = "malformedRequest", + .expectedErrorMessage = "Malformed request.", + }, }; } @@ -2872,6 +2927,36 @@ generateTestValuesForNormalPathTest() .expectedIndex = ripple::keylet::mptoken(ripple::makeMptID(2, account1), account1).key, .mockedEntity = createMpTokenObject(kACCOUNT, ripple::makeMptID(2, account1)) }, + NormalPathTestBundle{ + .testName = "PermissionedDomainViaString", + .testJson = fmt::format( + R"json({{ + "binary": true, + "permissioned_domain": "{}" + }})json", + kINDEX1 + ), + .expectedIndex = ripple::uint256(kINDEX1), + .mockedEntity = createPermissionedDomainObject(kACCOUNT, kINDEX1, kRANGE_MAX, 0, ripple::uint256{0}, 0) + }, + NormalPathTestBundle{ + .testName = "PermissionedDomainViaObject", + .testJson = fmt::format( + R"json({{ + "binary": true, + "permissioned_domain": {{ + "account": "{}", + "seq": {} + }} + }})json", + kACCOUNT, + kRANGE_MAX + ), + .expectedIndex = + ripple::keylet::permissionedDomain(ripple::parseBase58(kACCOUNT).value(), kRANGE_MAX) + .key, + .mockedEntity = createPermissionedDomainObject(kACCOUNT, kINDEX1, kRANGE_MAX, 0, ripple::uint256{0}, 0) + } }; } @@ -2900,15 +2985,14 @@ TEST_P(RPCLedgerEntryNormalPathTest, NormalPath) auto const req = json::parse(testBundle.testJson); auto const output = handler.process(req, Context{yield}); ASSERT_TRUE(output); - EXPECT_EQ(output.result.value().at("ledger_hash").as_string(), kLEDGER_HASH); - EXPECT_EQ(output.result.value().at("ledger_index").as_uint64(), kRANGE_MAX); + auto const& outputJson = output.result.value(); + EXPECT_EQ(outputJson.at("ledger_hash").as_string(), kLEDGER_HASH); + EXPECT_EQ(outputJson.at("ledger_index").as_uint64(), kRANGE_MAX); EXPECT_EQ( - output.result.value().at("node_binary").as_string(), - ripple::strHex(testBundle.mockedEntity.getSerializer().peekData()) + outputJson.at("node_binary").as_string(), ripple::strHex(testBundle.mockedEntity.getSerializer().peekData()) ); EXPECT_EQ( - ripple::uint256(boost::json::value_to(output.result.value().at("index")).data()), - testBundle.expectedIndex + ripple::uint256(boost::json::value_to(outputJson.at("index")).data()), testBundle.expectedIndex ); }); } diff --git a/tests/unit/util/LedgerUtilsTests.cpp b/tests/unit/util/LedgerUtilsTests.cpp index d1fdfc367..98a8a8d49 100644 --- a/tests/unit/util/LedgerUtilsTests.cpp +++ b/tests/unit/util/LedgerUtilsTests.cpp @@ -54,6 +54,7 @@ TEST(LedgerUtilsTests, LedgerObjectTypeList) JS(did), JS(mpt_issuance), JS(mptoken), + JS(permissioned_domain), JS(oracle), JS(credential), JS(nunl) @@ -88,7 +89,8 @@ TEST(LedgerUtilsTests, AccountOwnedTypeList) JS(oracle), JS(credential), JS(mpt_issuance), - JS(mptoken) + JS(mptoken), + JS(permissioned_domain), }; static_assert(std::size(kCORRECT_TYPES) == kACCOUNT_OWNED.size()); @@ -123,7 +125,8 @@ TEST(LedgerUtilsTests, DeletionBlockerTypes) ripple::ltXCHAIN_OWNED_CREATE_ACCOUNT_CLAIM_ID, ripple::ltBRIDGE, ripple::ltMPTOKEN_ISSUANCE, - ripple::ltMPTOKEN + ripple::ltMPTOKEN, + ripple::ltPERMISSIONED_DOMAIN }; static_assert(std::size(kDELETION_BLOCKERS) == kTESTED_TYPES.size());