diff --git a/conanfile.py b/conanfile.py index daf1a2b9..9ebdadb2 100644 --- a/conanfile.py +++ b/conanfile.py @@ -23,6 +23,7 @@ class Clio(ConanFile): 'boost/1.82.0', 'cassandra-cpp-driver/2.17.0', 'fmt/10.1.1', + 'protobuf/3.21.12', 'grpc/1.50.1', 'openssl/1.1.1u', 'xrpl/2.0.0-b2', diff --git a/src/rpc/handlers/LedgerEntry.cpp b/src/rpc/handlers/LedgerEntry.cpp index 18596ac0..d7bdf436 100644 --- a/src/rpc/handlers/LedgerEntry.cpp +++ b/src/rpc/handlers/LedgerEntry.cpp @@ -65,6 +65,21 @@ LedgerEntryHandler::process(LedgerEntryHandler::Input input, Context const& ctx) auto const id = ripple::parseBase58(input.ticket->at(JS(account)).as_string().c_str()); key = ripple::getTicketIndex(*id, input.ticket->at(JS(ticket_seq)).as_int64()); + } else if (input.amm) { + auto const getIssuerFromJson = [](auto const& assetJson) { + // the field check has been done in validator + auto const currency = ripple::to_currency(assetJson.at(JS(currency)).as_string().c_str()); + if (ripple::isXRP(currency)) { + return ripple::xrpIssue(); + } + auto const issuer = ripple::parseBase58(assetJson.at(JS(issuer)).as_string().c_str()); + return ripple::Issue{currency, *issuer}; + }; + + key = ripple::keylet::amm( + getIssuerFromJson(input.amm->at(JS(asset))), getIssuerFromJson(input.amm->at(JS(asset2))) + ) + .key; } else { // Must specify 1 of the following fields to indicate what type if (ctx.apiVersion == 1) @@ -179,7 +194,7 @@ tag_invoke(boost::json::value_to_tag, boost::json::va {JS(deposit_preauth), ripple::ltDEPOSIT_PREAUTH}, {JS(ticket), ripple::ltTICKET}, {JS(nft_page), ripple::ltNFTOKEN_PAGE}, - }; + {JS(amm), ripple::ltAMM}}; auto const indexFieldType = std::find_if(indexFieldTypeMap.begin(), indexFieldTypeMap.end(), [&jsonObject](auto const& pair) { @@ -208,6 +223,8 @@ tag_invoke(boost::json::value_to_tag, boost::json::va input.depositPreauth = jv.at(JS(deposit_preauth)).as_object(); } else if (jsonObject.contains(JS(ticket))) { input.ticket = jv.at(JS(ticket)).as_object(); + } else if (jsonObject.contains(JS(amm))) { + input.amm = jv.at(JS(amm)).as_object(); } return input; diff --git a/src/rpc/handlers/LedgerEntry.h b/src/rpc/handlers/LedgerEntry.h index c7e0a1de..9354ced7 100644 --- a/src/rpc/handlers/LedgerEntry.h +++ b/src/rpc/handlers/LedgerEntry.h @@ -63,6 +63,7 @@ public: std::optional escrow; std::optional depositPreauth; std::optional ticket; + std::optional amm; }; using Result = HandlerReturnType; @@ -100,6 +101,27 @@ public: static auto const malformedRequestIntValidator = meta::WithCustomError{validation::Type{}, Status(ClioError::rpcMALFORMED_REQUEST)}; + static auto const ammAssetValidator = + validation::CustomValidator{[](boost::json::value const& value, std::string_view /* key */) -> MaybeError { + if (!value.is_object()) { + return Error{Status{ClioError::rpcMALFORMED_REQUEST}}; + } + + Json::Value jvAsset; + if (value.as_object().contains(JS(issuer))) + jvAsset["issuer"] = value.at(JS(issuer)).as_string().c_str(); + if (value.as_object().contains(JS(currency))) + jvAsset["currency"] = value.at(JS(currency)).as_string().c_str(); + // same as rippled + try { + ripple::issueFromJson(jvAsset); + } catch (std::runtime_error const&) { + return Error{Status{ClioError::rpcMALFORMED_REQUEST}}; + } + + return MaybeError{}; + }}; + static auto const rpcSpec = RpcSpec{ {JS(binary), validation::Type{}}, {JS(ledger_hash), validation::Uint256HexStringValidator}, @@ -162,7 +184,23 @@ public: }, }}, {JS(nft_page), malformedRequestHexStringValidator}, - }; + {JS(amm), + validation::Type{}, + meta::IfType{malformedRequestHexStringValidator}, + meta::IfType{ + meta::Section{ + {JS(asset), + meta::WithCustomError{validation::Required{}, Status(ClioError::rpcMALFORMED_REQUEST)}, + meta::WithCustomError{ + validation::Type{}, Status(ClioError::rpcMALFORMED_REQUEST)}, + ammAssetValidator}, + {JS(asset2), + meta::WithCustomError{validation::Required{}, Status(ClioError::rpcMALFORMED_REQUEST)}, + meta::WithCustomError{ + validation::Type{}, Status(ClioError::rpcMALFORMED_REQUEST)}, + ammAssetValidator}, + }, + }}}; return rpcSpec; } diff --git a/unittests/rpc/handlers/LedgerEntryTests.cpp b/unittests/rpc/handlers/LedgerEntryTests.cpp index 1ed91503..2b3193bc 100644 --- a/unittests/rpc/handlers/LedgerEntryTests.cpp +++ b/unittests/rpc/handlers/LedgerEntryTests.cpp @@ -544,6 +544,245 @@ generateTestValuesForParametersTest() ), "malformedRequest", "Malformed request."}, + + ParamTestCaseBundle{ + "InvalidAMMStringIndex", + R"({ + "amm": "invalid" + })", + "malformedRequest", + "Malformed request."}, + + ParamTestCaseBundle{ + "EmptyAMMJson", + R"({ + "amm": {} + })", + "malformedRequest", + "Malformed request."}, + + ParamTestCaseBundle{ + "EmptyAMMAssetJson", + fmt::format( + R"({{ + "amm": + {{ + "asset":{{}}, + "asset2": + {{ + "currency" : "USD", + "issuer" : "{}" + }} + }} + }})", + ACCOUNT + ), + "malformedRequest", + "Malformed request."}, + + ParamTestCaseBundle{ + "EmptyAMMAsset2Json", + fmt::format( + R"({{ + "amm": + {{ + "asset2":{{}}, + "asset": + {{ + "currency" : "USD", + "issuer" : "{}" + }} + }} + }})", + ACCOUNT + ), + "malformedRequest", + "Malformed request."}, + + ParamTestCaseBundle{ + "MissingAMMAsset2Json", + fmt::format( + R"({{ + "amm": + {{ + "asset": + {{ + "currency" : "USD", + "issuer" : "{}" + }} + }} + }})", + ACCOUNT + ), + "malformedRequest", + "Malformed request."}, + + ParamTestCaseBundle{ + "MissingAMMAssetJson", + fmt::format( + R"({{ + "amm": + {{ + "asset2": + {{ + "currency" : "USD", + "issuer" : "{}" + }} + }} + }})", + ACCOUNT + ), + "malformedRequest", + "Malformed request."}, + + ParamTestCaseBundle{ + "AMMAssetNotJson", + fmt::format( + R"({{ + "amm": + {{ + "asset": "invalid", + "asset2": + {{ + "currency" : "USD", + "issuer" : "{}" + }} + }} + }})", + ACCOUNT + ), + "malformedRequest", + "Malformed request."}, + + ParamTestCaseBundle{ + "AMMAsset2NotJson", + fmt::format( + R"({{ + "amm": + {{ + "asset2": "invalid", + "asset": + {{ + "currency" : "USD", + "issuer" : "{}" + }} + }} + }})", + ACCOUNT + ), + "malformedRequest", + "Malformed request."}, + + ParamTestCaseBundle{ + "WrongAMMAssetCurrency", + fmt::format( + R"({{ + "amm": + {{ + "asset2": + {{ + "currency":"XRP" + }}, + "asset": + {{ + "currency" : "USD2", + "issuer" : "{}" + }} + }} + }})", + ACCOUNT + ), + "malformedRequest", + "Malformed request."}, + + ParamTestCaseBundle{ + "WrongAMMAssetIssuer", + fmt::format( + R"({{ + "amm": + {{ + "asset2": + {{ + "currency":"XRP" + }}, + "asset": + {{ + "currency" : "USD", + "issuer" : "aa{}" + }} + }} + }})", + ACCOUNT + ), + "malformedRequest", + "Malformed request."}, + + ParamTestCaseBundle{ + "MissingAMMAssetIssuerForNonXRP", + fmt::format( + R"({{ + "amm": + {{ + "asset2": + {{ + "currency":"JPY" + }}, + "asset": + {{ + "currency" : "USD", + "issuer" : "{}" + }} + }} + }})", + ACCOUNT + ), + "malformedRequest", + "Malformed request."}, + + ParamTestCaseBundle{ + "AMMAssetHasIssuerForXRP", + fmt::format( + R"({{ + "amm": + {{ + "asset2": + {{ + "currency":"XRP", + "issuer":"{}" + }}, + "asset": + {{ + "currency" : "USD", + "issuer" : "{}" + }} + }} + }})", + ACCOUNT, + ACCOUNT + ), + "malformedRequest", + "Malformed request."}, + + ParamTestCaseBundle{ + "MissingAMMAssetCurrency", + fmt::format( + R"({{ + "amm": + {{ + "asset2": + {{ + "currency":"XRP" + }}, + "asset": + {{ + "issuer" : "{}" + }} + }} + }})", + ACCOUNT + ), + "malformedRequest", + "Malformed request."}, }; } @@ -917,7 +1156,38 @@ generateTestValuesForNormalPathTest() ripple::keylet::offer(account1, 2).key, CreateOfferLedgerObject( ACCOUNT, 100, 200, "USD", "XRP", ACCOUNT2, ripple::toBase58(ripple::xrpAccount()), INDEX1 - )}}; + )}, + NormalPathTestBundle{ + "AMMViaIndex", + fmt::format( + R"({{ + "binary": true, + "amm": "{}" + }})", + INDEX1 + ), + ripple::uint256{INDEX1}, + CreateAMMObject(ACCOUNT, "XRP", ripple::toBase58(ripple::xrpAccount()), "JPY", ACCOUNT2)}, + NormalPathTestBundle{ + "AMMViaJson", + fmt::format( + R"({{ + "binary": true, + "amm": {{ + "asset": {{ + "currency": "XRP" + }}, + "asset2": {{ + "currency": "{}", + "issuer": "{}" + }} + }} + }})", + "JPY", + ACCOUNT2 + ), + ripple::keylet::amm(GetIssue("XRP", ripple::toBase58(ripple::xrpAccount())), GetIssue("JPY", ACCOUNT2)).key, + CreateAMMObject(ACCOUNT, "XRP", ripple::toBase58(ripple::xrpAccount()), "JPY", ACCOUNT2)}}; } INSTANTIATE_TEST_CASE_P( diff --git a/unittests/util/TestObject.cpp b/unittests/util/TestObject.cpp index 2bcae21f..7a558a03 100644 --- a/unittests/util/TestObject.cpp +++ b/unittests/util/TestObject.cpp @@ -27,6 +27,7 @@ #include constexpr static auto INDEX1 = "1B8590C01B0006EDFA9ED60296DD052DC5E90F99659B25014D08E1BC983515BC"; +constexpr static auto CURRENCY = "03930D02208264E2E40EC1B0C09E4DB96EE197B1"; ripple::AccountID GetAccountIDWithString(std::string_view id) @@ -749,3 +750,27 @@ CreateAmendmentsObject(std::vector const& enabledAmendments) amendments.setFieldV256(ripple::sfAmendments, list); return amendments; } + +ripple::STObject +CreateAMMObject( + std::string_view accountId, + std::string_view assetCurrency, + std::string_view assetIssuer, + std::string_view asset2Currency, + std::string_view asset2Issuer +) +{ + auto amm = ripple::STObject(ripple::sfLedgerEntry); + amm.setFieldU16(ripple::sfLedgerEntryType, ripple::ltAMM); + amm.setAccountID(ripple::sfAccount, GetAccountIDWithString(accountId)); + amm.setFieldU16(ripple::sfTradingFee, 5); + amm.setFieldU64(ripple::sfOwnerNode, 0); + amm.setFieldIssue(ripple::sfAsset, ripple::STIssue{ripple::sfAsset, GetIssue(assetCurrency, assetIssuer)}); + amm.setFieldIssue(ripple::sfAsset2, ripple::STIssue{ripple::sfAsset2, GetIssue(asset2Currency, asset2Issuer)}); + ripple::Issue const issue1( + ripple::Currency{CURRENCY}, ripple::parseBase58(std::string(accountId)).value() + ); + amm.setFieldAmount(ripple::sfLPTokenBalance, ripple::STAmount(issue1, 100)); + amm.setFieldU32(ripple::sfFlags, 0); + return amm; +} diff --git a/unittests/util/TestObject.h b/unittests/util/TestObject.h index ea21affe..48c83b9f 100644 --- a/unittests/util/TestObject.h +++ b/unittests/util/TestObject.h @@ -277,3 +277,12 @@ CreateCreateNFTOfferTxWithMetadata( [[nodiscard]] ripple::STObject CreateAmendmentsObject(std::vector const& enabledAmendments); + +[[nodiscard]] ripple::STObject +CreateAMMObject( + std::string_view accountId, + std::string_view assetCurrency, + std::string_view assetIssuer, + std::string_view asset2Currency, + std::string_view asset2Issuer +);