mirror of
https://github.com/XRPLF/rippled.git
synced 2025-12-06 17:27:55 +00:00
Merge branch 'ximinez/lending-refactoring-2' into ximinez/lending-refactoring-3
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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<int>(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
|
||||
|
||||
@@ -19,223 +19,11 @@
|
||||
|
||||
#include <test/jtx.h>
|
||||
|
||||
#include <xrpl/beast/unit_test.h>
|
||||
#include <xrpl/json/json_reader.h>
|
||||
#include <xrpl/json/to_string.h>
|
||||
#include <xrpl/protocol/SecretKey.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
#include <xrpl/protocol/st.h>
|
||||
|
||||
#include <array>
|
||||
#include <memory>
|
||||
#include <type_traits>
|
||||
|
||||
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<STObject> 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();
|
||||
}
|
||||
};
|
||||
|
||||
295
src/test/protocol/STParsedJSON_test.cpp
Normal file
295
src/test/protocol/STParsedJSON_test.cpp
Normal file
@@ -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 <test/jtx.h>
|
||||
|
||||
#include <xrpl/json/json_reader.h>
|
||||
#include <xrpl/protocol/st.h>
|
||||
|
||||
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<STObject> 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
|
||||
@@ -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<SLE>(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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<std::pair<Currency, Currency>, 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]);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<AccountID> holderID = std::nullopt);
|
||||
|
||||
// VFALCO NOTE Both STAmount parameters should just
|
||||
// be "Amount", a unit-less number.
|
||||
//
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
//==============================================================================
|
||||
|
||||
#include <xrpld/app/misc/CredentialHelpers.h>
|
||||
#include <xrpld/app/tx/detail/MPTokenAuthorize.h>
|
||||
#include <xrpld/ledger/ReadView.h>
|
||||
#include <xrpld/ledger/View.h>
|
||||
|
||||
@@ -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<AccountID> 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<SLE>(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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user