mirror of
https://github.com/XRPLF/rippled.git
synced 2026-04-29 15:37:57 +00:00
Merge branch 'ximinez/lending-refactoring-2' into ximinez/lending-refactoring-3
This commit is contained in:
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
80
src/test/app/NetworkOPs_test.cpp
Normal file
80
src/test/app/NetworkOPs_test.cpp
Normal 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
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user