#include #include namespace ripple { namespace test { namespace jtx { namespace oracle { struct Oracle_test : public beast::unit_test::suite { private: void testInvalidSet() { testcase("Invalid Set"); using namespace jtx; Account const owner("owner"); { // Invalid account Env env(*this); Account const bad("bad"); env.memoize(bad); Oracle oracle( env, {.owner = bad, .seq = seq(1), .fee = static_cast(env.current()->fees().base.drops()), .err = ter(terNO_ACCOUNT)}); } // Insufficient reserve { Env env(*this); env.fund(env.current()->fees().accountReserve(0), owner); Oracle oracle( env, {.owner = owner, .fee = static_cast(env.current()->fees().base.drops()), .err = ter(tecINSUFFICIENT_RESERVE)}); } // Insufficient reserve if the data series extends to greater than 5 { Env env(*this); env.fund( env.current()->fees().accountReserve(1) + env.current()->fees().base * 2, owner); Oracle oracle( env, {.owner = owner, .fee = static_cast(env.current()->fees().base.drops())}); BEAST_EXPECT(oracle.exists()); oracle.set(UpdateArg{ .series = { {"XRP", "EUR", 740, 1}, {"XRP", "GBP", 740, 1}, {"XRP", "CNY", 740, 1}, {"XRP", "CAD", 740, 1}, {"XRP", "AUD", 740, 1}, }, .fee = static_cast(env.current()->fees().base.drops()), .err = ter(tecINSUFFICIENT_RESERVE)}); } { Env env(*this); auto const baseFee = static_cast(env.current()->fees().base.drops()); env.fund(XRP(1'000), owner); Oracle oracle(env, {.owner = owner, .fee = baseFee}, false); // Invalid flag oracle.set(CreateArg{ .flags = tfSellNFToken, .fee = baseFee, .err = ter(temINVALID_FLAG)}); // Duplicate token pair oracle.set(CreateArg{ .series = {{"XRP", "USD", 740, 1}, {"XRP", "USD", 750, 1}}, .fee = baseFee, .err = ter(temMALFORMED)}); // Price is not included oracle.set(CreateArg{ .series = {{"XRP", "USD", 740, 1}, {"XRP", "EUR", std::nullopt, 1}}, .fee = baseFee, .err = ter(temMALFORMED)}); // Token pair is in update and delete oracle.set(CreateArg{ .series = {{"XRP", "USD", 740, 1}, {"XRP", "USD", std::nullopt, 1}}, .fee = baseFee, .err = ter(temMALFORMED)}); // Token pair is in add and delete oracle.set(CreateArg{ .series = {{"XRP", "EUR", 740, 1}, {"XRP", "EUR", std::nullopt, 1}}, .fee = baseFee, .err = ter(temMALFORMED)}); // Array of token pair is 0 or exceeds 10 oracle.set(CreateArg{ .series = {{"XRP", "US1", 740, 1}, {"XRP", "US2", 750, 1}, {"XRP", "US3", 740, 1}, {"XRP", "US4", 750, 1}, {"XRP", "US5", 740, 1}, {"XRP", "US6", 750, 1}, {"XRP", "US7", 740, 1}, {"XRP", "US8", 750, 1}, {"XRP", "US9", 740, 1}, {"XRP", "U10", 750, 1}, {"XRP", "U11", 740, 1}}, .fee = baseFee, .err = ter(temARRAY_TOO_LARGE)}); oracle.set(CreateArg{ .series = {}, .fee = baseFee, .err = ter(temARRAY_EMPTY)}); } // Array of token pair exceeds 10 after update { Env env{*this}; auto const baseFee = static_cast(env.current()->fees().base.drops()); env.fund(XRP(1'000), owner); Oracle oracle( env, CreateArg{ .owner = owner, .series = {{{"XRP", "USD", 740, 1}}}, .fee = baseFee}); oracle.set(UpdateArg{ .series = { {"XRP", "US1", 740, 1}, {"XRP", "US2", 750, 1}, {"XRP", "US3", 740, 1}, {"XRP", "US4", 750, 1}, {"XRP", "US5", 740, 1}, {"XRP", "US6", 750, 1}, {"XRP", "US7", 740, 1}, {"XRP", "US8", 750, 1}, {"XRP", "US9", 740, 1}, {"XRP", "U10", 750, 1}, }, .fee = baseFee, .err = ter(tecARRAY_TOO_LARGE)}); } { Env env(*this); auto const baseFee = static_cast(env.current()->fees().base.drops()); env.fund(XRP(1'000), owner); Oracle oracle(env, {.owner = owner, .fee = baseFee}, false); // Asset class or provider not included on create oracle.set(CreateArg{ .assetClass = std::nullopt, .provider = "provider", .fee = baseFee, .err = ter(temMALFORMED)}); oracle.set(CreateArg{ .assetClass = "currency", .provider = std::nullopt, .uri = "URI", .fee = baseFee, .err = ter(temMALFORMED)}); // Asset class or provider are included on update // and don't match the current values oracle.set(CreateArg{ .fee = static_cast(env.current()->fees().base.drops())}); BEAST_EXPECT(oracle.exists()); oracle.set(UpdateArg{ .series = {{"XRP", "USD", 740, 1}}, .provider = "provider1", .fee = baseFee, .err = ter(temMALFORMED)}); oracle.set(UpdateArg{ .series = {{"XRP", "USD", 740, 1}}, .assetClass = "currency1", .fee = baseFee, .err = ter(temMALFORMED)}); } { Env env(*this); auto const baseFee = static_cast(env.current()->fees().base.drops()); env.fund(XRP(1'000), owner); Oracle oracle(env, {.owner = owner, .fee = baseFee}, false); // Fields too long // Asset class std::string assetClass(17, '0'); oracle.set(CreateArg{ .assetClass = assetClass, .fee = baseFee, .err = ter(temMALFORMED)}); // provider std::string const large(257, '0'); oracle.set(CreateArg{ .provider = large, .fee = baseFee, .err = ter(temMALFORMED)}); // URI oracle.set(CreateArg{ .uri = large, .fee = baseFee, .err = ter(temMALFORMED)}); // Empty field // Asset class oracle.set(CreateArg{ .assetClass = "", .fee = baseFee, .err = ter(temMALFORMED)}); // provider oracle.set(CreateArg{ .provider = "", .fee = baseFee, .err = ter(temMALFORMED)}); // URI oracle.set( CreateArg{.uri = "", .fee = baseFee, .err = ter(temMALFORMED)}); } { // Different owner creates a new object and fails because // of missing fields currency/provider Env env(*this); auto const baseFee = static_cast(env.current()->fees().base.drops()); Account const some("some"); env.fund(XRP(1'000), owner); env.fund(XRP(1'000), some); Oracle oracle(env, {.owner = owner, .fee = baseFee}); BEAST_EXPECT(oracle.exists()); oracle.set(UpdateArg{ .owner = some, .series = {{"XRP", "USD", 740, 1}}, .fee = baseFee, .err = ter(temMALFORMED)}); } { // Invalid update time using namespace std::chrono; Env env(*this); auto const baseFee = static_cast(env.current()->fees().base.drops()); auto closeTime = [&]() { return duration_cast( env.current()->info().closeTime.time_since_epoch() - 10'000s) .count(); }; env.fund(XRP(1'000), owner); Oracle oracle(env, {.owner = owner, .fee = baseFee}); BEAST_EXPECT(oracle.exists()); env.close(seconds(400)); // Less than the last close time - 300s oracle.set(UpdateArg{ .series = {{"XRP", "USD", 740, 1}}, .lastUpdateTime = static_cast(closeTime() - 301), .fee = baseFee, .err = ter(tecINVALID_UPDATE_TIME)}); // Greater than last close time + 300s oracle.set(UpdateArg{ .series = {{"XRP", "USD", 740, 1}}, .lastUpdateTime = static_cast(closeTime() + 311), .fee = baseFee, .err = ter(tecINVALID_UPDATE_TIME)}); oracle.set( UpdateArg{.series = {{"XRP", "USD", 740, 1}}, .fee = baseFee}); BEAST_EXPECT(oracle.expectLastUpdateTime( static_cast(testStartTime.count() + 450))); // Less than the previous lastUpdateTime oracle.set(UpdateArg{ .series = {{"XRP", "USD", 740, 1}}, .lastUpdateTime = static_cast(449), .fee = baseFee, .err = ter(tecINVALID_UPDATE_TIME)}); // Less than the epoch time oracle.set(UpdateArg{ .series = {{"XRP", "USD", 740, 1}}, .lastUpdateTime = static_cast(epoch_offset.count() - 1), .fee = baseFee, .err = ter(tecINVALID_UPDATE_TIME)}); } { // delete token pair that doesn't exist Env env(*this); auto const baseFee = static_cast(env.current()->fees().base.drops()); env.fund(XRP(1'000), owner); Oracle oracle(env, {.owner = owner, .fee = baseFee}); BEAST_EXPECT(oracle.exists()); oracle.set(UpdateArg{ .series = {{"XRP", "EUR", std::nullopt, std::nullopt}}, .fee = baseFee, .err = ter(tecTOKEN_PAIR_NOT_FOUND)}); // delete all token pairs oracle.set(UpdateArg{ .series = {{"XRP", "USD", std::nullopt, std::nullopt}}, .fee = baseFee, .err = ter(tecARRAY_EMPTY)}); } { // same BaseAsset and QuoteAsset Env env(*this); auto const baseFee = static_cast(env.current()->fees().base.drops()); env.fund(XRP(1'000), owner); Oracle oracle( env, {.owner = owner, .series = {{"USD", "USD", 740, 1}}, .fee = baseFee, .err = ter(temMALFORMED)}); } { // Scale is greater than maxPriceScale Env env(*this); auto const baseFee = static_cast(env.current()->fees().base.drops()); env.fund(XRP(1'000), owner); Oracle oracle( env, {.owner = owner, .series = {{"USD", "BTC", 740, maxPriceScale + 1}}, .fee = baseFee, .err = ter(temMALFORMED)}); } { // Updating token pair to add and delete Env env(*this); auto const baseFee = static_cast(env.current()->fees().base.drops()); env.fund(XRP(1'000), owner); Oracle oracle(env, {.owner = owner, .fee = baseFee}); oracle.set(UpdateArg{ .series = {{"XRP", "EUR", std::nullopt, std::nullopt}, {"XRP", "EUR", 740, 1}}, .fee = baseFee, .err = ter(temMALFORMED)}); // Delete token pair that doesn't exist in this oracle oracle.set(UpdateArg{ .series = {{"XRP", "EUR", std::nullopt, std::nullopt}}, .fee = baseFee, .err = ter(tecTOKEN_PAIR_NOT_FOUND)}); // Delete token pair in oracle, which is not in the ledger oracle.set(UpdateArg{ .documentID = 10, .series = {{"XRP", "EUR", std::nullopt, std::nullopt}}, .fee = baseFee, .err = ter(temMALFORMED)}); } { // Bad fee Env env(*this); env.fund(XRP(1'000), owner); Oracle oracle( env, {.owner = owner, .fee = -1, .err = ter(temBAD_FEE)}); Oracle oracle1( env, {.owner = owner, .fee = static_cast(env.current()->fees().base.drops())}); oracle.set( UpdateArg{.owner = owner, .fee = -1, .err = ter(temBAD_FEE)}); } } void testCreate(FeatureBitset features) { testcase("Create"); using namespace jtx; Account const owner("owner"); auto test = [&](Env& env, DataSeries const& series, std::uint16_t adj) { auto const baseFee = static_cast(env.current()->fees().base.drops()); env.fund(XRP(1'000), owner); auto const count = ownerCount(env, owner); Oracle oracle( env, {.owner = owner, .series = series, .fee = baseFee}); BEAST_EXPECT(oracle.exists()); BEAST_EXPECT(ownerCount(env, owner) == (count + adj)); auto const entry = oracle.ledgerEntry(); BEAST_EXPECT(entry[jss::node][jss::Owner] == owner.human()); if (features[fixIncludeKeyletFields]) { BEAST_EXPECT( entry[jss::node][jss::OracleDocumentID] == oracle.documentID()); } else { BEAST_EXPECT(!entry[jss::node].isMember(jss::OracleDocumentID)); } BEAST_EXPECT(oracle.expectLastUpdateTime(946694810)); }; { // owner count is adjusted by 1 Env env(*this, features); test(env, {{"XRP", "USD", 740, 1}}, 1); } { // owner count is adjusted by 2 Env env(*this, features); test( env, {{"XRP", "USD", 740, 1}, {"BTC", "USD", 740, 1}, {"ETH", "USD", 740, 1}, {"CAN", "USD", 740, 1}, {"YAN", "USD", 740, 1}, {"GBP", "USD", 740, 1}}, 2); } { // Different owner creates a new object Env env(*this, features); auto const baseFee = static_cast(env.current()->fees().base.drops()); Account const some("some"); env.fund(XRP(1'000), owner); env.fund(XRP(1'000), some); Oracle oracle(env, {.owner = owner, .fee = baseFee}); BEAST_EXPECT(oracle.exists()); oracle.set(CreateArg{ .owner = some, .series = {{"912810RR9", "USD", 740, 1}}, .fee = baseFee}); BEAST_EXPECT(Oracle::exists(env, some, oracle.documentID())); } } void testInvalidDelete() { testcase("Invalid Delete"); using namespace jtx; Env env(*this); auto const baseFee = static_cast(env.current()->fees().base.drops()); Account const owner("owner"); env.fund(XRP(1'000), owner); Oracle oracle(env, {.owner = owner, .fee = baseFee}); BEAST_EXPECT(oracle.exists()); { // Invalid account Account const bad("bad"); env.memoize(bad); oracle.remove( {.owner = bad, .seq = seq(1), .fee = baseFee, .err = ter(terNO_ACCOUNT)}); } // Invalid DocumentID oracle.remove( {.documentID = 2, .fee = baseFee, .err = ter(tecNO_ENTRY)}); // Invalid owner Account const invalid("invalid"); env.fund(XRP(1'000), invalid); oracle.remove( {.owner = invalid, .fee = baseFee, .err = ter(tecNO_ENTRY)}); // Invalid flags oracle.remove( {.flags = tfSellNFToken, .fee = baseFee, .err = ter(temINVALID_FLAG)}); // Bad fee oracle.remove({.fee = -1, .err = ter(temBAD_FEE)}); } void testDelete() { testcase("Delete"); using namespace jtx; Account const owner("owner"); auto test = [&](Env& env, DataSeries const& series, std::uint16_t adj) { auto const baseFee = static_cast(env.current()->fees().base.drops()); env.fund(XRP(1'000), owner); Oracle oracle( env, {.owner = owner, .series = series, .fee = baseFee}); auto const count = ownerCount(env, owner); BEAST_EXPECT(oracle.exists()); oracle.remove({.fee = baseFee}); BEAST_EXPECT(!oracle.exists()); BEAST_EXPECT(ownerCount(env, owner) == (count - adj)); }; { // owner count is adjusted by 1 Env env(*this); test(env, {{"XRP", "USD", 740, 1}}, 1); } { // owner count is adjusted by 2 Env env(*this); test( env, { {"XRP", "USD", 740, 1}, {"BTC", "USD", 740, 1}, {"ETH", "USD", 740, 1}, {"CAN", "USD", 740, 1}, {"YAN", "USD", 740, 1}, {"GBP", "USD", 740, 1}, }, 2); } { // deleting the account deletes the oracles Env env(*this); auto const baseFee = static_cast(env.current()->fees().base.drops()); auto const alice = Account("alice"); auto const acctDelFee{drops(env.current()->fees().increment)}; env.fund(XRP(1'000), owner); env.fund(XRP(1'000), alice); Oracle oracle( env, {.owner = owner, .series = {{"XRP", "USD", 740, 1}}, .fee = baseFee}); Oracle oracle1( env, {.owner = owner, .documentID = 2, .series = {{"XRP", "EUR", 740, 1}}, .fee = baseFee}); BEAST_EXPECT(ownerCount(env, owner) == 2); BEAST_EXPECT(oracle.exists()); BEAST_EXPECT(oracle1.exists()); auto const index = env.closed()->seq(); auto const hash = env.closed()->info().hash; for (int i = 0; i < 256; ++i) env.close(); env(acctdelete(owner, alice), fee(acctDelFee)); env.close(); BEAST_EXPECT(!oracle.exists()); BEAST_EXPECT(!oracle1.exists()); // can still get the oracles via the ledger index or hash auto verifyLedgerData = [&](auto const& field, auto const& value) { Json::Value jvParams; jvParams[field] = value; jvParams[jss::binary] = false; jvParams[jss::type] = jss::oracle; Json::Value jrr = env.rpc("json", "ledger_data", to_string(jvParams)); BEAST_EXPECT(jrr[jss::result][jss::state].size() == 2); }; verifyLedgerData(jss::ledger_index, index); verifyLedgerData(jss::ledger_hash, to_string(hash)); } } void testUpdate() { testcase("Update"); using namespace jtx; Account const owner("owner"); { Env env(*this); auto const baseFee = static_cast(env.current()->fees().base.drops()); env.fund(XRP(1'000), owner); auto count = ownerCount(env, owner); Oracle oracle(env, {.owner = owner, .fee = baseFee}); BEAST_EXPECT(oracle.exists()); // update existing pair oracle.set( UpdateArg{.series = {{"XRP", "USD", 740, 2}}, .fee = baseFee}); BEAST_EXPECT(oracle.expectPrice({{"XRP", "USD", 740, 2}})); // owner count is increased by 1 since the oracle object is added // with one token pair count += 1; BEAST_EXPECT(ownerCount(env, owner) == count); // add new pairs, not-included pair is reset oracle.set( UpdateArg{.series = {{"XRP", "EUR", 700, 2}}, .fee = baseFee}); BEAST_EXPECT(oracle.expectPrice( {{"XRP", "USD", 0, 0}, {"XRP", "EUR", 700, 2}})); // owner count is not changed since the number of pairs is 2 BEAST_EXPECT(ownerCount(env, owner) == count); // update both pairs oracle.set(UpdateArg{ .series = {{"XRP", "USD", 741, 2}, {"XRP", "EUR", 710, 2}}, .fee = baseFee}); BEAST_EXPECT(oracle.expectPrice( {{"XRP", "USD", 741, 2}, {"XRP", "EUR", 710, 2}})); // owner count is not changed since the number of pairs is 2 BEAST_EXPECT(ownerCount(env, owner) == count); // owner count is increased by 1 since the number of pairs is 6 oracle.set(UpdateArg{ .series = { {"BTC", "USD", 741, 2}, {"ETH", "EUR", 710, 2}, {"YAN", "EUR", 710, 2}, {"CAN", "EUR", 710, 2}, }, .fee = baseFee}); count += 1; BEAST_EXPECT(ownerCount(env, owner) == count); // update two pairs and delete four oracle.set(UpdateArg{ .series = {{"BTC", "USD", std::nullopt, std::nullopt}}, .fee = baseFee}); oracle.set(UpdateArg{ .series = {{"XRP", "USD", 742, 2}, {"XRP", "EUR", 711, 2}, {"ETH", "EUR", std::nullopt, std::nullopt}, {"YAN", "EUR", std::nullopt, std::nullopt}, {"CAN", "EUR", std::nullopt, std::nullopt}}, .fee = baseFee}); BEAST_EXPECT(oracle.expectPrice( {{"XRP", "USD", 742, 2}, {"XRP", "EUR", 711, 2}})); // owner count is decreased by 1 since the number of pairs is 2 count -= 1; BEAST_EXPECT(ownerCount(env, owner) == count); } // Min reserve to create and update { Env env(*this); auto const baseFee = static_cast(env.current()->fees().base.drops()); env.fund( env.current()->fees().accountReserve(1) + env.current()->fees().base * 2, owner); Oracle oracle(env, {.owner = owner, .fee = baseFee}); 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 testMultisig() { testcase("Multisig"); using namespace jtx; Oracle::setFee(100'000); Env env(*this); auto const baseFee = static_cast(env.current()->fees().base.drops()); Account const alice{"alice", KeyType::secp256k1}; Account const bogie{"bogie", KeyType::secp256k1}; Account const ed{"ed", KeyType::secp256k1}; Account const becky{"becky", KeyType::ed25519}; Account const zelda{"zelda", KeyType::secp256k1}; Account const bob{"bob", KeyType::secp256k1}; env.fund(XRP(10'000), alice, becky, zelda, ed, bob); // alice uses a regular key with the master disabled. Account const alie{"alie", KeyType::secp256k1}; env(regkey(alice, alie)); env(fset(alice, asfDisableMaster), sig(alice)); // Attach signers to alice. env(signers(alice, 2, {{becky, 1}, {bogie, 1}, {ed, 2}}), sig(alie)); env.close(); env.require(owners(alice, 1)); // Create // Force close (true) and time advancement because the close time // is no longer 0. Oracle oracle( env, CreateArg{.owner = alice, .fee = baseFee, .close = true}, false); oracle.set(CreateArg{ .msig = msig(becky), .fee = baseFee, .err = ter(tefBAD_QUORUM)}); oracle.set(CreateArg{ .msig = msig(zelda), .fee = baseFee, .err = ter(tefBAD_SIGNATURE)}); oracle.set(CreateArg{.msig = msig(becky, bogie), .fee = baseFee}); BEAST_EXPECT(oracle.exists()); // Update oracle.set(UpdateArg{ .series = {{"XRP", "USD", 740, 1}}, .msig = msig(becky), .fee = baseFee, .err = ter(tefBAD_QUORUM)}); oracle.set(UpdateArg{ .series = {{"XRP", "USD", 740, 1}}, .msig = msig(zelda), .fee = baseFee, .err = ter(tefBAD_SIGNATURE)}); oracle.set(UpdateArg{ .series = {{"XRP", "USD", 741, 1}}, .msig = msig(becky, bogie), .fee = baseFee}); BEAST_EXPECT(oracle.expectPrice({{"XRP", "USD", 741, 1}})); // remove the signer list env(signers(alice, jtx::none), sig(alie)); env.close(); env.require(owners(alice, 1)); // create new signer list env(signers(alice, 2, {{zelda, 1}, {bob, 1}, {ed, 2}}), sig(alie)); env.close(); // old list fails oracle.set(UpdateArg{ .series = {{"XRP", "USD", 740, 1}}, .msig = msig(becky, bogie), .fee = baseFee, .err = ter(tefBAD_SIGNATURE)}); // updated list succeeds oracle.set(UpdateArg{ .series = {{"XRP", "USD", 7412, 2}}, .msig = msig(zelda, bob), .fee = baseFee}); BEAST_EXPECT(oracle.expectPrice({{"XRP", "USD", 7412, 2}})); oracle.set(UpdateArg{ .series = {{"XRP", "USD", 74245, 3}}, .msig = msig(ed), .fee = baseFee}); BEAST_EXPECT(oracle.expectPrice({{"XRP", "USD", 74245, 3}})); // Remove oracle.remove( {.msig = msig(bob), .fee = baseFee, .err = ter(tefBAD_QUORUM)}); oracle.remove( {.msig = msig(becky), .fee = baseFee, .err = ter(tefBAD_SIGNATURE)}); oracle.remove({.msig = msig(ed), .fee = baseFee}); BEAST_EXPECT(!oracle.exists()); } void testAmendment() { testcase("Amendment"); using namespace jtx; auto const features = testable_amendments() - featurePriceOracle; Account const owner("owner"); Env env(*this, features); auto const baseFee = static_cast(env.current()->fees().base.drops()); env.fund(XRP(1'000), owner); { Oracle oracle( env, {.owner = owner, .fee = baseFee, .err = ter(temDISABLED)}); } { Oracle oracle(env, {.owner = owner, .fee = baseFee}, false); oracle.remove({.fee = baseFee, .err = ter(temDISABLED)}); } } public: void run() override { using namespace jtx; auto const all = testable_amendments(); testInvalidSet(); testInvalidDelete(); testCreate(all); testCreate(all - fixIncludeKeyletFields); testDelete(); testUpdate(); testAmendment(); testMultisig(); } }; BEAST_DEFINE_TESTSUITE(Oracle, app, ripple); } // namespace oracle } // namespace jtx } // namespace test } // namespace ripple