Merge branch 'ximinez/lending-refactoring-2' into ximinez/lending-refactoring-3

This commit is contained in:
Ed Hennis
2025-09-15 11:13:40 -04:00
committed by GitHub
14 changed files with 357 additions and 95 deletions

View File

@@ -3501,6 +3501,10 @@ struct EscrowToken_test : public beast::unit_test::suite
BEAST_EXPECT(
transferRate.value == std::uint32_t(1'000'000'000 * 1.25));
BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 125);
BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == 125);
BEAST_EXPECT(env.balance(gw, MPT) == MPT(20'000));
// bob can finish escrow
env(escrow::finish(bob, alice, seq1),
escrow::condition(escrow::cb1),
@@ -3510,6 +3514,15 @@ struct EscrowToken_test : public beast::unit_test::suite
BEAST_EXPECT(env.balance(alice, MPT) == preAlice - delta);
BEAST_EXPECT(env.balance(bob, MPT) == MPT(10'100));
auto const escrowedWithFix =
env.current()->rules().enabled(fixTokenEscrowV1) ? 0 : 25;
auto const outstandingWithFix =
env.current()->rules().enabled(fixTokenEscrowV1) ? MPT(19'975)
: MPT(20'000);
BEAST_EXPECT(mptEscrowed(env, alice, MPT) == escrowedWithFix);
BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == escrowedWithFix);
BEAST_EXPECT(env.balance(gw, MPT) == outstandingWithFix);
}
// test locked rate: cancel
@@ -3554,6 +3567,60 @@ struct EscrowToken_test : public beast::unit_test::suite
BEAST_EXPECT(env.balance(alice, MPT) == preAlice);
BEAST_EXPECT(env.balance(bob, MPT) == preBob);
BEAST_EXPECT(env.balance(gw, MPT) == MPT(20'000));
BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 0);
BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == 0);
}
// test locked rate: issuer is destination
{
Env env{*this, features};
auto const baseFee = env.current()->fees().base;
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const gw = Account("gw");
MPTTester mptGw(env, gw, {.holders = {alice, bob}});
mptGw.create(
{.transferFee = 25000,
.ownerCount = 1,
.holderCount = 0,
.flags = tfMPTCanEscrow | tfMPTCanTransfer});
mptGw.authorize({.account = alice});
mptGw.authorize({.account = bob});
auto const MPT = mptGw["MPT"];
env(pay(gw, alice, MPT(10'000)));
env(pay(gw, bob, MPT(10'000)));
env.close();
// alice can create escrow w/ xfer rate
auto const preAlice = env.balance(alice, MPT);
auto const seq1 = env.seq(alice);
auto const delta = MPT(125);
env(escrow::create(alice, gw, MPT(125)),
escrow::condition(escrow::cb1),
escrow::finish_time(env.now() + 1s),
fee(baseFee * 150));
env.close();
auto const transferRate = escrow::rate(env, alice, seq1);
BEAST_EXPECT(
transferRate.value == std::uint32_t(1'000'000'000 * 1.25));
BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 125);
BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == 125);
BEAST_EXPECT(env.balance(gw, MPT) == MPT(20'000));
// bob can finish escrow
env(escrow::finish(gw, alice, seq1),
escrow::condition(escrow::cb1),
escrow::fulfillment(escrow::fb1),
fee(baseFee * 150));
env.close();
BEAST_EXPECT(env.balance(alice, MPT) == preAlice - delta);
BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 0);
BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == 0);
BEAST_EXPECT(env.balance(gw, MPT) == MPT(19'875));
}
}
@@ -3878,6 +3945,7 @@ public:
FeatureBitset const all{testable_amendments()};
testIOUWithFeats(all);
testMPTWithFeats(all);
testMPTWithFeats(all - fixTokenEscrowV1);
}
};

View File

@@ -0,0 +1,80 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2020 Dev Null Productions
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 <test/jtx.h>
#include <test/jtx/CaptureLogs.h>
#include <test/jtx/Env.h>
#include <xrpld/app/misc/HashRouter.h>
namespace ripple {
namespace test {
class NetworkOPs_test : public beast::unit_test::suite
{
public:
void
run() override
{
testAllBadHeldTransactions();
}
void
testAllBadHeldTransactions()
{
// All trasactions are already marked as SF_BAD, and we should be able
// to handle the case properly without an assertion failure
testcase("No valid transactions in batch");
std::string logs;
{
using namespace jtx;
auto const alice = Account{"alice"};
Env env{
*this,
envconfig(),
std::make_unique<CaptureLogs>(&logs),
beast::severities::kAll};
env.memoize(env.master);
env.memoize(alice);
auto const jtx = env.jt(ticket::create(alice, 1), seq(1), fee(10));
auto transacionId = jtx.stx->getTransactionID();
env.app().getHashRouter().setFlags(
transacionId, HashRouterFlags::HELD);
env(jtx, json(jss::Sequence, 1), ter(terNO_ACCOUNT));
env.app().getHashRouter().setFlags(
transacionId, HashRouterFlags::BAD);
env.close();
}
BEAST_EXPECT(
logs.find("No transaction to process!") != std::string::npos);
}
};
BEAST_DEFINE_TESTSUITE(NetworkOPs, app, ripple);
} // namespace test
} // namespace ripple

View File

@@ -1452,6 +1452,11 @@ NetworkOPsImp::processTransactionSet(CanonicalTXSet const& set)
for (auto& t : transactions)
mTransactions.push_back(std::move(t));
}
if (mTransactions.empty())
{
JLOG(m_journal.debug()) << "No transaction to process!";
return;
}
doTransactionSyncBatch(lock, [&](std::unique_lock<std::mutex> const&) {
XRPL_ASSERT(

View File

@@ -1013,8 +1013,13 @@ escrowUnlockApplyHelper<MPTIssue>(
// compute balance to transfer
finalAmt = amount.value() - xferFee;
}
return rippleUnlockEscrowMPT(view, sender, receiver, finalAmt, journal);
return rippleUnlockEscrowMPT(
view,
sender,
receiver,
finalAmt,
view.rules().enabled(fixTokenEscrowV1) ? amount : finalAmt,
journal);
}
TER

View File

@@ -735,7 +735,8 @@ rippleUnlockEscrowMPT(
ApplyView& view,
AccountID const& uGrantorID,
AccountID const& uGranteeID,
STAmount const& saAmount,
STAmount const& netAmount,
STAmount const& grossAmount,
beast::Journal j);
/** Calls static accountSendIOU if saAmount represents Issue.

View File

@@ -3067,11 +3067,17 @@ rippleUnlockEscrowMPT(
ApplyView& view,
AccountID const& sender,
AccountID const& receiver,
STAmount const& amount,
STAmount const& netAmount,
STAmount const& grossAmount,
beast::Journal j)
{
auto const issuer = amount.getIssuer();
auto const mptIssue = amount.get<MPTIssue>();
if (!view.rules().enabled(fixTokenEscrowV1))
XRPL_ASSERT(
netAmount == grossAmount,
"ripple::rippleUnlockEscrowMPT : netAmount == grossAmount");
auto const& issuer = netAmount.getIssuer();
auto const& mptIssue = netAmount.get<MPTIssue>();
auto const mptID = keylet::mptIssuance(mptIssue.getMptID());
auto sleIssuance = view.peek(mptID);
if (!sleIssuance)
@@ -3092,7 +3098,7 @@ rippleUnlockEscrowMPT(
} // LCOV_EXCL_STOP
auto const locked = sleIssuance->getFieldU64(sfLockedAmount);
auto const redeem = amount.mpt().value();
auto const redeem = grossAmount.mpt().value();
// Underflow check for subtraction
if (!canSubtract(
@@ -3125,7 +3131,7 @@ rippleUnlockEscrowMPT(
} // LCOV_EXCL_STOP
auto current = sle->getFieldU64(sfMPTAmount);
auto delta = amount.mpt().value();
auto delta = netAmount.mpt().value();
// Overflow check for addition
if (!canAdd(STAmount(mptIssue, current), STAmount(mptIssue, delta)))
@@ -3143,7 +3149,7 @@ rippleUnlockEscrowMPT(
{
// Decrease the Issuance OutstandingAmount
auto const outstanding = sleIssuance->getFieldU64(sfOutstandingAmount);
auto const redeem = amount.mpt().value();
auto const redeem = netAmount.mpt().value();
// Underflow check for subtraction
if (!canSubtract(
@@ -3187,7 +3193,7 @@ rippleUnlockEscrowMPT(
} // LCOV_EXCL_STOP
auto const locked = sle->getFieldU64(sfLockedAmount);
auto const delta = amount.mpt().value();
auto const delta = grossAmount.mpt().value();
// Underflow check for subtraction
if (!canSubtract(STAmount(mptIssue, locked), STAmount(mptIssue, delta)))
@@ -3205,6 +3211,28 @@ rippleUnlockEscrowMPT(
sle->setFieldU64(sfLockedAmount, newLocked);
view.update(sle);
}
// Note: The gross amount is the amount that was locked, the net
// amount is the amount that is being unlocked. The difference is the fee
// that was charged for the transfer. If this difference is greater than
// zero, we need to update the outstanding amount.
auto const diff = grossAmount.mpt().value() - netAmount.mpt().value();
if (diff != 0)
{
auto const outstanding = sleIssuance->getFieldU64(sfOutstandingAmount);
// Underflow check for subtraction
if (!canSubtract(
STAmount(mptIssue, outstanding), STAmount(mptIssue, diff)))
{ // LCOV_EXCL_START
JLOG(j.error())
<< "rippleUnlockEscrowMPT: insufficient outstanding amount for "
<< mptIssue.getMptID() << ": " << outstanding << " < " << diff;
return tecINTERNAL;
} // LCOV_EXCL_STOP
sleIssuance->setFieldU64(sfOutstandingAmount, outstanding - diff);
view.update(sleIssuance);
}
return tesSUCCESS;
}

View File

@@ -2880,6 +2880,9 @@ PeerImp::checkTransaction(
(stx->getFieldU32(sfLastLedgerSequence) <
app_.getLedgerMaster().getValidLedgerIndex()))
{
JLOG(p_journal_.info())
<< "Marking transaction " << stx->getTransactionID()
<< "as BAD because it's expired";
app_.getHashRouter().setFlags(
stx->getTransactionID(), HashRouterFlags::BAD);
charge(Resource::feeUselessData, "expired tx");
@@ -2936,7 +2939,7 @@ PeerImp::checkTransaction(
{
if (!validReason.empty())
{
JLOG(p_journal_.trace())
JLOG(p_journal_.debug())
<< "Exception checking transaction: " << validReason;
}
@@ -2963,7 +2966,7 @@ PeerImp::checkTransaction(
{
if (!reason.empty())
{
JLOG(p_journal_.trace())
JLOG(p_journal_.debug())
<< "Exception checking transaction: " << reason;
}
app_.getHashRouter().setFlags(

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