diff --git a/.github/workflows/check-format.yml b/.github/workflows/check-format.yml deleted file mode 100644 index c63589017d..0000000000 --- a/.github/workflows/check-format.yml +++ /dev/null @@ -1,44 +0,0 @@ -# This workflow checks if the code is properly formatted. -name: Check format - -# This workflow can only be triggered by other workflows. -on: workflow_call - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }}-format - cancel-in-progress: true - -defaults: - run: - shell: bash - -jobs: - pre-commit: - runs-on: ubuntu-latest - container: ghcr.io/xrplf/ci/tools-rippled-pre-commit - steps: - - name: Checkout repository - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 - - name: Prepare runner - uses: XRPLF/actions/.github/actions/prepare-runner@638e0dc11ea230f91bd26622fb542116bb5254d5 - - name: Format code - run: pre-commit run --show-diff-on-failure --color=always --all-files - - name: Check for differences - env: - MESSAGE: | - One or more files did not conform to the formatting. Maybe you did - not run 'pre-commit' before committing, or your version of - 'clang-format' or 'prettier' has an incompatibility with the ones - used here (see the "Check configuration" step above). - - Run 'pre-commit run --all-files' in your repo, and then commit and - push the changes. - run: | - DIFF=$(git status --porcelain) - if [ -n "${DIFF}" ]; then - # Print the files that changed to give the contributor a hint about - # what to expect when running pre-commit on their own machine. - git status - echo "${MESSAGE}" - exit 1 - fi diff --git a/.github/workflows/on-pr.yml b/.github/workflows/on-pr.yml index c480cc5476..24f27d5162 100644 --- a/.github/workflows/on-pr.yml +++ b/.github/workflows/on-pr.yml @@ -50,12 +50,9 @@ jobs: files: | # These paths are unique to `on-pr.yml`. .github/scripts/levelization/** - .github/workflows/check-format.yml .github/workflows/check-levelization.yml .github/workflows/notify-clio.yml .github/workflows/on-pr.yml - .clang-format - .pre-commit-config.yaml # Keep the paths below in sync with those in `on-trigger.yml`. .github/actions/build-deps/** @@ -93,11 +90,6 @@ jobs: outputs: go: ${{ steps.go.outputs.go == 'true' }} - check-format: - needs: should-run - if: needs.should-run.outputs.go == 'true' - uses: ./.github/workflows/check-format.yml - check-levelization: needs: should-run if: needs.should-run.outputs.go == 'true' @@ -130,7 +122,6 @@ jobs: if: failure() || cancelled() needs: - build-test - - check-format - check-levelization runs-on: ubuntu-latest steps: diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml new file mode 100644 index 0000000000..ead137308d --- /dev/null +++ b/.github/workflows/pre-commit.yml @@ -0,0 +1,14 @@ +name: Run pre-commit hooks + +on: + pull_request: + push: + branches: [develop, release, master] + workflow_dispatch: + +jobs: + run-hooks: + uses: XRPLF/actions/.github/workflows/pre-commit.yml@af1b0f0d764cda2e5435f5ac97b240d4bd4d95d3 + with: + runs_on: ubuntu-latest + container: '{ "image": "ghcr.io/xrplf/ci/tools-rippled-pre-commit" }' diff --git a/BUILD.md b/BUILD.md index 0b1da3703c..368400bf3e 100644 --- a/BUILD.md +++ b/BUILD.md @@ -132,7 +132,7 @@ higher index than the default Conan Center remote, so it is consulted first. You can do this by running: ```bash -conan remote add --index 0 xrplf "https://conan.ripplex.io" +conan remote add --index 0 xrplf https://conan.ripplex.io ``` Alternatively, you can pull the patched recipes into the repository and use them @@ -480,12 +480,24 @@ It is implicitly used when running `conan` commands, you don't need to specify i You have to update this file every time you add a new dependency or change a revision or version of an existing dependency. -To do that, run the following command in the repository root: +> [!NOTE] +> Conan uses local cache by default when creating a lockfile. +> +> To ensure, that lockfile creation works the same way on all developer machines, you should clear the local cache before creating a new lockfile. + +To create a new lockfile, run the following commands in the repository root: ```bash +conan remove '*' --confirm +rm conan.lock +# This ensure that xrplf remote is the first to be consulted +conan remote add --force --index 0 xrplf https://conan.ripplex.io conan lock create . -o '&:jemalloc=True' -o '&:rocksdb=True' ``` +> [!NOTE] +> If some dependencies are exclusive for some OS, you may need to run the last command for them adding `--profile:all `. + ## Coverage report The coverage report is intended for developers using compilers GCC @@ -587,6 +599,11 @@ After any updates or changes to dependencies, you may need to do the following: 4. [Regenerate lockfile](#conan-lockfile). 5. Re-run [conan install](#build-and-test). +#### ERROR: Package not resolved + +If you're seeing an error like `ERROR: Package 'snappy/1.1.10' not resolved: Unable to find 'snappy/1.1.10#968fef506ff261592ec30c574d4a7809%1756234314.246' in remotes.`, +please add `xrplf` remote or re-run `conan export` for [patched recipes](#patched-recipes). + ### `protobuf/port_def.inc` file not found If `cmake --build .` results in an error due to a missing a protobuf file, then diff --git a/src/test/rpc/Simulate_test.cpp b/src/test/rpc/Simulate_test.cpp index 5b3c0d2372..0a36a8a841 100644 --- a/src/test/rpc/Simulate_test.cpp +++ b/src/test/rpc/Simulate_test.cpp @@ -131,6 +131,32 @@ class Simulate_test : public beast::unit_test::suite std::to_string(env.current()->txCount())); } + void + testTxJsonMetadataField( + jtx::Env& env, + Json::Value const& tx, + std::function const& validate, + Json::Value const& expectedMetadataKey, + bool testSerialized = true) + { + env.close(); + + Json::Value params; + params[jss::tx_json] = tx; + validate( + env.rpc("json", "simulate", to_string(params)), + tx, + expectedMetadataKey); + validate(env.rpc("simulate", to_string(tx)), tx, expectedMetadataKey); + + BEAST_EXPECTS( + env.current()->txCount() == 0, + std::to_string(env.current()->txCount())); + } + Json::Value getJsonMetadata(Json::Value txResult) const { @@ -1186,6 +1212,83 @@ class Simulate_test : public beast::unit_test::suite } } + void + testSuccessfulTransactionAdditionalMetadata() + { + testcase("Successful transaction with additional metadata"); + + using namespace jtx; + Env env{*this, envconfig([&](std::unique_ptr cfg) { + cfg->NETWORK_ID = 1025; + return cfg; + })}; + + Account const alice("alice"); + + env.fund(XRP(10000), alice); + env.close(); + + { + auto validateOutput = [&](Json::Value const& resp, + Json::Value const& tx, + Json::Value const& expectedMetadataKey) { + auto result = resp[jss::result]; + + BEAST_EXPECT(result[jss::engine_result] == "tesSUCCESS"); + BEAST_EXPECT(result[jss::engine_result_code] == 0); + BEAST_EXPECT( + result[jss::engine_result_message] == + "The simulated transaction would have been applied."); + + if (BEAST_EXPECT( + result.isMember(jss::meta) || + result.isMember(jss::meta_blob))) + { + Json::Value const metadata = getJsonMetadata(result); + + BEAST_EXPECT(metadata[sfTransactionIndex.jsonName] == 0); + BEAST_EXPECT( + metadata[sfTransactionResult.jsonName] == "tesSUCCESS"); + BEAST_EXPECT( + metadata.isMember(expectedMetadataKey.asString())); + } + }; + + { + Json::Value tx; + tx[jss::Account] = env.master.human(); + tx[jss::TransactionType] = jss::Payment; + tx[sfDestination] = alice.human(); + tx[sfAmount] = "100"; + + // test delivered amount + testTxJsonMetadataField( + env, tx, validateOutput, jss::delivered_amount); + } + + { + Json::Value tx; + tx[jss::Account] = env.master.human(); + tx[jss::TransactionType] = jss::NFTokenMint; + tx[sfNFTokenTaxon] = 1; + + // test nft synthetic + testTxJsonMetadataField( + env, tx, validateOutput, jss::nftoken_id); + } + + { + Json::Value tx; + tx[jss::Account] = env.master.human(); + tx[jss::TransactionType] = jss::MPTokenIssuanceCreate; + + // test mpt issuance id + testTxJsonMetadataField( + env, tx, validateOutput, jss::mpt_issuance_id); + } + } + } + public: void run() override @@ -1202,6 +1305,7 @@ public: testMultisignedBadPubKey(); testDeleteExpiredCredentials(); testSuccessfulTransactionNetworkID(); + testSuccessfulTransactionAdditionalMetadata(); } }; diff --git a/src/xrpld/rpc/CTID.h b/src/xrpld/rpc/CTID.h index be531c536a..0e2b7e0d65 100644 --- a/src/xrpld/rpc/CTID.h +++ b/src/xrpld/rpc/CTID.h @@ -39,53 +39,96 @@ namespace RPC { // The Concise Transaction ID provides a way to identify a transaction // that includes which network the transaction was submitted to. +/** + * @brief Encodes ledger sequence, transaction index, and network ID into a CTID + * string. + * + * @param ledgerSeq Ledger sequence number (max 0x0FFF'FFFF). + * @param txnIndex Transaction index within the ledger (max 0xFFFF). + * @param networkID Network identifier (max 0xFFFF). + * @return Optional CTID string in uppercase hexadecimal, or std::nullopt if + * inputs are out of range. + */ inline std::optional encodeCTID(uint32_t ledgerSeq, uint32_t txnIndex, uint32_t networkID) noexcept { - if (ledgerSeq > 0x0FFF'FFFF || txnIndex > 0xFFFF || networkID > 0xFFFF) - return {}; + constexpr uint32_t maxLedgerSeq = 0x0FFF'FFFF; + constexpr uint32_t maxTxnIndex = 0xFFFF; + constexpr uint32_t maxNetworkID = 0xFFFF; + + if (ledgerSeq > maxLedgerSeq || txnIndex > maxTxnIndex || + networkID > maxNetworkID) + return std::nullopt; uint64_t ctidValue = - ((0xC000'0000ULL + static_cast(ledgerSeq)) << 32) + - (static_cast(txnIndex) << 16) + networkID; + ((0xC000'0000ULL + static_cast(ledgerSeq)) << 32) | + ((static_cast(txnIndex) << 16) | networkID); std::stringstream buffer; buffer << std::hex << std::uppercase << std::setfill('0') << std::setw(16) << ctidValue; - return {buffer.str()}; + return buffer.str(); } +/** + * @brief Decodes a CTID string or integer into its component parts. + * + * @tparam T Type of the CTID input (string, string_view, char*, integral). + * @param ctid CTID value to decode. + * @return Optional tuple of (ledgerSeq, txnIndex, networkID), or std::nullopt + * if invalid. + */ template inline std::optional> decodeCTID(T const ctid) noexcept { - uint64_t ctidValue{0}; + uint64_t ctidValue = 0; + if constexpr ( - std::is_same_v || std::is_same_v || - std::is_same_v || std::is_same_v) + std::is_same_v || std::is_same_v || + std::is_same_v || std::is_same_v) { std::string const ctidString(ctid); - if (ctidString.length() != 16) - return {}; + if (ctidString.size() != 16) + return std::nullopt; - if (!boost::regex_match(ctidString, boost::regex("^[0-9A-Fa-f]+$"))) - return {}; + static boost::regex const hexRegex("^[0-9A-Fa-f]{16}$"); + if (!boost::regex_match(ctidString, hexRegex)) + return std::nullopt; - ctidValue = std::stoull(ctidString, nullptr, 16); + try + { + ctidValue = std::stoull(ctidString, nullptr, 16); + } + // LCOV_EXCL_START + catch (...) + { + // should be impossible to hit given the length/regex check + return std::nullopt; + } + // LCOV_EXCL_STOP } else if constexpr (std::is_integral_v) - ctidValue = ctid; + { + ctidValue = static_cast(ctid); + } else - return {}; + { + return std::nullopt; + } - if ((ctidValue & 0xF000'0000'0000'0000ULL) != 0xC000'0000'0000'0000ULL) - return {}; + // Validate CTID prefix. + constexpr uint64_t ctidPrefixMask = 0xF000'0000'0000'0000ULL; + constexpr uint64_t ctidPrefix = 0xC000'0000'0000'0000ULL; + if ((ctidValue & ctidPrefixMask) != ctidPrefix) + return std::nullopt; - uint32_t ledger_seq = (ctidValue >> 32) & 0xFFFF'FFFUL; - uint16_t txn_index = (ctidValue >> 16) & 0xFFFFU; - uint16_t network_id = ctidValue & 0xFFFFU; - return {{ledger_seq, txn_index, network_id}}; + uint32_t ledgerSeq = static_cast((ctidValue >> 32) & 0x0FFF'FFFF); + uint16_t txnIndex = static_cast((ctidValue >> 16) & 0xFFFF); + uint16_t networkID = static_cast(ctidValue & 0xFFFF); + + return std::make_tuple(ledgerSeq, txnIndex, networkID); } } // namespace RPC diff --git a/src/xrpld/rpc/handlers/Simulate.cpp b/src/xrpld/rpc/handlers/Simulate.cpp index 3c175883c5..092b0b4562 100644 --- a/src/xrpld/rpc/handlers/Simulate.cpp +++ b/src/xrpld/rpc/handlers/Simulate.cpp @@ -24,10 +24,13 @@ #include #include #include +#include #include +#include #include #include +#include #include #include #include @@ -272,6 +275,17 @@ simulateTxn(RPC::JsonContext& context, std::shared_ptr transaction) else { jvResult[jss::meta] = result.metadata->getJson(JsonOptions::none); + RPC::insertDeliveredAmount( + jvResult[jss::meta], + view, + transaction->getSTransaction(), + *result.metadata); + RPC::insertNFTSyntheticInJson( + jvResult, transaction->getSTransaction(), *result.metadata); + RPC::insertMPTokenIssuanceID( + jvResult[jss::meta], + transaction->getSTransaction(), + *result.metadata); } }