From 2df7dcfdebcb0cdbd030c1f4b09ac748af95659c Mon Sep 17 00:00:00 2001 From: Michael Legleux Date: Wed, 27 Aug 2025 10:25:53 -0700 Subject: [PATCH 1/5] Set version to 2.6.0 --- src/libxrpl/protocol/BuildInfo.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libxrpl/protocol/BuildInfo.cpp b/src/libxrpl/protocol/BuildInfo.cpp index 4b55f82f49..d5077aa44d 100644 --- a/src/libxrpl/protocol/BuildInfo.cpp +++ b/src/libxrpl/protocol/BuildInfo.cpp @@ -36,7 +36,7 @@ namespace BuildInfo { // and follow the format described at http://semver.org/ //------------------------------------------------------------------------------ // clang-format off -char const* const versionString = "2.6.0-rc3" +char const* const versionString = "2.6.0" // clang-format on #if defined(DEBUG) || defined(SANITIZER) From 1e37d00d6c176f8bce562f5ecb19c2cbc0cf098f Mon Sep 17 00:00:00 2001 From: Ayaz Salikhov Date: Thu, 28 Aug 2025 20:32:49 +0100 Subject: [PATCH 2/5] ci: Use XRPLF/prepare-runner action (#5740) * ci: Use XRPLF/prepare-runner action * Remove some old boost workaround --- .github/workflows/build-test.yml | 34 +++++++------------------------- cmake/deps/Boost.cmake | 6 ------ 2 files changed, 7 insertions(+), 33 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 36145479e1..05b65edbfd 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -101,6 +101,7 @@ jobs: echo 'CMake arguments: ${{ matrix.cmake_args }}' echo 'CMake target: ${{ matrix.cmake_target }}' echo 'Config name: ${{ matrix.config_name }}' + - name: Clean workspace (MacOS) if: ${{ inputs.os == 'macos' }} run: | @@ -111,18 +112,12 @@ jobs: exit 1 fi find "${WORKSPACE}" -depth 1 | xargs rm -rfv + - name: Checkout repository uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 - - name: Set up Python (Windows) - if: ${{ inputs.os == 'windows' }} - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 - with: - python-version: 3.13 - - name: Install build tools (Windows) - if: ${{ inputs.os == 'windows' }} - run: | - echo 'Installing build tools.' - pip install wheel conan + - name: Prepare runner + uses: XRPLF/actions/.github/actions/prepare-runner@638e0dc11ea230f91bd26622fb542116bb5254d5 + - name: Check configuration (Windows) if: ${{ inputs.os == 'windows' }} run: | @@ -134,11 +129,6 @@ jobs: echo 'Checking Conan version.' conan --version - - name: Install build tools (MacOS) - if: ${{ inputs.os == 'macos' }} - run: | - echo 'Installing build tools.' - brew install --quiet cmake conan ninja coreutils - name: Check configuration (Linux and MacOS) if: ${{ inputs.os == 'linux' || inputs.os == 'macos' }} run: | @@ -162,18 +152,7 @@ jobs: echo 'Checking nproc version.' nproc --version - - name: Set up Conan home directory (MacOS) - if: ${{ inputs.os == 'macos' }} - run: | - echo 'Setting up Conan home directory.' - export CONAN_HOME=${{ github.workspace }}/.conan - mkdir -p ${CONAN_HOME} - - name: Set up Conan home directory (Windows) - if: ${{ inputs.os == 'windows' }} - run: | - echo 'Setting up Conan home directory.' - set CONAN_HOME=${{ github.workspace }}\.conan - mkdir -p %CONAN_HOME% + - name: Set up Conan configuration run: | echo 'Installing configuration.' @@ -196,6 +175,7 @@ jobs: echo 'Listing Conan remotes.' conan remote list + - name: Build dependencies uses: ./.github/actions/build-deps with: diff --git a/cmake/deps/Boost.cmake b/cmake/deps/Boost.cmake index bde40c0ce5..e431e57b0c 100644 --- a/cmake/deps/Boost.cmake +++ b/cmake/deps/Boost.cmake @@ -14,12 +14,6 @@ find_package(Boost 1.82 REQUIRED add_library(ripple_boost INTERFACE) add_library(Ripple::boost ALIAS ripple_boost) -if(XCODE) - target_include_directories(ripple_boost BEFORE INTERFACE ${Boost_INCLUDE_DIRS}) - target_compile_options(ripple_boost INTERFACE --system-header-prefix="boost/") -else() - target_include_directories(ripple_boost SYSTEM BEFORE INTERFACE ${Boost_INCLUDE_DIRS}) -endif() target_link_libraries(ripple_boost INTERFACE From 6e814d7ebdaf245464cece678e098c6fe7da17a2 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Thu, 28 Aug 2025 16:33:11 -0400 Subject: [PATCH 3/5] chore: Run CI jobs in more situations, and add "passed" job (#5739) Test jobs will run if * Either the PR is non-draft or has the "DraftRunCI" label set *AND* * One of the following: * Certain files were changed *OR* * The PR is non-draft and has the "Ready to merge" flag *OR* * The workflow is being run from the merge queue. Additionally, a meta "passed" job was added that is dependent on all the other test jobs, so the required jobs list under branch protection rules only needs to specify "passed" to ensure that *either* all the test jobs pass *or* all the test jobs are skipped because they don't need to be run. This allows PRs that don't affect the build or binary to be merged without overriding. --- .github/workflows/on-pr.yml | 70 ++++++++++++++++++++++++------------- 1 file changed, 46 insertions(+), 24 deletions(-) diff --git a/.github/workflows/on-pr.yml b/.github/workflows/on-pr.yml index a5f1d60c42..a4bbfd0997 100644 --- a/.github/workflows/on-pr.yml +++ b/.github/workflows/on-pr.yml @@ -28,30 +28,26 @@ env: CONAN_REMOTE_URL: https://conan.ripplex.io jobs: - # This job determines whether the workflow should run. It runs when the PR is - # not a draft or has the 'DraftRunCI' label. + # This job determines whether the rest of the workflow should run. It runs + # when the PR is not a draft (which should also cover merge-group) or + # has the 'DraftRunCI' label. should-run: if: ${{ !github.event.pull_request.draft || contains(github.event.pull_request.labels.*.name, 'DraftRunCI') }} runs-on: ubuntu-latest - steps: - - name: No-op - run: true - - # This job checks whether any files have changed that should cause the next - # jobs to run. We do it this way rather than using `paths` in the `on:` - # section, because all required checks must pass, even for changes that do not - # modify anything that affects those checks. We would therefore like to make - # the checks required only if the job runs, but GitHub does not support that - # directly. By always executing the workflow on new commits and by using the - # changed-files action below, we ensure that Github considers any skipped jobs - # to have passed, and in turn the required checks as well. - any-changed: - needs: should-run - runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 - name: Determine changed files + # This step checks whether any files have changed that should + # cause the next jobs to run. We do it this way rather than + # using `paths` in the `on:` section, because all required + # checks must pass, even for changes that do not modify anything + # that affects those checks. We would therefore like to make the + # checks required only if the job runs, but GitHub does not + # support that directly. By always executing the workflow on new + # commits and by using the changed-files action below, we ensure + # that Github considers any skipped jobs to have passed, and in + # turn the required checks as well. id: changes uses: tj-actions/changed-files@ed68ef82c095e0d48ec87eccea555d944a631a4c # v46.0.5 with: @@ -79,24 +75,40 @@ jobs: tests/** CMakeLists.txt conanfile.py + - name: Check whether to run + # This step determines whether the rest of the workflow should + # run. The rest of the workflow will run if this job runs AND at + # least one of: + # * Any of the files checked in the `changes` step were modified + # * The PR is NOT a draft and is labeled "Ready to merge" + # * The workflow is running from the merge queue + id: go + env: + FILES: ${{ steps.changes.outputs.any_changed }} + DRAFT: ${{ github.event.pull_request.draft }} + READY: ${{ contains(github.event.pull_request.labels.*.name, 'Ready to merge') }} + MERGE: ${{ github.event_name == 'merge_group' }} + run: | + echo "go=${{ (env.DRAFT != 'true' && env.READY == 'true') || env.FILES == 'true' || env.MERGE == 'true' }}" >> "${GITHUB_OUTPUT}" + cat "${GITHUB_OUTPUT}" outputs: - changed: ${{ steps.changes.outputs.any_changed }} + go: ${{ steps.go.outputs.go == 'true' }} check-format: - needs: any-changed - if: needs.any-changed.outputs.changed == 'true' + needs: should-run + if: needs.should-run.outputs.go == 'true' uses: ./.github/workflows/check-format.yml check-levelization: - needs: any-changed - if: needs.any-changed.outputs.changed == 'true' + needs: should-run + if: needs.should-run.outputs.go == 'true' uses: ./.github/workflows/check-levelization.yml # This job works around the limitation that GitHub Actions does not support # using environment variables as inputs for reusable workflows. generate-outputs: - needs: any-changed - if: needs.any-changed.outputs.changed == 'true' + needs: should-run + if: needs.should-run.outputs.go == 'true' runs-on: ubuntu-latest steps: - name: No-op @@ -130,3 +142,13 @@ jobs: clio_notify_token: ${{ secrets.CLIO_NOTIFY_TOKEN }} conan_remote_username: ${{ secrets.CONAN_REMOTE_USERNAME }} conan_remote_password: ${{ secrets.CONAN_REMOTE_PASSWORD }} + + passed: + needs: + - build-test + - check-format + - check-levelization + runs-on: ubuntu-latest + steps: + - name: No-op + run: true From e4fdf3315805c7678500bd7c2babad77cf66148a Mon Sep 17 00:00:00 2001 From: Vito Tumas <5780819+Tapanito@users.noreply.github.com> Date: Fri, 29 Aug 2025 02:00:38 +0200 Subject: [PATCH 4/5] adds additional logging to differentiate why connections were refused (#5690) This is a follow-up to PR #5664 that further improves the specificity of logging for refused peer connections. The previous changes did not account for several key scenarios, leading to potentially misleading log messages. It addresses the following - Inbound Disabled: Connections are now explicitly logged as rejected when the server is not configured to accept inbound peers. Previously, this was logged as the server being "full," which was technically correct but lacked diagnostic clarity. - Duplicate Connections: The logging now distinguishes between two types of duplicate connection refusals: - When a peer with the same node public key is already connected (duplicate connection). - When a connection is rejected because the limit for connections from a single IP address has been reached. These changes provide more accurate and actionable diagnostic information when analyzing peer connection behavior. --- src/test/overlay/tx_reduce_relay_test.cpp | 2 +- src/test/peerfinder/PeerFinder_test.cpp | 274 ++++++++++++++---- src/xrpld/overlay/detail/OverlayImpl.cpp | 11 +- src/xrpld/peerfinder/PeerfinderManager.h | 27 +- src/xrpld/peerfinder/detail/Counts.h | 2 +- src/xrpld/peerfinder/detail/Logic.h | 26 +- .../peerfinder/detail/PeerfinderConfig.cpp | 11 + .../peerfinder/detail/PeerfinderManager.cpp | 4 +- 8 files changed, 280 insertions(+), 77 deletions(-) diff --git a/src/test/overlay/tx_reduce_relay_test.cpp b/src/test/overlay/tx_reduce_relay_test.cpp index 0c67fd581c..83b3013514 100644 --- a/src/test/overlay/tx_reduce_relay_test.cpp +++ b/src/test/overlay/tx_reduce_relay_test.cpp @@ -183,7 +183,7 @@ private: boost::asio::ip::make_address("172.1.1." + std::to_string(rid_))); PublicKey key(std::get<0>(randomKeyPair(KeyType::ed25519))); auto consumer = overlay.resourceManager().newInboundEndpoint(remote); - auto slot = overlay.peerFinder().new_inbound_slot(local, remote); + auto [slot, _] = overlay.peerFinder().new_inbound_slot(local, remote); auto const peer = std::make_shared( env.app(), slot, diff --git a/src/test/peerfinder/PeerFinder_test.cpp b/src/test/peerfinder/PeerFinder_test.cpp index f35cbbdaae..64a1eb5091 100644 --- a/src/test/peerfinder/PeerFinder_test.cpp +++ b/src/test/peerfinder/PeerFinder_test.cpp @@ -20,6 +20,7 @@ #include #include +#include #include #include @@ -98,7 +99,7 @@ public: if (!list.empty()) { BEAST_EXPECT(list.size() == 1); - auto const slot = logic.new_outbound_slot(list.front()); + auto const [slot, _] = logic.new_outbound_slot(list.front()); BEAST_EXPECT(logic.onConnected( slot, beast::IP::Endpoint::from_string("65.0.0.2:5"))); logic.on_closed(slot); @@ -139,7 +140,7 @@ public: if (!list.empty()) { BEAST_EXPECT(list.size() == 1); - auto const slot = logic.new_outbound_slot(list.front()); + auto const [slot, _] = logic.new_outbound_slot(list.front()); if (!BEAST_EXPECT(logic.onConnected( slot, beast::IP::Endpoint::from_string("65.0.0.2:5")))) return; @@ -158,6 +159,7 @@ public: BEAST_EXPECT(n <= (seconds + 59) / 60); } + // test accepting an incoming slot for an already existing outgoing slot void test_duplicateOutIn() { @@ -166,8 +168,6 @@ public: TestChecker checker; TestStopwatch clock; Logic logic(clock, store, checker, journal_); - logic.addFixedPeer( - "test", beast::IP::Endpoint::from_string("65.0.0.1:5")); { Config c; c.autoConnect = false; @@ -176,28 +176,24 @@ public: logic.config(c); } - auto const list = logic.autoconnect(); - if (BEAST_EXPECT(!list.empty())) - { - BEAST_EXPECT(list.size() == 1); - auto const remote = list.front(); - auto const slot1 = logic.new_outbound_slot(remote); - if (BEAST_EXPECT(slot1 != nullptr)) - { - BEAST_EXPECT( - logic.connectedAddresses_.count(remote.address()) == 1); - auto const local = - beast::IP::Endpoint::from_string("65.0.0.2:1024"); - auto const slot2 = logic.new_inbound_slot(local, remote); - BEAST_EXPECT( - logic.connectedAddresses_.count(remote.address()) == 1); - if (!BEAST_EXPECT(slot2 == nullptr)) - logic.on_closed(slot2); - logic.on_closed(slot1); - } - } + auto const remote = beast::IP::Endpoint::from_string("65.0.0.1:5"); + auto const [slot1, r] = logic.new_outbound_slot(remote); + BEAST_EXPECT(slot1 != nullptr); + BEAST_EXPECT(r == Result::success); + BEAST_EXPECT(logic.connectedAddresses_.count(remote.address()) == 1); + + auto const local = beast::IP::Endpoint::from_string("65.0.0.2:1024"); + auto const [slot2, r2] = logic.new_inbound_slot(local, remote); + BEAST_EXPECT(logic.connectedAddresses_.count(remote.address()) == 1); + BEAST_EXPECT(r2 == Result::duplicatePeer); + + if (!BEAST_EXPECT(slot2 == nullptr)) + logic.on_closed(slot2); + + logic.on_closed(slot1); } + // test establishing outgoing slot for an already existing incoming slot void test_duplicateInOut() { @@ -206,8 +202,6 @@ public: TestChecker checker; TestStopwatch clock; Logic logic(clock, store, checker, journal_); - logic.addFixedPeer( - "test", beast::IP::Endpoint::from_string("65.0.0.1:5")); { Config c; c.autoConnect = false; @@ -216,33 +210,202 @@ public: logic.config(c); } - auto const list = logic.autoconnect(); - if (BEAST_EXPECT(!list.empty())) + auto const remote = beast::IP::Endpoint::from_string("65.0.0.1:5"); + auto const local = beast::IP::Endpoint::from_string("65.0.0.2:1024"); + + auto const [slot1, r] = logic.new_inbound_slot(local, remote); + BEAST_EXPECT(slot1 != nullptr); + BEAST_EXPECT(r == Result::success); + BEAST_EXPECT(logic.connectedAddresses_.count(remote.address()) == 1); + + auto const [slot2, r2] = logic.new_outbound_slot(remote); + BEAST_EXPECT(r2 == Result::duplicatePeer); + BEAST_EXPECT(logic.connectedAddresses_.count(remote.address()) == 1); + if (!BEAST_EXPECT(slot2 == nullptr)) + logic.on_closed(slot2); + logic.on_closed(slot1); + } + + void + test_peerLimitExceeded() + { + testcase("peer limit exceeded"); + TestStore store; + TestChecker checker; + TestStopwatch clock; + Logic logic(clock, store, checker, journal_); { - BEAST_EXPECT(list.size() == 1); - auto const remote = list.front(); - auto const local = - beast::IP::Endpoint::from_string("65.0.0.2:1024"); - auto const slot1 = logic.new_inbound_slot(local, remote); - if (BEAST_EXPECT(slot1 != nullptr)) - { - BEAST_EXPECT( - logic.connectedAddresses_.count(remote.address()) == 1); - auto const slot2 = logic.new_outbound_slot(remote); - BEAST_EXPECT( - logic.connectedAddresses_.count(remote.address()) == 1); - if (!BEAST_EXPECT(slot2 == nullptr)) - logic.on_closed(slot2); - logic.on_closed(slot1); - } + Config c; + c.autoConnect = false; + c.listeningPort = 1024; + c.ipLimit = 2; + logic.config(c); } + + auto const local = beast::IP::Endpoint::from_string("65.0.0.2:1024"); + auto const [slot, r] = logic.new_inbound_slot( + local, beast::IP::Endpoint::from_string("55.104.0.2:1025")); + BEAST_EXPECT(slot != nullptr); + BEAST_EXPECT(r == Result::success); + + auto const [slot1, r1] = logic.new_inbound_slot( + local, beast::IP::Endpoint::from_string("55.104.0.2:1026")); + BEAST_EXPECT(slot1 != nullptr); + BEAST_EXPECT(r1 == Result::success); + + auto const [slot2, r2] = logic.new_inbound_slot( + local, beast::IP::Endpoint::from_string("55.104.0.2:1027")); + BEAST_EXPECT(r2 == Result::ipLimitExceeded); + + if (!BEAST_EXPECT(slot2 == nullptr)) + logic.on_closed(slot2); + logic.on_closed(slot1); + logic.on_closed(slot); + } + + void + test_activate_duplicate_peer() + { + testcase("test activate duplicate peer"); + TestStore store; + TestChecker checker; + TestStopwatch clock; + Logic logic(clock, store, checker, journal_); + { + Config c; + c.autoConnect = false; + c.listeningPort = 1024; + c.ipLimit = 2; + logic.config(c); + } + + auto const local = beast::IP::Endpoint::from_string("65.0.0.2:1024"); + + PublicKey const pk1(randomKeyPair(KeyType::secp256k1).first); + + auto const [slot, rSlot] = logic.new_outbound_slot( + beast::IP::Endpoint::from_string("55.104.0.2:1025")); + BEAST_EXPECT(slot != nullptr); + BEAST_EXPECT(rSlot == Result::success); + + auto const [slot2, r2Slot] = logic.new_outbound_slot( + beast::IP::Endpoint::from_string("55.104.0.2:1026")); + BEAST_EXPECT(slot2 != nullptr); + BEAST_EXPECT(r2Slot == Result::success); + + BEAST_EXPECT(logic.onConnected(slot, local)); + BEAST_EXPECT(logic.onConnected(slot2, local)); + + BEAST_EXPECT(logic.activate(slot, pk1, false) == Result::success); + + // activating a different slot with the same node ID (pk) must fail + BEAST_EXPECT( + logic.activate(slot2, pk1, false) == Result::duplicatePeer); + + logic.on_closed(slot); + + // accept the same key for a new slot after removing the old slot + BEAST_EXPECT(logic.activate(slot2, pk1, false) == Result::success); + logic.on_closed(slot2); + } + + void + test_activate_inbound_disabled() + { + testcase("test activate inbound disabled"); + TestStore store; + TestChecker checker; + TestStopwatch clock; + Logic logic(clock, store, checker, journal_); + { + Config c; + c.autoConnect = false; + c.listeningPort = 1024; + c.ipLimit = 2; + logic.config(c); + } + + PublicKey const pk1(randomKeyPair(KeyType::secp256k1).first); + auto const local = beast::IP::Endpoint::from_string("65.0.0.2:1024"); + + auto const [slot, rSlot] = logic.new_inbound_slot( + local, beast::IP::Endpoint::from_string("55.104.0.2:1025")); + BEAST_EXPECT(slot != nullptr); + BEAST_EXPECT(rSlot == Result::success); + + BEAST_EXPECT( + logic.activate(slot, pk1, false) == Result::inboundDisabled); + + { + Config c; + c.autoConnect = false; + c.listeningPort = 1024; + c.ipLimit = 2; + c.inPeers = 1; + logic.config(c); + } + // new inbound slot must succeed when inbound connections are enabled + BEAST_EXPECT(logic.activate(slot, pk1, false) == Result::success); + + // creating a new inbound slot must succeed as IP Limit is not exceeded + auto const [slot2, r2Slot] = logic.new_inbound_slot( + local, beast::IP::Endpoint::from_string("55.104.0.2:1026")); + BEAST_EXPECT(slot2 != nullptr); + BEAST_EXPECT(r2Slot == Result::success); + + PublicKey const pk2(randomKeyPair(KeyType::secp256k1).first); + + // an inbound slot exceeding inPeers limit must fail + BEAST_EXPECT(logic.activate(slot2, pk2, false) == Result::full); + + logic.on_closed(slot2); + logic.on_closed(slot); + } + + void + test_addFixedPeer_no_port() + { + testcase("test addFixedPeer no port"); + TestStore store; + TestChecker checker; + TestStopwatch clock; + Logic logic(clock, store, checker, journal_); + try + { + logic.addFixedPeer( + "test", beast::IP::Endpoint::from_string("65.0.0.2")); + fail("invalid endpoint successfully added"); + } + catch (std::runtime_error const& e) + { + pass(); + } + } + + void + test_onConnected_self_connection() + { + testcase("test onConnected self connection"); + TestStore store; + TestChecker checker; + TestStopwatch clock; + Logic logic(clock, store, checker, journal_); + + auto const local = beast::IP::Endpoint::from_string("65.0.0.2:1234"); + auto const [slot, r] = logic.new_outbound_slot(local); + BEAST_EXPECT(slot != nullptr); + BEAST_EXPECT(r == Result::success); + + // Must fail when a slot is to our own IP address + BEAST_EXPECT(!logic.onConnected(slot, local)); + logic.on_closed(slot); } void test_config() { - // if peers_max is configured then peers_in_max and peers_out_max are - // ignored + // if peers_max is configured then peers_in_max and peers_out_max + // are ignored auto run = [&](std::string const& test, std::optional maxPeers, std::optional maxIn, @@ -282,13 +445,21 @@ public: Counts counts; counts.onConfig(config); BEAST_EXPECT( - counts.out_max() == expectOut && - counts.inboundSlots() == expectIn && + counts.out_max() == expectOut && counts.in_max() == expectIn && config.ipLimit == expectIpLimit); + + TestStore store; + TestChecker checker; + TestStopwatch clock; + Logic logic(clock, store, checker, journal_); + logic.config(config); + + BEAST_EXPECT(logic.config() == config); }; // if max_peers == 0 => maxPeers = 21, - // else if max_peers < 10 => maxPeers = 10 else maxPeers = max_peers + // else if max_peers < 10 => maxPeers = 10 else maxPeers = + // max_peers // expectOut => if legacy => max(0.15 * maxPeers, 10), // if legacy && !wantIncoming => maxPeers else max_out_peers // expectIn => if legacy && wantIncoming => maxPeers - outPeers @@ -364,6 +535,11 @@ public: test_duplicateInOut(); test_config(); test_invalid_config(); + test_peerLimitExceeded(); + test_activate_duplicate_peer(); + test_activate_inbound_disabled(); + test_addFixedPeer_no_port(); + test_onConnected_self_connection(); } }; diff --git a/src/xrpld/overlay/detail/OverlayImpl.cpp b/src/xrpld/overlay/detail/OverlayImpl.cpp index f2c683b69f..8d295faace 100644 --- a/src/xrpld/overlay/detail/OverlayImpl.cpp +++ b/src/xrpld/overlay/detail/OverlayImpl.cpp @@ -195,14 +195,16 @@ OverlayImpl::onHandoff( if (consumer.disconnect(journal)) return handoff; - auto const slot = m_peerFinder->new_inbound_slot( + auto const [slot, result] = m_peerFinder->new_inbound_slot( beast::IPAddressConversion::from_asio(local_endpoint), beast::IPAddressConversion::from_asio(remote_endpoint)); if (slot == nullptr) { - // self-connect, close + // connection refused either IP limit exceeded or self-connect handoff.moved = false; + JLOG(journal.debug()) + << "Peer " << remote_endpoint << " refused, " << to_string(result); return handoff; } @@ -402,10 +404,11 @@ OverlayImpl::connect(beast::IP::Endpoint const& remote_endpoint) return; } - auto const slot = peerFinder().new_outbound_slot(remote_endpoint); + auto const [slot, result] = peerFinder().new_outbound_slot(remote_endpoint); if (slot == nullptr) { - JLOG(journal_.debug()) << "Connect: No slot for " << remote_endpoint; + JLOG(journal_.debug()) << "Connect: No slot for " << remote_endpoint + << ": " << to_string(result); return; } diff --git a/src/xrpld/peerfinder/PeerfinderManager.h b/src/xrpld/peerfinder/PeerfinderManager.h index f399251c38..b7ea738a81 100644 --- a/src/xrpld/peerfinder/PeerfinderManager.h +++ b/src/xrpld/peerfinder/PeerfinderManager.h @@ -109,6 +109,9 @@ struct Config std::uint16_t port, bool validationPublicKey, int ipLimit); + + friend bool + operator==(Config const& lhs, Config const& rhs); }; //------------------------------------------------------------------------------ @@ -136,7 +139,13 @@ using Endpoints = std::vector; //------------------------------------------------------------------------------ /** Possible results from activating a slot. */ -enum class Result { duplicate, full, success }; +enum class Result { + inboundDisabled, + duplicatePeer, + ipLimitExceeded, + full, + success +}; /** * @brief Converts a `Result` enum value to its string representation. @@ -157,12 +166,16 @@ to_string(Result result) noexcept { switch (result) { - case Result::success: - return "success"; - case Result::duplicate: - return "duplicate connection"; + case Result::inboundDisabled: + return "inbound disabled"; + case Result::duplicatePeer: + return "peer already connected"; + case Result::ipLimitExceeded: + return "ip limit exceeded"; case Result::full: return "slots full"; + case Result::success: + return "success"; } return "unknown"; @@ -234,7 +247,7 @@ public: If nullptr is returned, then the slot could not be assigned. Usually this is because of a detected self-connection. */ - virtual std::shared_ptr + virtual std::pair, Result> new_inbound_slot( beast::IP::Endpoint const& local_endpoint, beast::IP::Endpoint const& remote_endpoint) = 0; @@ -243,7 +256,7 @@ public: If nullptr is returned, then the slot could not be assigned. Usually this is because of a duplicate connection. */ - virtual std::shared_ptr + virtual std::pair, Result> new_outbound_slot(beast::IP::Endpoint const& remote_endpoint) = 0; /** Called when mtENDPOINTS is received. */ diff --git a/src/xrpld/peerfinder/detail/Counts.h b/src/xrpld/peerfinder/detail/Counts.h index c91b27b026..821431c5bb 100644 --- a/src/xrpld/peerfinder/detail/Counts.h +++ b/src/xrpld/peerfinder/detail/Counts.h @@ -163,7 +163,7 @@ public: /** Returns the total number of inbound slots. */ int - inboundSlots() const + in_max() const { return m_in_max; } diff --git a/src/xrpld/peerfinder/detail/Logic.h b/src/xrpld/peerfinder/detail/Logic.h index e23bbc29e1..4b92a1d143 100644 --- a/src/xrpld/peerfinder/detail/Logic.h +++ b/src/xrpld/peerfinder/detail/Logic.h @@ -172,9 +172,7 @@ public: void addFixedPeer(std::string const& name, beast::IP::Endpoint const& ep) { - std::vector v; - v.push_back(ep); - addFixedPeer(name, v); + addFixedPeer(name, std::vector{ep}); } void @@ -261,7 +259,7 @@ public: //-------------------------------------------------------------------------- - SlotImp::ptr + std::pair new_inbound_slot( beast::IP::Endpoint const& local_endpoint, beast::IP::Endpoint const& remote_endpoint) @@ -277,12 +275,12 @@ public: { auto const count = connectedAddresses_.count(remote_endpoint.address()); - if (count > config_.ipLimit) + if (count + 1 > config_.ipLimit) { JLOG(m_journal.debug()) << beast::leftw(18) << "Logic dropping inbound " << remote_endpoint << " because of ip limits."; - return SlotImp::ptr(); + return {SlotImp::ptr(), Result::ipLimitExceeded}; } } @@ -292,7 +290,7 @@ public: JLOG(m_journal.debug()) << beast::leftw(18) << "Logic dropping " << remote_endpoint << " as duplicate incoming"; - return SlotImp::ptr(); + return {SlotImp::ptr(), Result::duplicatePeer}; } // Create the slot @@ -314,11 +312,11 @@ public: // Update counts counts_.add(*slot); - return result.first->second; + return {result.first->second, Result::success}; } // Can't check for self-connect because we don't know the local endpoint - SlotImp::ptr + std::pair new_outbound_slot(beast::IP::Endpoint const& remote_endpoint) { JLOG(m_journal.debug()) @@ -332,7 +330,7 @@ public: JLOG(m_journal.debug()) << beast::leftw(18) << "Logic dropping " << remote_endpoint << " as duplicate connect"; - return SlotImp::ptr(); + return {SlotImp::ptr(), Result::duplicatePeer}; } // Create the slot @@ -353,7 +351,7 @@ public: // Update counts counts_.add(*slot); - return result.first->second; + return {result.first->second, Result::success}; } bool @@ -417,7 +415,7 @@ public: // Check for duplicate connection by key if (keys_.find(key) != keys_.end()) - return Result::duplicate; + return Result::duplicatePeer; // If the peer belongs to a cluster or is reserved, // update the slot to reflect that. @@ -430,6 +428,8 @@ public: { if (!slot->inbound()) bootcache_.on_success(slot->remote_endpoint()); + if (slot->inbound() && counts_.in_max() == 0) + return Result::inboundDisabled; return Result::full; } @@ -651,7 +651,7 @@ public: // 2. We have slots // 3. We haven't failed the firewalled test // - if (config_.wantIncoming && counts_.inboundSlots() > 0) + if (config_.wantIncoming && counts_.in_max() > 0) { Endpoint ep; ep.hops = 0; diff --git a/src/xrpld/peerfinder/detail/PeerfinderConfig.cpp b/src/xrpld/peerfinder/detail/PeerfinderConfig.cpp index 3075224189..30eb778770 100644 --- a/src/xrpld/peerfinder/detail/PeerfinderConfig.cpp +++ b/src/xrpld/peerfinder/detail/PeerfinderConfig.cpp @@ -34,6 +34,17 @@ Config::Config() { } +bool +operator==(Config const& lhs, Config const& rhs) +{ + return lhs.autoConnect == rhs.autoConnect && + lhs.peerPrivate == rhs.peerPrivate && + lhs.wantIncoming == rhs.wantIncoming && lhs.inPeers == rhs.inPeers && + lhs.maxPeers == rhs.maxPeers && lhs.outPeers == rhs.outPeers && + lhs.features == lhs.features && lhs.ipLimit == rhs.ipLimit && + lhs.listeningPort == rhs.listeningPort; +} + std::size_t Config::calcOutPeers() const { diff --git a/src/xrpld/peerfinder/detail/PeerfinderManager.cpp b/src/xrpld/peerfinder/detail/PeerfinderManager.cpp index 205df67fa6..462820cca2 100644 --- a/src/xrpld/peerfinder/detail/PeerfinderManager.cpp +++ b/src/xrpld/peerfinder/detail/PeerfinderManager.cpp @@ -125,7 +125,7 @@ public: //-------------------------------------------------------------------------- - std::shared_ptr + std::pair, Result> new_inbound_slot( beast::IP::Endpoint const& local_endpoint, beast::IP::Endpoint const& remote_endpoint) override @@ -133,7 +133,7 @@ public: return m_logic.new_inbound_slot(local_endpoint, remote_endpoint); } - std::shared_ptr + std::pair, Result> new_outbound_slot(beast::IP::Endpoint const& remote_endpoint) override { return m_logic.new_outbound_slot(remote_endpoint); From e0b9812fc590fb87796b8dae7253aeca6f1737e7 Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Fri, 29 Aug 2025 15:52:09 -0400 Subject: [PATCH 5/5] Refactor `ledger_entry` RPC source code and tests (#5237) This is a major refactor of LedgerEntry.cpp. It adds a number of helper functions to make the code easier to maintain. It also splits up the ledger and ledger_entry tests into different files, and cleans up the ledger_entry tests to make them easier to write and maintain. This refactor also caught a few bugs in some of the other RPC processing, so those are fixed along the way. --- include/xrpl/protocol/ErrorCodes.h | 7 +- include/xrpl/protocol/jss.h | 4 + src/libxrpl/json/json_value.cpp | 5 +- src/libxrpl/protocol/ErrorCodes.cpp | 5 +- src/libxrpl/protocol/STXChainBridge.cpp | 19 +- src/test/app/Vault_test.cpp | 12 - src/test/jtx/impl/xchain_bridge.cpp | 16 +- src/test/rpc/LedgerEntry_test.cpp | 1869 ++++++++----------- src/xrpld/rpc/detail/RPCHelpers.cpp | 17 +- src/xrpld/rpc/handlers/LedgerEntry.cpp | 1171 +++++------- src/xrpld/rpc/handlers/LedgerEntryHelpers.h | 299 +++ 11 files changed, 1577 insertions(+), 1847 deletions(-) create mode 100644 src/xrpld/rpc/handlers/LedgerEntryHelpers.h diff --git a/include/xrpl/protocol/ErrorCodes.h b/include/xrpl/protocol/ErrorCodes.h index f06b927566..5da3ad0b33 100644 --- a/include/xrpl/protocol/ErrorCodes.h +++ b/include/xrpl/protocol/ErrorCodes.h @@ -157,7 +157,12 @@ enum error_code_i { // Pathfinding rpcDOMAIN_MALFORMED = 97, - rpcLAST = rpcDOMAIN_MALFORMED // rpcLAST should always equal the last code. + // ledger_entry + rpcENTRY_NOT_FOUND = 98, + rpcUNEXPECTED_LEDGER_TYPE = 99, + + rpcLAST = + rpcUNEXPECTED_LEDGER_TYPE // rpcLAST should always equal the last code. }; /** Codes returned in the `warnings` array of certain RPC commands. diff --git a/include/xrpl/protocol/jss.h b/include/xrpl/protocol/jss.h index 67a045fa58..68d2497aca 100644 --- a/include/xrpl/protocol/jss.h +++ b/include/xrpl/protocol/jss.h @@ -68,9 +68,13 @@ JSS(Flags); // in/out: TransactionSign; field. JSS(Holder); // field. JSS(Invalid); // JSS(Issuer); // in: Credential transactions +JSS(IssuingChainDoor); // field. +JSS(IssuingChainIssue); // field. JSS(LastLedgerSequence); // in: TransactionSign; field JSS(LastUpdateTime); // field. JSS(LimitAmount); // field. +JSS(LockingChainDoor); // field. +JSS(LockingChainIssue); // field. JSS(NetworkID); // field. JSS(LPTokenOut); // in: AMM Liquidity Provider deposit tokens JSS(LPTokenIn); // in: AMM Liquidity Provider withdraw tokens diff --git a/src/libxrpl/json/json_value.cpp b/src/libxrpl/json/json_value.cpp index a1e0a04875..1df8f6cf31 100644 --- a/src/libxrpl/json/json_value.cpp +++ b/src/libxrpl/json/json_value.cpp @@ -24,6 +24,7 @@ #include #include +#include #include #include #include @@ -685,7 +686,9 @@ Value::isConvertibleTo(ValueType other) const (other == intValue && value_.real_ >= minInt && value_.real_ <= maxInt) || (other == uintValue && value_.real_ >= 0 && - value_.real_ <= maxUInt) || + value_.real_ <= maxUInt && + std::fabs(round(value_.real_) - value_.real_) < + std::numeric_limits::epsilon()) || other == realValue || other == stringValue || other == booleanValue; diff --git a/src/libxrpl/protocol/ErrorCodes.cpp b/src/libxrpl/protocol/ErrorCodes.cpp index 3109f51d05..ec295343ce 100644 --- a/src/libxrpl/protocol/ErrorCodes.cpp +++ b/src/libxrpl/protocol/ErrorCodes.cpp @@ -117,7 +117,10 @@ constexpr static ErrorInfo unorderedErrorInfos[]{ {rpcORACLE_MALFORMED, "oracleMalformed", "Oracle request is malformed.", 400}, {rpcBAD_CREDENTIALS, "badCredentials", "Credentials do not exist, are not accepted, or have expired.", 400}, {rpcTX_SIGNED, "transactionSigned", "Transaction should not be signed.", 400}, - {rpcDOMAIN_MALFORMED, "domainMalformed", "Domain is malformed.", 400}}; + {rpcDOMAIN_MALFORMED, "domainMalformed", "Domain is malformed.", 400}, + {rpcENTRY_NOT_FOUND, "entryNotFound", "Entry not found.", 400}, + {rpcUNEXPECTED_LEDGER_TYPE, "unexpectedLedgerType", "Unexpected ledger type.", 400}, +}; // clang-format on // Sort and validate unorderedErrorInfos at compile time. Should be diff --git a/src/libxrpl/protocol/STXChainBridge.cpp b/src/libxrpl/protocol/STXChainBridge.cpp index fb192d82d6..e835735f08 100644 --- a/src/libxrpl/protocol/STXChainBridge.cpp +++ b/src/libxrpl/protocol/STXChainBridge.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include @@ -98,12 +99,10 @@ STXChainBridge::STXChainBridge(SField const& name, Json::Value const& v) }; checkExtra(v); - Json::Value const& lockingChainDoorStr = - v[sfLockingChainDoor.getJsonName()]; - Json::Value const& lockingChainIssue = v[sfLockingChainIssue.getJsonName()]; - Json::Value const& issuingChainDoorStr = - v[sfIssuingChainDoor.getJsonName()]; - Json::Value const& issuingChainIssue = v[sfIssuingChainIssue.getJsonName()]; + Json::Value const& lockingChainDoorStr = v[jss::LockingChainDoor]; + Json::Value const& lockingChainIssue = v[jss::LockingChainIssue]; + Json::Value const& issuingChainDoorStr = v[jss::IssuingChainDoor]; + Json::Value const& issuingChainIssue = v[jss::IssuingChainIssue]; if (!lockingChainDoorStr.isString()) { @@ -161,10 +160,10 @@ Json::Value STXChainBridge::getJson(JsonOptions jo) const { Json::Value v; - v[sfLockingChainDoor.getJsonName()] = lockingChainDoor_.getJson(jo); - v[sfLockingChainIssue.getJsonName()] = lockingChainIssue_.getJson(jo); - v[sfIssuingChainDoor.getJsonName()] = issuingChainDoor_.getJson(jo); - v[sfIssuingChainIssue.getJsonName()] = issuingChainIssue_.getJson(jo); + v[jss::LockingChainDoor] = lockingChainDoor_.getJson(jo); + v[jss::LockingChainIssue] = lockingChainIssue_.getJson(jo); + v[jss::IssuingChainDoor] = issuingChainDoor_.getJson(jo); + v[jss::IssuingChainIssue] = issuingChainIssue_.getJson(jo); return v; } diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp index 7ea38db2b1..7add8b3eda 100644 --- a/src/test/app/Vault_test.cpp +++ b/src/test/app/Vault_test.cpp @@ -3028,18 +3028,6 @@ class Vault_test : public beast::unit_test::suite "malformedRequest"); } - { - testcase("RPC ledger_entry zero seq"); - Json::Value jvParams; - jvParams[jss::ledger_index] = jss::validated; - jvParams[jss::vault][jss::owner] = issuer.human(); - jvParams[jss::vault][jss::seq] = 0; - auto jvVault = env.rpc("json", "ledger_entry", to_string(jvParams)); - BEAST_EXPECT( - jvVault[jss::result][jss::error].asString() == - "malformedRequest"); - } - { testcase("RPC ledger_entry negative seq"); Json::Value jvParams; diff --git a/src/test/jtx/impl/xchain_bridge.cpp b/src/test/jtx/impl/xchain_bridge.cpp index 6f167d7508..9e8fa4795f 100644 --- a/src/test/jtx/impl/xchain_bridge.cpp +++ b/src/test/jtx/impl/xchain_bridge.cpp @@ -44,10 +44,10 @@ bridge( Issue const& issuingChainIssue) { Json::Value jv; - jv[sfLockingChainDoor.getJsonName()] = lockingChainDoor.human(); - jv[sfLockingChainIssue.getJsonName()] = to_json(lockingChainIssue); - jv[sfIssuingChainDoor.getJsonName()] = issuingChainDoor.human(); - jv[sfIssuingChainIssue.getJsonName()] = to_json(issuingChainIssue); + jv[jss::LockingChainDoor] = lockingChainDoor.human(); + jv[jss::LockingChainIssue] = to_json(lockingChainIssue); + jv[jss::IssuingChainDoor] = issuingChainDoor.human(); + jv[jss::IssuingChainIssue] = to_json(issuingChainIssue); return jv; } @@ -60,10 +60,10 @@ bridge_rpc( Issue const& issuingChainIssue) { Json::Value jv; - jv[sfLockingChainDoor.getJsonName()] = lockingChainDoor.human(); - jv[sfLockingChainIssue.getJsonName()] = to_json(lockingChainIssue); - jv[sfIssuingChainDoor.getJsonName()] = issuingChainDoor.human(); - jv[sfIssuingChainIssue.getJsonName()] = to_json(issuingChainIssue); + jv[jss::LockingChainDoor] = lockingChainDoor.human(); + jv[jss::LockingChainIssue] = to_json(lockingChainIssue); + jv[jss::IssuingChainDoor] = issuingChainDoor.human(); + jv[jss::IssuingChainIssue] = to_json(issuingChainIssue); return jv; } diff --git a/src/test/rpc/LedgerEntry_test.cpp b/src/test/rpc/LedgerEntry_test.cpp index 89cb7b72eb..a88f6ab612 100644 --- a/src/test/rpc/LedgerEntry_test.cpp +++ b/src/test/rpc/LedgerEntry_test.cpp @@ -31,40 +31,435 @@ #include #include +#if (defined(__clang_major__) && __clang_major__ < 15) +#include +using source_location = std::experimental::source_location; +#else +#include +using std::source_location; +#endif namespace ripple { namespace test { +enum class FieldType { + AccountField, + BlobField, + ArrayField, + CurrencyField, + HashField, + HashOrObjectField, + ObjectField, + StringField, + TwoAccountArrayField, + UInt32Field, + UInt64Field, +}; + +std::vector> mappings{ + {jss::account, FieldType::AccountField}, + {jss::accounts, FieldType::TwoAccountArrayField}, + {jss::authorize, FieldType::AccountField}, + {jss::authorized, FieldType::AccountField}, + {jss::credential_type, FieldType::BlobField}, + {jss::currency, FieldType::CurrencyField}, + {jss::issuer, FieldType::AccountField}, + {jss::oracle_document_id, FieldType::UInt32Field}, + {jss::owner, FieldType::AccountField}, + {jss::seq, FieldType::UInt32Field}, + {jss::subject, FieldType::AccountField}, + {jss::ticket_seq, FieldType::UInt32Field}, +}; + +FieldType +getFieldType(Json::StaticString fieldName) +{ + auto it = std::ranges::find_if(mappings, [&fieldName](auto const& pair) { + return pair.first == fieldName; + }); + if (it != mappings.end()) + { + return it->second; + } + else + { + Throw( + "`mappings` is missing field " + std::string(fieldName.c_str())); + } +} + +std::string +getTypeName(FieldType typeID) +{ + switch (typeID) + { + case FieldType::UInt32Field: + return "number"; + case FieldType::UInt64Field: + return "number"; + case FieldType::HashField: + return "hex string"; + case FieldType::AccountField: + return "AccountID"; + case FieldType::BlobField: + return "hex string"; + case FieldType::CurrencyField: + return "Currency"; + case FieldType::ArrayField: + return "array"; + case FieldType::HashOrObjectField: + return "hex string or object"; + case FieldType::TwoAccountArrayField: + return "length-2 array of Accounts"; + default: + Throw( + "unknown type " + std::to_string(static_cast(typeID))); + } +} + class LedgerEntry_test : public beast::unit_test::suite { void checkErrorValue( Json::Value const& jv, std::string const& err, - std::string const& msg) + std::string const& msg, + source_location const location = source_location::current()) { if (BEAST_EXPECT(jv.isMember(jss::status))) - BEAST_EXPECT(jv[jss::status] == "error"); + BEAST_EXPECTS( + jv[jss::status] == "error", std::to_string(location.line())); if (BEAST_EXPECT(jv.isMember(jss::error))) - BEAST_EXPECT(jv[jss::error] == err); + BEAST_EXPECTS( + jv[jss::error] == err, + "Expected error " + err + ", received " + + jv[jss::error].asString() + ", at line " + + std::to_string(location.line()) + ", " + + jv.toStyledString()); if (msg.empty()) { - BEAST_EXPECT( + BEAST_EXPECTS( jv[jss::error_message] == Json::nullValue || - jv[jss::error_message] == ""); + jv[jss::error_message] == "", + "Expected no error message, received \"" + + jv[jss::error_message].asString() + "\", at line " + + std::to_string(location.line()) + ", " + + jv.toStyledString()); } else if (BEAST_EXPECT(jv.isMember(jss::error_message))) - BEAST_EXPECT(jv[jss::error_message] == msg); + BEAST_EXPECTS( + jv[jss::error_message] == msg, + "Expected error message \"" + msg + "\", received \"" + + jv[jss::error_message].asString() + "\", at line " + + std::to_string(location.line()) + ", " + + jv.toStyledString()); } - // Corrupt a valid address by replacing the 10th character with '!'. - // '!' is not part of the ripple alphabet. - std::string - makeBadAddress(std::string good) + std::vector + getBadValues(FieldType fieldType) { - std::string ret = std::move(good); - ret.replace(10, 1, 1, '!'); - return ret; + static Json::Value const injectObject = []() { + Json::Value obj(Json::objectValue); + obj[jss::account] = "rhigTLJJyXXSRUyRCQtqi1NoAZZzZnS4KU"; + obj[jss::ledger_index] = "validated"; + return obj; + }(); + static Json::Value const injectArray = []() { + Json::Value arr(Json::arrayValue); + arr[0u] = "rhigTLJJyXXSRUyRCQtqi1NoAZZzZnS4KU"; + arr[1u] = "validated"; + return arr; + }(); + static std::array const allBadValues = { + "", // 0 + true, // 1 + 1, // 2 + "1", // 3 + -1, // 4 + 1.1, // 5 + "-1", // 6 + "abcdef", // 7 + "ABCDEF", // 8 + "12KK", // 9 + "0123456789ABCDEFGH", // 10 + "rJxKV9e9p6wiPw!!!!xrJ4X1n98LosPL1sgcJW", // 11 + "rPSTrR5yEr11uMkfsz1kHCp9jK4aoa3Avv", // 12 + "n9K2isxwTxcSHJKxMkJznDoWXAUs7NNy49H9Fknz1pC7oHAH3kH9", // 13 + "USD", // 14 + "USDollars", // 15 + "5233D68B4D44388F98559DE42903767803EFA7C1F8D01413FC16EE6B01403D" + "6D", // 16 + Json::arrayValue, // 17 + Json::objectValue, // 18 + injectObject, // 19 + injectArray // 20 + }; + + auto remove = + [&](std::vector indices) -> std::vector { + std::unordered_set indexSet( + indices.begin(), indices.end()); + std::vector values; + values.reserve(allBadValues.size() - indexSet.size()); + for (std::size_t i = 0; i < allBadValues.size(); ++i) + { + if (indexSet.find(i) == indexSet.end()) + { + values.push_back(allBadValues[i]); + } + } + return values; + }; + + static auto const& badUInt32Values = remove({2, 3}); + static auto const& badUInt64Values = remove({2, 3}); + static auto const& badHashValues = remove({2, 3, 7, 8, 16}); + static auto const& badAccountValues = remove({12}); + static auto const& badBlobValues = remove({3, 7, 8, 16}); + static auto const& badCurrencyValues = remove({14}); + static auto const& badArrayValues = remove({17, 20}); + static auto const& badIndexValues = remove({12, 16, 18, 19}); + + switch (fieldType) + { + case FieldType::UInt32Field: + return badUInt32Values; + case FieldType::UInt64Field: + return badUInt64Values; + case FieldType::HashField: + return badHashValues; + case FieldType::AccountField: + return badAccountValues; + case FieldType::BlobField: + return badBlobValues; + case FieldType::CurrencyField: + return badCurrencyValues; + case FieldType::ArrayField: + case FieldType::TwoAccountArrayField: + return badArrayValues; + case FieldType::HashOrObjectField: + return badIndexValues; + default: + Throw( + "unknown type " + + std::to_string(static_cast(fieldType))); + } + } + + Json::Value + getCorrectValue(Json::StaticString fieldName) + { + static Json::Value const twoAccountArray = []() { + Json::Value arr(Json::arrayValue); + arr[0u] = "rhigTLJJyXXSRUyRCQtqi1NoAZZzZnS4KU"; + arr[1u] = "r4MrUGTdB57duTnRs6KbsRGQXgkseGb1b5"; + return arr; + }(); + + auto const typeID = getFieldType(fieldName); + switch (typeID) + { + case FieldType::UInt32Field: + return 1; + case FieldType::UInt64Field: + return 1; + case FieldType::HashField: + return "5233D68B4D44388F98559DE42903767803EFA7C1F8D01413FC16EE6" + "B01403D6D"; + case FieldType::AccountField: + return "r4MrUGTdB57duTnRs6KbsRGQXgkseGb1b5"; + case FieldType::BlobField: + return "ABCDEF"; + case FieldType::CurrencyField: + return "USD"; + case FieldType::ArrayField: + return Json::arrayValue; + case FieldType::HashOrObjectField: + return "5233D68B4D44388F98559DE42903767803EFA7C1F8D01413FC16EE6" + "B01403D6D"; + case FieldType::TwoAccountArrayField: + return twoAccountArray; + default: + Throw( + "unknown type " + + std::to_string(static_cast(typeID))); + } + } + + void + testMalformedField( + test::jtx::Env& env, + Json::Value correctRequest, + Json::StaticString const fieldName, + FieldType const typeID, + std::string const& expectedError, + bool required = true, + source_location const location = source_location::current()) + { + forAllApiVersions([&, this](unsigned apiVersion) { + if (required) + { + correctRequest.removeMember(fieldName); + Json::Value const jrr = env.rpc( + apiVersion, + "json", + "ledger_entry", + to_string(correctRequest))[jss::result]; + if (apiVersion < 2u) + checkErrorValue(jrr, "unknownOption", "", location); + else + checkErrorValue( + jrr, + "invalidParams", + "No ledger_entry params provided.", + location); + } + auto tryField = [&](Json::Value fieldValue) -> void { + correctRequest[fieldName] = fieldValue; + Json::Value const jrr = env.rpc( + apiVersion, + "json", + "ledger_entry", + to_string(correctRequest))[jss::result]; + auto const expectedErrMsg = + RPC::expected_field_message(fieldName, getTypeName(typeID)); + checkErrorValue(jrr, expectedError, expectedErrMsg, location); + }; + + auto const& badValues = getBadValues(typeID); + for (auto const& value : badValues) + { + tryField(value); + } + if (required) + { + tryField(Json::nullValue); + } + }); + } + + void + testMalformedSubfield( + test::jtx::Env& env, + Json::Value correctRequest, + Json::StaticString parentFieldName, + Json::StaticString fieldName, + FieldType typeID, + std::string const& expectedError, + bool required = true, + source_location const location = source_location::current()) + { + forAllApiVersions([&, this](unsigned apiVersion) { + if (required) + { + correctRequest[parentFieldName].removeMember(fieldName); + Json::Value const jrr = env.rpc( + apiVersion, + "json", + "ledger_entry", + to_string(correctRequest))[jss::result]; + checkErrorValue( + jrr, + "malformedRequest", + RPC::missing_field_message(fieldName.c_str()), + location); + + correctRequest[parentFieldName][fieldName] = Json::nullValue; + Json::Value const jrr2 = env.rpc( + apiVersion, + "json", + "ledger_entry", + to_string(correctRequest))[jss::result]; + checkErrorValue( + jrr2, + "malformedRequest", + RPC::missing_field_message(fieldName.c_str()), + location); + } + auto tryField = [&](Json::Value fieldValue) -> void { + correctRequest[parentFieldName][fieldName] = fieldValue; + + Json::Value const jrr = env.rpc( + apiVersion, + "json", + "ledger_entry", + to_string(correctRequest))[jss::result]; + checkErrorValue( + jrr, + expectedError, + RPC::expected_field_message(fieldName, getTypeName(typeID)), + location); + }; + + auto const& badValues = getBadValues(typeID); + for (auto const& value : badValues) + { + tryField(value); + } + }); + } + + // No subfields + void + runLedgerEntryTest( + test::jtx::Env& env, + Json::StaticString const& parentField, + source_location const location = source_location::current()) + { + testMalformedField( + env, + Json::Value{}, + parentField, + FieldType::HashField, + "malformedRequest", + true, + location); + } + + struct Subfield + { + Json::StaticString fieldName; + std::string malformedErrorMsg; + bool required = true; + }; + + void + runLedgerEntryTest( + test::jtx::Env& env, + Json::StaticString const& parentField, + std::vector const& subfields, + source_location const location = source_location::current()) + { + testMalformedField( + env, + Json::Value{}, + parentField, + FieldType::HashOrObjectField, + "malformedRequest", + true, + location); + + Json::Value correctOutput; + correctOutput[parentField] = Json::objectValue; + for (auto const& subfield : subfields) + { + correctOutput[parentField][subfield.fieldName] = + getCorrectValue(subfield.fieldName); + } + + for (auto const& subfield : subfields) + { + auto const fieldType = getFieldType(subfield.fieldName); + testMalformedSubfield( + env, + correctOutput, + parentField, + subfield.fieldName, + fieldType, + subfield.malformedErrorMsg, + subfield.required, + location); + } } void @@ -76,7 +471,6 @@ class LedgerEntry_test : public beast::unit_test::suite Account const alice{"alice"}; env.fund(XRP(10000), alice); env.close(); - { // Missing ledger_entry ledger_hash Json::Value jvParams; @@ -88,6 +482,33 @@ class LedgerEntry_test : public beast::unit_test::suite "json", "ledger_entry", to_string(jvParams))[jss::result]; checkErrorValue(jrr, "lgrNotFound", "ledgerNotFound"); } + { + // Missing ledger_entry ledger_hash + Json::Value jvParams; + jvParams[jss::account_root] = alice.human(); + auto const typeId = FieldType::HashField; + + forAllApiVersions([&, this](unsigned apiVersion) { + auto tryField = [&](Json::Value fieldValue) -> void { + jvParams[jss::ledger_hash] = fieldValue; + Json::Value const jrr = env.rpc( + apiVersion, + "json", + "ledger_entry", + to_string(jvParams))[jss::result]; + auto const expectedErrMsg = fieldValue.isString() + ? "ledgerHashMalformed" + : "ledgerHashNotString"; + checkErrorValue(jrr, "invalidParams", expectedErrMsg); + }; + + auto const& badValues = getBadValues(typeId); + for (auto const& value : badValues) + { + tryField(value); + } + }); + } { // ask for an zero index @@ -95,17 +516,38 @@ class LedgerEntry_test : public beast::unit_test::suite jvParams[jss::ledger_index] = "validated"; jvParams[jss::index] = "00000000000000000000000000000000000000000000000000000000000000" - "0000"; + "00"; auto const jrr = env.rpc( "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "malformedRequest", ""); + checkErrorValue(jrr, "entryNotFound", "Entry not found."); } + + forAllApiVersions([&, this](unsigned apiVersion) { + // "features" is not an option supported by ledger_entry. + { + Json::Value jvParams = Json::objectValue; + jvParams[jss::features] = + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAA"; + jvParams[jss::api_version] = apiVersion; + Json::Value const jrr = env.rpc( + "json", "ledger_entry", to_string(jvParams))[jss::result]; + + if (apiVersion < 2u) + checkErrorValue(jrr, "unknownOption", ""); + else + checkErrorValue( + jrr, + "invalidParams", + "No ledger_entry params provided."); + } + }); } void testLedgerEntryAccountRoot() { - testcase("ledger_entry Request AccountRoot"); + testcase("AccountRoot"); using namespace test::jtx; auto cfg = envconfig(); @@ -176,13 +618,26 @@ class LedgerEntry_test : public beast::unit_test::suite BEAST_EXPECT(jrr[jss::node][sfBalance.jsonName] == "10000000000"); } { - // Request using a corrupted AccountID. + // Check alias Json::Value jvParams; - jvParams[jss::account_root] = makeBadAddress(alice.human()); + jvParams[jss::account] = alice.human(); jvParams[jss::ledger_hash] = ledgerHash; Json::Value const jrr = env.rpc( "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "malformedAddress", ""); + BEAST_EXPECT(jrr.isMember(jss::node)); + BEAST_EXPECT(jrr[jss::node][jss::Account] == alice.human()); + BEAST_EXPECT(jrr[jss::node][sfBalance.jsonName] == "10000000000"); + accountRootIndex = jrr[jss::index].asString(); + } + { + // Check malformed cases + Json::Value jvParams; + testMalformedField( + env, + jvParams, + jss::account_root, + FieldType::AccountField, + "malformedAddress"); } { // Request an account that is not in the ledger. @@ -191,14 +646,14 @@ class LedgerEntry_test : public beast::unit_test::suite jvParams[jss::ledger_hash] = ledgerHash; Json::Value const jrr = env.rpc( "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "entryNotFound", ""); + checkErrorValue(jrr, "entryNotFound", "Entry not found."); } } void testLedgerEntryCheck() { - testcase("ledger_entry Request Check"); + testcase("Check"); using namespace test::jtx; Env env{*this}; Account const alice{"alice"}; @@ -238,14 +693,19 @@ class LedgerEntry_test : public beast::unit_test::suite jvParams[jss::ledger_hash] = ledgerHash; Json::Value const jrr = env.rpc( "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "unexpectedLedgerType", ""); + checkErrorValue( + jrr, "unexpectedLedgerType", "Unexpected ledger type."); + } + { + // Check malformed cases + runLedgerEntryTest(env, jss::check); } } void testLedgerEntryCredentials() { - testcase("ledger_entry credentials"); + testcase("Credentials"); using namespace test::jtx; @@ -287,163 +747,33 @@ class LedgerEntry_test : public beast::unit_test::suite jss::Credential); } - { - // Fail, index not a hash - auto const jv = credentials::ledgerEntry(env, ""); - checkErrorValue(jv[jss::result], "malformedRequest", ""); - } - { // Fail, credential doesn't exist auto const jv = credentials::ledgerEntry( env, "48004829F915654A81B11C4AB8218D96FED67F209B58328A72314FB6EA288B" "E4"); - checkErrorValue(jv[jss::result], "entryNotFound", ""); + checkErrorValue( + jv[jss::result], "entryNotFound", "Entry not found."); } { - // Fail, invalid subject - Json::Value jv; - jv[jss::ledger_index] = jss::validated; - jv[jss::credential][jss::subject] = 42; - jv[jss::credential][jss::issuer] = issuer.human(); - jv[jss::credential][jss::credential_type] = - strHex(std::string_view(credType)); - auto const jrr = env.rpc("json", "ledger_entry", to_string(jv)); - checkErrorValue(jrr[jss::result], "malformedRequest", ""); - } - - { - // Fail, invalid issuer - Json::Value jv; - jv[jss::ledger_index] = jss::validated; - jv[jss::credential][jss::subject] = alice.human(); - jv[jss::credential][jss::issuer] = 42; - jv[jss::credential][jss::credential_type] = - strHex(std::string_view(credType)); - auto const jrr = env.rpc("json", "ledger_entry", to_string(jv)); - checkErrorValue(jrr[jss::result], "malformedRequest", ""); - } - - { - // Fail, invalid credentials type - Json::Value jv; - jv[jss::ledger_index] = jss::validated; - jv[jss::credential][jss::subject] = alice.human(); - jv[jss::credential][jss::issuer] = issuer.human(); - jv[jss::credential][jss::credential_type] = 42; - auto const jrr = env.rpc("json", "ledger_entry", to_string(jv)); - checkErrorValue(jrr[jss::result], "malformedRequest", ""); - } - - { - // Fail, empty subject - Json::Value jv; - jv[jss::ledger_index] = jss::validated; - jv[jss::credential][jss::subject] = ""; - jv[jss::credential][jss::issuer] = issuer.human(); - jv[jss::credential][jss::credential_type] = - strHex(std::string_view(credType)); - auto const jrr = env.rpc("json", "ledger_entry", to_string(jv)); - checkErrorValue(jrr[jss::result], "malformedRequest", ""); - } - - { - // Fail, empty issuer - Json::Value jv; - jv[jss::ledger_index] = jss::validated; - jv[jss::credential][jss::subject] = alice.human(); - jv[jss::credential][jss::issuer] = ""; - jv[jss::credential][jss::credential_type] = - strHex(std::string_view(credType)); - auto const jrr = env.rpc("json", "ledger_entry", to_string(jv)); - checkErrorValue(jrr[jss::result], "malformedRequest", ""); - } - - { - // Fail, empty credentials type - Json::Value jv; - jv[jss::ledger_index] = jss::validated; - jv[jss::credential][jss::subject] = alice.human(); - jv[jss::credential][jss::issuer] = issuer.human(); - jv[jss::credential][jss::credential_type] = ""; - auto const jrr = env.rpc("json", "ledger_entry", to_string(jv)); - checkErrorValue(jrr[jss::result], "malformedRequest", ""); - } - - { - // Fail, no subject - Json::Value jv; - jv[jss::ledger_index] = jss::validated; - jv[jss::credential][jss::issuer] = issuer.human(); - jv[jss::credential][jss::credential_type] = - strHex(std::string_view(credType)); - auto const jrr = env.rpc("json", "ledger_entry", to_string(jv)); - checkErrorValue(jrr[jss::result], "malformedRequest", ""); - } - - { - // Fail, no issuer - Json::Value jv; - jv[jss::ledger_index] = jss::validated; - jv[jss::credential][jss::subject] = alice.human(); - jv[jss::credential][jss::credential_type] = - strHex(std::string_view(credType)); - auto const jrr = env.rpc("json", "ledger_entry", to_string(jv)); - checkErrorValue(jrr[jss::result], "malformedRequest", ""); - } - - { - // Fail, no credentials type - Json::Value jv; - jv[jss::ledger_index] = jss::validated; - jv[jss::credential][jss::subject] = alice.human(); - jv[jss::credential][jss::issuer] = issuer.human(); - auto const jrr = env.rpc("json", "ledger_entry", to_string(jv)); - checkErrorValue(jrr[jss::result], "malformedRequest", ""); - } - - { - // Fail, not AccountID subject - Json::Value jv; - jv[jss::ledger_index] = jss::validated; - jv[jss::credential][jss::subject] = "wehsdbvasbdfvj"; - jv[jss::credential][jss::issuer] = issuer.human(); - jv[jss::credential][jss::credential_type] = - strHex(std::string_view(credType)); - auto const jrr = env.rpc("json", "ledger_entry", to_string(jv)); - checkErrorValue(jrr[jss::result], "malformedRequest", ""); - } - - { - // Fail, not AccountID issuer - Json::Value jv; - jv[jss::ledger_index] = jss::validated; - jv[jss::credential][jss::subject] = alice.human(); - jv[jss::credential][jss::issuer] = "c4p93ugndfbsiu"; - jv[jss::credential][jss::credential_type] = - strHex(std::string_view(credType)); - auto const jrr = env.rpc("json", "ledger_entry", to_string(jv)); - checkErrorValue(jrr[jss::result], "malformedRequest", ""); - } - - { - // Fail, credentials type isn't hex encoded - Json::Value jv; - jv[jss::ledger_index] = jss::validated; - jv[jss::credential][jss::subject] = alice.human(); - jv[jss::credential][jss::issuer] = issuer.human(); - jv[jss::credential][jss::credential_type] = "12KK"; - auto const jrr = env.rpc("json", "ledger_entry", to_string(jv)); - checkErrorValue(jrr[jss::result], "malformedRequest", ""); + // Check all malformed cases + runLedgerEntryTest( + env, + jss::credential, + { + {jss::subject, "malformedRequest"}, + {jss::issuer, "malformedRequest"}, + {jss::credential_type, "malformedRequest"}, + }); } } void testLedgerEntryDelegate() { - testcase("ledger_entry Delegate"); + testcase("Delegate"); using namespace test::jtx; @@ -482,78 +812,23 @@ class LedgerEntry_test : public beast::unit_test::suite BEAST_EXPECT(jrr[jss::node][sfAccount.jsonName] == alice.human()); BEAST_EXPECT(jrr[jss::node][sfAuthorize.jsonName] == bob.human()); } + { - // Malformed request: delegate neither object nor string. - Json::Value jvParams; - jvParams[jss::delegate] = 5; - jvParams[jss::ledger_hash] = ledgerHash; - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "malformedRequest", ""); - } - { - // Malformed request: delegate not hex string. - Json::Value jvParams; - jvParams[jss::delegate] = "0123456789ABCDEFG"; - jvParams[jss::ledger_hash] = ledgerHash; - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "malformedRequest", ""); - } - { - // Malformed request: account not a string - Json::Value jvParams; - jvParams[jss::delegate][jss::account] = 5; - jvParams[jss::delegate][jss::authorize] = bob.human(); - jvParams[jss::ledger_hash] = ledgerHash; - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "malformedAddress", ""); - } - { - // Malformed request: authorize not a string - Json::Value jvParams; - jvParams[jss::delegate][jss::account] = alice.human(); - jvParams[jss::delegate][jss::authorize] = 5; - jvParams[jss::ledger_hash] = ledgerHash; - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "malformedAddress", ""); - } - { - // this lambda function is used test malformed account and authroize - auto testMalformedAccount = - [&](std::optional const& account, - std::optional const& authorize, - std::string const& error) { - Json::Value jvParams; - jvParams[jss::ledger_hash] = ledgerHash; - if (account) - jvParams[jss::delegate][jss::account] = *account; - if (authorize) - jvParams[jss::delegate][jss::authorize] = *authorize; - auto const jrr = env.rpc( - "json", - "ledger_entry", - to_string(jvParams))[jss::result]; - checkErrorValue(jrr, error, ""); - }; - // missing account - testMalformedAccount(std::nullopt, bob.human(), "malformedRequest"); - // missing authorize - testMalformedAccount( - alice.human(), std::nullopt, "malformedRequest"); - // malformed account - testMalformedAccount("-", bob.human(), "malformedAddress"); - // malformed authorize - testMalformedAccount(alice.human(), "-", "malformedAddress"); + // Check all malformed cases + runLedgerEntryTest( + env, + jss::delegate, + { + {jss::account, "malformedAddress"}, + {jss::authorize, "malformedAddress"}, + }); } } void testLedgerEntryDepositPreauth() { - testcase("ledger_entry Deposit Preauth"); + testcase("Deposit Preauth"); using namespace test::jtx; @@ -600,91 +875,21 @@ class LedgerEntry_test : public beast::unit_test::suite BEAST_EXPECT(jrr[jss::node][sfAuthorize.jsonName] == becky.human()); } { - // Malformed request: deposit_preauth neither object nor string. - Json::Value jvParams; - jvParams[jss::deposit_preauth] = -5; - jvParams[jss::ledger_hash] = ledgerHash; - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "malformedRequest", ""); - } - { - // Malformed request: deposit_preauth not hex string. - Json::Value jvParams; - jvParams[jss::deposit_preauth] = "0123456789ABCDEFG"; - jvParams[jss::ledger_hash] = ledgerHash; - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "malformedRequest", ""); - } - { - // Malformed request: missing [jss::deposit_preauth][jss::owner] - Json::Value jvParams; - jvParams[jss::deposit_preauth][jss::authorized] = becky.human(); - jvParams[jss::ledger_hash] = ledgerHash; - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "malformedRequest", ""); - } - { - // Malformed request: [jss::deposit_preauth][jss::owner] not string. - Json::Value jvParams; - jvParams[jss::deposit_preauth][jss::owner] = 7; - jvParams[jss::deposit_preauth][jss::authorized] = becky.human(); - jvParams[jss::ledger_hash] = ledgerHash; - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "malformedRequest", ""); - } - { - // Malformed: missing [jss::deposit_preauth][jss::authorized] - Json::Value jvParams; - jvParams[jss::deposit_preauth][jss::owner] = alice.human(); - jvParams[jss::ledger_hash] = ledgerHash; - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "malformedRequest", ""); - } - { - // Malformed: [jss::deposit_preauth][jss::authorized] not string. - Json::Value jvParams; - jvParams[jss::deposit_preauth][jss::owner] = alice.human(); - jvParams[jss::deposit_preauth][jss::authorized] = 47; - jvParams[jss::ledger_hash] = ledgerHash; - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "malformedRequest", ""); - } - { - // Malformed: [jss::deposit_preauth][jss::owner] is malformed. - Json::Value jvParams; - jvParams[jss::deposit_preauth][jss::owner] = - "rP6P9ypfAmc!pw8SZHNwM4nvZHFXDraQas"; - - jvParams[jss::deposit_preauth][jss::authorized] = becky.human(); - jvParams[jss::ledger_hash] = ledgerHash; - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "malformedOwner", ""); - } - { - // Malformed: [jss::deposit_preauth][jss::authorized] is malformed. - Json::Value jvParams; - jvParams[jss::deposit_preauth][jss::owner] = alice.human(); - jvParams[jss::deposit_preauth][jss::authorized] = - "rP6P9ypfAmc!pw8SZHNwM4nvZHFXDraQas"; - - jvParams[jss::ledger_hash] = ledgerHash; - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "malformedAuthorized", ""); + // test all missing/malformed field cases + runLedgerEntryTest( + env, + jss::deposit_preauth, + { + {jss::owner, "malformedOwner"}, + {jss::authorized, "malformedAuthorized", false}, + }); } } void testLedgerEntryDepositPreauthCred() { - testcase("ledger_entry Deposit Preauth with credentials"); + testcase("Deposit Preauth with credentials"); using namespace test::jtx; @@ -739,19 +944,30 @@ class LedgerEntry_test : public beast::unit_test::suite jvParams[jss::ledger_index] = jss::validated; jvParams[jss::deposit_preauth][jss::owner] = bob.human(); - jvParams[jss::deposit_preauth][jss::authorized_credentials] = - Json::arrayValue; - auto& arr( - jvParams[jss::deposit_preauth][jss::authorized_credentials]); + auto tryField = [&](Json::Value fieldValue) -> void { + Json::Value arr = Json::arrayValue; + Json::Value jo; + jo[jss::issuer] = fieldValue; + jo[jss::credential_type] = strHex(std::string_view(credType)); + arr.append(jo); + jvParams[jss::deposit_preauth][jss::authorized_credentials] = + arr; - Json::Value jo; - jo[jss::issuer] = to_string(xrpAccount()); - jo[jss::credential_type] = strHex(std::string_view(credType)); - arr.append(std::move(jo)); - auto const jrr = - env.rpc("json", "ledger_entry", to_string(jvParams)); - checkErrorValue( - jrr[jss::result], "malformedAuthorizedCredentials", ""); + Json::Value const jrr = env.rpc( + "json", "ledger_entry", to_string(jvParams))[jss::result]; + auto const expectedErrMsg = fieldValue.isNull() + ? RPC::missing_field_message(jss::issuer.c_str()) + : RPC::expected_field_message(jss::issuer, "AccountID"); + checkErrorValue( + jrr, "malformedAuthorizedCredentials", expectedErrMsg); + }; + + auto const& badValues = getBadValues(FieldType::AccountField); + for (auto const& value : badValues) + { + tryField(value); + } + tryField(Json::nullValue); } { @@ -773,7 +989,10 @@ class LedgerEntry_test : public beast::unit_test::suite auto const jrr = env.rpc("json", "ledger_entry", to_string(jvParams)); checkErrorValue( - jrr[jss::result], "malformedAuthorizedCredentials", ""); + jrr[jss::result], + "malformedAuthorizedCredentials", + RPC::expected_field_message( + jss::authorized_credentials, "array")); } { @@ -782,20 +1001,31 @@ class LedgerEntry_test : public beast::unit_test::suite jvParams[jss::ledger_index] = jss::validated; jvParams[jss::deposit_preauth][jss::owner] = bob.human(); - jvParams[jss::deposit_preauth][jss::authorized_credentials] = - Json::arrayValue; - auto& arr( - jvParams[jss::deposit_preauth][jss::authorized_credentials]); + auto tryField = [&](Json::Value fieldValue) -> void { + Json::Value arr = Json::arrayValue; + Json::Value jo; + jo[jss::issuer] = issuer.human(); + jo[jss::credential_type] = fieldValue; + arr.append(jo); + jvParams[jss::deposit_preauth][jss::authorized_credentials] = + arr; - Json::Value jo; - jo[jss::issuer] = issuer.human(); - jo[jss::credential_type] = ""; - arr.append(std::move(jo)); + Json::Value const jrr = env.rpc( + "json", "ledger_entry", to_string(jvParams))[jss::result]; + auto const expectedErrMsg = fieldValue.isNull() + ? RPC::missing_field_message(jss::credential_type.c_str()) + : RPC::expected_field_message( + jss::credential_type, "hex string"); + checkErrorValue( + jrr, "malformedAuthorizedCredentials", expectedErrMsg); + }; - auto const jrr = - env.rpc("json", "ledger_entry", to_string(jvParams)); - checkErrorValue( - jrr[jss::result], "malformedAuthorizedCredentials", ""); + auto const& badValues = getBadValues(FieldType::BlobField); + for (auto const& value : badValues) + { + tryField(value); + } + tryField(Json::nullValue); } { @@ -817,7 +1047,11 @@ class LedgerEntry_test : public beast::unit_test::suite auto const jrr = env.rpc("json", "ledger_entry", to_string(jvParams)); - checkErrorValue(jrr[jss::result], "malformedRequest", ""); + checkErrorValue( + jrr[jss::result], + "malformedRequest", + "Must have exactly one of `authorized` and " + "`authorized_credentials`."); } { @@ -825,11 +1059,14 @@ class LedgerEntry_test : public beast::unit_test::suite Json::Value jvParams; jvParams[jss::ledger_index] = jss::validated; jvParams[jss::deposit_preauth][jss::owner] = bob.human(); - jvParams[jss::deposit_preauth][jss::authorized_credentials] = 42; - - auto const jrr = - env.rpc("json", "ledger_entry", to_string(jvParams)); - checkErrorValue(jrr[jss::result], "malformedRequest", ""); + testMalformedSubfield( + env, + jvParams, + jss::deposit_preauth, + jss::authorized_credentials, + FieldType::ArrayField, + "malformedAuthorizedCredentials", + false); } { @@ -846,7 +1083,9 @@ class LedgerEntry_test : public beast::unit_test::suite auto const jrr = env.rpc("json", "ledger_entry", to_string(jvParams)); checkErrorValue( - jrr[jss::result], "malformedAuthorizedCredentials", ""); + jrr[jss::result], + "malformedAuthorizedCredentials", + "Invalid field 'authorized_credentials', not array."); } { @@ -865,7 +1104,9 @@ class LedgerEntry_test : public beast::unit_test::suite auto const jrr = env.rpc("json", "ledger_entry", to_string(jvParams)); checkErrorValue( - jrr[jss::result], "malformedAuthorizedCredentials", ""); + jrr[jss::result], + "malformedAuthorizedCredentials", + "Invalid field 'authorized_credentials', not array."); } { @@ -879,13 +1120,14 @@ class LedgerEntry_test : public beast::unit_test::suite auto const jrr = env.rpc("json", "ledger_entry", to_string(jvParams)); checkErrorValue( - jrr[jss::result], "malformedAuthorizedCredentials", ""); + jrr[jss::result], + "malformedAuthorizedCredentials", + "Invalid field 'authorized_credentials', not array."); } { // Failed, authorized_credentials is too long - - static std::string_view const credTypes[] = { + static std::array const credTypes = { "cred1", "cred2", "cred3", @@ -908,205 +1150,27 @@ class LedgerEntry_test : public beast::unit_test::suite auto& arr( jvParams[jss::deposit_preauth][jss::authorized_credentials]); - for (unsigned i = 0; i < sizeof(credTypes) / sizeof(credTypes[0]); - ++i) + for (auto cred : credTypes) { Json::Value jo; jo[jss::issuer] = issuer.human(); - jo[jss::credential_type] = - strHex(std::string_view(credTypes[i])); + jo[jss::credential_type] = strHex(std::string_view(cred)); arr.append(std::move(jo)); } auto const jrr = env.rpc("json", "ledger_entry", to_string(jvParams)); checkErrorValue( - jrr[jss::result], "malformedAuthorizedCredentials", ""); - } - - { - // Failed, issuer is not set - Json::Value jvParams; - jvParams[jss::ledger_index] = jss::validated; - jvParams[jss::deposit_preauth][jss::owner] = bob.human(); - - jvParams[jss::deposit_preauth][jss::authorized_credentials] = - Json::arrayValue; - auto& arr( - jvParams[jss::deposit_preauth][jss::authorized_credentials]); - - Json::Value jo; - jo[jss::credential_type] = strHex(std::string_view(credType)); - arr.append(std::move(jo)); - - auto const jrr = - env.rpc("json", "ledger_entry", to_string(jvParams)); - checkErrorValue( - jrr[jss::result], "malformedAuthorizedCredentials", ""); - } - - { - // Failed, issuer isn't string - Json::Value jvParams; - jvParams[jss::ledger_index] = jss::validated; - jvParams[jss::deposit_preauth][jss::owner] = bob.human(); - - jvParams[jss::deposit_preauth][jss::authorized_credentials] = - Json::arrayValue; - auto& arr( - jvParams[jss::deposit_preauth][jss::authorized_credentials]); - - Json::Value jo; - jo[jss::issuer] = 42; - jo[jss::credential_type] = strHex(std::string_view(credType)); - arr.append(std::move(jo)); - - auto const jrr = - env.rpc("json", "ledger_entry", to_string(jvParams)); - checkErrorValue( - jrr[jss::result], "malformedAuthorizedCredentials", ""); - } - - { - // Failed, issuer is an array - Json::Value jvParams; - jvParams[jss::ledger_index] = jss::validated; - jvParams[jss::deposit_preauth][jss::owner] = bob.human(); - - jvParams[jss::deposit_preauth][jss::authorized_credentials] = - Json::arrayValue; - auto& arr( - jvParams[jss::deposit_preauth][jss::authorized_credentials]); - - Json::Value jo; - Json::Value payload = Json::arrayValue; - payload.append(42); - jo[jss::issuer] = std::move(payload); - jo[jss::credential_type] = strHex(std::string_view(credType)); - arr.append(std::move(jo)); - - auto const jrr = - env.rpc("json", "ledger_entry", to_string(jvParams)); - checkErrorValue( - jrr[jss::result], "malformedAuthorizedCredentials", ""); - } - - { - // Failed, issuer isn't valid encoded account - Json::Value jvParams; - jvParams[jss::ledger_index] = jss::validated; - jvParams[jss::deposit_preauth][jss::owner] = bob.human(); - - jvParams[jss::deposit_preauth][jss::authorized_credentials] = - Json::arrayValue; - auto& arr( - jvParams[jss::deposit_preauth][jss::authorized_credentials]); - - Json::Value jo; - jo[jss::issuer] = "invalid_account"; - jo[jss::credential_type] = strHex(std::string_view(credType)); - arr.append(std::move(jo)); - - auto const jrr = - env.rpc("json", "ledger_entry", to_string(jvParams)); - checkErrorValue( - jrr[jss::result], "malformedAuthorizedCredentials", ""); - } - - { - // Failed, credential_type is not set - Json::Value jvParams; - jvParams[jss::ledger_index] = jss::validated; - jvParams[jss::deposit_preauth][jss::owner] = bob.human(); - - jvParams[jss::deposit_preauth][jss::authorized_credentials] = - Json::arrayValue; - auto& arr( - jvParams[jss::deposit_preauth][jss::authorized_credentials]); - - Json::Value jo; - jo[jss::issuer] = issuer.human(); - arr.append(std::move(jo)); - - auto const jrr = - env.rpc("json", "ledger_entry", to_string(jvParams)); - checkErrorValue( - jrr[jss::result], "malformedAuthorizedCredentials", ""); - } - - { - // Failed, credential_type isn't string - Json::Value jvParams; - jvParams[jss::ledger_index] = jss::validated; - jvParams[jss::deposit_preauth][jss::owner] = bob.human(); - - jvParams[jss::deposit_preauth][jss::authorized_credentials] = - Json::arrayValue; - auto& arr( - jvParams[jss::deposit_preauth][jss::authorized_credentials]); - - Json::Value jo; - jo[jss::issuer] = issuer.human(); - jo[jss::credential_type] = 42; - arr.append(std::move(jo)); - - auto const jrr = - env.rpc("json", "ledger_entry", to_string(jvParams)); - checkErrorValue( - jrr[jss::result], "malformedAuthorizedCredentials", ""); - } - - { - // Failed, credential_type is an array - Json::Value jvParams; - jvParams[jss::ledger_index] = jss::validated; - jvParams[jss::deposit_preauth][jss::owner] = bob.human(); - - jvParams[jss::deposit_preauth][jss::authorized_credentials] = - Json::arrayValue; - auto& arr( - jvParams[jss::deposit_preauth][jss::authorized_credentials]); - - Json::Value jo; - jo[jss::issuer] = issuer.human(); - Json::Value payload = Json::arrayValue; - payload.append(42); - jo[jss::credential_type] = std::move(payload); - arr.append(std::move(jo)); - - auto const jrr = - env.rpc("json", "ledger_entry", to_string(jvParams)); - checkErrorValue( - jrr[jss::result], "malformedAuthorizedCredentials", ""); - } - - { - // Failed, credential_type isn't hex encoded - Json::Value jvParams; - jvParams[jss::ledger_index] = jss::validated; - jvParams[jss::deposit_preauth][jss::owner] = bob.human(); - - jvParams[jss::deposit_preauth][jss::authorized_credentials] = - Json::arrayValue; - auto& arr( - jvParams[jss::deposit_preauth][jss::authorized_credentials]); - - Json::Value jo; - jo[jss::issuer] = issuer.human(); - jo[jss::credential_type] = "12KK"; - arr.append(std::move(jo)); - - auto const jrr = - env.rpc("json", "ledger_entry", to_string(jvParams)); - checkErrorValue( - jrr[jss::result], "malformedAuthorizedCredentials", ""); + jrr[jss::result], + "malformedAuthorizedCredentials", + "Invalid field 'authorized_credentials', not array."); } } void testLedgerEntryDirectory() { - testcase("ledger_entry Request Directory"); + testcase("Directory"); using namespace test::jtx; Env env{*this}; Account const alice{"alice"}; @@ -1188,39 +1252,48 @@ class LedgerEntry_test : public beast::unit_test::suite BEAST_EXPECT(jrr[jss::node][sfIndexes.jsonName].size() == 2); } { - // Null directory argument. + // Bad directory argument. Json::Value jvParams; - jvParams[jss::directory] = Json::nullValue; jvParams[jss::ledger_hash] = ledgerHash; - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "malformedRequest", ""); + testMalformedField( + env, + jvParams, + jss::directory, + FieldType::HashOrObjectField, + "malformedRequest"); } { // Non-integer sub_index. Json::Value jvParams; jvParams[jss::directory] = Json::objectValue; jvParams[jss::directory][jss::dir_root] = dirRootIndex; - jvParams[jss::directory][jss::sub_index] = 1.5; jvParams[jss::ledger_hash] = ledgerHash; - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "malformedRequest", ""); + testMalformedSubfield( + env, + jvParams, + jss::directory, + jss::sub_index, + FieldType::UInt64Field, + "malformedRequest", + false); } { // Malformed owner entry. Json::Value jvParams; jvParams[jss::directory] = Json::objectValue; - std::string const badAddress = makeBadAddress(alice.human()); - jvParams[jss::directory][jss::owner] = badAddress; jvParams[jss::ledger_hash] = ledgerHash; - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "malformedAddress", ""); + testMalformedSubfield( + env, + jvParams, + jss::directory, + jss::owner, + FieldType::AccountField, + "malformedAddress", + false); } { - // Malformed directory object. Specify both dir_root and owner. + // Malformed directory object. Specifies both dir_root and owner. Json::Value jvParams; jvParams[jss::directory] = Json::objectValue; jvParams[jss::directory][jss::owner] = alice.human(); @@ -1228,7 +1301,10 @@ class LedgerEntry_test : public beast::unit_test::suite jvParams[jss::ledger_hash] = ledgerHash; Json::Value const jrr = env.rpc( "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "malformedRequest", ""); + checkErrorValue( + jrr, + "malformedRequest", + "Must have exactly one of `owner` and `dir_root` fields."); } { // Incomplete directory object. Missing both dir_root and owner. @@ -1238,14 +1314,17 @@ class LedgerEntry_test : public beast::unit_test::suite jvParams[jss::ledger_hash] = ledgerHash; Json::Value const jrr = env.rpc( "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "malformedRequest", ""); + checkErrorValue( + jrr, + "malformedRequest", + "Must have exactly one of `owner` and `dir_root` fields."); } } void testLedgerEntryEscrow() { - testcase("ledger_entry Request Escrow"); + testcase("Escrow"); using namespace test::jtx; Env env{*this}; Account const alice{"alice"}; @@ -1296,56 +1375,18 @@ class LedgerEntry_test : public beast::unit_test::suite jrr[jss::node][jss::Amount] == XRP(333).value().getText()); } { - // Malformed owner entry. - Json::Value jvParams; - jvParams[jss::escrow] = Json::objectValue; - - std::string const badAddress = makeBadAddress(alice.human()); - jvParams[jss::escrow][jss::owner] = badAddress; - jvParams[jss::escrow][jss::seq] = env.seq(alice) - 1; - jvParams[jss::ledger_hash] = ledgerHash; - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "malformedOwner", ""); - } - { - // Missing owner. - Json::Value jvParams; - jvParams[jss::escrow] = Json::objectValue; - jvParams[jss::escrow][jss::seq] = env.seq(alice) - 1; - jvParams[jss::ledger_hash] = ledgerHash; - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "malformedRequest", ""); - } - { - // Missing sequence. - Json::Value jvParams; - jvParams[jss::escrow] = Json::objectValue; - jvParams[jss::escrow][jss::owner] = alice.human(); - jvParams[jss::ledger_hash] = ledgerHash; - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "malformedRequest", ""); - } - { - // Non-integer sequence. - Json::Value jvParams; - jvParams[jss::escrow] = Json::objectValue; - jvParams[jss::escrow][jss::owner] = alice.human(); - jvParams[jss::escrow][jss::seq] = - std::to_string(env.seq(alice) - 1); - jvParams[jss::ledger_hash] = ledgerHash; - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "malformedRequest", ""); + // Malformed escrow fields + runLedgerEntryTest( + env, + jss::escrow, + {{jss::owner, "malformedOwner"}, {jss::seq, "malformedSeq"}}); } } void testLedgerEntryOffer() { - testcase("ledger_entry Request Offer"); + testcase("Offer"); using namespace test::jtx; Env env{*this}; Account const alice{"alice"}; @@ -1379,56 +1420,21 @@ class LedgerEntry_test : public beast::unit_test::suite "json", "ledger_entry", to_string(jvParams))[jss::result]; BEAST_EXPECT(jrr[jss::node][jss::TakerGets] == "322000000"); } - { - // Malformed account entry. - Json::Value jvParams; - jvParams[jss::offer] = Json::objectValue; - std::string const badAddress = makeBadAddress(alice.human()); - jvParams[jss::offer][jss::account] = badAddress; - jvParams[jss::offer][jss::seq] = env.seq(alice) - 1; - jvParams[jss::ledger_hash] = ledgerHash; - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "malformedAddress", ""); - } { - // Malformed offer object. Missing account member. - Json::Value jvParams; - jvParams[jss::offer] = Json::objectValue; - jvParams[jss::offer][jss::seq] = env.seq(alice) - 1; - jvParams[jss::ledger_hash] = ledgerHash; - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "malformedRequest", ""); - } - { - // Malformed offer object. Missing seq member. - Json::Value jvParams; - jvParams[jss::offer] = Json::objectValue; - jvParams[jss::offer][jss::account] = alice.human(); - jvParams[jss::ledger_hash] = ledgerHash; - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "malformedRequest", ""); - } - { - // Malformed offer object. Non-integral seq member. - Json::Value jvParams; - jvParams[jss::offer] = Json::objectValue; - jvParams[jss::offer][jss::account] = alice.human(); - jvParams[jss::offer][jss::seq] = std::to_string(env.seq(alice) - 1); - jvParams[jss::ledger_hash] = ledgerHash; - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "malformedRequest", ""); + // Malformed offer fields + runLedgerEntryTest( + env, + jss::offer, + {{jss::account, "malformedAddress"}, + {jss::seq, "malformedRequest"}}); } } void testLedgerEntryPayChan() { - testcase("ledger_entry Request Pay Chan"); + testcase("Pay Chan"); using namespace test::jtx; using namespace std::literals::chrono_literals; Env env{*this}; @@ -1478,14 +1484,19 @@ class LedgerEntry_test : public beast::unit_test::suite jvParams[jss::ledger_hash] = ledgerHash; Json::Value const jrr = env.rpc( "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "entryNotFound", ""); + checkErrorValue(jrr, "entryNotFound", "Entry not found."); + } + + { + // Malformed paychan field + runLedgerEntryTest(env, jss::payment_channel); } } void testLedgerEntryRippleState() { - testcase("ledger_entry Request RippleState"); + testcase("RippleState"); using namespace test::jtx; Env env{*this}; Account const alice{"alice"}; @@ -1521,36 +1532,14 @@ class LedgerEntry_test : public beast::unit_test::suite jrr[jss::node][sfHighLimit.jsonName][jss::value] == "999"); } { - // ripple_state is not an object. - Json::Value jvParams; - jvParams[fieldName] = "ripple_state"; - jvParams[jss::ledger_hash] = ledgerHash; - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "malformedRequest", ""); - } - { - // ripple_state.currency is missing. - Json::Value jvParams; - jvParams[fieldName] = Json::objectValue; - jvParams[fieldName][jss::accounts] = Json::arrayValue; - jvParams[fieldName][jss::accounts][0u] = alice.human(); - jvParams[fieldName][jss::accounts][1u] = gw.human(); - jvParams[jss::ledger_hash] = ledgerHash; - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "malformedRequest", ""); - } - { - // ripple_state accounts is not an array. - Json::Value jvParams; - jvParams[fieldName] = Json::objectValue; - jvParams[fieldName][jss::accounts] = 2; - jvParams[fieldName][jss::currency] = "USD"; - jvParams[jss::ledger_hash] = ledgerHash; - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "malformedRequest", ""); + // test basic malformed scenarios + runLedgerEntryTest( + env, + fieldName, + { + {jss::accounts, "malformedRequest"}, + {jss::currency, "malformedCurrency"}, + }); } { // ripple_state one of the accounts is missing. @@ -1562,7 +1551,11 @@ class LedgerEntry_test : public beast::unit_test::suite jvParams[jss::ledger_hash] = ledgerHash; Json::Value const jrr = env.rpc( "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "malformedRequest", ""); + checkErrorValue( + jrr, + "malformedRequest", + "Invalid field 'accounts', not length-2 array of " + "Accounts."); } { // ripple_state more than 2 accounts. @@ -1576,33 +1569,60 @@ class LedgerEntry_test : public beast::unit_test::suite jvParams[jss::ledger_hash] = ledgerHash; Json::Value const jrr = env.rpc( "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "malformedRequest", ""); + checkErrorValue( + jrr, + "malformedRequest", + "Invalid field 'accounts', not length-2 array of " + "Accounts."); } { - // ripple_state account[0] is not a string. + // ripple_state account[0] / account[1] is not an account. Json::Value jvParams; jvParams[fieldName] = Json::objectValue; - jvParams[fieldName][jss::accounts] = Json::arrayValue; - jvParams[fieldName][jss::accounts][0u] = 44; - jvParams[fieldName][jss::accounts][1u] = gw.human(); - jvParams[fieldName][jss::currency] = "USD"; - jvParams[jss::ledger_hash] = ledgerHash; - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "malformedRequest", ""); - } - { - // ripple_state account[1] is not a string. - Json::Value jvParams; - jvParams[fieldName] = Json::objectValue; - jvParams[fieldName][jss::accounts] = Json::arrayValue; - jvParams[fieldName][jss::accounts][0u] = alice.human(); - jvParams[fieldName][jss::accounts][1u] = 21; - jvParams[fieldName][jss::currency] = "USD"; - jvParams[jss::ledger_hash] = ledgerHash; - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "malformedRequest", ""); + auto tryField = [&](Json::Value badAccount) -> void { + { + // account[0] + jvParams[fieldName][jss::accounts] = Json::arrayValue; + jvParams[fieldName][jss::accounts][0u] = badAccount; + jvParams[fieldName][jss::accounts][1u] = gw.human(); + jvParams[fieldName][jss::currency] = "USD"; + + Json::Value const jrr = env.rpc( + "json", + "ledger_entry", + to_string(jvParams))[jss::result]; + checkErrorValue( + jrr, + "malformedAddress", + RPC::expected_field_message( + jss::accounts, "array of Accounts")); + } + + { + // account[1] + jvParams[fieldName][jss::accounts] = Json::arrayValue; + jvParams[fieldName][jss::accounts][0u] = alice.human(); + jvParams[fieldName][jss::accounts][1u] = badAccount; + jvParams[fieldName][jss::currency] = "USD"; + + Json::Value const jrr = env.rpc( + "json", + "ledger_entry", + to_string(jvParams))[jss::result]; + checkErrorValue( + jrr, + "malformedAddress", + RPC::expected_field_message( + jss::accounts, "array of Accounts")); + } + }; + + auto const& badValues = getBadValues(FieldType::AccountField); + for (auto const& value : badValues) + { + tryField(value); + } + tryField(Json::nullValue); } { // ripple_state account[0] == account[1]. @@ -1615,48 +1635,10 @@ class LedgerEntry_test : public beast::unit_test::suite jvParams[jss::ledger_hash] = ledgerHash; Json::Value const jrr = env.rpc( "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "malformedRequest", ""); - } - { - // ripple_state malformed account[0]. - Json::Value jvParams; - jvParams[fieldName] = Json::objectValue; - jvParams[fieldName][jss::accounts] = Json::arrayValue; - jvParams[fieldName][jss::accounts][0u] = - makeBadAddress(alice.human()); - jvParams[fieldName][jss::accounts][1u] = gw.human(); - jvParams[fieldName][jss::currency] = "USD"; - jvParams[jss::ledger_hash] = ledgerHash; - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "malformedAddress", ""); - } - { - // ripple_state malformed account[1]. - Json::Value jvParams; - jvParams[fieldName] = Json::objectValue; - jvParams[fieldName][jss::accounts] = Json::arrayValue; - jvParams[fieldName][jss::accounts][0u] = alice.human(); - jvParams[fieldName][jss::accounts][1u] = - makeBadAddress(gw.human()); - jvParams[fieldName][jss::currency] = "USD"; - jvParams[jss::ledger_hash] = ledgerHash; - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "malformedAddress", ""); - } - { - // ripple_state malformed currency. - Json::Value jvParams; - jvParams[fieldName] = Json::objectValue; - jvParams[fieldName][jss::accounts] = Json::arrayValue; - jvParams[fieldName][jss::accounts][0u] = alice.human(); - jvParams[fieldName][jss::accounts][1u] = gw.human(); - jvParams[fieldName][jss::currency] = "USDollars"; - jvParams[jss::ledger_hash] = ledgerHash; - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "malformedCurrency", ""); + checkErrorValue( + jrr, + "malformedRequest", + "Cannot have a trustline to self."); } } } @@ -1664,7 +1646,7 @@ class LedgerEntry_test : public beast::unit_test::suite void testLedgerEntryTicket() { - testcase("ledger_entry Request Ticket"); + testcase("Ticket"); using namespace test::jtx; Env env{*this}; env.close(); @@ -1686,7 +1668,7 @@ class LedgerEntry_test : public beast::unit_test::suite jvParams[jss::ledger_hash] = ledgerHash; Json::Value const jrr = env.rpc( "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "entryNotFound", ""); + checkErrorValue(jrr, "entryNotFound", "Entry not found."); } { // First real ticket requested by index. @@ -1721,7 +1703,7 @@ class LedgerEntry_test : public beast::unit_test::suite jvParams[jss::ledger_hash] = ledgerHash; Json::Value const jrr = env.rpc( "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "entryNotFound", ""); + checkErrorValue(jrr, "entryNotFound", "Entry not found."); } { // Request a ticket using an account root entry. @@ -1730,59 +1712,26 @@ class LedgerEntry_test : public beast::unit_test::suite jvParams[jss::ledger_hash] = ledgerHash; Json::Value const jrr = env.rpc( "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "unexpectedLedgerType", ""); + checkErrorValue( + jrr, "unexpectedLedgerType", "Unexpected ledger type."); } - { - // Malformed account entry. - Json::Value jvParams; - jvParams[jss::ticket] = Json::objectValue; - std::string const badAddress = makeBadAddress(env.master.human()); - jvParams[jss::ticket][jss::account] = badAddress; - jvParams[jss::ticket][jss::ticket_seq] = env.seq(env.master) - 1; - jvParams[jss::ledger_hash] = ledgerHash; - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "malformedAddress", ""); - } { - // Malformed ticket object. Missing account member. - Json::Value jvParams; - jvParams[jss::ticket] = Json::objectValue; - jvParams[jss::ticket][jss::ticket_seq] = env.seq(env.master) - 1; - jvParams[jss::ledger_hash] = ledgerHash; - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "malformedRequest", ""); - } - { - // Malformed ticket object. Missing seq member. - Json::Value jvParams; - jvParams[jss::ticket] = Json::objectValue; - jvParams[jss::ticket][jss::account] = env.master.human(); - jvParams[jss::ledger_hash] = ledgerHash; - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "malformedRequest", ""); - } - { - // Malformed ticket object. Non-integral seq member. - Json::Value jvParams; - jvParams[jss::ticket] = Json::objectValue; - jvParams[jss::ticket][jss::account] = env.master.human(); - jvParams[jss::ticket][jss::ticket_seq] = - std::to_string(env.seq(env.master) - 1); - jvParams[jss::ledger_hash] = ledgerHash; - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "malformedRequest", ""); + // test basic malformed scenarios + runLedgerEntryTest( + env, + jss::ticket, + { + {jss::account, "malformedAddress"}, + {jss::ticket_seq, "malformedRequest"}, + }); } } void testLedgerEntryDID() { - testcase("ledger_entry Request DID"); + testcase("DID"); using namespace test::jtx; using namespace std::literals::chrono_literals; Env env{*this}; @@ -1826,230 +1775,17 @@ class LedgerEntry_test : public beast::unit_test::suite jvParams[jss::ledger_hash] = ledgerHash; Json::Value const jrr = env.rpc( "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "entryNotFound", ""); + checkErrorValue(jrr, "entryNotFound", "Entry not found."); } - } - - void - testLedgerEntryInvalidParams(unsigned int apiVersion) - { - testcase( - "ledger_entry Request With Invalid Parameters v" + - std::to_string(apiVersion)); - using namespace test::jtx; - Env env{*this}; - - std::string const ledgerHash{to_string(env.closed()->info().hash)}; - - auto makeParams = [&apiVersion](std::function f) { - Json::Value params; - params[jss::api_version] = apiVersion; - f(params); - return params; - }; - // "features" is not an option supported by ledger_entry. { - auto const jvParams = - makeParams([&ledgerHash](Json::Value& jvParams) { - jvParams[jss::features] = ledgerHash; - jvParams[jss::ledger_hash] = ledgerHash; - }); - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - - if (apiVersion < 2u) - checkErrorValue(jrr, "unknownOption", ""); - else - checkErrorValue(jrr, "invalidParams", ""); - } - Json::Value const injectObject = []() { - Json::Value obj(Json::objectValue); - obj[jss::account] = "rhigTLJJyXXSRUyRCQtqi1NoAZZzZnS4KU"; - obj[jss::ledger_index] = "validated"; - return obj; - }(); - Json::Value const injectArray = []() { - Json::Value arr(Json::arrayValue); - arr[0u] = "rhigTLJJyXXSRUyRCQtqi1NoAZZzZnS4KU"; - arr[1u] = "validated"; - return arr; - }(); - - // invalid input for fields that can handle an object, but can't handle - // an array - for (auto const& field : - {jss::directory, jss::escrow, jss::offer, jss::ticket, jss::amm}) - { - auto const jvParams = - makeParams([&field, &injectArray](Json::Value& jvParams) { - jvParams[field] = injectArray; - }); - - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - - if (apiVersion < 2u) - checkErrorValue(jrr, "internal", "Internal error."); - else - checkErrorValue(jrr, "invalidParams", ""); - } - // Fields that can handle objects just fine - for (auto const& field : - {jss::directory, jss::escrow, jss::offer, jss::ticket, jss::amm}) - { - auto const jvParams = - makeParams([&field, &injectObject](Json::Value& jvParams) { - jvParams[field] = injectObject; - }); - - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - - checkErrorValue(jrr, "malformedRequest", ""); - } - - for (auto const& inject : {injectObject, injectArray}) - { - // invalid input for fields that can't handle an object or an array - for (auto const& field : - {jss::index, - jss::account_root, - jss::check, - jss::payment_channel}) - { - auto const jvParams = - makeParams([&field, &inject](Json::Value& jvParams) { - jvParams[field] = inject; - }); - - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - - if (apiVersion < 2u) - checkErrorValue(jrr, "internal", "Internal error."); - else - checkErrorValue(jrr, "invalidParams", ""); - } - // directory sub-fields - for (auto const& field : {jss::dir_root, jss::owner}) - { - auto const jvParams = - makeParams([&field, &inject](Json::Value& jvParams) { - jvParams[jss::directory][field] = inject; - }); - - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - - if (apiVersion < 2u) - checkErrorValue(jrr, "internal", "Internal error."); - else - checkErrorValue(jrr, "invalidParams", ""); - } - // escrow sub-fields - { - auto const jvParams = - makeParams([&inject](Json::Value& jvParams) { - jvParams[jss::escrow][jss::owner] = inject; - jvParams[jss::escrow][jss::seq] = 99; - }); - - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - - if (apiVersion < 2u) - checkErrorValue(jrr, "internal", "Internal error."); - else - checkErrorValue(jrr, "invalidParams", ""); - } - // offer sub-fields - { - auto const jvParams = - makeParams([&inject](Json::Value& jvParams) { - jvParams[jss::offer][jss::account] = inject; - jvParams[jss::offer][jss::seq] = 99; - }); - - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - - if (apiVersion < 2u) - checkErrorValue(jrr, "internal", "Internal error."); - else - checkErrorValue(jrr, "invalidParams", ""); - } - // ripple_state sub-fields - { - auto const jvParams = - makeParams([&inject](Json::Value& jvParams) { - Json::Value rs(Json::objectValue); - rs[jss::currency] = "FOO"; - rs[jss::accounts] = Json::Value(Json::arrayValue); - rs[jss::accounts][0u] = - "rhigTLJJyXXSRUyRCQtqi1NoAZZzZnS4KU"; - rs[jss::accounts][1u] = - "rKssEq6pg1KbqEqAFnua5mFAL6Ggpsh2wv"; - rs[jss::currency] = inject; - jvParams[jss::ripple_state] = std::move(rs); - }); - - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - - if (apiVersion < 2u) - checkErrorValue(jrr, "internal", "Internal error."); - else - checkErrorValue(jrr, "invalidParams", ""); - } - // ticket sub-fields - { - auto const jvParams = - makeParams([&inject](Json::Value& jvParams) { - jvParams[jss::ticket][jss::account] = inject; - jvParams[jss::ticket][jss::ticket_seq] = 99; - }); - - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - - if (apiVersion < 2u) - checkErrorValue(jrr, "internal", "Internal error."); - else - checkErrorValue(jrr, "invalidParams", ""); - } - - // Fields that can handle malformed inputs just fine - for (auto const& field : {jss::nft_page, jss::deposit_preauth}) - { - auto const jvParams = - makeParams([&field, &inject](Json::Value& jvParams) { - jvParams[field] = inject; - }); - - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - - checkErrorValue(jrr, "malformedRequest", ""); - } - // Subfields of deposit_preauth that can handle malformed inputs - // fine - for (auto const& field : {jss::owner, jss::authorized}) - { - auto const jvParams = - makeParams([&field, &inject](Json::Value& jvParams) { - auto pa = Json::Value(Json::objectValue); - pa[jss::owner] = "rhigTLJJyXXSRUyRCQtqi1NoAZZzZnS4KU"; - pa[jss::authorized] = - "rKssEq6pg1KbqEqAFnua5mFAL6Ggpsh2wv"; - pa[field] = inject; - jvParams[jss::deposit_preauth] = std::move(pa); - }); - - Json::Value const jrr = env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - - checkErrorValue(jrr, "malformedRequest", ""); - } + // Malformed DID index + Json::Value jvParams; + testMalformedField( + env, + jvParams, + jss::did, + FieldType::AccountField, + "malformedAddress"); } } @@ -2068,28 +1804,16 @@ class LedgerEntry_test : public beast::unit_test::suite {.owner = owner, .fee = static_cast(env.current()->fees().base.drops())}); - // Malformed document id - auto res = Oracle::ledgerEntry(env, owner, NoneTag); - BEAST_EXPECT(res[jss::error].asString() == "invalidParams"); - std::vector invalid = {-1, 1.2, "", "Invalid"}; - for (auto const& v : invalid) { - auto const res = Oracle::ledgerEntry(env, owner, v); - BEAST_EXPECT(res[jss::error].asString() == "malformedDocumentID"); + // test basic malformed scenarios + runLedgerEntryTest( + env, + jss::oracle, + { + {jss::account, "malformedAccount"}, + {jss::oracle_document_id, "malformedDocumentID"}, + }); } - // Missing document id - res = Oracle::ledgerEntry(env, owner, std::nullopt); - BEAST_EXPECT(res[jss::error].asString() == "malformedRequest"); - - // Missing account - res = Oracle::ledgerEntry(env, std::nullopt, 1); - BEAST_EXPECT(res[jss::error].asString() == "malformedRequest"); - - // Malformed account - std::string malfAccount = to_string(owner.id()); - malfAccount.replace(10, 1, 1, '!'); - res = Oracle::ledgerEntry(env, malfAccount, 1); - BEAST_EXPECT(res[jss::error].asString() == "malformedAddress"); } void @@ -2144,7 +1868,7 @@ class LedgerEntry_test : public beast::unit_test::suite void testLedgerEntryMPT() { - testcase("ledger_entry Request MPT"); + testcase("MPT"); using namespace test::jtx; using namespace std::literals::chrono_literals; Env env{*this}; @@ -2185,7 +1909,7 @@ class LedgerEntry_test : public beast::unit_test::suite jvParams[jss::ledger_hash] = ledgerHash; Json::Value const jrr = env.rpc( "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "entryNotFound", ""); + checkErrorValue(jrr, "entryNotFound", "Entry not found."); } { // Request the MPToken using its owner + mptIssuanceID. @@ -2210,14 +1934,24 @@ class LedgerEntry_test : public beast::unit_test::suite jvParams[jss::ledger_hash] = ledgerHash; Json::Value const jrr = env.rpc( "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "entryNotFound", ""); + checkErrorValue(jrr, "entryNotFound", "Entry not found."); + } + { + // Malformed MPTIssuance index + Json::Value jvParams; + testMalformedField( + env, + jvParams, + jss::mptoken, + FieldType::HashOrObjectField, + "malformedRequest"); } } void testLedgerEntryPermissionedDomain() { - testcase("ledger_entry PermissionedDomain"); + testcase("PermissionedDomain"); using namespace test::jtx; @@ -2278,73 +2012,25 @@ class LedgerEntry_test : public beast::unit_test::suite "12F1F1F1F180D67377B2FAB292A31C922470326268D2B9B74CD1E582645B9A" "DE"; auto const jrr = env.rpc("json", "ledger_entry", to_string(params)); - checkErrorValue(jrr[jss::result], "entryNotFound", ""); + checkErrorValue( + jrr[jss::result], "entryNotFound", "Entry not found."); } - { - // Fail, invalid permissioned domain index - Json::Value params; - params[jss::ledger_index] = jss::validated; - params[jss::permissioned_domain] = "NotAHexString"; - auto const jrr = env.rpc("json", "ledger_entry", to_string(params)); - checkErrorValue(jrr[jss::result], "malformedRequest", ""); - } - - { - // Fail, permissioned domain is not an object - Json::Value params; - params[jss::ledger_index] = jss::validated; - params[jss::permissioned_domain] = 10; - auto const jrr = env.rpc("json", "ledger_entry", to_string(params)); - checkErrorValue(jrr[jss::result], "malformedRequest", ""); - } - - { - // Fail, invalid account - Json::Value params; - params[jss::ledger_index] = jss::validated; - params[jss::permissioned_domain][jss::account] = 1; - params[jss::permissioned_domain][jss::seq] = seq; - auto const jrr = env.rpc("json", "ledger_entry", to_string(params)); - checkErrorValue(jrr[jss::result], "malformedAddress", ""); - } - - { - // Fail, account is an object - Json::Value params; - params[jss::ledger_index] = jss::validated; - params[jss::permissioned_domain][jss::account] = - Json::Value{Json::ValueType::objectValue}; - params[jss::permissioned_domain][jss::seq] = seq; - auto const jrr = env.rpc("json", "ledger_entry", to_string(params)); - checkErrorValue(jrr[jss::result], "malformedAddress", ""); - } - - { - // Fail, no account - Json::Value params; - params[jss::ledger_index] = jss::validated; - params[jss::permissioned_domain][jss::account] = ""; - params[jss::permissioned_domain][jss::seq] = seq; - auto const jrr = env.rpc("json", "ledger_entry", to_string(params)); - checkErrorValue(jrr[jss::result], "malformedAddress", ""); - } - - { - // Fail, invalid sequence - Json::Value params; - params[jss::ledger_index] = jss::validated; - params[jss::permissioned_domain][jss::account] = alice.human(); - params[jss::permissioned_domain][jss::seq] = "12g"; - auto const jrr = env.rpc("json", "ledger_entry", to_string(params)); - checkErrorValue(jrr[jss::result], "malformedRequest", ""); + // test basic malformed scenarios + runLedgerEntryTest( + env, + jss::permissioned_domain, + { + {jss::account, "malformedAddress"}, + {jss::seq, "malformedRequest"}, + }); } } void testLedgerEntryCLI() { - testcase("ledger_entry command-line"); + testcase("command-line"); using namespace test::jtx; Env env{*this}; @@ -2391,9 +2077,6 @@ public: testLedgerEntryMPT(); testLedgerEntryPermissionedDomain(); testLedgerEntryCLI(); - - forAllApiVersions(std::bind_front( - &LedgerEntry_test::testLedgerEntryInvalidParams, this)); } }; @@ -2444,7 +2127,6 @@ class LedgerEntry_XChain_test : public beast::unit_test::suite, BEAST_EXPECT(jrr.isMember(jss::node)); auto r = jrr[jss::node]; - // std::cout << to_string(r) << '\n'; BEAST_EXPECT(r.isMember(jss::Account)); BEAST_EXPECT(r[jss::Account] == mcDoor.human()); @@ -2486,7 +2168,7 @@ class LedgerEntry_XChain_test : public beast::unit_test::suite, Json::Value const jrr = mcEnv.rpc( "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "entryNotFound", ""); + checkErrorValue(jrr, "entryNotFound", "Entry not found."); } { // create two claim ids and verify that the bridge counter was @@ -2500,7 +2182,6 @@ class LedgerEntry_XChain_test : public beast::unit_test::suite, Json::Value jvParams; jvParams[jss::bridge_account] = mcDoor.human(); jvParams[jss::bridge] = jvb; - // std::cout << to_string(jvParams) << '\n'; Json::Value const jrr = mcEnv.rpc( "json", "ledger_entry", to_string(jvParams))[jss::result]; @@ -2536,13 +2217,11 @@ class LedgerEntry_XChain_test : public beast::unit_test::suite, jvParams[jss::xchain_owned_claim_id] = jvXRPBridgeRPC; jvParams[jss::xchain_owned_claim_id][jss::xchain_owned_claim_id] = 1; - // std::cout << to_string(jvParams) << '\n'; Json::Value const jrr = scEnv.rpc( "json", "ledger_entry", to_string(jvParams))[jss::result]; BEAST_EXPECT(jrr.isMember(jss::node)); auto r = jrr[jss::node]; - // std::cout << to_string(r) << '\n'; BEAST_EXPECT(r.isMember(jss::Account)); BEAST_EXPECT(r[jss::Account] == scAlice.human()); @@ -2563,7 +2242,6 @@ class LedgerEntry_XChain_test : public beast::unit_test::suite, BEAST_EXPECT(jrr.isMember(jss::node)); auto r = jrr[jss::node]; - // std::cout << to_string(r) << '\n'; BEAST_EXPECT(r.isMember(jss::Account)); BEAST_EXPECT(r[jss::Account] == scBob.human()); @@ -2622,10 +2300,8 @@ class LedgerEntry_XChain_test : public beast::unit_test::suite, jvXRPBridgeRPC; jvParams[jss::xchain_owned_create_account_claim_id] [jss::xchain_owned_create_account_claim_id] = 1; - // std::cout << to_string(jvParams) << '\n'; Json::Value const jrr = scEnv.rpc( "json", "ledger_entry", to_string(jvParams))[jss::result]; - // std::cout << to_string(jrr) << '\n'; BEAST_EXPECT(jrr.isMember(jss::node)); auto r = jrr[jss::node]; @@ -2694,10 +2370,9 @@ class LedgerEntry_XChain_test : public beast::unit_test::suite, jvXRPBridgeRPC; jvParams[jss::xchain_owned_create_account_claim_id] [jss::xchain_owned_create_account_claim_id] = 1; - // std::cout << to_string(jvParams) << '\n'; Json::Value const jrr = scEnv.rpc( "json", "ledger_entry", to_string(jvParams))[jss::result]; - checkErrorValue(jrr, "entryNotFound", ""); + checkErrorValue(jrr, "entryNotFound", "Entry not found."); } } diff --git a/src/xrpld/rpc/detail/RPCHelpers.cpp b/src/xrpld/rpc/detail/RPCHelpers.cpp index b98f31340a..52a69eb79e 100644 --- a/src/xrpld/rpc/detail/RPCHelpers.cpp +++ b/src/xrpld/rpc/detail/RPCHelpers.cpp @@ -190,7 +190,7 @@ getAccountObjects( auto& jvObjects = (jvResult[jss::account_objects] = Json::arrayValue); - // this is a mutable version of limit, used to seemlessly switch + // this is a mutable version of limit, used to seamlessly switch // to iterating directory entries when nftokenpages are exhausted uint32_t mlimit = limit; @@ -373,7 +373,7 @@ ledgerFromRequest(T& ledger, JsonContext& context) indexValue = legacyLedger; } - if (hashValue) + if (!hashValue.isNull()) { if (!hashValue.isString()) return {rpcINVALID_PARAMS, "ledgerHashNotString"}; @@ -384,6 +384,9 @@ ledgerFromRequest(T& ledger, JsonContext& context) return getLedger(ledger, ledgerHash, context); } + if (!indexValue.isConvertibleTo(Json::stringValue)) + return {rpcINVALID_PARAMS, "ledgerIndexMalformed"}; + auto const index = indexValue.asString(); if (index == "current" || index.empty()) @@ -395,11 +398,11 @@ ledgerFromRequest(T& ledger, JsonContext& context) if (index == "closed") return getLedger(ledger, LedgerShortcut::CLOSED, context); - std::uint32_t iVal; - if (beast::lexicalCastChecked(iVal, index)) - return getLedger(ledger, iVal, context); + std::uint32_t val; + if (!beast::lexicalCastChecked(val, index)) + return {rpcINVALID_PARAMS, "ledgerIndexMalformed"}; - return {rpcINVALID_PARAMS, "ledgerIndexMalformed"}; + return getLedger(ledger, val, context); } } // namespace @@ -586,7 +589,7 @@ getLedger(T& ledger, LedgerShortcut shortcut, Context& context) return Status::OK; } -// Explicit instantiaion of above three functions +// Explicit instantiation of above three functions template Status getLedger<>(std::shared_ptr&, uint32_t, Context&); diff --git a/src/xrpld/rpc/handlers/LedgerEntry.cpp b/src/xrpld/rpc/handlers/LedgerEntry.cpp index fb82788907..61a7e2fb2c 100644 --- a/src/xrpld/rpc/handlers/LedgerEntry.cpp +++ b/src/xrpld/rpc/handlers/LedgerEntry.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include @@ -38,50 +39,57 @@ namespace ripple { -static std::optional -parseIndex(Json::Value const& params, Json::Value& jvResult) +static Expected +parseObjectID( + Json::Value const& params, + Json::StaticString const fieldName, + std::string const& expectedType = "hex string or object") { - uint256 uNodeIndex; - if (!uNodeIndex.parseHex(params.asString())) + if (auto const uNodeIndex = LedgerEntryHelpers::parse(params)) { - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; + return *uNodeIndex; } - - return uNodeIndex; + return LedgerEntryHelpers::invalidFieldError( + "malformedRequest", fieldName, expectedType); } -static std::optional -parseAccountRoot(Json::Value const& params, Json::Value& jvResult) +static Expected +parseIndex(Json::Value const& params, Json::StaticString const fieldName) { - auto const account = parseBase58(params.asString()); - if (!account || account->isZero()) - { - jvResult[jss::error] = "malformedAddress"; - return std::nullopt; - } - - return keylet::account(*account).key; + return parseObjectID(params, fieldName, "hex string"); } -static std::optional -parseAMM(Json::Value const& params, Json::Value& jvResult) +static Expected +parseAccountRoot(Json::Value const& params, Json::StaticString const fieldName) +{ + if (auto const account = LedgerEntryHelpers::parse(params)) + { + return keylet::account(*account).key; + } + + return LedgerEntryHelpers::invalidFieldError( + "malformedAddress", fieldName, "AccountID"); +} + +static Expected +parseAmendments(Json::Value const& params, Json::StaticString const fieldName) +{ + return parseObjectID(params, fieldName, "hex string"); +} + +static Expected +parseAMM(Json::Value const& params, Json::StaticString const fieldName) { if (!params.isObject()) { - uint256 uNodeIndex; - if (!uNodeIndex.parseHex(params.asString())) - { - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; - } - return uNodeIndex; + return parseObjectID(params, fieldName); } - if (!params.isMember(jss::asset) || !params.isMember(jss::asset2)) + if (auto const value = + LedgerEntryHelpers::hasRequired(params, {jss::asset, jss::asset2}); + !value) { - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; + return Unexpected(value.error()); } try @@ -92,135 +100,136 @@ parseAMM(Json::Value const& params, Json::Value& jvResult) } catch (std::runtime_error const&) { - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; + return LedgerEntryHelpers::malformedError("malformedRequest", ""); } } -static std::optional -parseBridge(Json::Value const& params, Json::Value& jvResult) +static Expected +parseBridge(Json::Value const& params, Json::StaticString const fieldName) { - // return the keylet for the specified bridge or nullopt if the - // request is malformed - auto const maybeKeylet = [&]() -> std::optional { - try - { - if (!params.isMember(jss::bridge_account)) - return std::nullopt; - - auto const& jsBridgeAccount = params[jss::bridge_account]; - if (!jsBridgeAccount.isString()) - { - return std::nullopt; - } - - auto const account = - parseBase58(jsBridgeAccount.asString()); - if (!account || account->isZero()) - { - return std::nullopt; - } - - // This may throw and is the reason for the `try` block. The - // try block has a larger scope so the `bridge` variable - // doesn't need to be an optional. - STXChainBridge const bridge(params[jss::bridge]); - STXChainBridge::ChainType const chainType = - STXChainBridge::srcChain(account == bridge.lockingChainDoor()); - - if (account != bridge.door(chainType)) - return std::nullopt; - - return keylet::bridge(bridge, chainType); - } - catch (...) - { - return std::nullopt; - } - }(); - - if (maybeKeylet) + if (!params.isMember(jss::bridge)) { - return maybeKeylet->key; + return Unexpected(LedgerEntryHelpers::missingFieldError(jss::bridge)); } - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; + if (params[jss::bridge].isString()) + { + return parseObjectID(params, fieldName); + } + + auto const bridge = + LedgerEntryHelpers::parseBridgeFields(params[jss::bridge]); + if (!bridge) + return Unexpected(bridge.error()); + + auto const account = LedgerEntryHelpers::requiredAccountID( + params, jss::bridge_account, "malformedBridgeAccount"); + if (!account) + return Unexpected(account.error()); + + STXChainBridge::ChainType const chainType = + STXChainBridge::srcChain(account.value() == bridge->lockingChainDoor()); + if (account.value() != bridge->door(chainType)) + return LedgerEntryHelpers::malformedError("malformedRequest", ""); + + return keylet::bridge(*bridge, chainType).key; } -static std::optional -parseCheck(Json::Value const& params, Json::Value& jvResult) +static Expected +parseCheck(Json::Value const& params, Json::StaticString const fieldName) { - uint256 uNodeIndex; - if (!uNodeIndex.parseHex(params.asString())) - { - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; - } - - return uNodeIndex; + return parseObjectID(params, fieldName, "hex string"); } -static std::optional -parseCredential(Json::Value const& cred, Json::Value& jvResult) +static Expected +parseCredential(Json::Value const& cred, Json::StaticString const fieldName) { - if (cred.isString()) + if (!cred.isObject()) { - uint256 uNodeIndex; - if (!uNodeIndex.parseHex(cred.asString())) - { - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; - } - return uNodeIndex; + return parseObjectID(cred, fieldName); } - if ((!cred.isMember(jss::subject) || !cred[jss::subject].isString()) || - (!cred.isMember(jss::issuer) || !cred[jss::issuer].isString()) || - (!cred.isMember(jss::credential_type) || - !cred[jss::credential_type].isString())) - { - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; - } + auto const subject = LedgerEntryHelpers::requiredAccountID( + cred, jss::subject, "malformedRequest"); + if (!subject) + return Unexpected(subject.error()); - auto const subject = parseBase58(cred[jss::subject].asString()); - auto const issuer = parseBase58(cred[jss::issuer].asString()); - auto const credType = strUnHex(cred[jss::credential_type].asString()); + auto const issuer = LedgerEntryHelpers::requiredAccountID( + cred, jss::issuer, "malformedRequest"); + if (!issuer) + return Unexpected(issuer.error()); - if (!subject || subject->isZero() || !issuer || issuer->isZero() || - !credType || credType->empty()) - { - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; - } + auto const credType = LedgerEntryHelpers::requiredHexBlob( + cred, + jss::credential_type, + maxCredentialTypeLength, + "malformedRequest"); + if (!credType) + return Unexpected(credType.error()); return keylet::credential( *subject, *issuer, Slice(credType->data(), credType->size())) .key; } -static STArray +static Expected +parseDelegate(Json::Value const& params, Json::StaticString const fieldName) +{ + if (!params.isObject()) + { + return parseObjectID(params, fieldName); + } + + auto const account = LedgerEntryHelpers::requiredAccountID( + params, jss::account, "malformedAddress"); + if (!account) + return Unexpected(account.error()); + + auto const authorize = LedgerEntryHelpers::requiredAccountID( + params, jss::authorize, "malformedAddress"); + if (!authorize) + return Unexpected(authorize.error()); + + return keylet::delegate(*account, *authorize).key; +} + +static Expected parseAuthorizeCredentials(Json::Value const& jv) { + if (!jv.isArray()) + return LedgerEntryHelpers::invalidFieldError( + "malformedAuthorizedCredentials", + jss::authorized_credentials, + "array"); STArray arr(sfAuthorizeCredentials, jv.size()); for (auto const& jo : jv) { - if (!jo.isObject() || // - !jo.isMember(jss::issuer) || !jo[jss::issuer].isString() || - !jo.isMember(jss::credential_type) || - !jo[jss::credential_type].isString()) - return {}; + if (!jo.isObject()) + return LedgerEntryHelpers::invalidFieldError( + "malformedAuthorizedCredentials", + jss::authorized_credentials, + "array"); + if (auto const value = LedgerEntryHelpers::hasRequired( + jo, + {jss::issuer, jss::credential_type}, + "malformedAuthorizedCredentials"); + !value) + { + return Unexpected(value.error()); + } - auto const issuer = parseBase58(jo[jss::issuer].asString()); - if (!issuer || !*issuer) - return {}; + auto const issuer = LedgerEntryHelpers::requiredAccountID( + jo, jss::issuer, "malformedAuthorizedCredentials"); + if (!issuer) + return Unexpected(issuer.error()); - auto const credentialType = - strUnHex(jo[jss::credential_type].asString()); - if (!credentialType || credentialType->empty() || - credentialType->size() > maxCredentialTypeLength) - return {}; + auto const credentialType = LedgerEntryHelpers::requiredHexBlob( + jo, + jss::credential_type, + maxCredentialTypeLength, + "malformedAuthorizedCredentials"); + if (!credentialType) + return Unexpected(credentialType.error()); auto credential = STObject::makeInnerObject(sfCredential); credential.setAccountID(sfIssuer, *issuer); @@ -231,703 +240,450 @@ parseAuthorizeCredentials(Json::Value const& jv) return arr; } -static std::optional -parseDelegate(Json::Value const& params, Json::Value& jvResult) -{ - if (!params.isObject()) - { - uint256 uNodeIndex; - if (!params.isString() || !uNodeIndex.parseHex(params.asString())) - { - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; - } - return uNodeIndex; - } - if (!params.isMember(jss::account) || !params.isMember(jss::authorize)) - { - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; - } - if (!params[jss::account].isString() || !params[jss::authorize].isString()) - { - jvResult[jss::error] = "malformedAddress"; - return std::nullopt; - } - auto const account = - parseBase58(params[jss::account].asString()); - if (!account) - { - jvResult[jss::error] = "malformedAddress"; - return std::nullopt; - } - auto const authorize = - parseBase58(params[jss::authorize].asString()); - if (!authorize) - { - jvResult[jss::error] = "malformedAddress"; - return std::nullopt; - } - return keylet::delegate(*account, *authorize).key; -} - -static std::optional -parseDepositPreauth(Json::Value const& dp, Json::Value& jvResult) +static Expected +parseDepositPreauth(Json::Value const& dp, Json::StaticString const fieldName) { if (!dp.isObject()) { - uint256 uNodeIndex; - if (!dp.isString() || !uNodeIndex.parseHex(dp.asString())) - { - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; - } - return uNodeIndex; + return parseObjectID(dp, fieldName); } - // clang-format off - if ( - (!dp.isMember(jss::owner) || !dp[jss::owner].isString()) || - (dp.isMember(jss::authorized) == dp.isMember(jss::authorized_credentials)) || - (dp.isMember(jss::authorized) && !dp[jss::authorized].isString()) || - (dp.isMember(jss::authorized_credentials) && !dp[jss::authorized_credentials].isArray()) - ) - // clang-format on + if ((dp.isMember(jss::authorized) == + dp.isMember(jss::authorized_credentials))) { - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; + return LedgerEntryHelpers::malformedError( + "malformedRequest", + "Must have exactly one of `authorized` and " + "`authorized_credentials`."); } - auto const owner = parseBase58(dp[jss::owner].asString()); + auto const owner = + LedgerEntryHelpers::requiredAccountID(dp, jss::owner, "malformedOwner"); if (!owner) { - jvResult[jss::error] = "malformedOwner"; - return std::nullopt; + return Unexpected(owner.error()); } if (dp.isMember(jss::authorized)) { - auto const authorized = - parseBase58(dp[jss::authorized].asString()); - if (!authorized) + if (auto const authorized = + LedgerEntryHelpers::parse(dp[jss::authorized])) { - jvResult[jss::error] = "malformedAuthorized"; - return std::nullopt; + return keylet::depositPreauth(*owner, *authorized).key; } - return keylet::depositPreauth(*owner, *authorized).key; + return LedgerEntryHelpers::invalidFieldError( + "malformedAuthorized", jss::authorized, "AccountID"); } auto const& ac(dp[jss::authorized_credentials]); - STArray const arr = parseAuthorizeCredentials(ac); - - if (arr.empty() || (arr.size() > maxCredentialsArraySize)) + auto const arr = parseAuthorizeCredentials(ac); + if (!arr.has_value()) + return Unexpected(arr.error()); + if (arr->empty() || (arr->size() > maxCredentialsArraySize)) { - jvResult[jss::error] = "malformedAuthorizedCredentials"; - return std::nullopt; + return LedgerEntryHelpers::invalidFieldError( + "malformedAuthorizedCredentials", + jss::authorized_credentials, + "array"); } - auto const& sorted = credentials::makeSorted(arr); + auto const& sorted = credentials::makeSorted(arr.value()); if (sorted.empty()) { - jvResult[jss::error] = "malformedAuthorizedCredentials"; - return std::nullopt; + // TODO: this error message is bad/inaccurate + return LedgerEntryHelpers::invalidFieldError( + "malformedAuthorizedCredentials", + jss::authorized_credentials, + "array"); } - return keylet::depositPreauth(*owner, sorted).key; + return keylet::depositPreauth(*owner, std::move(sorted)).key; } -static std::optional -parseDID(Json::Value const& params, Json::Value& jvResult) +static Expected +parseDID(Json::Value const& params, Json::StaticString const fieldName) { - auto const account = parseBase58(params.asString()); - if (!account || account->isZero()) + auto const account = LedgerEntryHelpers::parse(params); + if (!account) { - jvResult[jss::error] = "malformedAddress"; - return std::nullopt; + return LedgerEntryHelpers::invalidFieldError( + "malformedAddress", fieldName, "AccountID"); } return keylet::did(*account).key; } -static std::optional -parseDirectory(Json::Value const& params, Json::Value& jvResult) +static Expected +parseDirectoryNode( + Json::Value const& params, + Json::StaticString const fieldName) { - if (params.isNull()) - { - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; - } - if (!params.isObject()) { - uint256 uNodeIndex; - if (!uNodeIndex.parseHex(params.asString())) - { - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; - } - return uNodeIndex; + return parseObjectID(params, fieldName); } - if (params.isMember(jss::sub_index) && !params[jss::sub_index].isIntegral()) + if (params.isMember(jss::sub_index) && + (!params[jss::sub_index].isConvertibleTo(Json::uintValue) || + params[jss::sub_index].isBool())) { - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; + return LedgerEntryHelpers::invalidFieldError( + "malformedRequest", jss::sub_index, "number"); } - std::uint64_t uSubIndex = - params.isMember(jss::sub_index) ? params[jss::sub_index].asUInt() : 0; + if (params.isMember(jss::owner) == params.isMember(jss::dir_root)) + { + return LedgerEntryHelpers::malformedError( + "malformedRequest", + "Must have exactly one of `owner` and `dir_root` fields."); + } + + std::uint64_t uSubIndex = params.get(jss::sub_index, 0).asUInt(); if (params.isMember(jss::dir_root)) { - uint256 uDirRoot; - - if (params.isMember(jss::owner)) + if (auto const uDirRoot = + LedgerEntryHelpers::parse(params[jss::dir_root])) { - // May not specify both dir_root and owner. - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; + return keylet::page(*uDirRoot, uSubIndex).key; } - if (!uDirRoot.parseHex(params[jss::dir_root].asString())) - { - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; - } - return keylet::page(uDirRoot, uSubIndex).key; + return LedgerEntryHelpers::invalidFieldError( + "malformedDirRoot", jss::dir_root, "hash"); } if (params.isMember(jss::owner)) { auto const ownerID = - parseBase58(params[jss::owner].asString()); - + LedgerEntryHelpers::parse(params[jss::owner]); if (!ownerID) { - jvResult[jss::error] = "malformedAddress"; - return std::nullopt; + return LedgerEntryHelpers::invalidFieldError( + "malformedAddress", jss::owner, "AccountID"); } return keylet::page(keylet::ownerDir(*ownerID), uSubIndex).key; } - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; + return LedgerEntryHelpers::malformedError("malformedRequest", ""); } -static std::optional -parseEscrow(Json::Value const& params, Json::Value& jvResult) +static Expected +parseEscrow(Json::Value const& params, Json::StaticString const fieldName) { if (!params.isObject()) { - uint256 uNodeIndex; - if (!uNodeIndex.parseHex(params.asString())) - { - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; - } - - return uNodeIndex; + return parseObjectID(params, fieldName); } - if (!params.isMember(jss::owner) || !params.isMember(jss::seq) || - !params[jss::seq].isIntegral()) - { - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; - } - - auto const id = parseBase58(params[jss::owner].asString()); - + auto const id = LedgerEntryHelpers::requiredAccountID( + params, jss::owner, "malformedOwner"); if (!id) - { - jvResult[jss::error] = "malformedOwner"; - return std::nullopt; - } + return Unexpected(id.error()); + auto const seq = + LedgerEntryHelpers::requiredUInt32(params, jss::seq, "malformedSeq"); + if (!seq) + return Unexpected(seq.error()); - return keylet::escrow(*id, params[jss::seq].asUInt()).key; + return keylet::escrow(*id, *seq).key; } -static std::optional -parseMPToken(Json::Value const& mptJson, Json::Value& jvResult) +static Expected +parseFeeSettings(Json::Value const& params, Json::StaticString const fieldName) { - if (!mptJson.isObject()) - { - uint256 uNodeIndex; - if (!uNodeIndex.parseHex(mptJson.asString())) - { - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; - } - return uNodeIndex; - } - - if (!mptJson.isMember(jss::mpt_issuance_id) || - !mptJson.isMember(jss::account)) - { - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; - } - - try - { - auto const mptIssuanceIdStr = mptJson[jss::mpt_issuance_id].asString(); - - uint192 mptIssuanceID; - if (!mptIssuanceID.parseHex(mptIssuanceIdStr)) - Throw("Cannot parse mpt_issuance_id"); - - auto const account = - parseBase58(mptJson[jss::account].asString()); - - if (!account || account->isZero()) - { - jvResult[jss::error] = "malformedAddress"; - return std::nullopt; - } - - return keylet::mptoken(mptIssuanceID, *account).key; - } - catch (std::runtime_error const&) - { - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; - } + return parseObjectID(params, fieldName, "hex string"); } -static std::optional +static Expected +parseLedgerHashes(Json::Value const& params, Json::StaticString const fieldName) +{ + return parseObjectID(params, fieldName, "hex string"); +} + +static Expected +parseMPToken(Json::Value const& params, Json::StaticString const fieldName) +{ + if (!params.isObject()) + { + return parseObjectID(params, fieldName); + } + + auto const mptIssuanceID = LedgerEntryHelpers::requiredUInt192( + params, jss::mpt_issuance_id, "malformedMPTIssuanceID"); + if (!mptIssuanceID) + return Unexpected(mptIssuanceID.error()); + + auto const account = LedgerEntryHelpers::requiredAccountID( + params, jss::account, "malformedAccount"); + if (!account) + return Unexpected(account.error()); + + return keylet::mptoken(*mptIssuanceID, *account).key; +} + +static Expected parseMPTokenIssuance( - Json::Value const& unparsedMPTIssuanceID, - Json::Value& jvResult) + Json::Value const& params, + Json::StaticString const fieldName) { - if (unparsedMPTIssuanceID.isString()) - { - uint192 mptIssuanceID; - if (!mptIssuanceID.parseHex(unparsedMPTIssuanceID.asString())) - { - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; - } + auto const mptIssuanceID = LedgerEntryHelpers::parse(params); + if (!mptIssuanceID) + return LedgerEntryHelpers::invalidFieldError( + "malformedMPTokenIssuance", fieldName, "Hash192"); - return keylet::mptIssuance(mptIssuanceID).key; - } - - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; + return keylet::mptIssuance(*mptIssuanceID).key; } -static std::optional -parseNFTokenPage(Json::Value const& params, Json::Value& jvResult) +static Expected +parseNFTokenOffer(Json::Value const& params, Json::StaticString const fieldName) { - if (params.isString()) - { - uint256 uNodeIndex; - if (!uNodeIndex.parseHex(params.asString())) - { - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; - } - return uNodeIndex; - } - - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; + return parseObjectID(params, fieldName, "hex string"); } -static std::optional -parseOffer(Json::Value const& params, Json::Value& jvResult) +static Expected +parseNFTokenPage(Json::Value const& params, Json::StaticString const fieldName) +{ + return parseObjectID(params, fieldName, "hex string"); +} + +static Expected +parseNegativeUNL(Json::Value const& params, Json::StaticString const fieldName) +{ + return parseObjectID(params, fieldName, "hex string"); +} + +static Expected +parseOffer(Json::Value const& params, Json::StaticString const fieldName) { if (!params.isObject()) { - uint256 uNodeIndex; - if (!uNodeIndex.parseHex(params.asString())) - { - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; - } - return uNodeIndex; + return parseObjectID(params, fieldName); } - if (!params.isMember(jss::account) || !params.isMember(jss::seq) || - !params[jss::seq].isIntegral()) - { - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; - } - - auto const id = parseBase58(params[jss::account].asString()); + auto const id = LedgerEntryHelpers::requiredAccountID( + params, jss::account, "malformedAddress"); if (!id) - { - jvResult[jss::error] = "malformedAddress"; - return std::nullopt; - } + return Unexpected(id.error()); - return keylet::offer(*id, params[jss::seq].asUInt()).key; + auto const seq = LedgerEntryHelpers::requiredUInt32( + params, jss::seq, "malformedRequest"); + if (!seq) + return Unexpected(seq.error()); + + return keylet::offer(*id, *seq).key; } -static std::optional -parseOracle(Json::Value const& params, Json::Value& jvResult) +static Expected +parseOracle(Json::Value const& params, Json::StaticString const fieldName) { if (!params.isObject()) { - uint256 uNodeIndex; - if (!uNodeIndex.parseHex(params.asString())) - { - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; - } - return uNodeIndex; + return parseObjectID(params, fieldName); } - if (!params.isMember(jss::oracle_document_id) || - !params.isMember(jss::account)) - { - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; - } + auto const id = LedgerEntryHelpers::requiredAccountID( + params, jss::account, "malformedAccount"); + if (!id) + return Unexpected(id.error()); - auto const& oracle = params; - auto const documentID = [&]() -> std::optional { - auto const id = oracle[jss::oracle_document_id]; - if (id.isUInt() || (id.isInt() && id.asInt() >= 0)) - return std::make_optional(id.asUInt()); + auto const seq = LedgerEntryHelpers::requiredUInt32( + params, jss::oracle_document_id, "malformedDocumentID"); + if (!seq) + return Unexpected(seq.error()); - if (id.isString()) - { - std::uint32_t v; - if (beast::lexicalCastChecked(v, id.asString())) - return std::make_optional(v); - } - - return std::nullopt; - }(); - - auto const account = - parseBase58(oracle[jss::account].asString()); - if (!account || account->isZero()) - { - jvResult[jss::error] = "malformedAddress"; - return std::nullopt; - } - - if (!documentID) - { - jvResult[jss::error] = "malformedDocumentID"; - return std::nullopt; - } - - return keylet::oracle(*account, *documentID).key; + return keylet::oracle(*id, *seq).key; } -static std::optional -parsePaymentChannel(Json::Value const& params, Json::Value& jvResult) +static Expected +parsePayChannel(Json::Value const& params, Json::StaticString const fieldName) { - uint256 uNodeIndex; - if (!uNodeIndex.parseHex(params.asString())) - { - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; - } - - return uNodeIndex; + return parseObjectID(params, fieldName, "hex string"); } -static std::optional -parsePermissionedDomains(Json::Value const& pd, Json::Value& jvResult) +static Expected +parsePermissionedDomain( + Json::Value const& pd, + Json::StaticString const fieldName) { if (pd.isString()) { - auto const index = parseIndex(pd, jvResult); - return index; + return parseObjectID(pd, fieldName); } if (!pd.isObject()) { - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; + return LedgerEntryHelpers::invalidFieldError( + "malformedRequest", fieldName, "hex string or object"); } - if (!pd.isMember(jss::account)) - { - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; - } - - if (!pd[jss::account].isString()) - { - jvResult[jss::error] = "malformedAddress"; - return std::nullopt; - } - - if (!pd.isMember(jss::seq) || - (pd[jss::seq].isInt() && pd[jss::seq].asInt() < 0) || - (!pd[jss::seq].isInt() && !pd[jss::seq].isUInt())) - { - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; - } - - auto const account = parseBase58(pd[jss::account].asString()); + auto const account = LedgerEntryHelpers::requiredAccountID( + pd, jss::account, "malformedAddress"); if (!account) - { - jvResult[jss::error] = "malformedAddress"; - return std::nullopt; - } + return Unexpected(account.error()); + + auto const seq = + LedgerEntryHelpers::requiredUInt32(pd, jss::seq, "malformedRequest"); + if (!seq) + return Unexpected(seq.error()); return keylet::permissionedDomain(*account, pd[jss::seq].asUInt()).key; } -static std::optional -parseRippleState(Json::Value const& jvRippleState, Json::Value& jvResult) +static Expected +parseRippleState( + Json::Value const& jvRippleState, + Json::StaticString const fieldName) { Currency uCurrency; - if (!jvRippleState.isObject() || !jvRippleState.isMember(jss::currency) || - !jvRippleState.isMember(jss::accounts) || - !jvRippleState[jss::accounts].isArray() || - 2 != jvRippleState[jss::accounts].size() || - !jvRippleState[jss::accounts][0u].isString() || - !jvRippleState[jss::accounts][1u].isString() || - (jvRippleState[jss::accounts][0u].asString() == - jvRippleState[jss::accounts][1u].asString())) + if (!jvRippleState.isObject()) { - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; + return parseObjectID(jvRippleState, fieldName); + } + + if (auto const value = LedgerEntryHelpers::hasRequired( + jvRippleState, {jss::currency, jss::accounts}); + !value) + { + return Unexpected(value.error()); + } + + if (!jvRippleState[jss::accounts].isArray() || + jvRippleState[jss::accounts].size() != 2) + { + return LedgerEntryHelpers::invalidFieldError( + "malformedRequest", jss::accounts, "length-2 array of Accounts"); } auto const id1 = - parseBase58(jvRippleState[jss::accounts][0u].asString()); + LedgerEntryHelpers::parse(jvRippleState[jss::accounts][0u]); auto const id2 = - parseBase58(jvRippleState[jss::accounts][1u].asString()); + LedgerEntryHelpers::parse(jvRippleState[jss::accounts][1u]); if (!id1 || !id2) { - jvResult[jss::error] = "malformedAddress"; - return std::nullopt; + return LedgerEntryHelpers::invalidFieldError( + "malformedAddress", jss::accounts, "array of Accounts"); + } + if (id1 == id2) + { + return LedgerEntryHelpers::malformedError( + "malformedRequest", "Cannot have a trustline to self."); } - if (!to_currency(uCurrency, jvRippleState[jss::currency].asString())) + if (!jvRippleState[jss::currency].isString() || + jvRippleState[jss::currency] == "" || + !to_currency(uCurrency, jvRippleState[jss::currency].asString())) { - jvResult[jss::error] = "malformedCurrency"; - return std::nullopt; + return LedgerEntryHelpers::invalidFieldError( + "malformedCurrency", jss::currency, "Currency"); } return keylet::line(*id1, *id2, uCurrency).key; } -static std::optional -parseTicket(Json::Value const& params, Json::Value& jvResult) +static Expected +parseSignerList(Json::Value const& params, Json::StaticString const fieldName) +{ + return parseObjectID(params, fieldName, "hex string"); +} + +static Expected +parseTicket(Json::Value const& params, Json::StaticString const fieldName) { if (!params.isObject()) { - uint256 uNodeIndex; - if (!uNodeIndex.parseHex(params.asString())) - { - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; - } - return uNodeIndex; + return parseObjectID(params, fieldName); } - if (!params.isMember(jss::account) || !params.isMember(jss::ticket_seq) || - !params[jss::ticket_seq].isIntegral()) - { - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; - } - - auto const id = parseBase58(params[jss::account].asString()); + auto const id = LedgerEntryHelpers::requiredAccountID( + params, jss::account, "malformedAddress"); if (!id) - { - jvResult[jss::error] = "malformedAddress"; - return std::nullopt; - } + return Unexpected(id.error()); - return getTicketIndex(*id, params[jss::ticket_seq].asUInt()); + auto const seq = LedgerEntryHelpers::requiredUInt32( + params, jss::ticket_seq, "malformedRequest"); + if (!seq) + return Unexpected(seq.error()); + + return getTicketIndex(*id, *seq); } -static std::optional -parseVault(Json::Value const& params, Json::Value& jvResult) +static Expected +parseVault(Json::Value const& params, Json::StaticString const fieldName) { if (!params.isObject()) { - uint256 uNodeIndex; - if (!uNodeIndex.parseHex(params.asString())) - { - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; - } - return uNodeIndex; + return parseObjectID(params, fieldName); } - if (!params.isMember(jss::owner) || !params.isMember(jss::seq) || - !(params[jss::seq].isInt() || params[jss::seq].isUInt()) || - params[jss::seq].asDouble() <= 0.0 || - params[jss::seq].asDouble() > double(Json::Value::maxUInt)) - { - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; - } - - auto const id = parseBase58(params[jss::owner].asString()); + auto const id = LedgerEntryHelpers::requiredAccountID( + params, jss::owner, "malformedOwner"); if (!id) - { - jvResult[jss::error] = "malformedOwner"; - return std::nullopt; - } + return Unexpected(id.error()); - return keylet::vault(*id, params[jss::seq].asUInt()).key; + auto const seq = LedgerEntryHelpers::requiredUInt32( + params, jss::seq, "malformedRequest"); + if (!seq) + return Unexpected(seq.error()); + + return keylet::vault(*id, *seq).key; } -static std::optional -parseXChainOwnedClaimID(Json::Value const& claim_id, Json::Value& jvResult) +static Expected +parseXChainOwnedClaimID( + Json::Value const& claim_id, + Json::StaticString const fieldName) { - if (claim_id.isString()) + if (!claim_id.isObject()) { - uint256 uNodeIndex; - // we accept a node id as specifier of a xchain claim id - if (!uNodeIndex.parseHex(claim_id.asString())) - { - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; - } - return uNodeIndex; + return parseObjectID(claim_id, fieldName); } - if (!claim_id.isObject() || - !(claim_id.isMember(sfIssuingChainDoor.getJsonName()) && - claim_id[sfIssuingChainDoor.getJsonName()].isString()) || - !(claim_id.isMember(sfLockingChainDoor.getJsonName()) && - claim_id[sfLockingChainDoor.getJsonName()].isString()) || - !claim_id.isMember(sfIssuingChainIssue.getJsonName()) || - !claim_id.isMember(sfLockingChainIssue.getJsonName()) || - !claim_id.isMember(jss::xchain_owned_claim_id)) + auto const bridge_spec = LedgerEntryHelpers::parseBridgeFields(claim_id); + if (!bridge_spec) + return Unexpected(bridge_spec.error()); + + auto const seq = LedgerEntryHelpers::requiredUInt32( + claim_id, jss::xchain_owned_claim_id, "malformedXChainOwnedClaimID"); + if (!seq) { - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; + return Unexpected(seq.error()); } - // if not specified with a node id, a claim_id is specified by - // four strings defining the bridge (locking_chain_door, - // locking_chain_issue, issuing_chain_door, issuing_chain_issue) - // and the claim id sequence number. - auto const lockingChainDoor = parseBase58( - claim_id[sfLockingChainDoor.getJsonName()].asString()); - auto const issuingChainDoor = parseBase58( - claim_id[sfIssuingChainDoor.getJsonName()].asString()); - Issue lockingChainIssue, issuingChainIssue; - bool valid = lockingChainDoor && issuingChainDoor; - - if (valid) - { - try - { - lockingChainIssue = - issueFromJson(claim_id[sfLockingChainIssue.getJsonName()]); - issuingChainIssue = - issueFromJson(claim_id[sfIssuingChainIssue.getJsonName()]); - } - catch (std::runtime_error const& ex) - { - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; - } - } - - if (valid && claim_id[jss::xchain_owned_claim_id].isIntegral()) - { - auto const seq = claim_id[jss::xchain_owned_claim_id].asUInt(); - - STXChainBridge bridge_spec( - *lockingChainDoor, - lockingChainIssue, - *issuingChainDoor, - issuingChainIssue); - Keylet keylet = keylet::xChainClaimID(bridge_spec, seq); - return keylet.key; - } - - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; + Keylet keylet = keylet::xChainClaimID(*bridge_spec, *seq); + return keylet.key; } -static std::optional +static Expected parseXChainOwnedCreateAccountClaimID( Json::Value const& claim_id, - Json::Value& jvResult) + Json::StaticString const fieldName) { - if (claim_id.isString()) + if (!claim_id.isObject()) { - uint256 uNodeIndex; - // we accept a node id as specifier of a xchain create account - // claim_id - if (!uNodeIndex.parseHex(claim_id.asString())) - { - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; - } - return uNodeIndex; + return parseObjectID(claim_id, fieldName); } - if (!claim_id.isObject() || - !(claim_id.isMember(sfIssuingChainDoor.getJsonName()) && - claim_id[sfIssuingChainDoor.getJsonName()].isString()) || - !(claim_id.isMember(sfLockingChainDoor.getJsonName()) && - claim_id[sfLockingChainDoor.getJsonName()].isString()) || - !claim_id.isMember(sfIssuingChainIssue.getJsonName()) || - !claim_id.isMember(sfLockingChainIssue.getJsonName()) || - !claim_id.isMember(jss::xchain_owned_create_account_claim_id)) + auto const bridge_spec = LedgerEntryHelpers::parseBridgeFields(claim_id); + if (!bridge_spec) + return Unexpected(bridge_spec.error()); + + auto const seq = LedgerEntryHelpers::requiredUInt32( + claim_id, + jss::xchain_owned_create_account_claim_id, + "malformedXChainOwnedCreateAccountClaimID"); + if (!seq) { - jvResult[jss::error] = "malformedRequest"; - return std::nullopt; + return Unexpected(seq.error()); } - // if not specified with a node id, a create account claim_id is - // specified by four strings defining the bridge - // (locking_chain_door, locking_chain_issue, issuing_chain_door, - // issuing_chain_issue) and the create account claim id sequence - // number. - auto const lockingChainDoor = parseBase58( - claim_id[sfLockingChainDoor.getJsonName()].asString()); - auto const issuingChainDoor = parseBase58( - claim_id[sfIssuingChainDoor.getJsonName()].asString()); - Issue lockingChainIssue, issuingChainIssue; - bool valid = lockingChainDoor && issuingChainDoor; - if (valid) - { - try - { - lockingChainIssue = - issueFromJson(claim_id[sfLockingChainIssue.getJsonName()]); - issuingChainIssue = - issueFromJson(claim_id[sfIssuingChainIssue.getJsonName()]); - } - catch (std::runtime_error const& ex) - { - valid = false; - jvResult[jss::error] = "malformedRequest"; - } - } - - if (valid && - claim_id[jss::xchain_owned_create_account_claim_id].isIntegral()) - { - auto const seq = - claim_id[jss::xchain_owned_create_account_claim_id].asUInt(); - - STXChainBridge bridge_spec( - *lockingChainDoor, - lockingChainIssue, - *issuingChainDoor, - issuingChainIssue); - Keylet keylet = keylet::xChainCreateAccountClaimID(bridge_spec, seq); - return keylet.key; - } - - return std::nullopt; + Keylet keylet = keylet::xChainCreateAccountClaimID(*bridge_spec, *seq); + return keylet.key; } -using FunctionType = - std::function(Json::Value const&, Json::Value&)>; +using FunctionType = Expected (*)( + Json::Value const&, + Json::StaticString const); struct LedgerEntry { @@ -944,50 +700,49 @@ struct LedgerEntry Json::Value doLedgerEntry(RPC::JsonContext& context) { + static auto ledgerEntryParsers = std::to_array({ +#pragma push_macro("LEDGER_ENTRY") +#undef LEDGER_ENTRY + +#define LEDGER_ENTRY(tag, value, name, rpcName, fields) \ + {jss::rpcName, parse##name, tag}, + +#include + +#undef LEDGER_ENTRY +#pragma pop_macro("LEDGER_ENTRY") + {jss::index, parseIndex, ltANY}, + // aliases + {jss::account_root, parseAccountRoot, ltACCOUNT_ROOT}, + {jss::ripple_state, parseRippleState, ltRIPPLE_STATE}, + }); + + auto hasMoreThanOneMember = [&]() { + int count = 0; + + for (auto const& ledgerEntry : ledgerEntryParsers) + { + if (context.params.isMember(ledgerEntry.fieldName)) + { + count++; + if (count > 1) // Early exit if more than one is found + return true; + } + } + return false; // Return false if <= 1 is found + }(); + + if (hasMoreThanOneMember) + { + return RPC::make_param_error("Too many fields provided."); + } + std::shared_ptr lpLedger; auto jvResult = RPC::lookupLedger(lpLedger, context); if (!lpLedger) return jvResult; - static auto ledgerEntryParsers = std::to_array({ - {jss::index, parseIndex, ltANY}, - {jss::account_root, parseAccountRoot, ltACCOUNT_ROOT}, - // TODO: add amendments - {jss::amm, parseAMM, ltAMM}, - {jss::bridge, parseBridge, ltBRIDGE}, - {jss::check, parseCheck, ltCHECK}, - {jss::credential, parseCredential, ltCREDENTIAL}, - {jss::delegate, parseDelegate, ltDELEGATE}, - {jss::deposit_preauth, parseDepositPreauth, ltDEPOSIT_PREAUTH}, - {jss::did, parseDID, ltDID}, - {jss::directory, parseDirectory, ltDIR_NODE}, - {jss::escrow, parseEscrow, ltESCROW}, - // TODO: add fee, hashes - {jss::mpt_issuance, parseMPTokenIssuance, ltMPTOKEN_ISSUANCE}, - {jss::mptoken, parseMPToken, ltMPTOKEN}, - // TODO: add NFT Offers - {jss::nft_page, parseNFTokenPage, ltNFTOKEN_PAGE}, - // TODO: add NegativeUNL - {jss::offer, parseOffer, ltOFFER}, - {jss::oracle, parseOracle, ltORACLE}, - {jss::payment_channel, parsePaymentChannel, ltPAYCHAN}, - {jss::permissioned_domain, - parsePermissionedDomains, - ltPERMISSIONED_DOMAIN}, - {jss::ripple_state, parseRippleState, ltRIPPLE_STATE}, - // This is an alias, since the `ledger_data` filter uses jss::state - {jss::state, parseRippleState, ltRIPPLE_STATE}, - {jss::ticket, parseTicket, ltTICKET}, - {jss::xchain_owned_claim_id, - parseXChainOwnedClaimID, - ltXCHAIN_OWNED_CLAIM_ID}, - {jss::xchain_owned_create_account_claim_id, - parseXChainOwnedCreateAccountClaimID, - ltXCHAIN_OWNED_CREATE_ACCOUNT_CLAIM_ID}, - {jss::vault, parseVault, ltVAULT}, - }); - uint256 uNodeIndex; LedgerEntryType expectedType = ltANY; @@ -1006,34 +761,33 @@ doLedgerEntry(RPC::JsonContext& context) Json::Value const& params = ledgerEntry.fieldName == jss::bridge ? context.params : context.params[ledgerEntry.fieldName]; - uNodeIndex = ledgerEntry.parseFunction(params, jvResult) - .value_or(beast::zero); - if (jvResult.isMember(jss::error)) - { - return jvResult; - } + auto const result = + ledgerEntry.parseFunction(params, ledgerEntry.fieldName); + if (!result) + return result.error(); + + uNodeIndex = result.value(); found = true; break; } } - if (!found) { if (context.apiVersion < 2u) + { jvResult[jss::error] = "unknownOption"; - else - jvResult[jss::error] = "invalidParams"; - return jvResult; + return jvResult; + } + return RPC::make_param_error("No ledger_entry params provided."); } } catch (Json::error& e) { if (context.apiVersion > 1u) { - // For apiVersion 2 onwards, any parsing failures that throw this - // exception return an invalidParam error. - jvResult[jss::error] = "invalidParams"; - return jvResult; + // For apiVersion 2 onwards, any parsing failures that throw + // this exception return an invalidParam error. + return RPC::make_error(rpcINVALID_PARAMS); } else throw; @@ -1041,8 +795,7 @@ doLedgerEntry(RPC::JsonContext& context) if (uNodeIndex.isZero()) { - jvResult[jss::error] = "entryNotFound"; - return jvResult; + return RPC::make_error(rpcENTRY_NOT_FOUND); } auto const sleNode = lpLedger->read(keylet::unchecked(uNodeIndex)); @@ -1054,14 +807,12 @@ doLedgerEntry(RPC::JsonContext& context) if (!sleNode) { // Not found. - jvResult[jss::error] = "entryNotFound"; - return jvResult; + return RPC::make_error(rpcENTRY_NOT_FOUND); } if ((expectedType != ltANY) && (expectedType != sleNode->getType())) { - jvResult[jss::error] = "unexpectedLedgerType"; - return jvResult; + return RPC::make_error(rpcUNEXPECTED_LEDGER_TYPE); } if (bNodeBinary) @@ -1091,7 +842,7 @@ doLedgerEntryGrpc( grpc::Status status = grpc::Status::OK; std::shared_ptr ledger; - if (auto const status = RPC::ledgerFromRequest(ledger, context)) + if (auto status = RPC::ledgerFromRequest(ledger, context)) { grpc::Status errorStatus; if (status.toErrorCode() == rpcINVALID_PARAMS) diff --git a/src/xrpld/rpc/handlers/LedgerEntryHelpers.h b/src/xrpld/rpc/handlers/LedgerEntryHelpers.h new file mode 100644 index 0000000000..12b99dbbff --- /dev/null +++ b/src/xrpld/rpc/handlers/LedgerEntryHelpers.h @@ -0,0 +1,299 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2012-2025 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 +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace ripple { + +namespace LedgerEntryHelpers { + +Unexpected +missingFieldError( + Json::StaticString const field, + std::optional err = std::nullopt) +{ + Json::Value json = Json::objectValue; + auto error = RPC::missing_field_message(std::string(field.c_str())); + json[jss::error] = err.value_or("malformedRequest"); + json[jss::error_code] = rpcINVALID_PARAMS; + json[jss::error_message] = std::move(error); + return Unexpected(json); +} + +Unexpected +invalidFieldError( + std::string const& err, + Json::StaticString const field, + std::string const& type) +{ + Json::Value json = Json::objectValue; + auto error = RPC::expected_field_message(field, type); + json[jss::error] = err; + json[jss::error_code] = rpcINVALID_PARAMS; + json[jss::error_message] = std::move(error); + return Unexpected(json); +} + +Unexpected +malformedError(std::string const& err, std::string const& message) +{ + Json::Value json = Json::objectValue; + json[jss::error] = err; + json[jss::error_code] = rpcINVALID_PARAMS; + json[jss::error_message] = message; + return Unexpected(json); +} + +Expected +hasRequired( + Json::Value const& params, + std::initializer_list fields, + std::optional err = std::nullopt) +{ + for (auto const field : fields) + { + if (!params.isMember(field) || params[field].isNull()) + { + return missingFieldError(field, err); + } + } + return true; +} + +template +std::optional +parse(Json::Value const& param); + +template +Expected +required( + Json::Value const& params, + Json::StaticString const fieldName, + std::string const& err, + std::string const& expectedType) +{ + if (!params.isMember(fieldName) || params[fieldName].isNull()) + { + return missingFieldError(fieldName); + } + if (auto obj = parse(params[fieldName])) + { + return *obj; + } + return invalidFieldError(err, fieldName, expectedType); +} + +template <> +std::optional +parse(Json::Value const& param) +{ + if (!param.isString()) + return std::nullopt; + + auto const account = parseBase58(param.asString()); + if (!account || account->isZero()) + { + return std::nullopt; + } + + return account; +} + +Expected +requiredAccountID( + Json::Value const& params, + Json::StaticString const fieldName, + std::string const& err) +{ + return required(params, fieldName, err, "AccountID"); +} + +std::optional +parseHexBlob(Json::Value const& param, std::size_t maxLength) +{ + if (!param.isString()) + return std::nullopt; + + auto const blob = strUnHex(param.asString()); + if (!blob || blob->empty() || blob->size() > maxLength) + return std::nullopt; + + return blob; +} + +Expected +requiredHexBlob( + Json::Value const& params, + Json::StaticString const fieldName, + std::size_t maxLength, + std::string const& err) +{ + if (!params.isMember(fieldName) || params[fieldName].isNull()) + { + return missingFieldError(fieldName); + } + if (auto blob = parseHexBlob(params[fieldName], maxLength)) + { + return *blob; + } + return invalidFieldError(err, fieldName, "hex string"); +} + +template <> +std::optional +parse(Json::Value const& param) +{ + if (param.isUInt() || (param.isInt() && param.asInt() >= 0)) + return param.asUInt(); + + if (param.isString()) + { + std::uint32_t v; + if (beast::lexicalCastChecked(v, param.asString())) + return v; + } + + return std::nullopt; +} + +Expected +requiredUInt32( + Json::Value const& params, + Json::StaticString const fieldName, + std::string const& err) +{ + return required(params, fieldName, err, "number"); +} + +template <> +std::optional +parse(Json::Value const& param) +{ + uint256 uNodeIndex; + if (!param.isString() || !uNodeIndex.parseHex(param.asString())) + { + return std::nullopt; + } + + return uNodeIndex; +} + +Expected +requiredUInt256( + Json::Value const& params, + Json::StaticString const fieldName, + std::string const& err) +{ + return required(params, fieldName, err, "Hash256"); +} + +template <> +std::optional +parse(Json::Value const& param) +{ + uint192 field; + if (!param.isString() || !field.parseHex(param.asString())) + { + return std::nullopt; + } + + return field; +} + +Expected +requiredUInt192( + Json::Value const& params, + Json::StaticString const fieldName, + std::string const& err) +{ + return required(params, fieldName, err, "Hash192"); +} + +Expected +parseBridgeFields(Json::Value const& params) +{ + if (auto const value = hasRequired( + params, + {jss::LockingChainDoor, + jss::LockingChainIssue, + jss::IssuingChainDoor, + jss::IssuingChainIssue}); + !value) + { + return Unexpected(value.error()); + } + + auto const lockingChainDoor = requiredAccountID( + params, jss::LockingChainDoor, "malformedLockingChainDoor"); + if (!lockingChainDoor) + { + return Unexpected(lockingChainDoor.error()); + } + + auto const issuingChainDoor = requiredAccountID( + params, jss::IssuingChainDoor, "malformedIssuingChainDoor"); + if (!issuingChainDoor) + { + return Unexpected(issuingChainDoor.error()); + } + + Issue lockingChainIssue; + try + { + lockingChainIssue = issueFromJson(params[jss::LockingChainIssue]); + } + catch (std::runtime_error const& ex) + { + return invalidFieldError( + "malformedIssue", jss::LockingChainIssue, "Issue"); + } + + Issue issuingChainIssue; + try + { + issuingChainIssue = issueFromJson(params[jss::IssuingChainIssue]); + } + catch (std::runtime_error const& ex) + { + return invalidFieldError( + "malformedIssue", jss::IssuingChainIssue, "Issue"); + } + + return STXChainBridge( + *lockingChainDoor, + lockingChainIssue, + *issuingChainDoor, + issuingChainIssue); +} + +} // namespace LedgerEntryHelpers + +} // namespace ripple