From 524821c0b04a3ea7150560142b57f6c93426edba Mon Sep 17 00:00:00 2001 From: Alex Kremer Date: Tue, 4 Jul 2023 15:39:34 +0100 Subject: [PATCH] Add strict field support (#731) Fixes #729 --- src/rpc/handlers/AccountCurrencies.h | 3 +- src/rpc/handlers/AccountInfo.h | 2 + src/rpc/handlers/AccountOffers.h | 3 +- src/rpc/handlers/AccountTx.h | 1 - src/rpc/handlers/GatewayBalances.h | 3 +- src/rpc/handlers/NFTHistory.h | 1 - src/rpc/handlers/Tx.h | 1 - .../rpc/handlers/AccountCurrenciesTest.cpp | 112 +++++++++++++++++- unittests/rpc/handlers/AccountInfoTest.cpp | 71 +++++++++++ unittests/rpc/handlers/AccountOffersTest.cpp | 108 +++++++++++++++++ .../rpc/handlers/GatewayBalancesTest.cpp | 91 +++++++++++++- 11 files changed, 388 insertions(+), 8 deletions(-) diff --git a/src/rpc/handlers/AccountCurrencies.h b/src/rpc/handlers/AccountCurrencies.h index ddc213a6..83fddfe1 100644 --- a/src/rpc/handlers/AccountCurrencies.h +++ b/src/rpc/handlers/AccountCurrencies.h @@ -50,7 +50,7 @@ public: bool validated = true; }; - // TODO: We did not implement the "strict" field (can't be implemented?) + // Note: clio only supports XRP Ledger addresses (i.e. `strict` is unsupported for `false`) struct Input { std::string account; @@ -72,6 +72,7 @@ public: {JS(account), validation::Required{}, validation::AccountValidator}, {JS(ledger_hash), validation::Uint256HexStringValidator}, {JS(ledger_index), validation::LedgerIndexValidator}, + {JS(strict), validation::IfType{validation::NotSupported{false}}}, }; return rpcSpec; diff --git a/src/rpc/handlers/AccountInfo.h b/src/rpc/handlers/AccountInfo.h index 7b529404..fb2078fe 100644 --- a/src/rpc/handlers/AccountInfo.h +++ b/src/rpc/handlers/AccountInfo.h @@ -65,6 +65,7 @@ public: // "queue" is not available in Reporting mode // "ident" is deprecated, keep it for now, in line with rippled + // Note: clio only supports XRP Ledger addresses (i.e. `strict` is unsupported for `false`) struct Input { std::optional account; @@ -89,6 +90,7 @@ public: {JS(ledger_hash), validation::Uint256HexStringValidator}, {JS(ledger_index), validation::LedgerIndexValidator}, {JS(signer_lists), validation::Type{}}, + {JS(strict), validation::IfType{validation::NotSupported{false}}}, }; return rpcSpec; diff --git a/src/rpc/handlers/AccountOffers.h b/src/rpc/handlers/AccountOffers.h index 9231ad57..ee26ed8b 100644 --- a/src/rpc/handlers/AccountOffers.h +++ b/src/rpc/handlers/AccountOffers.h @@ -57,7 +57,7 @@ public: bool validated = true; }; - // TODO: We did not implement the "strict" field + // Note: clio only supports XRP Ledger addresses (i.e. `strict` is unsupported for `false`) struct Input { std::string account; @@ -83,6 +83,7 @@ public: {JS(ledger_index), validation::LedgerIndexValidator}, {JS(marker), validation::AccountMarkerValidator}, {JS(limit), validation::Type{}, validation::Between{10, 400}}, + {JS(strict), validation::IfType{validation::NotSupported{false}}}, }; return rpcSpec; diff --git a/src/rpc/handlers/AccountTx.h b/src/rpc/handlers/AccountTx.h index c1b9726b..a467e4f7 100644 --- a/src/rpc/handlers/AccountTx.h +++ b/src/rpc/handlers/AccountTx.h @@ -57,7 +57,6 @@ public: bool validated = true; }; - // TODO:we did not implement the "strict" field struct Input { std::string account; diff --git a/src/rpc/handlers/GatewayBalances.h b/src/rpc/handlers/GatewayBalances.h index 767a68f8..83b7096e 100644 --- a/src/rpc/handlers/GatewayBalances.h +++ b/src/rpc/handlers/GatewayBalances.h @@ -51,7 +51,7 @@ public: bool validated = true; }; - // TODO:we did not implement the "strict" field + // Note: clio only supports XRP Ledger addresses (i.e. `strict` is unsupported for `false`) struct Input { std::string account; @@ -106,6 +106,7 @@ public: {JS(ledger_hash), validation::Uint256HexStringValidator}, {JS(ledger_index), validation::LedgerIndexValidator}, {JS(hotwallet), hotWalletValidator}, + {JS(strict), validation::IfType{validation::NotSupported{false}}}, }; return rpcSpec; diff --git a/src/rpc/handlers/NFTHistory.h b/src/rpc/handlers/NFTHistory.h index e900b881..7a1514ba 100644 --- a/src/rpc/handlers/NFTHistory.h +++ b/src/rpc/handlers/NFTHistory.h @@ -58,7 +58,6 @@ public: bool validated = true; }; - // TODO: We did not implement the "strict" field struct Input { std::string nftID; diff --git a/src/rpc/handlers/Tx.h b/src/rpc/handlers/Tx.h index d4012199..1376c405 100644 --- a/src/rpc/handlers/Tx.h +++ b/src/rpc/handlers/Tx.h @@ -48,7 +48,6 @@ public: bool validated = true; }; - // TODO: we did not implement the "strict" field struct Input { std::string transaction; diff --git a/unittests/rpc/handlers/AccountCurrenciesTest.cpp b/unittests/rpc/handlers/AccountCurrenciesTest.cpp index 56a04df9..046c22a1 100644 --- a/unittests/rpc/handlers/AccountCurrenciesTest.cpp +++ b/unittests/rpc/handlers/AccountCurrenciesTest.cpp @@ -40,7 +40,31 @@ class RPCAccountCurrenciesHandlerTest : public HandlerBaseTest { }; -TEST_F(RPCAccountCurrenciesHandlerTest, AccountNotExsit) +TEST_F(RPCAccountCurrenciesHandlerTest, StrictSetToFalseUnsupported) +{ + mockBackendPtr->updateRange(10); // min + mockBackendPtr->updateRange(30); // max + + auto const static input = boost::json::parse(fmt::format( + R"({{ + "account": "{}", + "strict": false + }})", + ACCOUNT)); + + auto const handler = AnyHandler{AccountCurrenciesHandler{mockBackendPtr}}; + + runSpawn([&](auto& yield) { + auto const output = handler.process(input, Context{std::ref(yield)}); + ASSERT_FALSE(output); + + auto const err = RPC::makeError(output.error()); + EXPECT_EQ(err.at("error").as_string(), "notSupported"); + EXPECT_EQ(err.at("error_message").as_string(), "Not supported field 'strict's value 'false'"); + }); +} + +TEST_F(RPCAccountCurrenciesHandlerTest, AccountNotExist) { auto const rawBackendPtr = static_cast(mockBackendPtr.get()); mockBackendPtr->updateRange(10); // min @@ -290,3 +314,89 @@ TEST_F(RPCAccountCurrenciesHandlerTest, RequestViaLegderSeq) EXPECT_EQ((*output).as_object().at("ledger_index").as_uint64(), ledgerSeq); }); } + +TEST_F(RPCAccountCurrenciesHandlerTest, RequestViaLegderSeqWithStrictTrue) +{ + auto const rawBackendPtr = static_cast(mockBackendPtr.get()); + mockBackendPtr->updateRange(10); // min + mockBackendPtr->updateRange(30); // max + auto const ledgerSeq = 29; + // return valid ledgerinfo + auto const ledgerinfo = CreateLedgerInfo(LEDGERHASH, ledgerSeq); + EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence).Times(1); + ON_CALL(*rawBackendPtr, fetchLedgerBySequence(ledgerSeq, _)).WillByDefault(Return(ledgerinfo)); + // return valid account + auto const accountKk = ripple::keylet::account(GetAccountIDWithString(ACCOUNT)).key; + ON_CALL(*rawBackendPtr, doFetchLedgerObject(accountKk, ledgerSeq, _)) + .WillByDefault(Return(Blob{'f', 'a', 'k', 'e'})); + + auto const ownerDir = CreateOwnerDirLedgerObject({ripple::uint256{INDEX1}}, INDEX1); + auto const ownerDirKk = ripple::keylet::ownerDir(GetAccountIDWithString(ACCOUNT)).key; + ON_CALL(*rawBackendPtr, doFetchLedgerObject(ownerDirKk, ledgerSeq, _)) + .WillByDefault(Return(ownerDir.getSerializer().peekData())); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(2); + std::vector bbs; + auto const line1 = + CreateRippleStateLedgerObject(ACCOUNT, "USD", ISSUER, 100, ACCOUNT, 10, ACCOUNT2, 20, TXNID, 123, 0); + bbs.push_back(line1.getSerializer().peekData()); + + ON_CALL(*rawBackendPtr, doFetchLedgerObjects).WillByDefault(Return(bbs)); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObjects).Times(1); + auto const static input = boost::json::parse(fmt::format( + R"({{ + "account": "{}", + "ledger_index": {}, + "strict": true + }})", + ACCOUNT, + ledgerSeq)); + auto const handler = AnyHandler{AccountCurrenciesHandler{mockBackendPtr}}; + runSpawn([&](auto& yield) { + auto const output = handler.process(input, Context{std::ref(yield)}); + ASSERT_TRUE(output); + EXPECT_EQ((*output).as_object().at("ledger_index").as_uint64(), ledgerSeq); + }); +} + +TEST_F(RPCAccountCurrenciesHandlerTest, RequestViaLegderSeqWithStrictOfInvalidType) +{ + auto const rawBackendPtr = static_cast(mockBackendPtr.get()); + mockBackendPtr->updateRange(10); // min + mockBackendPtr->updateRange(30); // max + auto const ledgerSeq = 29; + // return valid ledgerinfo + auto const ledgerinfo = CreateLedgerInfo(LEDGERHASH, ledgerSeq); + EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence).Times(1); + ON_CALL(*rawBackendPtr, fetchLedgerBySequence(ledgerSeq, _)).WillByDefault(Return(ledgerinfo)); + // return valid account + auto const accountKk = ripple::keylet::account(GetAccountIDWithString(ACCOUNT)).key; + ON_CALL(*rawBackendPtr, doFetchLedgerObject(accountKk, ledgerSeq, _)) + .WillByDefault(Return(Blob{'f', 'a', 'k', 'e'})); + + auto const ownerDir = CreateOwnerDirLedgerObject({ripple::uint256{INDEX1}}, INDEX1); + auto const ownerDirKk = ripple::keylet::ownerDir(GetAccountIDWithString(ACCOUNT)).key; + ON_CALL(*rawBackendPtr, doFetchLedgerObject(ownerDirKk, ledgerSeq, _)) + .WillByDefault(Return(ownerDir.getSerializer().peekData())); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(2); + std::vector bbs; + auto const line1 = + CreateRippleStateLedgerObject(ACCOUNT, "USD", ISSUER, 100, ACCOUNT, 10, ACCOUNT2, 20, TXNID, 123, 0); + bbs.push_back(line1.getSerializer().peekData()); + + ON_CALL(*rawBackendPtr, doFetchLedgerObjects).WillByDefault(Return(bbs)); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObjects).Times(1); + auto const static input = boost::json::parse(fmt::format( + R"({{ + "account": "{}", + "ledger_index": {}, + "strict": "test" + }})", + ACCOUNT, + ledgerSeq)); + auto const handler = AnyHandler{AccountCurrenciesHandler{mockBackendPtr}}; + runSpawn([&](auto& yield) { + auto const output = handler.process(input, Context{std::ref(yield)}); + ASSERT_TRUE(output); + EXPECT_EQ((*output).as_object().at("ledger_index").as_uint64(), ledgerSeq); + }); +} diff --git a/unittests/rpc/handlers/AccountInfoTest.cpp b/unittests/rpc/handlers/AccountInfoTest.cpp index f56bd8a2..419140be 100644 --- a/unittests/rpc/handlers/AccountInfoTest.cpp +++ b/unittests/rpc/handlers/AccountInfoTest.cpp @@ -91,6 +91,11 @@ generateTestValuesForParametersTest() R"({"ident":"rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", "ledger_index":"a"})", "invalidParams", "ledgerIndexMalformed"}, + AccountInfoParamTestCaseBundle{ + "StrictFieldUnsupportedValue", + R"({"ident": "rLEsXccBGNR3UPuPu2hUXPjziKC3qKSBun", "strict": false})", + "notSupported", + "Not supported field 'strict's value 'false'"}, }; } @@ -368,6 +373,72 @@ TEST_F(RPCAccountInfoHandlerTest, SignerListsTrue) }); } +TEST_F(RPCAccountInfoHandlerTest, StrictTrue) +{ + auto const rawBackendPtr = static_cast(mockBackendPtr.get()); + + mockBackendPtr->updateRange(10); // min + mockBackendPtr->updateRange(30); // max + + auto const ledgerinfo = CreateLedgerInfo(LEDGERHASH, 30); + EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence).Times(1); + ON_CALL(*rawBackendPtr, fetchLedgerBySequence).WillByDefault(Return(ledgerinfo)); + + auto const account = GetAccountIDWithString(ACCOUNT); + auto const accountKk = ripple::keylet::account(account).key; + auto const accountRoot = CreateAccountRootObject(ACCOUNT, 0, 2, 200, 2, INDEX1, 2); + ON_CALL(*rawBackendPtr, doFetchLedgerObject(accountKk, 30, _)) + .WillByDefault(Return(accountRoot.getSerializer().peekData())); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(1); + + auto const static input = boost::json::parse(fmt::format( + R"({{ + "account": "{}", + "strict": true + }})", + ACCOUNT)); + + auto const handler = AnyHandler{AccountInfoHandler{mockBackendPtr}}; + + runSpawn([&](auto& yield) { + auto const output = handler.process(input, Context{std::ref(yield)}); + ASSERT_TRUE(output); + }); +} + +TEST_F(RPCAccountInfoHandlerTest, StrictInvalidTypeHasNoEffect) +{ + auto const rawBackendPtr = static_cast(mockBackendPtr.get()); + + mockBackendPtr->updateRange(10); // min + mockBackendPtr->updateRange(30); // max + + auto const ledgerinfo = CreateLedgerInfo(LEDGERHASH, 30); + EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence).Times(1); + ON_CALL(*rawBackendPtr, fetchLedgerBySequence).WillByDefault(Return(ledgerinfo)); + + auto const account = GetAccountIDWithString(ACCOUNT); + auto const accountKk = ripple::keylet::account(account).key; + auto const accountRoot = CreateAccountRootObject(ACCOUNT, 0, 2, 200, 2, INDEX1, 2); + ON_CALL(*rawBackendPtr, doFetchLedgerObject(accountKk, 30, _)) + .WillByDefault(Return(accountRoot.getSerializer().peekData())); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(1); + + auto const static input = boost::json::parse(fmt::format( + R"({{ + "account": "{}", + "strict": "test" + }})", + ACCOUNT)); + + auto const handler = AnyHandler{AccountInfoHandler{mockBackendPtr}}; + + runSpawn([&](auto& yield) { + auto const output = handler.process(input, Context{std::ref(yield)}); + ASSERT_TRUE(output); + }); +} + TEST_F(RPCAccountInfoHandlerTest, IdentAndSignerListsFalse) { auto const rawBackendPtr = static_cast(mockBackendPtr.get()); diff --git a/unittests/rpc/handlers/AccountOffersTest.cpp b/unittests/rpc/handlers/AccountOffersTest.cpp index ea769248..aa9c28c5 100644 --- a/unittests/rpc/handlers/AccountOffersTest.cpp +++ b/unittests/rpc/handlers/AccountOffersTest.cpp @@ -131,6 +131,12 @@ generateTestValuesForParametersTest() "invalidParams", "Malformed cursor", }, + { + "StrictFieldUnsupportedValue", + R"({"account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "strict": false})", + "notSupported", + "Not supported field 'strict's value 'false'", + }, }; } @@ -337,6 +343,108 @@ TEST_F(RPCAccountOffersHandlerTest, DefaultParams) }); } +TEST_F(RPCAccountOffersHandlerTest, DefaultParamsWithStrictTrue) +{ + auto constexpr ledgerSeq = 30; + auto const rawBackendPtr = static_cast(mockBackendPtr.get()); + + mockBackendPtr->updateRange(10); // min + mockBackendPtr->updateRange(ledgerSeq); // max + + auto const ledgerinfo = CreateLedgerInfo(LEDGERHASH, ledgerSeq); + EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence).Times(1); + + ON_CALL(*rawBackendPtr, fetchLedgerBySequence).WillByDefault(Return(ledgerinfo)); + auto const accountKk = ripple::keylet::account(GetAccountIDWithString(ACCOUNT)).key; + ON_CALL(*rawBackendPtr, doFetchLedgerObject(accountKk, ledgerSeq, _)) + .WillByDefault(Return(Blob{'f', 'a', 'k', 'e'})); + + auto const ownerDir = CreateOwnerDirLedgerObject({ripple::uint256{INDEX1}}, INDEX1); + auto const ownerDirKk = ripple::keylet::ownerDir(GetAccountIDWithString(ACCOUNT)).key; + ON_CALL(*rawBackendPtr, doFetchLedgerObject(ownerDirKk, ledgerSeq, _)) + .WillByDefault(Return(ownerDir.getSerializer().peekData())); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(2); + + std::vector bbs; + auto offer = CreateOfferLedgerObject( + ACCOUNT, + 10, + 20, + ripple::to_string(ripple::to_currency("USD")), + ripple::to_string(ripple::xrpCurrency()), + ACCOUNT2, + toBase58(ripple::xrpAccount()), + INDEX1); + offer.setFieldU32(ripple::sfExpiration, 123); + bbs.push_back(offer.getSerializer().peekData()); + + ON_CALL(*rawBackendPtr, doFetchLedgerObjects).WillByDefault(Return(bbs)); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObjects).Times(1); + + auto const static input = boost::json::parse(fmt::format( + R"({{ + "account": "{}", + "strict": true + }})", + ACCOUNT)); + auto const handler = AnyHandler{AccountOffersHandler{mockBackendPtr}}; + runSpawn([&](auto& yield) { + auto const output = handler.process(input, Context{std::ref(yield)}); + ASSERT_TRUE(output); + }); +} + +TEST_F(RPCAccountOffersHandlerTest, DefaultParamsWithInvalidTypeStrict) +{ + auto constexpr ledgerSeq = 30; + auto const rawBackendPtr = static_cast(mockBackendPtr.get()); + + mockBackendPtr->updateRange(10); // min + mockBackendPtr->updateRange(ledgerSeq); // max + + auto const ledgerinfo = CreateLedgerInfo(LEDGERHASH, ledgerSeq); + EXPECT_CALL(*rawBackendPtr, fetchLedgerBySequence).Times(1); + + ON_CALL(*rawBackendPtr, fetchLedgerBySequence).WillByDefault(Return(ledgerinfo)); + auto const accountKk = ripple::keylet::account(GetAccountIDWithString(ACCOUNT)).key; + ON_CALL(*rawBackendPtr, doFetchLedgerObject(accountKk, ledgerSeq, _)) + .WillByDefault(Return(Blob{'f', 'a', 'k', 'e'})); + + auto const ownerDir = CreateOwnerDirLedgerObject({ripple::uint256{INDEX1}}, INDEX1); + auto const ownerDirKk = ripple::keylet::ownerDir(GetAccountIDWithString(ACCOUNT)).key; + ON_CALL(*rawBackendPtr, doFetchLedgerObject(ownerDirKk, ledgerSeq, _)) + .WillByDefault(Return(ownerDir.getSerializer().peekData())); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObject).Times(2); + + std::vector bbs; + auto offer = CreateOfferLedgerObject( + ACCOUNT, + 10, + 20, + ripple::to_string(ripple::to_currency("USD")), + ripple::to_string(ripple::xrpCurrency()), + ACCOUNT2, + toBase58(ripple::xrpAccount()), + INDEX1); + offer.setFieldU32(ripple::sfExpiration, 123); + bbs.push_back(offer.getSerializer().peekData()); + + ON_CALL(*rawBackendPtr, doFetchLedgerObjects).WillByDefault(Return(bbs)); + EXPECT_CALL(*rawBackendPtr, doFetchLedgerObjects).Times(1); + + auto const static input = boost::json::parse(fmt::format( + R"({{ + "account": "{}", + "strict": "test" + }})", + ACCOUNT)); + auto const handler = AnyHandler{AccountOffersHandler{mockBackendPtr}}; + runSpawn([&](auto& yield) { + auto const output = handler.process(input, Context{std::ref(yield)}); + ASSERT_TRUE(output); + }); +} + TEST_F(RPCAccountOffersHandlerTest, Limit) { auto constexpr ledgerSeq = 30; diff --git a/unittests/rpc/handlers/GatewayBalancesTest.cpp b/unittests/rpc/handlers/GatewayBalancesTest.cpp index 98753795..54cdecae 100644 --- a/unittests/rpc/handlers/GatewayBalancesTest.cpp +++ b/unittests/rpc/handlers/GatewayBalancesTest.cpp @@ -170,6 +170,18 @@ generateParameterTestBundles() ACCOUNT), "invalidParams", "hotwalletMalformed"}, + ParameterTestBundle{ + "StrictFieldUnsupportedValue", + fmt::format( + R"({{ + "account": "{}", + "hotwallet": "{}", + "strict": false + }})", + ACCOUNT, + ACCOUNT2), + "notSupported", + "Not supported field 'strict's value 'false'"}, }; } @@ -607,7 +619,84 @@ generateNormalPathTestBundles() ACCOUNT3, ACCOUNT2, ACCOUNT), - fmt::format(R"("hotwallet": ["{}", "{}"])", ACCOUNT2, ACCOUNT3)}}; + fmt::format(R"("hotwallet": ["{}", "{}"])", ACCOUNT2, ACCOUNT3)}, + NormalTestBundle{ + "StrictTrue", + CreateOwnerDirLedgerObject( + {ripple::uint256{INDEX2}, ripple::uint256{INDEX2}, ripple::uint256{INDEX2}}, INDEX1), + std::vector{ + CreateRippleStateLedgerObject(ACCOUNT, "USD", ISSUER, -10, ACCOUNT, 100, ACCOUNT2, 200, TXNID, 123), + CreateRippleStateLedgerObject(ACCOUNT, "CNY", ISSUER, -20, ACCOUNT, 100, ACCOUNT2, 200, TXNID, 123), + CreateRippleStateLedgerObject(ACCOUNT, "EUR", ISSUER, -30, ACCOUNT, 100, ACCOUNT3, 200, TXNID, 123) + + }, + fmt::format( + R"({{ + "balances": {{ + "{}": [ + {{ + "currency": "EUR", + "value": "30" + }} + ], + "{}": [ + {{ + "currency": "USD", + "value": "10" + }}, + {{ + "currency": "CNY", + "value": "20" + }} + ] + }}, + "account": "{}", + "ledger_index": 300, + "ledger_hash": "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652" + }})", + ACCOUNT3, + ACCOUNT2, + ACCOUNT), + fmt::format(R"("hotwallet": ["{}", "{}"], "strict": true)", ACCOUNT2, ACCOUNT3)}, + NormalTestBundle{ + "StrictInvalidTypeHasNoEffect", + CreateOwnerDirLedgerObject( + {ripple::uint256{INDEX2}, ripple::uint256{INDEX2}, ripple::uint256{INDEX2}}, INDEX1), + std::vector{ + CreateRippleStateLedgerObject(ACCOUNT, "USD", ISSUER, -10, ACCOUNT, 100, ACCOUNT2, 200, TXNID, 123), + CreateRippleStateLedgerObject(ACCOUNT, "CNY", ISSUER, -20, ACCOUNT, 100, ACCOUNT2, 200, TXNID, 123), + CreateRippleStateLedgerObject(ACCOUNT, "EUR", ISSUER, -30, ACCOUNT, 100, ACCOUNT3, 200, TXNID, 123) + + }, + fmt::format( + R"({{ + "balances": {{ + "{}": [ + {{ + "currency": "EUR", + "value": "30" + }} + ], + "{}": [ + {{ + "currency": "USD", + "value": "10" + }}, + {{ + "currency": "CNY", + "value": "20" + }} + ] + }}, + "account": "{}", + "ledger_index": 300, + "ledger_hash": "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A652" + }})", + ACCOUNT3, + ACCOUNT2, + ACCOUNT), + fmt::format(R"("hotwallet": ["{}", "{}"], "strict": "test")", ACCOUNT2, ACCOUNT3)}, + }; } INSTANTIATE_TEST_SUITE_P(