diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro index e36a466971..e2725d1fc0 100644 --- a/include/xrpl/protocol/detail/features.macro +++ b/include/xrpl/protocol/detail/features.macro @@ -32,6 +32,7 @@ // If you add an amendment here, then do not forget to increment `numFeatures` // in include/xrpl/protocol/Feature.h. +XRPL_FIX (PriceOracleOrder, Supported::no, VoteBehavior::DefaultNo) XRPL_FIX (MPTDeliveredAmount, Supported::no, VoteBehavior::DefaultNo) XRPL_FIX (AMMClawbackRounding, Supported::no, VoteBehavior::DefaultNo) XRPL_FEATURE(TokenEscrow, Supported::yes, VoteBehavior::DefaultNo) @@ -39,7 +40,7 @@ XRPL_FIX (EnforceNFTokenTrustlineV2, Supported::yes, VoteBehavior::DefaultNo XRPL_FIX (AMMv1_3, Supported::yes, VoteBehavior::DefaultNo) XRPL_FEATURE(PermissionedDEX, Supported::yes, VoteBehavior::DefaultNo) XRPL_FEATURE(Batch, Supported::yes, VoteBehavior::DefaultNo) -XRPL_FEATURE(SingleAssetVault, Supported::no, VoteBehavior::DefaultNo) +XRPL_FEATURE(SingleAssetVault, Supported::no, VoteBehavior::DefaultNo) XRPL_FEATURE(PermissionDelegation, Supported::yes, VoteBehavior::DefaultNo) XRPL_FIX (PayChanCancelAfter, Supported::yes, VoteBehavior::DefaultNo) // Check flags in Credential transactions diff --git a/src/test/app/Oracle_test.cpp b/src/test/app/Oracle_test.cpp index aaa7f9a746..fdd7ad941e 100644 --- a/src/test/app/Oracle_test.cpp +++ b/src/test/app/Oracle_test.cpp @@ -678,6 +678,61 @@ private: oracle.set( UpdateArg{.series = {{"XRP", "USD", 742, 2}}, .fee = baseFee}); } + + for (bool const withFixOrder : {false, true}) + { + // Should be same order as creation + Env env( + *this, + withFixOrder ? testable_amendments() + : testable_amendments() - fixPriceOracleOrder); + auto const baseFee = + static_cast(env.current()->fees().base.drops()); + + auto test = [&](Env& env, DataSeries const& series) { + env.fund(XRP(1'000), owner); + Oracle oracle( + env, {.owner = owner, .series = series, .fee = baseFee}); + BEAST_EXPECT(oracle.exists()); + auto sle = env.le(keylet::oracle(owner, oracle.documentID())); + BEAST_EXPECT( + sle->getFieldArray(sfPriceDataSeries).size() == + series.size()); + + auto const beforeQuoteAssetName1 = + sle->getFieldArray(sfPriceDataSeries)[0] + .getFieldCurrency(sfQuoteAsset) + .getText(); + auto const beforeQuoteAssetName2 = + sle->getFieldArray(sfPriceDataSeries)[1] + .getFieldCurrency(sfQuoteAsset) + .getText(); + + oracle.set(UpdateArg{.series = series, .fee = baseFee}); + sle = env.le(keylet::oracle(owner, oracle.documentID())); + + auto const afterQuoteAssetName1 = + sle->getFieldArray(sfPriceDataSeries)[0] + .getFieldCurrency(sfQuoteAsset) + .getText(); + auto const afterQuoteAssetName2 = + sle->getFieldArray(sfPriceDataSeries)[1] + .getFieldCurrency(sfQuoteAsset) + .getText(); + + if (env.current()->rules().enabled(fixPriceOracleOrder)) + { + BEAST_EXPECT(afterQuoteAssetName1 == beforeQuoteAssetName1); + BEAST_EXPECT(afterQuoteAssetName2 == beforeQuoteAssetName2); + } + else + { + BEAST_EXPECT(afterQuoteAssetName1 != beforeQuoteAssetName1); + BEAST_EXPECT(afterQuoteAssetName2 != beforeQuoteAssetName2); + } + }; + test(env, {{"XRP", "USD", 742, 2}, {"XRP", "EUR", 711, 2}}); + } } void diff --git a/src/test/protocol/STObject_test.cpp b/src/test/protocol/STObject_test.cpp index e02cfeeea4..47a7ab0ad2 100644 --- a/src/test/protocol/STObject_test.cpp +++ b/src/test/protocol/STObject_test.cpp @@ -19,223 +19,11 @@ #include -#include -#include -#include -#include -#include -#include - -#include -#include -#include - namespace ripple { class STObject_test : public beast::unit_test::suite { public: - bool - parseJSONString(std::string const& json, Json::Value& to) - { - Json::Reader reader; - return reader.parse(json, to) && to.isObject(); - } - - void - testParseJSONArrayWithInvalidChildrenObjects() - { - testcase("parse json array invalid children"); - try - { - /* - - STArray/STObject constructs don't really map perfectly to json - arrays/objects. - - STObject is an associative container, mapping fields to value, but - an STObject may also have a Field as its name, stored outside the - associative structure. The name is important, so to maintain - fidelity, it will take TWO json objects to represent them. - - */ - std::string faulty( - "{\"Template\":[{" - "\"ModifiedNode\":{\"Sequence\":1}, " - "\"DeletedNode\":{\"Sequence\":1}" - "}]}"); - - std::unique_ptr so; - Json::Value faultyJson; - bool parsedOK(parseJSONString(faulty, faultyJson)); - unexpected(!parsedOK, "failed to parse"); - STParsedJSONObject parsed("test", faultyJson); - BEAST_EXPECT(!parsed.object); - } - catch (std::runtime_error& e) - { - std::string what(e.what()); - unexpected(what.find("First level children of `Template`") != 0); - } - } - - void - testParseJSONArray() - { - testcase("parse json array"); - std::string const json( - "{\"Template\":[{\"ModifiedNode\":{\"Sequence\":1}}]}"); - - Json::Value jsonObject; - bool parsedOK(parseJSONString(json, jsonObject)); - if (parsedOK) - { - STParsedJSONObject parsed("test", jsonObject); - BEAST_EXPECT(parsed.object); - std::string const& serialized( - to_string(parsed.object->getJson(JsonOptions::none))); - BEAST_EXPECT(serialized == json); - } - else - { - fail("Couldn't parse json: " + json); - } - } - - void - testParseJSONEdgeCases() - { - testcase("parse json object"); - - { - std::string const goodJson(R"({"CloseResolution":19,"Method":250,)" - R"("TransactionResult":"tecFROZEN"})"); - - Json::Value jv; - if (BEAST_EXPECT(parseJSONString(goodJson, jv))) - { - STParsedJSONObject parsed("test", jv); - if (BEAST_EXPECT(parsed.object)) - { - std::string const& serialized( - to_string(parsed.object->getJson(JsonOptions::none))); - BEAST_EXPECT(serialized == goodJson); - } - } - } - - { - std::string const goodJson( - R"({"CloseResolution":19,"Method":"250",)" - R"("TransactionResult":"tecFROZEN"})"); - std::string const expectedJson( - R"({"CloseResolution":19,"Method":250,)" - R"("TransactionResult":"tecFROZEN"})"); - - Json::Value jv; - if (BEAST_EXPECT(parseJSONString(goodJson, jv))) - { - // Integer values are always parsed as int, - // unless they're too big. We want a small uint. - jv["CloseResolution"] = Json::UInt(19); - STParsedJSONObject parsed("test", jv); - if (BEAST_EXPECT(parsed.object)) - { - std::string const& serialized( - to_string(parsed.object->getJson(JsonOptions::none))); - BEAST_EXPECT(serialized == expectedJson); - } - } - } - - { - std::string const json(R"({"CloseResolution":19,"Method":250,)" - R"("TransactionResult":"terQUEUED"})"); - - Json::Value jv; - if (BEAST_EXPECT(parseJSONString(json, jv))) - { - STParsedJSONObject parsed("test", jv); - BEAST_EXPECT(!parsed.object); - BEAST_EXPECT(parsed.error); - BEAST_EXPECT(parsed.error[jss::error] == "invalidParams"); - BEAST_EXPECT( - parsed.error[jss::error_message] == - "Field 'test.TransactionResult' is out of range."); - } - } - - { - std::string const json(R"({"CloseResolution":19,"Method":"pony",)" - R"("TransactionResult":"tesSUCCESS"})"); - - Json::Value jv; - if (BEAST_EXPECT(parseJSONString(json, jv))) - { - STParsedJSONObject parsed("test", jv); - BEAST_EXPECT(!parsed.object); - BEAST_EXPECT(parsed.error); - BEAST_EXPECT(parsed.error[jss::error] == "invalidParams"); - BEAST_EXPECT( - parsed.error[jss::error_message] == - "Field 'test.Method' has bad type."); - } - } - - { - std::string const json( - R"({"CloseResolution":19,"Method":3294967296,)" - R"("TransactionResult":"tesSUCCESS"})"); - - Json::Value jv; - if (BEAST_EXPECT(parseJSONString(json, jv))) - { - STParsedJSONObject parsed("test", jv); - BEAST_EXPECT(!parsed.object); - BEAST_EXPECT(parsed.error); - BEAST_EXPECT(parsed.error[jss::error] == "invalidParams"); - BEAST_EXPECT( - parsed.error[jss::error_message] == - "Field 'test.Method' is out of range."); - } - } - - { - std::string const json(R"({"CloseResolution":-10,"Method":42,)" - R"("TransactionResult":"tesSUCCESS"})"); - - Json::Value jv; - if (BEAST_EXPECT(parseJSONString(json, jv))) - { - STParsedJSONObject parsed("test", jv); - BEAST_EXPECT(!parsed.object); - BEAST_EXPECT(parsed.error); - BEAST_EXPECT(parsed.error[jss::error] == "invalidParams"); - BEAST_EXPECT( - parsed.error[jss::error_message] == - "Field 'test.CloseResolution' is out of range."); - } - } - - { - std::string const json( - R"({"CloseResolution":19,"Method":3.141592653,)" - R"("TransactionResult":"tesSUCCESS"})"); - - Json::Value jv; - if (BEAST_EXPECT(parseJSONString(json, jv))) - { - STParsedJSONObject parsed("test", jv); - BEAST_EXPECT(!parsed.object); - BEAST_EXPECT(parsed.error); - BEAST_EXPECT(parsed.error[jss::error] == "invalidParams"); - BEAST_EXPECT( - parsed.error[jss::error_message] == - "Field 'test.Method' has bad type."); - } - } - } - void testSerialization() { @@ -730,9 +518,6 @@ public: testFields(); testSerialization(); - testParseJSONArray(); - testParseJSONArrayWithInvalidChildrenObjects(); - testParseJSONEdgeCases(); testMalformed(); } }; diff --git a/src/test/protocol/STParsedJSON_test.cpp b/src/test/protocol/STParsedJSON_test.cpp new file mode 100644 index 0000000000..082a4624f2 --- /dev/null +++ b/src/test/protocol/STParsedJSON_test.cpp @@ -0,0 +1,295 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2012, 2013 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 + +#include +#include + +namespace ripple { + +class STParsedJSON_test : public beast::unit_test::suite +{ +public: + bool + parseJSONString(std::string const& json, Json::Value& to) + { + Json::Reader reader; + return reader.parse(json, to) && to.isObject(); + } + + void + testParseJSONArrayWithInvalidChildrenObjects() + { + testcase("parse json array invalid children"); + try + { + /* + + STArray/STObject constructs don't really map perfectly to json + arrays/objects. + + STObject is an associative container, mapping fields to value, but + an STObject may also have a Field as its name, stored outside the + associative structure. The name is important, so to maintain + fidelity, it will take TWO json objects to represent them. + + */ + std::string faulty( + "{\"Template\":[{" + "\"ModifiedNode\":{\"Sequence\":1}, " + "\"DeletedNode\":{\"Sequence\":1}" + "}]}"); + + std::unique_ptr so; + Json::Value faultyJson; + bool parsedOK(parseJSONString(faulty, faultyJson)); + unexpected(!parsedOK, "failed to parse"); + STParsedJSONObject parsed("test", faultyJson); + BEAST_EXPECT(!parsed.object); + } + catch (std::runtime_error& e) + { + std::string what(e.what()); + unexpected(what.find("First level children of `Template`") != 0); + } + } + + void + testParseJSONArray() + { + testcase("parse json array"); + std::string const json( + "{\"Template\":[{\"ModifiedNode\":{\"Sequence\":1}}]}"); + + Json::Value jsonObject; + bool parsedOK(parseJSONString(json, jsonObject)); + if (parsedOK) + { + STParsedJSONObject parsed("test", jsonObject); + BEAST_EXPECT(parsed.object); + std::string const& serialized( + to_string(parsed.object->getJson(JsonOptions::none))); + BEAST_EXPECT(serialized == json); + } + else + { + fail("Couldn't parse json: " + json); + } + } + + void + testParseJSONEdgeCases() + { + testcase("parse json object"); + + { + std::string const goodJson(R"({"CloseResolution":19,"Method":250,)" + R"("TransactionResult":"tecFROZEN"})"); + + Json::Value jv; + if (BEAST_EXPECT(parseJSONString(goodJson, jv))) + { + STParsedJSONObject parsed("test", jv); + if (BEAST_EXPECT(parsed.object)) + { + std::string const& serialized( + to_string(parsed.object->getJson(JsonOptions::none))); + BEAST_EXPECT(serialized == goodJson); + } + } + } + + { + std::string const goodJson( + R"({"CloseResolution":19,"Method":"250",)" + R"("TransactionResult":"tecFROZEN"})"); + std::string const expectedJson( + R"({"CloseResolution":19,"Method":250,)" + R"("TransactionResult":"tecFROZEN"})"); + + Json::Value jv; + if (BEAST_EXPECT(parseJSONString(goodJson, jv))) + { + // Integer values are always parsed as int, + // unless they're too big. We want a small uint. + jv["CloseResolution"] = Json::UInt(19); + STParsedJSONObject parsed("test", jv); + if (BEAST_EXPECT(parsed.object)) + { + std::string const& serialized( + to_string(parsed.object->getJson(JsonOptions::none))); + BEAST_EXPECT(serialized == expectedJson); + } + } + } + + { + std::string const json(R"({"CloseResolution":19,"Method":250,)" + R"("TransactionResult":"terQUEUED"})"); + + Json::Value jv; + if (BEAST_EXPECT(parseJSONString(json, jv))) + { + STParsedJSONObject parsed("test", jv); + BEAST_EXPECT(!parsed.object); + BEAST_EXPECT(parsed.error); + BEAST_EXPECT(parsed.error[jss::error] == "invalidParams"); + BEAST_EXPECT( + parsed.error[jss::error_message] == + "Field 'test.TransactionResult' is out of range."); + } + } + + { + std::string const json(R"({"CloseResolution":19,"Method":"pony",)" + R"("TransactionResult":"tesSUCCESS"})"); + + Json::Value jv; + if (BEAST_EXPECT(parseJSONString(json, jv))) + { + STParsedJSONObject parsed("test", jv); + BEAST_EXPECT(!parsed.object); + BEAST_EXPECT(parsed.error); + BEAST_EXPECT(parsed.error[jss::error] == "invalidParams"); + BEAST_EXPECT( + parsed.error[jss::error_message] == + "Field 'test.Method' has bad type."); + } + } + + { + std::string const json( + R"({"CloseResolution":19,"Method":3294967296,)" + R"("TransactionResult":"tesSUCCESS"})"); + + Json::Value jv; + if (BEAST_EXPECT(parseJSONString(json, jv))) + { + STParsedJSONObject parsed("test", jv); + BEAST_EXPECT(!parsed.object); + BEAST_EXPECT(parsed.error); + BEAST_EXPECT(parsed.error[jss::error] == "invalidParams"); + BEAST_EXPECT( + parsed.error[jss::error_message] == + "Field 'test.Method' is out of range."); + } + } + + { + std::string const json(R"({"CloseResolution":-10,"Method":42,)" + R"("TransactionResult":"tesSUCCESS"})"); + + Json::Value jv; + if (BEAST_EXPECT(parseJSONString(json, jv))) + { + STParsedJSONObject parsed("test", jv); + BEAST_EXPECT(!parsed.object); + BEAST_EXPECT(parsed.error); + BEAST_EXPECT(parsed.error[jss::error] == "invalidParams"); + BEAST_EXPECT( + parsed.error[jss::error_message] == + "Field 'test.CloseResolution' is out of range."); + } + } + + { + std::string const json( + R"({"CloseResolution":19,"Method":3.141592653,)" + R"("TransactionResult":"tesSUCCESS"})"); + + Json::Value jv; + if (BEAST_EXPECT(parseJSONString(json, jv))) + { + STParsedJSONObject parsed("test", jv); + BEAST_EXPECT(!parsed.object); + BEAST_EXPECT(parsed.error); + BEAST_EXPECT(parsed.error[jss::error] == "invalidParams"); + BEAST_EXPECT( + parsed.error[jss::error_message] == + "Field 'test.Method' has bad type."); + } + } + + { + std::string const json(R"({"CloseResolution":19,"Method":250,)" + R"("TransferFee":"65536"})"); + + Json::Value jv; + if (BEAST_EXPECT(parseJSONString(json, jv))) + { + STParsedJSONObject parsed("test", jv); + BEAST_EXPECT(!parsed.object); + BEAST_EXPECT(parsed.error); + BEAST_EXPECT(parsed.error[jss::error] == "invalidParams"); + BEAST_EXPECT( + parsed.error[jss::error_message] == + "Field 'test.TransferFee' has invalid data."); + } + } + + { + std::string const json(R"({"CloseResolution":19,"Method":250,)" + R"("TransferFee":"Payment"})"); + + Json::Value jv; + if (BEAST_EXPECT(parseJSONString(json, jv))) + { + STParsedJSONObject parsed("test", jv); + BEAST_EXPECT(!parsed.object); + BEAST_EXPECT(parsed.error); + BEAST_EXPECT(parsed.error[jss::error] == "invalidParams"); + BEAST_EXPECT( + parsed.error[jss::error_message] == + "Field 'test.TransferFee' has invalid data."); + } + } + + { + std::string const json(R"({"CloseResolution":19,"Method":250,)" + R"("TransferFee":true})"); + + Json::Value jv; + if (BEAST_EXPECT(parseJSONString(json, jv))) + { + STParsedJSONObject parsed("test", jv); + BEAST_EXPECT(!parsed.object); + BEAST_EXPECT(parsed.error); + BEAST_EXPECT(parsed.error[jss::error] == "invalidParams"); + BEAST_EXPECT( + parsed.error[jss::error_message] == + "Field 'test.TransferFee' has bad type."); + } + } + } + + void + run() override + { + // Instantiate a jtx::Env so debugLog writes are exercised. + test::jtx::Env env(*this); + testParseJSONArrayWithInvalidChildrenObjects(); + testParseJSONArray(); + testParseJSONEdgeCases(); + } +}; + +BEAST_DEFINE_TESTSUITE(STParsedJSON, protocol, ripple); + +} // namespace ripple diff --git a/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp b/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp index f9dbb0995f..521b81944e 100644 --- a/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp +++ b/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp @@ -178,126 +178,18 @@ MPTokenAuthorize::createMPToken( return tesSUCCESS; } -TER -MPTokenAuthorize::authorize( - ApplyView& view, - beast::Journal journal, - MPTAuthorizeArgs const& args) -{ - auto const sleAcct = view.peek(keylet::account(args.account)); - if (!sleAcct) - return tecINTERNAL; - - // If the account that submitted the tx is a holder - // Note: `account_` is holder's account - // `holderID` is NOT used - if (!args.holderID) - { - // When a holder wants to unauthorize/delete a MPT, the ledger must - // - delete mptokenKey from owner directory - // - delete the MPToken - if (args.flags & tfMPTUnauthorize) - { - auto const mptokenKey = - keylet::mptoken(args.mptIssuanceID, args.account); - auto const sleMpt = view.peek(mptokenKey); - if (!sleMpt || (*sleMpt)[sfMPTAmount] != 0) - return tecINTERNAL; // LCOV_EXCL_LINE - - if (!view.dirRemove( - keylet::ownerDir(args.account), - (*sleMpt)[sfOwnerNode], - sleMpt->key(), - false)) - return tecINTERNAL; // LCOV_EXCL_LINE - - adjustOwnerCount(view, sleAcct, -1, journal); - - view.erase(sleMpt); - return tesSUCCESS; - } - - // A potential holder wants to authorize/hold a mpt, the ledger must: - // - add the new mptokenKey to the owner directory - // - create the MPToken object for the holder - - // The reserve that is required to create the MPToken. Note - // that although the reserve increases with every item - // an account owns, in the case of MPTokens we only - // *enforce* a reserve if the user owns more than two - // items. This is similar to the reserve requirements of trust lines. - std::uint32_t const uOwnerCount = sleAcct->getFieldU32(sfOwnerCount); - XRPAmount const reserveCreate( - (uOwnerCount < 2) ? XRPAmount(beast::zero) - : view.fees().accountReserve(uOwnerCount + 1)); - - if (args.priorBalance < reserveCreate) - return tecINSUFFICIENT_RESERVE; - - auto const mptokenKey = - keylet::mptoken(args.mptIssuanceID, args.account); - auto mptoken = std::make_shared(mptokenKey); - if (auto ter = dirLink(view, args.account, mptoken)) - return ter; // LCOV_EXCL_LINE - - (*mptoken)[sfAccount] = args.account; - (*mptoken)[sfMPTokenIssuanceID] = args.mptIssuanceID; - (*mptoken)[sfFlags] = 0; - view.insert(mptoken); - - // Update owner count. - adjustOwnerCount(view, sleAcct, 1, journal); - - return tesSUCCESS; - } - - auto const sleMptIssuance = - view.read(keylet::mptIssuance(args.mptIssuanceID)); - if (!sleMptIssuance) - return tecINTERNAL; - - // If the account that submitted this tx is the issuer of the MPT - // Note: `account_` is issuer's account - // `holderID` is holder's account - if (args.account != (*sleMptIssuance)[sfIssuer]) - return tecINTERNAL; - - auto const sleMpt = - view.peek(keylet::mptoken(args.mptIssuanceID, *args.holderID)); - if (!sleMpt) - return tecINTERNAL; - - std::uint32_t const flagsIn = sleMpt->getFieldU32(sfFlags); - std::uint32_t flagsOut = flagsIn; - - // Issuer wants to unauthorize the holder, unset lsfMPTAuthorized on - // their MPToken - if (args.flags & tfMPTUnauthorize) - flagsOut &= ~lsfMPTAuthorized; - // Issuer wants to authorize a holder, set lsfMPTAuthorized on their - // MPToken - else - flagsOut |= lsfMPTAuthorized; - - if (flagsIn != flagsOut) - sleMpt->setFieldU32(sfFlags, flagsOut); - - view.update(sleMpt); - return tesSUCCESS; -} - TER MPTokenAuthorize::doApply() { auto const& tx = ctx_.tx; - return authorize( + return authorizeMPToken( ctx_.view(), + mPriorBalance, + tx[sfMPTokenIssuanceID], + account_, ctx_.journal, - {.priorBalance = mPriorBalance, - .mptIssuanceID = tx[sfMPTokenIssuanceID], - .account = account_, - .flags = tx.getFlags(), - .holderID = tx[~sfHolder]}); + tx.getFlags(), + tx[~sfHolder]); } } // namespace ripple diff --git a/src/xrpld/app/tx/detail/MPTokenAuthorize.h b/src/xrpld/app/tx/detail/MPTokenAuthorize.h index 5c386db530..60419e1827 100644 --- a/src/xrpld/app/tx/detail/MPTokenAuthorize.h +++ b/src/xrpld/app/tx/detail/MPTokenAuthorize.h @@ -54,12 +54,6 @@ public: static TER preclaim(PreclaimContext const& ctx); - static TER - authorize( - ApplyView& view, - beast::Journal journal, - MPTAuthorizeArgs const& args); - static TER createMPToken( ApplyView& view, diff --git a/src/xrpld/app/tx/detail/SetOracle.cpp b/src/xrpld/app/tx/detail/SetOracle.cpp index 411f4b6e49..98c4530fba 100644 --- a/src/xrpld/app/tx/detail/SetOracle.cpp +++ b/src/xrpld/app/tx/detail/SetOracle.cpp @@ -206,6 +206,17 @@ SetOracle::doApply() { auto const oracleID = keylet::oracle(account_, ctx_.tx[sfOracleDocumentID]); + auto populatePriceData = [](STObject& priceData, STObject const& entry) { + setPriceDataInnerObjTemplate(priceData); + priceData.setFieldCurrency( + sfBaseAsset, entry.getFieldCurrency(sfBaseAsset)); + priceData.setFieldCurrency( + sfQuoteAsset, entry.getFieldCurrency(sfQuoteAsset)); + priceData.setFieldU64(sfAssetPrice, entry.getFieldU64(sfAssetPrice)); + if (entry.isFieldPresent(sfScale)) + priceData.setFieldU8(sfScale, entry.getFieldU8(sfScale)); + }; + if (auto sle = ctx_.view().peek(oracleID)) { // update @@ -246,15 +257,7 @@ SetOracle::doApply() { // add a token pair with the price STObject priceData{sfPriceData}; - setPriceDataInnerObjTemplate(priceData); - priceData.setFieldCurrency( - sfBaseAsset, entry.getFieldCurrency(sfBaseAsset)); - priceData.setFieldCurrency( - sfQuoteAsset, entry.getFieldCurrency(sfQuoteAsset)); - priceData.setFieldU64( - sfAssetPrice, entry.getFieldU64(sfAssetPrice)); - if (entry.isFieldPresent(sfScale)) - priceData.setFieldU8(sfScale, entry.getFieldU8(sfScale)); + populatePriceData(priceData, entry); pairs.emplace(key, std::move(priceData)); } } @@ -282,7 +285,26 @@ SetOracle::doApply() sle->setFieldVL(sfProvider, ctx_.tx[sfProvider]); if (ctx_.tx.isFieldPresent(sfURI)) sle->setFieldVL(sfURI, ctx_.tx[sfURI]); - auto const& series = ctx_.tx.getFieldArray(sfPriceDataSeries); + + STArray series; + if (!ctx_.view().rules().enabled(fixPriceOracleOrder)) + { + series = ctx_.tx.getFieldArray(sfPriceDataSeries); + } + else + { + std::map, STObject> pairs; + for (auto const& entry : ctx_.tx.getFieldArray(sfPriceDataSeries)) + { + auto const key = tokenPairKey(entry); + STObject priceData{sfPriceData}; + populatePriceData(priceData, entry); + pairs.emplace(key, std::move(priceData)); + } + for (auto const& iter : pairs) + series.push_back(std::move(iter.second)); + } + sle->setFieldArray(sfPriceDataSeries, series); sle->setFieldVL(sfAssetClass, ctx_.tx[sfAssetClass]); sle->setFieldU32(sfLastUpdateTime, ctx_.tx[sfLastUpdateTime]); diff --git a/src/xrpld/app/tx/detail/VaultDeposit.cpp b/src/xrpld/app/tx/detail/VaultDeposit.cpp index 162a314ff2..bd39731067 100644 --- a/src/xrpld/app/tx/detail/VaultDeposit.cpp +++ b/src/xrpld/app/tx/detail/VaultDeposit.cpp @@ -207,12 +207,12 @@ VaultDeposit::doApply() auto sleMpt = view().read(keylet::mptoken(mptIssuanceID, account_)); if (!sleMpt) { - if (auto const err = MPTokenAuthorize::authorize( + if (auto const err = authorizeMPToken( view(), - ctx_.journal, - {.priorBalance = mPriorBalance, - .mptIssuanceID = mptIssuanceID->value(), - .account = account_}); + mPriorBalance, + mptIssuanceID->value(), + account_, + ctx_.journal); !isTesSuccess(err)) return err; } @@ -220,15 +220,15 @@ VaultDeposit::doApply() // If the vault is private, set the authorized flag for the vault owner if (vault->isFlag(tfVaultPrivate)) { - if (auto const err = MPTokenAuthorize::authorize( + if (auto const err = authorizeMPToken( view(), + mPriorBalance, // priorBalance + mptIssuanceID->value(), // mptIssuanceID + sleIssuance->at(sfIssuer), // account ctx_.journal, - { - .priorBalance = mPriorBalance, - .mptIssuanceID = mptIssuanceID->value(), - .account = sleIssuance->at(sfIssuer), - .holderID = account_, - }); + {}, // flags + account_ // holderID + ); !isTesSuccess(err)) return err; } diff --git a/src/xrpld/ledger/View.h b/src/xrpld/ledger/View.h index 44b3e845f1..80dfeb3cd2 100644 --- a/src/xrpld/ledger/View.h +++ b/src/xrpld/ledger/View.h @@ -616,6 +616,16 @@ addEmptyHolding( asset.value()); } +[[nodiscard]] TER +authorizeMPToken( + ApplyView& view, + XRPAmount const& priorBalance, + MPTID const& mptIssuanceID, + AccountID const& account, + beast::Journal journal, + std::uint32_t flags = 0, + std::optional holderID = std::nullopt); + // VFALCO NOTE Both STAmount parameters should just // be "Amount", a unit-less number. // diff --git a/src/xrpld/ledger/detail/View.cpp b/src/xrpld/ledger/detail/View.cpp index 3c1fb350f5..4348579325 100644 --- a/src/xrpld/ledger/detail/View.cpp +++ b/src/xrpld/ledger/detail/View.cpp @@ -18,7 +18,6 @@ //============================================================================== #include -#include #include #include @@ -1272,12 +1271,115 @@ addEmptyHolding( if (view.peek(keylet::mptoken(mptID, accountID))) return tecDUPLICATE; - return MPTokenAuthorize::authorize( - view, - journal, - {.priorBalance = priorBalance, - .mptIssuanceID = mptID, - .account = accountID}); + return authorizeMPToken(view, priorBalance, mptID, accountID, journal); +} + +[[nodiscard]] TER +authorizeMPToken( + ApplyView& view, + XRPAmount const& priorBalance, + MPTID const& mptIssuanceID, + AccountID const& account, + beast::Journal journal, + std::uint32_t flags, + std::optional holderID) +{ + auto const sleAcct = view.peek(keylet::account(account)); + if (!sleAcct) + return tecINTERNAL; + + // If the account that submitted the tx is a holder + // Note: `account_` is holder's account + // `holderID` is NOT used + if (!holderID) + { + // When a holder wants to unauthorize/delete a MPT, the ledger must + // - delete mptokenKey from owner directory + // - delete the MPToken + if (flags & tfMPTUnauthorize) + { + auto const mptokenKey = keylet::mptoken(mptIssuanceID, account); + auto const sleMpt = view.peek(mptokenKey); + if (!sleMpt || (*sleMpt)[sfMPTAmount] != 0) + return tecINTERNAL; // LCOV_EXCL_LINE + + if (!view.dirRemove( + keylet::ownerDir(account), + (*sleMpt)[sfOwnerNode], + sleMpt->key(), + false)) + return tecINTERNAL; // LCOV_EXCL_LINE + + adjustOwnerCount(view, sleAcct, -1, journal); + + view.erase(sleMpt); + return tesSUCCESS; + } + + // A potential holder wants to authorize/hold a mpt, the ledger must: + // - add the new mptokenKey to the owner directory + // - create the MPToken object for the holder + + // The reserve that is required to create the MPToken. Note + // that although the reserve increases with every item + // an account owns, in the case of MPTokens we only + // *enforce* a reserve if the user owns more than two + // items. This is similar to the reserve requirements of trust lines. + std::uint32_t const uOwnerCount = sleAcct->getFieldU32(sfOwnerCount); + XRPAmount const reserveCreate( + (uOwnerCount < 2) ? XRPAmount(beast::zero) + : view.fees().accountReserve(uOwnerCount + 1)); + + if (priorBalance < reserveCreate) + return tecINSUFFICIENT_RESERVE; + + auto const mptokenKey = keylet::mptoken(mptIssuanceID, account); + auto mptoken = std::make_shared(mptokenKey); + if (auto ter = dirLink(view, account, mptoken)) + return ter; // LCOV_EXCL_LINE + + (*mptoken)[sfAccount] = account; + (*mptoken)[sfMPTokenIssuanceID] = mptIssuanceID; + (*mptoken)[sfFlags] = 0; + view.insert(mptoken); + + // Update owner count. + adjustOwnerCount(view, sleAcct, 1, journal); + + return tesSUCCESS; + } + + auto const sleMptIssuance = view.read(keylet::mptIssuance(mptIssuanceID)); + if (!sleMptIssuance) + return tecINTERNAL; + + // If the account that submitted this tx is the issuer of the MPT + // Note: `account_` is issuer's account + // `holderID` is holder's account + if (account != (*sleMptIssuance)[sfIssuer]) + return tecINTERNAL; + + auto const sleMpt = view.peek(keylet::mptoken(mptIssuanceID, *holderID)); + if (!sleMpt) + return tecINTERNAL; + + std::uint32_t const flagsIn = sleMpt->getFieldU32(sfFlags); + std::uint32_t flagsOut = flagsIn; + + // Issuer wants to unauthorize the holder, unset lsfMPTAuthorized on + // their MPToken + if (flags & tfMPTUnauthorize) + flagsOut &= ~lsfMPTAuthorized; + // Issuer wants to authorize a holder, set lsfMPTAuthorized on their + // MPToken + else + flagsOut |= lsfMPTAuthorized; + + if (flagsIn != flagsOut) + sleMpt->setFieldU32(sfFlags, flagsOut); + + view.update(sleMpt); + return tesSUCCESS; } TER @@ -1475,13 +1577,14 @@ removeEmptyHolding( if (mptoken->at(sfMPTAmount) != 0) return tecHAS_OBLIGATIONS; - return MPTokenAuthorize::authorize( + return authorizeMPToken( view, + {}, // priorBalance + mptID, + accountID, journal, - {.priorBalance = {}, - .mptIssuanceID = mptID, - .account = accountID, - .flags = tfMPTUnauthorize}); + tfMPTUnauthorize // flags + ); } TER @@ -2554,15 +2657,12 @@ enforceMPTokenAuthorization( XRPL_ASSERT( maybeDomainID.has_value() && sleToken == nullptr, "ripple::enforceMPTokenAuthorization : new MPToken for domain"); - if (auto const err = MPTokenAuthorize::authorize( + if (auto const err = authorizeMPToken( view, - j, - { - .priorBalance = priorBalance, - .mptIssuanceID = mptIssuanceID, - .account = account, - .flags = 0, - }); + priorBalance, // priorBalance + mptIssuanceID, // mptIssuanceID + account, // account + j); !isTesSuccess(err)) return err;