feat: Account permission support (#2145)

Fixes: #1975.
Also switch to libxrpl 2.5.0-b1. Fixes: #2139, #2140.
This commit is contained in:
Sergey Kuznetsov
2025-05-20 13:40:38 +01:00
committed by GitHub
parent 670eaaa51d
commit b3da9adb03
8 changed files with 198 additions and 4 deletions

View File

@@ -31,7 +31,7 @@ class Clio(ConanFile):
'protobuf/3.21.9',
'grpc/1.50.1',
'openssl/1.1.1v',
'xrpl/2.4.0',
'xrpl/2.5.0-b1',
'zlib/1.3.1',
'libbacktrace/cci.20210118'
]

View File

@@ -137,6 +137,8 @@ struct Amendments {
REGISTER(fixInvalidTxFlags);
REGISTER(fixFrozenLPTokenTransfer);
REGISTER(DeepFreeze);
REGISTER(PermissionDelegation);
REGISTER(fixPayChanCancelAfter);
// Obsolete but supported by libxrpl
REGISTER(CryptoConditionsSuite);

View File

@@ -185,6 +185,13 @@ LedgerEntryHandler::process(LedgerEntryHandler::Input input, Context const& ctx)
);
auto const seq = input.permissionedDomain->at(JS(seq)).as_int64();
key = ripple::keylet::permissionedDomain(*account, seq).key;
} else if (input.delegate) {
auto const account =
ripple::parseBase58<ripple::AccountID>(boost::json::value_to<std::string>(input.delegate->at(JS(account))));
auto const authorize =
ripple::parseBase58<ripple::AccountID>(boost::json::value_to<std::string>(input.delegate->at(JS(authorize)))
);
key = ripple::keylet::delegate(*account, *authorize).key;
} else {
// Must specify 1 of the following fields to indicate what type
if (ctx.apiVersion == 1)
@@ -319,7 +326,8 @@ tag_invoke(boost::json::value_to_tag<LedgerEntryHandler::Input>, boost::json::va
{JS(oracle), ripple::ltORACLE},
{JS(credential), ripple::ltCREDENTIAL},
{JS(mptoken), ripple::ltMPTOKEN},
{JS(permissioned_domain), ripple::ltPERMISSIONED_DOMAIN}
{JS(permissioned_domain), ripple::ltPERMISSIONED_DOMAIN},
{JS(delegate), ripple::ltDELEGATE}
};
auto const parseBridgeFromJson = [](boost::json::value const& bridgeJson) {
@@ -408,6 +416,8 @@ tag_invoke(boost::json::value_to_tag<LedgerEntryHandler::Input>, boost::json::va
input.mptoken = jv.at(JS(mptoken)).as_object();
} else if (jsonObject.contains(JS(permissioned_domain))) {
input.permissionedDomain = jv.at(JS(permissioned_domain)).as_object();
} else if (jsonObject.contains(JS(delegate))) {
input.delegate = jv.at(JS(delegate)).as_object();
}
if (jsonObject.contains("include_deleted"))

View File

@@ -110,6 +110,7 @@ public:
std::optional<uint32_t> createAccountClaimId;
std::optional<ripple::uint256> oracleNode;
std::optional<ripple::uint256> credential;
std::optional<boost::json::object> delegate;
bool includeDeleted = false;
};
@@ -392,6 +393,23 @@ public:
},
},
}}},
{JS(delegate),
meta::WithCustomError{
validation::Type<std::string, boost::json::object>{}, Status(ClioError::RpcMalformedRequest)
},
meta::IfType<std::string>{kMALFORMED_REQUEST_HEX_STRING_VALIDATOR},
meta::IfType<boost::json::object>{meta::Section{
{JS(account),
meta::WithCustomError{validation::Required{}, Status(ClioError::RpcMalformedRequest)},
meta::WithCustomError{
validation::CustomValidators::accountBase58Validator, Status(ClioError::RpcMalformedAddress)
}},
{JS(authorize),
meta::WithCustomError{validation::Required{}, Status(ClioError::RpcMalformedRequest)},
meta::WithCustomError{
validation::CustomValidators::accountBase58Validator, Status(ClioError::RpcMalformedAddress)
}}
}}},
{JS(ledger), check::Deprecated{}},
{"include_deleted", validation::Type<bool>{}},
};

View File

@@ -1516,6 +1516,31 @@ createPermissionedDomainObject(
return object;
}
ripple::STObject
createDelegateObject(
std::string_view accountId,
std::string_view authorize,
std::string_view ledgerIndex,
uint64_t ownerNode,
ripple::uint256 previousTxId,
uint32_t previousTxSeq
)
{
ripple::STObject object(ripple::sfLedgerEntry);
object.setFieldH256(ripple::sfLedgerIndex, ripple::uint256(ledgerIndex));
object.setFieldU16(ripple::sfLedgerEntryType, ripple::ltDELEGATE);
object.setAccountID(ripple::sfAccount, getAccountIdWithString(accountId));
object.setAccountID(ripple::sfAuthorize, getAccountIdWithString(authorize));
object.setFieldArray(ripple::sfPermissions, ripple::STArray{});
object.setFieldU64(ripple::sfOwnerNode, ownerNode);
object.setFieldH256(ripple::sfPreviousTxnID, previousTxId);
object.setFieldU32(ripple::sfPreviousTxnLgrSeq, previousTxSeq);
object.setFieldU32(ripple::sfFlags, 0);
return object;
}
ripple::STObject
createOraclePriceData(
uint64_t assetPrice,

View File

@@ -464,6 +464,16 @@ createPermissionedDomainObject(
uint32_t previousTxSeq
);
[[nodiscard]] ripple::STObject
createDelegateObject(
std::string_view accountId,
std::string_view authorize,
std::string_view ledgerIndex,
uint64_t ownerNode,
ripple::uint256 previousTxId,
uint32_t previousTxSeq
);
[[nodiscard]] ripple::STObject
createOraclePriceData(
uint64_t assetPrice,

View File

@@ -2193,6 +2193,106 @@ generateTestValuesForParametersTest()
.expectedError = "malformedRequest",
.expectedErrorMessage = "Malformed request.",
},
ParamTestCaseBundle{
.testName = "Delegate_InvalidType",
.testJson = R"json({"delegate": 123})json",
.expectedError = "malformedRequest",
.expectedErrorMessage = "Malformed request."
},
ParamTestCaseBundle{
.testName = "Delegate_InvalidStringIndex",
.testJson = R"json({"delegate": "invalid_hex_string"})json",
.expectedError = "malformedRequest",
.expectedErrorMessage = "Malformed request."
},
ParamTestCaseBundle{
.testName = "Delegate_EmptyObject",
.testJson = R"json({"delegate": {}})json",
.expectedError = "malformedRequest",
.expectedErrorMessage = "Malformed request."
},
ParamTestCaseBundle{
.testName = "Delegate_MissingAccount",
.testJson = fmt::format(
R"json({{
"delegate": {{
"authorize": "{}"
}}
}})json",
kACCOUNT2
),
.expectedError = "malformedRequest",
.expectedErrorMessage = "Malformed request."
},
ParamTestCaseBundle{
.testName = "Delegate_AccountNotString",
.testJson = fmt::format(
R"json({{
"delegate": {{
"account": 123,
"authorize": "{}"
}}
}})json",
kACCOUNT2
),
.expectedError = "malformedAddress",
.expectedErrorMessage = "Malformed address."
},
ParamTestCaseBundle{
.testName = "Delegate_AccountInvalid",
.testJson = fmt::format(
R"json({{
"delegate": {{
"account": "invalid_address",
"authorize": "{}"
}}
}})json",
kACCOUNT2
),
.expectedError = "malformedAddress",
.expectedErrorMessage = "Malformed address."
},
ParamTestCaseBundle{
.testName = "Delegate_MissingAuthorize",
.testJson = fmt::format(
R"json({{
"delegate": {{
"account": "{}"
}}
}})json",
kACCOUNT
),
.expectedError = "malformedRequest",
.expectedErrorMessage = "Malformed request."
},
ParamTestCaseBundle{
.testName = "Delegate_AuthorizeNotString",
.testJson = fmt::format(
R"json({{
"delegate": {{
"account": "{}",
"authorize": 123
}}
}})json",
kACCOUNT
),
.expectedError = "malformedAddress",
.expectedErrorMessage = "Malformed address."
},
ParamTestCaseBundle{
.testName = "Delegate_AuthorizeInvalid",
.testJson = fmt::format(
R"json({{
"delegate": {{
"account": "{}",
"authorize": "invalid_address"
}}
}})json",
kACCOUNT
),
.expectedError = "malformedAddress",
.expectedErrorMessage = "Malformed address."
},
};
}
@@ -2957,7 +3057,36 @@ generateTestValuesForNormalPathTest()
ripple::keylet::permissionedDomain(ripple::parseBase58<ripple::AccountID>(kACCOUNT).value(), kRANGE_MAX)
.key,
.mockedEntity = createPermissionedDomainObject(kACCOUNT, kINDEX1, kRANGE_MAX, 0, ripple::uint256{0}, 0)
}
},
NormalPathTestBundle{
.testName = "DelegateViaStringIndex",
.testJson = fmt::format(
R"json({{
"binary": true,
"delegate": "{}"
}})json",
kINDEX1
),
.expectedIndex = ripple::uint256{kINDEX1},
.mockedEntity = createDelegateObject(kACCOUNT, kACCOUNT2, kINDEX1, 0, ripple::uint256{0}, 0)
},
NormalPathTestBundle{
.testName = "DelegateViaObject",
.testJson = fmt::format(
R"json({{
"binary": true,
"delegate": {{
"account": "{}",
"authorize": "{}"
}}
}})json",
kACCOUNT,
kACCOUNT2
),
.expectedIndex =
ripple::keylet::delegate(getAccountIdWithString(kACCOUNT), getAccountIdWithString(kACCOUNT2)).key,
.mockedEntity = createDelegateObject(kACCOUNT, kACCOUNT2, kINDEX1, 0, ripple::uint256{0}, 0)
},
};
}

View File

@@ -834,7 +834,7 @@ TEST_F(RPCTxTest, CTIDNotMatch)
ASSERT_FALSE(output);
auto const err = rpc::makeError(output.result.error());
EXPECT_EQ(err.at("error").as_string(), "unknown");
EXPECT_EQ(err.at("error").as_string(), "wrongNetwork");
EXPECT_EQ(err.at("error_code").as_uint64(), rpc::RippledError::rpcWRONG_NETWORK);
EXPECT_EQ(
err.at("error_message").as_string(),