Merge branch 'develop' into ripple/smart-escrow

This commit is contained in:
Mayukha Vadari
2025-09-11 16:41:14 -04:00
committed by GitHub
7 changed files with 215 additions and 76 deletions

View File

@@ -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

View File

@@ -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:

14
.github/workflows/pre-commit.yml vendored Normal file
View File

@@ -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" }'

View File

@@ -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 <PROFILE>`.
## 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

View File

@@ -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<void(
Json::Value const&,
Json::Value const&,
Json::Value const&)> 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<Config> 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();
}
};

View File

@@ -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<std::string>
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<uint64_t>(ledgerSeq)) << 32) +
(static_cast<uint64_t>(txnIndex) << 16) + networkID;
((0xC000'0000ULL + static_cast<uint64_t>(ledgerSeq)) << 32) |
((static_cast<uint64_t>(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 <typename T>
inline std::optional<std::tuple<uint32_t, uint16_t, uint16_t>>
decodeCTID(T const ctid) noexcept
{
uint64_t ctidValue{0};
uint64_t ctidValue = 0;
if constexpr (
std::is_same_v<T, std::string> || std::is_same_v<T, char*> ||
std::is_same_v<T, char const*> || std::is_same_v<T, std::string_view>)
std::is_same_v<T, std::string> || std::is_same_v<T, std::string_view> ||
std::is_same_v<T, char*> || std::is_same_v<T, char const*>)
{
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<T>)
ctidValue = ctid;
{
ctidValue = static_cast<uint64_t>(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<uint32_t>((ctidValue >> 32) & 0x0FFF'FFFF);
uint16_t txnIndex = static_cast<uint16_t>((ctidValue >> 16) & 0xFFFF);
uint16_t networkID = static_cast<uint16_t>(ctidValue & 0xFFFF);
return std::make_tuple(ledgerSeq, txnIndex, networkID);
}
} // namespace RPC

View File

@@ -24,10 +24,13 @@
#include <xrpld/app/misc/TxQ.h>
#include <xrpld/app/tx/apply.h>
#include <xrpld/rpc/Context.h>
#include <xrpld/rpc/DeliveredAmount.h>
#include <xrpld/rpc/GRPCHandlers.h>
#include <xrpld/rpc/MPTokenIssuanceID.h>
#include <xrpld/rpc/detail/TransactionSign.h>
#include <xrpl/protocol/ErrorCodes.h>
#include <xrpl/protocol/NFTSyntheticSerializer.h>
#include <xrpl/protocol/RPCErr.h>
#include <xrpl/protocol/STParsedJSON.h>
#include <xrpl/resource/Fees.h>
@@ -272,6 +275,17 @@ simulateTxn(RPC::JsonContext& context, std::shared_ptr<Transaction> 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);
}
}