mirror of
https://github.com/XRPLF/rippled.git
synced 2025-12-06 01:07:57 +00:00
Compare commits
84 Commits
a123456/di
...
ximinez/fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a85440ffa | ||
|
|
0e436292ae | ||
|
|
e24da785ec | ||
|
|
12625e99c0 | ||
|
|
10f9c18dd3 | ||
|
|
13ab98fb4c | ||
|
|
fec2f60697 | ||
|
|
528ede7864 | ||
|
|
4dac87a1b8 | ||
|
|
7c933e06a0 | ||
|
|
d94722e4d0 | ||
|
|
059092d773 | ||
|
|
1cb0887436 | ||
|
|
b9e289df55 | ||
|
|
fff08cb2b1 | ||
|
|
d3842b3908 | ||
|
|
759bd4d199 | ||
|
|
4c88dfc384 | ||
|
|
f241378e6e | ||
|
|
c8ff090d8e | ||
|
|
945c362559 | ||
|
|
9546c52013 | ||
|
|
3a099aeb64 | ||
|
|
8567bd0d12 | ||
|
|
3cb1851537 | ||
|
|
55feea308e | ||
|
|
a6692af17a | ||
|
|
1d058a5d78 | ||
|
|
a70821adc5 | ||
|
|
bfeb60d3f5 | ||
|
|
c86bfa32f7 | ||
|
|
0f44d619b6 | ||
|
|
2add6a7917 | ||
|
|
d21c4f3218 | ||
|
|
265a504301 | ||
|
|
b1e576d3d1 | ||
|
|
d2d3039ce6 | ||
|
|
16e85a7b79 | ||
|
|
4ae1c01e13 | ||
|
|
8807afc074 | ||
|
|
a4e13e07d5 | ||
|
|
6067d59336 | ||
|
|
e14aecee66 | ||
|
|
0e4c3e3427 | ||
|
|
da5c563426 | ||
|
|
b9f5d8b1c5 | ||
|
|
5ea7b562a2 | ||
|
|
8450970b80 | ||
|
|
14d4cff530 | ||
|
|
88ac659d86 | ||
|
|
43fdbf27b9 | ||
|
|
1e33f8e868 | ||
|
|
a6e30857df | ||
|
|
563f24edb0 | ||
|
|
a89f6d5da2 | ||
|
|
5b2e91986a | ||
|
|
fbc5056817 | ||
|
|
b7cafed040 | ||
|
|
b6ebd34b30 | ||
|
|
74c2765159 | ||
|
|
62c7fdadba | ||
|
|
372c66e684 | ||
|
|
a2fab5bcaf | ||
|
|
2e4f41571c | ||
|
|
8799a6dbfd | ||
|
|
e655087027 | ||
|
|
46bd2a4090 | ||
|
|
f3b8a8aef3 | ||
|
|
ee0c917e95 | ||
|
|
375aedd340 | ||
|
|
95397a7710 | ||
|
|
7af8a52d27 | ||
|
|
326a8093c9 | ||
|
|
9f0e159205 | ||
|
|
2fda101b44 | ||
|
|
e6e1e4f9dd | ||
|
|
70591265e5 | ||
|
|
ed1d477a45 | ||
|
|
cd58f636d3 | ||
|
|
3152f2233f | ||
|
|
0c147a895c | ||
|
|
cf80710ef1 | ||
|
|
fc929ab984 | ||
|
|
145a8817dc |
@@ -655,7 +655,7 @@ createPseudoAccount(
|
||||
uint256 const& pseudoOwnerKey,
|
||||
SField const& ownerField);
|
||||
|
||||
// Returns true if sleAcct is a pseudo-account or specific
|
||||
// Returns true iff sleAcct is a pseudo-account or specific
|
||||
// pseudo-accounts in pseudoFieldFilter.
|
||||
//
|
||||
// Returns false if sleAcct is
|
||||
|
||||
@@ -1,642 +0,0 @@
|
||||
#include <xrpl/beast/unit_test/suite.h>
|
||||
// DO NOT REMOVE
|
||||
#include <test/jtx.h>
|
||||
#include <test/jtx/Account.h>
|
||||
#include <test/jtx/amount.h>
|
||||
#include <test/jtx/mpt.h>
|
||||
|
||||
#include <xrpld/app/misc/LendingHelpers.h>
|
||||
#include <xrpld/app/misc/LoadFeeTrack.h>
|
||||
#include <xrpld/app/tx/detail/Batch.h>
|
||||
#include <xrpld/app/tx/detail/LoanSet.h>
|
||||
|
||||
#include <xrpl/beast/xor_shift_engine.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
|
||||
class LendingHelpers_test : public beast::unit_test::suite
|
||||
{
|
||||
void
|
||||
testComputeRaisedRate()
|
||||
{
|
||||
using namespace jtx;
|
||||
using namespace ripple::detail;
|
||||
struct TestCase
|
||||
{
|
||||
std::string name;
|
||||
Number periodicRate;
|
||||
std::uint32_t paymentsRemaining;
|
||||
Number expectedRaisedRate;
|
||||
};
|
||||
|
||||
auto const testCases = std::vector<TestCase>{
|
||||
{
|
||||
.name = "Zero payments remaining",
|
||||
.periodicRate = Number{5, -2},
|
||||
.paymentsRemaining = 0,
|
||||
.expectedRaisedRate = Number{1}, // (1 + r)^0 = 1
|
||||
},
|
||||
{
|
||||
.name = "One payment remaining",
|
||||
.periodicRate = Number{5, -2},
|
||||
.paymentsRemaining = 1,
|
||||
.expectedRaisedRate = Number{105, -2},
|
||||
}, // 1.05^1
|
||||
{
|
||||
.name = "Multiple payments remaining",
|
||||
.periodicRate = Number{5, -2},
|
||||
.paymentsRemaining = 3,
|
||||
.expectedRaisedRate = Number{1157625, -6},
|
||||
}, // 1.05^3
|
||||
{
|
||||
.name = "Zero periodic rate",
|
||||
.periodicRate = Number{0},
|
||||
.paymentsRemaining = 5,
|
||||
.expectedRaisedRate = Number{1}, // (1 + 0)^5 = 1
|
||||
}};
|
||||
|
||||
for (auto const& tc : testCases)
|
||||
{
|
||||
testcase("computeRaisedRate: " + tc.name);
|
||||
|
||||
auto const computedRaisedRate =
|
||||
computeRaisedRate(tc.periodicRate, tc.paymentsRemaining);
|
||||
BEAST_EXPECTS(
|
||||
computedRaisedRate == tc.expectedRaisedRate,
|
||||
"Raised rate mismatch: expected " +
|
||||
to_string(tc.expectedRaisedRate) + ", got " +
|
||||
to_string(computedRaisedRate));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testComputePaymentFactor()
|
||||
{
|
||||
using namespace jtx;
|
||||
using namespace ripple::detail;
|
||||
struct TestCase
|
||||
{
|
||||
std::string name;
|
||||
Number periodicRate;
|
||||
std::uint32_t paymentsRemaining;
|
||||
Number expectedPaymentFactor;
|
||||
};
|
||||
|
||||
auto const testCases = std::vector<TestCase>{
|
||||
{
|
||||
.name = "Zero periodic rate",
|
||||
.periodicRate = Number{0},
|
||||
.paymentsRemaining = 4,
|
||||
.expectedPaymentFactor = Number{25, -2},
|
||||
}, // 1/4 = 0.25
|
||||
{
|
||||
.name = "One payment remaining",
|
||||
.periodicRate = Number{5, -2},
|
||||
.paymentsRemaining = 1,
|
||||
.expectedPaymentFactor = Number{105, -2},
|
||||
}, // 0.05/1 = 1.05
|
||||
{
|
||||
.name = "Multiple payments remaining",
|
||||
.periodicRate = Number{5, -2},
|
||||
.paymentsRemaining = 3,
|
||||
.expectedPaymentFactor = Number{367208564631245, -15},
|
||||
}, // from calc
|
||||
{
|
||||
.name = "Zero payments remaining",
|
||||
.periodicRate = Number{5, -2},
|
||||
.paymentsRemaining = 0,
|
||||
.expectedPaymentFactor = Number{0},
|
||||
} // edge case
|
||||
};
|
||||
|
||||
for (auto const& tc : testCases)
|
||||
{
|
||||
testcase("computePaymentFactor: " + tc.name);
|
||||
|
||||
auto const computedPaymentFactor =
|
||||
computePaymentFactor(tc.periodicRate, tc.paymentsRemaining);
|
||||
BEAST_EXPECTS(
|
||||
computedPaymentFactor == tc.expectedPaymentFactor,
|
||||
"Payment factor mismatch: expected " +
|
||||
to_string(tc.expectedPaymentFactor) + ", got " +
|
||||
to_string(computedPaymentFactor));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testLoanPeriodicPayment()
|
||||
{
|
||||
using namespace jtx;
|
||||
using namespace ripple::detail;
|
||||
|
||||
struct TestCase
|
||||
{
|
||||
std::string name;
|
||||
Number principalOutstanding;
|
||||
Number periodicRate;
|
||||
std::uint32_t paymentsRemaining;
|
||||
Number expectedPeriodicPayment;
|
||||
};
|
||||
|
||||
auto const testCases = std::vector<TestCase>{
|
||||
{
|
||||
.name = "Zero principal outstanding",
|
||||
.principalOutstanding = Number{0},
|
||||
.periodicRate = Number{5, -2},
|
||||
.paymentsRemaining = 5,
|
||||
.expectedPeriodicPayment = Number{0},
|
||||
},
|
||||
{
|
||||
.name = "Zero payments remaining",
|
||||
.principalOutstanding = Number{1'000},
|
||||
.periodicRate = Number{5, -2},
|
||||
.paymentsRemaining = 0,
|
||||
.expectedPeriodicPayment = Number{0},
|
||||
},
|
||||
{
|
||||
.name = "Zero periodic rate",
|
||||
.principalOutstanding = Number{1'000},
|
||||
.periodicRate = Number{0},
|
||||
.paymentsRemaining = 4,
|
||||
.expectedPeriodicPayment = Number{250},
|
||||
},
|
||||
{
|
||||
.name = "Standard case",
|
||||
.principalOutstanding = Number{1'000},
|
||||
.periodicRate =
|
||||
loanPeriodicRate(TenthBips32(100'000), 30 * 24 * 60 * 60),
|
||||
.paymentsRemaining = 3,
|
||||
.expectedPeriodicPayment =
|
||||
Number{3895690663961231, -13}, // from calc
|
||||
},
|
||||
};
|
||||
|
||||
for (auto const& tc : testCases)
|
||||
{
|
||||
testcase("loanPeriodicPayment: " + tc.name);
|
||||
|
||||
auto const computedPeriodicPayment = loanPeriodicPayment(
|
||||
tc.principalOutstanding, tc.periodicRate, tc.paymentsRemaining);
|
||||
BEAST_EXPECTS(
|
||||
computedPeriodicPayment == tc.expectedPeriodicPayment,
|
||||
"Periodic payment mismatch: expected " +
|
||||
to_string(tc.expectedPeriodicPayment) + ", got " +
|
||||
to_string(computedPeriodicPayment));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testLoanPrincipalFromPeriodicPayment()
|
||||
{
|
||||
using namespace jtx;
|
||||
using namespace ripple::detail;
|
||||
|
||||
struct TestCase
|
||||
{
|
||||
std::string name;
|
||||
Number periodicPayment;
|
||||
Number periodicRate;
|
||||
std::uint32_t paymentsRemaining;
|
||||
Number expectedPrincipalOutstanding;
|
||||
};
|
||||
|
||||
auto const testCases = std::vector<TestCase>{
|
||||
{
|
||||
.name = "Zero periodic payment",
|
||||
.periodicPayment = Number{0},
|
||||
.periodicRate = Number{5, -2},
|
||||
.paymentsRemaining = 5,
|
||||
.expectedPrincipalOutstanding = Number{0},
|
||||
},
|
||||
{
|
||||
.name = "Zero payments remaining",
|
||||
.periodicPayment = Number{1'000},
|
||||
.periodicRate = Number{5, -2},
|
||||
.paymentsRemaining = 0,
|
||||
.expectedPrincipalOutstanding = Number{0},
|
||||
},
|
||||
{
|
||||
.name = "Zero periodic rate",
|
||||
.periodicPayment = Number{250},
|
||||
.periodicRate = Number{0},
|
||||
.paymentsRemaining = 4,
|
||||
.expectedPrincipalOutstanding = Number{1'000},
|
||||
},
|
||||
{
|
||||
.name = "Standard case",
|
||||
.periodicPayment = Number{3895690663961231, -13}, // from calc
|
||||
.periodicRate =
|
||||
loanPeriodicRate(TenthBips32(100'000), 30 * 24 * 60 * 60),
|
||||
.paymentsRemaining = 3,
|
||||
.expectedPrincipalOutstanding = Number{1'000},
|
||||
},
|
||||
};
|
||||
|
||||
for (auto const& tc : testCases)
|
||||
{
|
||||
testcase("loanPrincipalFromPeriodicPayment: " + tc.name);
|
||||
|
||||
auto const computedPrincipalOutstanding =
|
||||
loanPrincipalFromPeriodicPayment(
|
||||
tc.periodicPayment, tc.periodicRate, tc.paymentsRemaining);
|
||||
BEAST_EXPECTS(
|
||||
computedPrincipalOutstanding == tc.expectedPrincipalOutstanding,
|
||||
"Principal outstanding mismatch: expected " +
|
||||
to_string(tc.expectedPrincipalOutstanding) + ", got " +
|
||||
to_string(computedPrincipalOutstanding));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testComputeOverpaymentComponents()
|
||||
{
|
||||
testcase("computeOverpaymentComponents");
|
||||
using namespace jtx;
|
||||
using namespace ripple::detail;
|
||||
|
||||
Account const issuer{"issuer"};
|
||||
PrettyAsset const IOU = issuer["IOU"];
|
||||
int32_t const loanScale = 1;
|
||||
auto const overpayment = Number{1'000};
|
||||
auto const overpaymentInterestRate = TenthBips32{10'000}; // 10%
|
||||
auto const overpaymentFeeRate = TenthBips32{50'000}; // 50%
|
||||
auto const managementFeeRate = TenthBips16{10'000}; // 10%
|
||||
|
||||
auto const expectedOverpaymentFee = Number{500}; // 50% of 1,000
|
||||
auto const expectedOverpaymentInterestGross =
|
||||
Number{100}; // 10% of 1,000
|
||||
auto const expectedOverpaymentInterestNet =
|
||||
Number{90}; // 100 - 10% of 100
|
||||
auto const expectedOverpaymentManagementFee = Number{10}; // 10% of 100
|
||||
auto const expectedPrincipalPortion = Number{400}; // 1,000 - 100 - 500
|
||||
|
||||
auto const components = detail::computeOverpaymentComponents(
|
||||
IOU,
|
||||
loanScale,
|
||||
overpayment,
|
||||
overpaymentInterestRate,
|
||||
overpaymentFeeRate,
|
||||
managementFeeRate);
|
||||
|
||||
BEAST_EXPECT(
|
||||
components.untrackedManagementFee == expectedOverpaymentFee);
|
||||
|
||||
BEAST_EXPECT(
|
||||
components.untrackedInterest == expectedOverpaymentInterestNet);
|
||||
BEAST_EXPECT(
|
||||
components.trackedManagementFeeDelta ==
|
||||
expectedOverpaymentManagementFee);
|
||||
BEAST_EXPECT(
|
||||
components.trackedPrincipalDelta == expectedPrincipalPortion);
|
||||
BEAST_EXPECT(
|
||||
components.trackedManagementFeeDelta +
|
||||
components.untrackedInterest ==
|
||||
expectedOverpaymentInterestGross);
|
||||
|
||||
BEAST_EXPECT(
|
||||
components.trackedManagementFeeDelta +
|
||||
components.untrackedInterest +
|
||||
components.trackedPrincipalDelta +
|
||||
components.untrackedManagementFee ==
|
||||
overpayment);
|
||||
}
|
||||
|
||||
void
|
||||
testComputeInterestAndFeeParts()
|
||||
{
|
||||
using namespace jtx;
|
||||
using namespace ripple::detail;
|
||||
|
||||
struct TestCase
|
||||
{
|
||||
std::string name;
|
||||
Number interest;
|
||||
TenthBips16 managementFeeRate;
|
||||
Number expectedInterestPart;
|
||||
Number expectedFeePart;
|
||||
};
|
||||
|
||||
Account const issuer{"issuer"};
|
||||
PrettyAsset const IOU = issuer["IOU"];
|
||||
std::int32_t const loanScale = 1;
|
||||
|
||||
auto const testCases = std::vector<TestCase>{
|
||||
{.name = "Zero interest",
|
||||
.interest = Number{0},
|
||||
.managementFeeRate = TenthBips16{10'000},
|
||||
.expectedInterestPart = Number{0},
|
||||
.expectedFeePart = Number{0}},
|
||||
{.name = "Zero fee rate",
|
||||
.interest = Number{1'000},
|
||||
.managementFeeRate = TenthBips16{0},
|
||||
.expectedInterestPart = Number{1'000},
|
||||
.expectedFeePart = Number{0}},
|
||||
{.name = "10% fee rate",
|
||||
.interest = Number{1'000},
|
||||
.managementFeeRate = TenthBips16{10'000},
|
||||
.expectedInterestPart = Number{900},
|
||||
.expectedFeePart = Number{100}},
|
||||
};
|
||||
|
||||
for (auto const& tc : testCases)
|
||||
{
|
||||
testcase("computeInterestAndFeeParts: " + tc.name);
|
||||
|
||||
auto const [computedInterestPart, computedFeePart] =
|
||||
computeInterestAndFeeParts(
|
||||
IOU, tc.interest, tc.managementFeeRate, loanScale);
|
||||
BEAST_EXPECTS(
|
||||
computedInterestPart == tc.expectedInterestPart,
|
||||
"Interest part mismatch: expected " +
|
||||
to_string(tc.expectedInterestPart) + ", got " +
|
||||
to_string(computedInterestPart));
|
||||
BEAST_EXPECTS(
|
||||
computedFeePart == tc.expectedFeePart,
|
||||
"Fee part mismatch: expected " + to_string(tc.expectedFeePart) +
|
||||
", got " + to_string(computedFeePart));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testLoanLatePaymentInterest()
|
||||
{
|
||||
using namespace jtx;
|
||||
using namespace ripple::detail;
|
||||
struct TestCase
|
||||
{
|
||||
std::string name;
|
||||
Number principalOutstanding;
|
||||
TenthBips32 lateInterestRate;
|
||||
NetClock::time_point parentCloseTime;
|
||||
std::uint32_t nextPaymentDueDate;
|
||||
Number expectedLateInterest;
|
||||
};
|
||||
|
||||
auto const testCases = std::vector<TestCase>{
|
||||
{
|
||||
.name = "On-time payment",
|
||||
.principalOutstanding = Number{1'000},
|
||||
.lateInterestRate = TenthBips32{10'000}, // 10%
|
||||
.parentCloseTime =
|
||||
NetClock::time_point{NetClock::duration{3'000}},
|
||||
.nextPaymentDueDate = 3'000,
|
||||
.expectedLateInterest = Number{0},
|
||||
},
|
||||
{
|
||||
.name = "Early payment",
|
||||
.principalOutstanding = Number{1'000},
|
||||
.lateInterestRate = TenthBips32{10'000}, // 10%
|
||||
.parentCloseTime =
|
||||
NetClock::time_point{NetClock::duration{3'000}},
|
||||
.nextPaymentDueDate = 4'000,
|
||||
.expectedLateInterest = Number{0},
|
||||
},
|
||||
{
|
||||
.name = "No principal outstanding",
|
||||
.principalOutstanding = Number{0},
|
||||
.lateInterestRate = TenthBips32{10'000}, // 10%
|
||||
.parentCloseTime =
|
||||
NetClock::time_point{NetClock::duration{3'000}},
|
||||
.nextPaymentDueDate = 2'000,
|
||||
.expectedLateInterest = Number{0},
|
||||
},
|
||||
{
|
||||
.name = "No late interest rate",
|
||||
.principalOutstanding = Number{1'000},
|
||||
.lateInterestRate = TenthBips32{0}, // 0%
|
||||
.parentCloseTime =
|
||||
NetClock::time_point{NetClock::duration{3'000}},
|
||||
.nextPaymentDueDate = 2'000,
|
||||
.expectedLateInterest = Number{0},
|
||||
},
|
||||
{
|
||||
.name = "Late payment",
|
||||
.principalOutstanding = Number{1'000},
|
||||
.lateInterestRate = TenthBips32{100'000}, // 100%
|
||||
.parentCloseTime =
|
||||
NetClock::time_point{NetClock::duration{3'000}},
|
||||
.nextPaymentDueDate = 2'000,
|
||||
.expectedLateInterest =
|
||||
Number{3170979198376459, -17}, // from calc
|
||||
},
|
||||
};
|
||||
|
||||
for (auto const& tc : testCases)
|
||||
{
|
||||
testcase("loanLatePaymentInterest: " + tc.name);
|
||||
|
||||
auto const computedLateInterest = loanLatePaymentInterest(
|
||||
tc.principalOutstanding,
|
||||
tc.lateInterestRate,
|
||||
tc.parentCloseTime,
|
||||
tc.nextPaymentDueDate);
|
||||
BEAST_EXPECTS(
|
||||
computedLateInterest == tc.expectedLateInterest,
|
||||
"Late interest mismatch: expected " +
|
||||
to_string(tc.expectedLateInterest) + ", got " +
|
||||
to_string(computedLateInterest));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testLoanAccruedInterest()
|
||||
{
|
||||
using namespace jtx;
|
||||
using namespace ripple::detail;
|
||||
struct TestCase
|
||||
{
|
||||
std::string name;
|
||||
Number principalOutstanding;
|
||||
Number periodicRate;
|
||||
NetClock::time_point parentCloseTime;
|
||||
std::uint32_t startDate;
|
||||
std::uint32_t prevPaymentDate;
|
||||
std::uint32_t paymentInterval;
|
||||
Number expectedAccruedInterest;
|
||||
};
|
||||
|
||||
auto const testCases = std::vector<TestCase>{
|
||||
{
|
||||
.name = "Zero principal outstanding",
|
||||
.principalOutstanding = Number{0},
|
||||
.periodicRate = Number{5, -2},
|
||||
.parentCloseTime =
|
||||
NetClock::time_point{NetClock::duration{3'000}},
|
||||
.startDate = 2'000,
|
||||
.prevPaymentDate = 2'500,
|
||||
.paymentInterval = 30 * 24 * 60 * 60,
|
||||
.expectedAccruedInterest = Number{0},
|
||||
},
|
||||
{
|
||||
.name = "Before start date",
|
||||
.principalOutstanding = Number{1'000},
|
||||
.periodicRate = Number{5, -2},
|
||||
.parentCloseTime =
|
||||
NetClock::time_point{NetClock::duration{1'000}},
|
||||
.startDate = 2'000,
|
||||
.prevPaymentDate = 1'500,
|
||||
.paymentInterval = 30 * 24 * 60 * 60,
|
||||
.expectedAccruedInterest = Number{0},
|
||||
},
|
||||
{
|
||||
.name = "Zero periodic rate",
|
||||
.principalOutstanding = Number{1'000},
|
||||
.periodicRate = Number{0},
|
||||
.parentCloseTime =
|
||||
NetClock::time_point{NetClock::duration{3'000}},
|
||||
.startDate = 2'000,
|
||||
.prevPaymentDate = 2'500,
|
||||
.paymentInterval = 30 * 24 * 60 * 60,
|
||||
.expectedAccruedInterest = Number{0},
|
||||
},
|
||||
{
|
||||
.name = "Zero payment interval",
|
||||
.principalOutstanding = Number{1'000},
|
||||
.periodicRate = Number{5, -2},
|
||||
.parentCloseTime =
|
||||
NetClock::time_point{NetClock::duration{3'000}},
|
||||
.startDate = 2'000,
|
||||
.prevPaymentDate = 2'500,
|
||||
.paymentInterval = 0,
|
||||
.expectedAccruedInterest = Number{0},
|
||||
},
|
||||
{
|
||||
.name = "Standard case",
|
||||
.principalOutstanding = Number{1'000},
|
||||
.periodicRate = Number{5, -2},
|
||||
.parentCloseTime =
|
||||
NetClock::time_point{NetClock::duration{3'000}},
|
||||
.startDate = 1'000,
|
||||
.prevPaymentDate = 2'000,
|
||||
.paymentInterval = 30 * 24 * 60 * 60,
|
||||
.expectedAccruedInterest =
|
||||
Number{1929012345679012, -17}, // from calc
|
||||
},
|
||||
};
|
||||
|
||||
for (auto const& tc : testCases)
|
||||
{
|
||||
testcase("loanAccruedInterest: " + tc.name);
|
||||
|
||||
auto const computedAccruedInterest = loanAccruedInterest(
|
||||
tc.principalOutstanding,
|
||||
tc.periodicRate,
|
||||
tc.parentCloseTime,
|
||||
tc.startDate,
|
||||
tc.prevPaymentDate,
|
||||
tc.paymentInterval);
|
||||
BEAST_EXPECTS(
|
||||
computedAccruedInterest == tc.expectedAccruedInterest,
|
||||
"Accrued interest mismatch: expected " +
|
||||
to_string(tc.expectedAccruedInterest) + ", got " +
|
||||
to_string(computedAccruedInterest));
|
||||
}
|
||||
}
|
||||
|
||||
// This test overlaps with testLoanAccruedInterest, the test cases only
|
||||
// exercise the computeFullPaymentInterest parts unique to it.
|
||||
void
|
||||
testComputeFullPaymentInterest()
|
||||
{
|
||||
using namespace jtx;
|
||||
using namespace ripple::detail;
|
||||
|
||||
struct TestCase
|
||||
{
|
||||
std::string name;
|
||||
Number rawPrincipalOutstanding;
|
||||
Number periodicRate;
|
||||
NetClock::time_point parentCloseTime;
|
||||
std::uint32_t paymentInterval;
|
||||
std::uint32_t prevPaymentDate;
|
||||
std::uint32_t startDate;
|
||||
TenthBips32 closeInterestRate;
|
||||
Number expectedFullPaymentInterest;
|
||||
};
|
||||
|
||||
auto const testCases = std::vector<TestCase>{
|
||||
{
|
||||
.name = "Zero principal outstanding",
|
||||
.rawPrincipalOutstanding = Number{0},
|
||||
.periodicRate = Number{5, -2},
|
||||
.parentCloseTime =
|
||||
NetClock::time_point{NetClock::duration{3'000}},
|
||||
.paymentInterval = 30 * 24 * 60 * 60,
|
||||
.prevPaymentDate = 2'000,
|
||||
.startDate = 1'000,
|
||||
.closeInterestRate = TenthBips32{10'000},
|
||||
.expectedFullPaymentInterest = Number{0},
|
||||
},
|
||||
{
|
||||
.name = "Zero close interest rate",
|
||||
.rawPrincipalOutstanding = Number{1'000},
|
||||
.periodicRate = Number{5, -2},
|
||||
.parentCloseTime =
|
||||
NetClock::time_point{NetClock::duration{3'000}},
|
||||
.paymentInterval = 30 * 24 * 60 * 60,
|
||||
.prevPaymentDate = 2'000,
|
||||
.startDate = 1'000,
|
||||
.closeInterestRate = TenthBips32{0},
|
||||
.expectedFullPaymentInterest =
|
||||
Number{1929012345679012, -17}, // from calc
|
||||
},
|
||||
{
|
||||
.name = "Standard case",
|
||||
.rawPrincipalOutstanding = Number{1'000},
|
||||
.periodicRate = Number{5, -2},
|
||||
.parentCloseTime =
|
||||
NetClock::time_point{NetClock::duration{3'000}},
|
||||
.paymentInterval = 30 * 24 * 60 * 60,
|
||||
.prevPaymentDate = 2'000,
|
||||
.startDate = 1'000,
|
||||
.closeInterestRate = TenthBips32{10'000},
|
||||
.expectedFullPaymentInterest =
|
||||
Number{1000192901234568, -13}, // from calc
|
||||
},
|
||||
};
|
||||
|
||||
for (auto const& tc : testCases)
|
||||
{
|
||||
testcase("computeFullPaymentInterest: " + tc.name);
|
||||
|
||||
auto const computedFullPaymentInterest = computeFullPaymentInterest(
|
||||
tc.rawPrincipalOutstanding,
|
||||
tc.periodicRate,
|
||||
tc.parentCloseTime,
|
||||
tc.paymentInterval,
|
||||
tc.prevPaymentDate,
|
||||
tc.startDate,
|
||||
tc.closeInterestRate);
|
||||
BEAST_EXPECTS(
|
||||
computedFullPaymentInterest == tc.expectedFullPaymentInterest,
|
||||
"Full payment interest mismatch: expected " +
|
||||
to_string(tc.expectedFullPaymentInterest) + ", got " +
|
||||
to_string(computedFullPaymentInterest));
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testComputeFullPaymentInterest();
|
||||
testLoanAccruedInterest();
|
||||
testLoanLatePaymentInterest();
|
||||
testLoanPeriodicPayment();
|
||||
testLoanPrincipalFromPeriodicPayment();
|
||||
testComputeRaisedRate();
|
||||
testComputePaymentFactor();
|
||||
testComputeOverpaymentComponents();
|
||||
testComputeInterestAndFeeParts();
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(LendingHelpers, app, ripple);
|
||||
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
@@ -1024,12 +1024,6 @@ class LoanBroker_test : public beast::unit_test::suite
|
||||
destination(dest),
|
||||
ter(tecFROZEN),
|
||||
THISLINE);
|
||||
|
||||
// preclaim: tecPSEUDO_ACCOUNT
|
||||
env(coverWithdraw(alice, brokerKeylet.key, asset(10)),
|
||||
destination(vaultInfo.pseudoAccount),
|
||||
ter(tecPSEUDO_ACCOUNT),
|
||||
THISLINE);
|
||||
}
|
||||
|
||||
if (brokerTest == CoverClawback)
|
||||
|
||||
@@ -1271,8 +1271,7 @@ protected:
|
||||
verifyLoanStatus,
|
||||
issuer,
|
||||
lender,
|
||||
borrower,
|
||||
PaymentParameters{.showStepBalances = true});
|
||||
borrower);
|
||||
}
|
||||
|
||||
/** Runs through the complete lifecycle of a loan
|
||||
@@ -6145,16 +6144,15 @@ protected:
|
||||
// Accrued + prepayment-penalty interest based on current periodic
|
||||
// schedule
|
||||
auto const fullPaymentInterest = computeFullPaymentInterest(
|
||||
detail::loanPrincipalFromPeriodicPayment(
|
||||
after.periodicPayment, periodicRate2, after.paymentRemaining),
|
||||
after.periodicPayment,
|
||||
periodicRate2,
|
||||
after.paymentRemaining,
|
||||
env.current()->parentCloseTime(),
|
||||
after.paymentInterval,
|
||||
after.previousPaymentDate,
|
||||
static_cast<std::uint32_t>(
|
||||
after.startDate.time_since_epoch().count()),
|
||||
closeInterestRate);
|
||||
|
||||
// Round to asset scale and split interest/fee parts
|
||||
auto const roundedInterest =
|
||||
roundToAsset(asset.raw(), fullPaymentInterest, after.loanScale);
|
||||
@@ -6182,9 +6180,9 @@ protected:
|
||||
// window by clamping prevPaymentDate to 'now' for the full-pay path.
|
||||
auto const prevClamped = std::min(after.previousPaymentDate, nowSecs);
|
||||
auto const fullPaymentInterestClamped = computeFullPaymentInterest(
|
||||
detail::loanPrincipalFromPeriodicPayment(
|
||||
after.periodicPayment, periodicRate2, after.paymentRemaining),
|
||||
after.periodicPayment,
|
||||
periodicRate2,
|
||||
after.paymentRemaining,
|
||||
env.current()->parentCloseTime(),
|
||||
after.paymentInterval,
|
||||
prevClamped,
|
||||
@@ -7195,15 +7193,15 @@ class LoanArbitrary_test : public LoanBatch_test
|
||||
.vaultDeposit = 10000,
|
||||
.debtMax = 0,
|
||||
.coverRateMin = TenthBips32{0},
|
||||
.managementFeeRate = TenthBips16{0},
|
||||
// .managementFeeRate = TenthBips16{5919},
|
||||
.coverRateLiquidation = TenthBips32{0}};
|
||||
LoanParameters const loanParams{
|
||||
.account = Account("lender"),
|
||||
.counter = Account("borrower"),
|
||||
.principalRequest = Number{200000, -6},
|
||||
.interest = TenthBips32{50000},
|
||||
.payTotal = 2,
|
||||
.payInterval = 200};
|
||||
.principalRequest = Number{10000, 0},
|
||||
// .interest = TenthBips32{0},
|
||||
// .payTotal = 5816,
|
||||
.payInterval = 150};
|
||||
|
||||
runLoan(AssetType::XRP, brokerParams, loanParams);
|
||||
}
|
||||
|
||||
@@ -644,7 +644,7 @@ MPTTester::operator[](std::string const& name) const
|
||||
}
|
||||
|
||||
PrettyAmount
|
||||
MPTTester::operator()(std::int64_t amount) const
|
||||
MPTTester::operator()(std::uint64_t amount) const
|
||||
{
|
||||
return MPT("", issuanceID())(amount);
|
||||
}
|
||||
|
||||
@@ -272,7 +272,7 @@ public:
|
||||
operator[](std::string const& name) const;
|
||||
|
||||
PrettyAmount
|
||||
operator()(std::int64_t amount) const;
|
||||
operator()(std::uint64_t amount) const;
|
||||
|
||||
operator Asset() const;
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
#include <test/jtx/multisign.h>
|
||||
#include <test/jtx/xchain_bridge.h>
|
||||
|
||||
#include <xrpld/app/tx/apply.h>
|
||||
|
||||
#include <xrpl/beast/unit_test.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
@@ -2008,6 +2010,370 @@ class LedgerEntry_test : public beast::unit_test::suite
|
||||
}
|
||||
}
|
||||
|
||||
/// Test the ledger entry types that don't take parameters
|
||||
void
|
||||
testLedgerEntryFixed()
|
||||
{
|
||||
using namespace test::jtx;
|
||||
|
||||
Account const alice{"alice"};
|
||||
Account const bob{"bob"};
|
||||
|
||||
Env env{*this, envconfig([](auto cfg) {
|
||||
cfg->START_UP = Config::FRESH;
|
||||
return cfg;
|
||||
})};
|
||||
|
||||
env.close();
|
||||
|
||||
/** Verifies that the RPC result has the expected data
|
||||
*
|
||||
* @param good: Indicates that the request should have succeeded
|
||||
* and returned a ledger object of `expectedType` type.
|
||||
* @param jv: The RPC result Json value
|
||||
* @param expectedType: The type that the ledger object should
|
||||
* have if "good".
|
||||
* @param expectedError: Optional. The expected error if not
|
||||
* good. Defaults to "entryNotFound".
|
||||
*/
|
||||
auto checkResult =
|
||||
[&](bool good,
|
||||
Json::Value const& jv,
|
||||
Json::StaticString const& expectedType,
|
||||
std::optional<std::string> const& expectedError = {}) {
|
||||
if (good)
|
||||
{
|
||||
BEAST_EXPECTS(
|
||||
jv.isObject() && jv.isMember(jss::result) &&
|
||||
!jv[jss::result].isMember(jss::error) &&
|
||||
jv[jss::result].isMember(jss::node) &&
|
||||
jv[jss::result][jss::node].isMember(
|
||||
sfLedgerEntryType.jsonName) &&
|
||||
jv[jss::result][jss::node]
|
||||
[sfLedgerEntryType.jsonName] == expectedType,
|
||||
to_string(jv));
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECTS(
|
||||
jv.isObject() && jv.isMember(jss::result) &&
|
||||
jv[jss::result].isMember(jss::error) &&
|
||||
!jv[jss::result].isMember(jss::node) &&
|
||||
jv[jss::result][jss::error] ==
|
||||
expectedError.value_or("entryNotFound"),
|
||||
to_string(jv));
|
||||
}
|
||||
};
|
||||
|
||||
/** Runs a series of tests for a given fixed-position ledger
|
||||
* entry.
|
||||
*
|
||||
* @param field: The Json request field to use.
|
||||
* @param expectedType: The type that the ledger object should
|
||||
* have if "good".
|
||||
* @param expectedKey: The keylet of the fixed object.
|
||||
* @param good: Indicates whether the object is expected to
|
||||
* exist.
|
||||
*/
|
||||
auto test = [&](Json::StaticString const& field,
|
||||
Json::StaticString const& expectedType,
|
||||
Keylet const& expectedKey,
|
||||
bool good) {
|
||||
testcase << "ledger_entry " << expectedType.c_str()
|
||||
<< (good ? "" : " not") << " found";
|
||||
|
||||
auto const hexKey = strHex(expectedKey.key);
|
||||
|
||||
// Test bad values
|
||||
// "field":null
|
||||
Json::Value params;
|
||||
params[jss::ledger_index] = jss::validated;
|
||||
params[field] = Json::nullValue;
|
||||
auto jv = env.rpc("json", "ledger_entry", to_string(params));
|
||||
checkResult(false, jv, expectedType, "malformedRequest");
|
||||
BEAST_EXPECT(!jv[jss::result].isMember(jss::index));
|
||||
|
||||
// "field":"string"
|
||||
params.clear();
|
||||
params[jss::ledger_index] = jss::validated;
|
||||
params[field] = "arbitrary string";
|
||||
jv = env.rpc("json", "ledger_entry", to_string(params));
|
||||
checkResult(false, jv, expectedType, "malformedRequest");
|
||||
BEAST_EXPECT(!jv[jss::result].isMember(jss::index));
|
||||
|
||||
// "field":false
|
||||
params.clear();
|
||||
params[jss::ledger_index] = jss::validated;
|
||||
params[field] = false;
|
||||
jv = env.rpc("json", "ledger_entry", to_string(params));
|
||||
checkResult(false, jv, expectedType, "invalidParams");
|
||||
BEAST_EXPECT(!jv[jss::result].isMember(jss::index));
|
||||
|
||||
{
|
||||
// "field":[incorrect index hash]
|
||||
auto const badKey = strHex(expectedKey.key + uint256{1});
|
||||
params.clear();
|
||||
params[jss::ledger_index] = jss::validated;
|
||||
params[field] = badKey;
|
||||
jv = env.rpc("json", "ledger_entry", to_string(params));
|
||||
checkResult(false, jv, expectedType, "entryNotFound");
|
||||
BEAST_EXPECTS(
|
||||
jv[jss::result][jss::index] == badKey, to_string(jv));
|
||||
}
|
||||
|
||||
// "index":"field" using API 2
|
||||
params.clear();
|
||||
params[jss::ledger_index] = jss::validated;
|
||||
params[jss::index] = field;
|
||||
params[jss::api_version] = 2;
|
||||
jv = env.rpc("json", "ledger_entry", to_string(params));
|
||||
checkResult(false, jv, expectedType, "malformedRequest");
|
||||
BEAST_EXPECT(!jv[jss::result].isMember(jss::index));
|
||||
|
||||
// Test good values
|
||||
// Use the "field":true notation
|
||||
params.clear();
|
||||
params[jss::ledger_index] = jss::validated;
|
||||
params[field] = true;
|
||||
jv = env.rpc("json", "ledger_entry", to_string(params));
|
||||
// Index will always be returned for valid parameters.
|
||||
std::string const pdIdx = jv[jss::result][jss::index].asString();
|
||||
BEAST_EXPECTS(hexKey == pdIdx, to_string(jv));
|
||||
checkResult(good, jv, expectedType);
|
||||
|
||||
// "field":"[index hash]"
|
||||
params.clear();
|
||||
params[jss::ledger_index] = jss::validated;
|
||||
params[field] = hexKey;
|
||||
jv = env.rpc("json", "ledger_entry", to_string(params));
|
||||
checkResult(good, jv, expectedType);
|
||||
BEAST_EXPECT(jv[jss::result][jss::index].asString() == hexKey);
|
||||
|
||||
// Use the "index":"field" notation with API 3
|
||||
params.clear();
|
||||
params[jss::ledger_index] = jss::validated;
|
||||
params[jss::index] = field;
|
||||
params[jss::api_version] = 3;
|
||||
jv = env.rpc("json", "ledger_entry", to_string(params));
|
||||
// Index is correct either way
|
||||
BEAST_EXPECT(jv[jss::result][jss::index].asString() == hexKey);
|
||||
checkResult(good, jv, expectedType);
|
||||
|
||||
// Use the "index":"[index hash]" notation
|
||||
params.clear();
|
||||
params[jss::ledger_index] = jss::validated;
|
||||
params[jss::index] = pdIdx;
|
||||
jv = env.rpc("json", "ledger_entry", to_string(params));
|
||||
// Index is correct either way
|
||||
BEAST_EXPECT(jv[jss::result][jss::index].asString() == hexKey);
|
||||
checkResult(good, jv, expectedType);
|
||||
};
|
||||
|
||||
test(jss::amendments, jss::Amendments, keylet::amendments(), true);
|
||||
test(jss::fee, jss::FeeSettings, keylet::fees(), true);
|
||||
// There won't be an nunl
|
||||
test(jss::nunl, jss::NegativeUNL, keylet::negativeUNL(), false);
|
||||
// Can only get the short skip list this way
|
||||
test(jss::hashes, jss::LedgerHashes, keylet::skip(), true);
|
||||
}
|
||||
|
||||
void
|
||||
testLedgerEntryHashes()
|
||||
{
|
||||
using namespace test::jtx;
|
||||
|
||||
Account const alice{"alice"};
|
||||
Account const bob{"bob"};
|
||||
|
||||
Env env{*this, envconfig([](auto cfg) {
|
||||
cfg->START_UP = Config::FRESH;
|
||||
return cfg;
|
||||
})};
|
||||
|
||||
env.close();
|
||||
|
||||
/** Verifies that the RPC result has the expected data
|
||||
*
|
||||
* @param good: Indicates that the request should have succeeded
|
||||
* and returned a ledger object of `expectedType` type.
|
||||
* @param jv: The RPC result Json value
|
||||
* @param expectedCount: The number of Hashes expected in the
|
||||
* object if "good".
|
||||
* @param expectedError: Optional. The expected error if not
|
||||
* good. Defaults to "entryNotFound".
|
||||
*/
|
||||
auto checkResult =
|
||||
[&](bool good,
|
||||
Json::Value const& jv,
|
||||
int expectedCount,
|
||||
std::optional<std::string> const& expectedError = {}) {
|
||||
if (good)
|
||||
{
|
||||
BEAST_EXPECTS(
|
||||
jv.isObject() && jv.isMember(jss::result) &&
|
||||
!jv[jss::result].isMember(jss::error) &&
|
||||
jv[jss::result].isMember(jss::node) &&
|
||||
jv[jss::result][jss::node].isMember(
|
||||
sfLedgerEntryType.jsonName) &&
|
||||
jv[jss::result][jss::node]
|
||||
[sfLedgerEntryType.jsonName] == jss::LedgerHashes,
|
||||
to_string(jv));
|
||||
BEAST_EXPECTS(
|
||||
jv[jss::result].isMember(jss::node) &&
|
||||
jv[jss::result][jss::node].isMember("Hashes") &&
|
||||
jv[jss::result][jss::node]["Hashes"].size() ==
|
||||
expectedCount,
|
||||
to_string(jv[jss::result][jss::node]["Hashes"].size()));
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECTS(
|
||||
jv.isObject() && jv.isMember(jss::result) &&
|
||||
jv[jss::result].isMember(jss::error) &&
|
||||
!jv[jss::result].isMember(jss::node) &&
|
||||
jv[jss::result][jss::error] ==
|
||||
expectedError.value_or("entryNotFound"),
|
||||
to_string(jv));
|
||||
}
|
||||
};
|
||||
|
||||
/** Runs a series of tests for a given ledger index.
|
||||
*
|
||||
* @param ledger: The ledger index value of the "hashes" request
|
||||
* parameter. May not necessarily be a number.
|
||||
* @param expectedKey: The expected keylet of the object.
|
||||
* @param good: Indicates whether the object is expected to
|
||||
* exist.
|
||||
* @param expectedCount: The number of Hashes expected in the
|
||||
* object if "good".
|
||||
*/
|
||||
auto test = [&](Json::Value ledger,
|
||||
Keylet const& expectedKey,
|
||||
bool good,
|
||||
int expectedCount = 0) {
|
||||
testcase << "ledger_entry LedgerHashes: seq: "
|
||||
<< env.current()->info().seq
|
||||
<< " \"hashes\":" << to_string(ledger)
|
||||
<< (good ? "" : " not") << " found";
|
||||
|
||||
auto const hexKey = strHex(expectedKey.key);
|
||||
|
||||
// Test bad values
|
||||
// "hashes":null
|
||||
Json::Value params;
|
||||
params[jss::ledger_index] = jss::validated;
|
||||
params[jss::hashes] = Json::nullValue;
|
||||
auto jv = env.rpc("json", "ledger_entry", to_string(params));
|
||||
checkResult(false, jv, 0, "malformedRequest");
|
||||
BEAST_EXPECT(!jv[jss::result].isMember(jss::index));
|
||||
|
||||
// "hashes":"non-uint string"
|
||||
params.clear();
|
||||
params[jss::ledger_index] = jss::validated;
|
||||
params[jss::hashes] = "arbitrary string";
|
||||
jv = env.rpc("json", "ledger_entry", to_string(params));
|
||||
checkResult(false, jv, 0, "malformedRequest");
|
||||
BEAST_EXPECT(!jv[jss::result].isMember(jss::index));
|
||||
|
||||
// "hashes":"uint string" is invalid, too
|
||||
params.clear();
|
||||
params[jss::ledger_index] = jss::validated;
|
||||
params[jss::hashes] = "10";
|
||||
jv = env.rpc("json", "ledger_entry", to_string(params));
|
||||
checkResult(false, jv, 0, "malformedRequest");
|
||||
BEAST_EXPECT(!jv[jss::result].isMember(jss::index));
|
||||
|
||||
// "hashes":false
|
||||
params.clear();
|
||||
params[jss::ledger_index] = jss::validated;
|
||||
params[jss::hashes] = false;
|
||||
jv = env.rpc("json", "ledger_entry", to_string(params));
|
||||
checkResult(false, jv, 0, "invalidParams");
|
||||
BEAST_EXPECT(!jv[jss::result].isMember(jss::index));
|
||||
|
||||
// "hashes":-1
|
||||
params.clear();
|
||||
params[jss::ledger_index] = jss::validated;
|
||||
params[jss::hashes] = -1;
|
||||
jv = env.rpc("json", "ledger_entry", to_string(params));
|
||||
checkResult(false, jv, 0, "internal");
|
||||
BEAST_EXPECT(!jv[jss::result].isMember(jss::index));
|
||||
|
||||
// "hashes":[incorrect index hash]
|
||||
{
|
||||
auto const badKey = strHex(expectedKey.key + uint256{1});
|
||||
params.clear();
|
||||
params[jss::ledger_index] = jss::validated;
|
||||
params[jss::hashes] = badKey;
|
||||
jv = env.rpc("json", "ledger_entry", to_string(params));
|
||||
checkResult(false, jv, 0, "entryNotFound");
|
||||
BEAST_EXPECT(jv[jss::result][jss::index] == badKey);
|
||||
}
|
||||
|
||||
// Test good values
|
||||
// Use the "hashes":ledger notation
|
||||
params.clear();
|
||||
params[jss::ledger_index] = jss::validated;
|
||||
params[jss::hashes] = ledger;
|
||||
jv = env.rpc("json", "ledger_entry", to_string(params));
|
||||
checkResult(good, jv, expectedCount);
|
||||
// Index will always be returned for valid parameters.
|
||||
std::string const pdIdx = jv[jss::result][jss::index].asString();
|
||||
BEAST_EXPECTS(hexKey == pdIdx, strHex(pdIdx));
|
||||
|
||||
// "hashes":"[index hash]"
|
||||
params.clear();
|
||||
params[jss::ledger_index] = jss::validated;
|
||||
params[jss::hashes] = hexKey;
|
||||
jv = env.rpc("json", "ledger_entry", to_string(params));
|
||||
checkResult(good, jv, expectedCount);
|
||||
// Index is correct either way
|
||||
BEAST_EXPECTS(
|
||||
hexKey == jv[jss::result][jss::index].asString(),
|
||||
strHex(jv[jss::result][jss::index].asString()));
|
||||
|
||||
// Use the "index":"[index hash]" notation
|
||||
params.clear();
|
||||
params[jss::ledger_index] = jss::validated;
|
||||
params[jss::index] = hexKey;
|
||||
jv = env.rpc("json", "ledger_entry", to_string(params));
|
||||
checkResult(good, jv, expectedCount);
|
||||
// Index is correct either way
|
||||
BEAST_EXPECTS(
|
||||
hexKey == jv[jss::result][jss::index].asString(),
|
||||
strHex(jv[jss::result][jss::index].asString()));
|
||||
};
|
||||
|
||||
// short skip list
|
||||
test(true, keylet::skip(), true, 2);
|
||||
// long skip list at index 0
|
||||
test(1, keylet::skip(1), false);
|
||||
// long skip list at index 1
|
||||
test(1 << 17, keylet::skip(1 << 17), false);
|
||||
|
||||
// Close more ledgers, but stop short of the flag ledger
|
||||
for (auto i = env.current()->seq(); i <= 250; ++i)
|
||||
env.close();
|
||||
|
||||
// short skip list
|
||||
test(true, keylet::skip(), true, 249);
|
||||
// long skip list at index 0
|
||||
test(1, keylet::skip(1), false);
|
||||
// long skip list at index 1
|
||||
test(1 << 17, keylet::skip(1 << 17), false);
|
||||
|
||||
// Close a flag ledger so the first "long" skip list is created
|
||||
for (auto i = env.current()->seq(); i <= 260; ++i)
|
||||
env.close();
|
||||
|
||||
// short skip list
|
||||
test(true, keylet::skip(), true, 256);
|
||||
// long skip list at index 0
|
||||
test(1, keylet::skip(1), true, 1);
|
||||
// long skip list at index 1
|
||||
test(1 << 17, keylet::skip(1 << 17), false);
|
||||
}
|
||||
|
||||
void
|
||||
testLedgerEntryCLI()
|
||||
{
|
||||
@@ -2057,6 +2423,8 @@ public:
|
||||
testOracleLedgerEntry();
|
||||
testLedgerEntryMPT();
|
||||
testLedgerEntryPermissionedDomain();
|
||||
testLedgerEntryFixed();
|
||||
testLedgerEntryHashes();
|
||||
testLedgerEntryCLI();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -202,6 +202,14 @@ computeRawLoanState(
|
||||
std::uint32_t const paymentRemaining,
|
||||
TenthBips32 const managementFeeRate);
|
||||
|
||||
LoanState
|
||||
computeRawLoanState(
|
||||
Number const& periodicPayment,
|
||||
TenthBips32 interestRate,
|
||||
std::uint32_t paymentInterval,
|
||||
std::uint32_t const paymentRemaining,
|
||||
TenthBips32 const managementFeeRate);
|
||||
|
||||
// Constructs a valid LoanState object from arbitrary inputs
|
||||
LoanState
|
||||
constructLoanState(
|
||||
@@ -231,6 +239,17 @@ computeFullPaymentInterest(
|
||||
std::uint32_t startDate,
|
||||
TenthBips32 closeInterestRate);
|
||||
|
||||
Number
|
||||
computeFullPaymentInterest(
|
||||
Number const& periodicPayment,
|
||||
Number const& periodicRate,
|
||||
std::uint32_t paymentRemaining,
|
||||
NetClock::time_point parentCloseTime,
|
||||
std::uint32_t paymentInterval,
|
||||
std::uint32_t prevPaymentDate,
|
||||
std::uint32_t startDate,
|
||||
TenthBips32 closeInterestRate);
|
||||
|
||||
namespace detail {
|
||||
// These classes and functions should only be accessed by LendingHelper
|
||||
// functions and unit tests
|
||||
@@ -368,58 +387,6 @@ struct LoanStateDeltas
|
||||
nonNegative();
|
||||
};
|
||||
|
||||
Number
|
||||
computeRaisedRate(Number const& periodicRate, std::uint32_t paymentsRemaining);
|
||||
|
||||
Number
|
||||
computePaymentFactor(
|
||||
Number const& periodicRate,
|
||||
std::uint32_t paymentsRemaining);
|
||||
|
||||
std::pair<Number, Number>
|
||||
computeInterestAndFeeParts(
|
||||
Asset const& asset,
|
||||
Number const& interest,
|
||||
TenthBips16 managementFeeRate,
|
||||
std::int32_t loanScale);
|
||||
|
||||
Number
|
||||
loanPeriodicPayment(
|
||||
Number const& principalOutstanding,
|
||||
Number const& periodicRate,
|
||||
std::uint32_t paymentsRemaining);
|
||||
|
||||
Number
|
||||
loanPrincipalFromPeriodicPayment(
|
||||
Number const& periodicPayment,
|
||||
Number const& periodicRate,
|
||||
std::uint32_t paymentsRemaining);
|
||||
|
||||
Number
|
||||
loanLatePaymentInterest(
|
||||
Number const& principalOutstanding,
|
||||
TenthBips32 lateInterestRate,
|
||||
NetClock::time_point parentCloseTime,
|
||||
std::uint32_t nextPaymentDueDate);
|
||||
|
||||
Number
|
||||
loanAccruedInterest(
|
||||
Number const& principalOutstanding,
|
||||
Number const& periodicRate,
|
||||
NetClock::time_point parentCloseTime,
|
||||
std::uint32_t startDate,
|
||||
std::uint32_t prevPaymentDate,
|
||||
std::uint32_t paymentInterval);
|
||||
|
||||
ExtendedPaymentComponents
|
||||
computeOverpaymentComponents(
|
||||
Asset const& asset,
|
||||
int32_t const loanScale,
|
||||
Number const& overpayment,
|
||||
TenthBips32 const overpaymentInterestRate,
|
||||
TenthBips32 const overpaymentFeeRate,
|
||||
TenthBips16 const managementFeeRate);
|
||||
|
||||
PaymentComponents
|
||||
computePaymentComponents(
|
||||
Asset const& asset,
|
||||
|
||||
@@ -100,9 +100,6 @@ computePaymentFactor(
|
||||
Number const& periodicRate,
|
||||
std::uint32_t paymentsRemaining)
|
||||
{
|
||||
if (paymentsRemaining == 0)
|
||||
return numZero;
|
||||
|
||||
// For zero interest, payment factor is simply 1/paymentsRemaining
|
||||
if (periodicRate == beast::zero)
|
||||
return Number{1} / paymentsRemaining;
|
||||
@@ -135,6 +132,27 @@ loanPeriodicPayment(
|
||||
computePaymentFactor(periodicRate, paymentsRemaining);
|
||||
}
|
||||
|
||||
/* Calculates the periodic payment amount from annualized interest rate.
|
||||
* Converts the annual rate to periodic rate before computing payment.
|
||||
*
|
||||
* Equation (7) from XLS-66 spec, Section A-2 Equation Glossary
|
||||
*/
|
||||
Number
|
||||
loanPeriodicPayment(
|
||||
Number const& principalOutstanding,
|
||||
TenthBips32 interestRate,
|
||||
std::uint32_t paymentInterval,
|
||||
std::uint32_t paymentsRemaining)
|
||||
{
|
||||
if (principalOutstanding == 0 || paymentsRemaining == 0)
|
||||
return 0;
|
||||
|
||||
Number const periodicRate = loanPeriodicRate(interestRate, paymentInterval);
|
||||
|
||||
return loanPeriodicPayment(
|
||||
principalOutstanding, periodicRate, paymentsRemaining);
|
||||
}
|
||||
|
||||
/* Reverse-calculates principal from periodic payment amount.
|
||||
* Used to determine theoretical principal at any point in the schedule.
|
||||
*
|
||||
@@ -146,9 +164,6 @@ loanPrincipalFromPeriodicPayment(
|
||||
Number const& periodicRate,
|
||||
std::uint32_t paymentsRemaining)
|
||||
{
|
||||
if (paymentsRemaining == 0)
|
||||
return numZero;
|
||||
|
||||
if (periodicRate == 0)
|
||||
return periodicPayment * paymentsRemaining;
|
||||
|
||||
@@ -156,6 +171,21 @@ loanPrincipalFromPeriodicPayment(
|
||||
computePaymentFactor(periodicRate, paymentsRemaining);
|
||||
}
|
||||
|
||||
/* Splits gross interest into net interest (to vault) and management fee (to
|
||||
* broker). Returns pair of (net interest, management fee).
|
||||
*
|
||||
* Equation (33) from XLS-66 spec, Section A-2 Equation Glossary
|
||||
*/
|
||||
std::pair<Number, Number>
|
||||
computeInterestAndFeeParts(
|
||||
Number const& interest,
|
||||
TenthBips16 managementFeeRate)
|
||||
{
|
||||
auto const fee = tenthBipsOfValue(interest, managementFeeRate);
|
||||
|
||||
return std::make_pair(interest - fee, fee);
|
||||
}
|
||||
|
||||
/*
|
||||
* Computes the interest and management fee parts from interest amount.
|
||||
*
|
||||
@@ -186,12 +216,6 @@ loanLatePaymentInterest(
|
||||
NetClock::time_point parentCloseTime,
|
||||
std::uint32_t nextPaymentDueDate)
|
||||
{
|
||||
if (principalOutstanding == beast::zero)
|
||||
return numZero;
|
||||
|
||||
if (lateInterestRate == TenthBips32{0})
|
||||
return numZero;
|
||||
|
||||
auto const now = parentCloseTime.time_since_epoch().count();
|
||||
|
||||
// If the payment is not late by any amount of time, then there's no late
|
||||
@@ -224,9 +248,6 @@ loanAccruedInterest(
|
||||
if (periodicRate == beast::zero)
|
||||
return numZero;
|
||||
|
||||
if (paymentInterval == 0)
|
||||
return numZero;
|
||||
|
||||
auto const lastPaymentDate = std::max(prevPaymentDate, startDate);
|
||||
auto const now = parentCloseTime.time_since_epoch().count();
|
||||
|
||||
@@ -526,14 +547,6 @@ tryOverpayment(
|
||||
|
||||
auto const deltas = rounded - newRounded;
|
||||
|
||||
// The change in loan management fee is equal to the change between the old
|
||||
// and the new outstanding management fees
|
||||
XRPL_ASSERT_PARTS(
|
||||
deltas.managementFee ==
|
||||
rounded.managementFeeDue - managementFeeOutstanding,
|
||||
"ripple::detail::tryOverpayment",
|
||||
"no fee change");
|
||||
|
||||
auto const hypotheticalValueOutstanding =
|
||||
rounded.valueOutstanding - deltas.principal;
|
||||
|
||||
@@ -548,6 +561,7 @@ tryOverpayment(
|
||||
"the loan. Ignore the overpayment";
|
||||
return Unexpected(tesSUCCESS);
|
||||
}
|
||||
|
||||
return LoanPaymentParts{
|
||||
// Principal paid is the reduction in principal outstanding
|
||||
.principalPaid = deltas.principal,
|
||||
@@ -662,6 +676,12 @@ doOverpayment(
|
||||
"ripple::detail::doOverpayment",
|
||||
"principal change agrees");
|
||||
|
||||
XRPL_ASSERT_PARTS(
|
||||
overpaymentComponents.trackedManagementFeeDelta ==
|
||||
managementFeeOutstandingProxy - managementFeeOutstanding,
|
||||
"ripple::detail::doOverpayment",
|
||||
"no fee change");
|
||||
|
||||
// I'm not 100% sure the following asserts are correct. If in doubt, and
|
||||
// everything else works, remove any that cause trouble.
|
||||
|
||||
@@ -692,6 +712,13 @@ doOverpayment(
|
||||
"ripple::detail::doOverpayment",
|
||||
"principal payment matches");
|
||||
|
||||
XRPL_ASSERT_PARTS(
|
||||
loanPaymentParts.feePaid ==
|
||||
overpaymentComponents.untrackedManagementFee +
|
||||
overpaymentComponents.trackedManagementFeeDelta,
|
||||
"ripple::detail::doOverpayment",
|
||||
"fee payment matches");
|
||||
|
||||
// All validations passed, so update the proxy objects (which will
|
||||
// modify the actual Loan ledger object)
|
||||
totalValueOutstandingProxy = totalValueOutstanding;
|
||||
@@ -1204,12 +1231,17 @@ computeOverpaymentComponents(
|
||||
// This interest doesn't follow the normal amortization schedule - it's
|
||||
// a one-time charge for paying early.
|
||||
// Equation (20) and (21) from XLS-66 spec, Section A-2 Equation Glossary
|
||||
auto const [rawOverpaymentInterest, _] = [&]() {
|
||||
Number const interest =
|
||||
tenthBipsOfValue(overpayment, overpaymentInterestRate);
|
||||
return detail::computeInterestAndFeeParts(interest, managementFeeRate);
|
||||
}();
|
||||
|
||||
// Round the penalty interest components to the loan scale
|
||||
auto const [roundedOverpaymentInterest, roundedOverpaymentManagementFee] =
|
||||
[&]() {
|
||||
auto const interest = roundToAsset(
|
||||
asset,
|
||||
tenthBipsOfValue(overpayment, overpaymentInterestRate),
|
||||
loanScale);
|
||||
Number const interest =
|
||||
roundToAsset(asset, rawOverpaymentInterest, loanScale);
|
||||
return detail::computeInterestAndFeeParts(
|
||||
asset, interest, managementFeeRate, loanScale);
|
||||
}();
|
||||
@@ -1403,6 +1435,31 @@ computeFullPaymentInterest(
|
||||
return accruedInterest + prepaymentPenalty;
|
||||
}
|
||||
|
||||
Number
|
||||
computeFullPaymentInterest(
|
||||
Number const& periodicPayment,
|
||||
Number const& periodicRate,
|
||||
std::uint32_t paymentRemaining,
|
||||
NetClock::time_point parentCloseTime,
|
||||
std::uint32_t paymentInterval,
|
||||
std::uint32_t prevPaymentDate,
|
||||
std::uint32_t startDate,
|
||||
TenthBips32 closeInterestRate)
|
||||
{
|
||||
Number const rawPrincipalOutstanding =
|
||||
detail::loanPrincipalFromPeriodicPayment(
|
||||
periodicPayment, periodicRate, paymentRemaining);
|
||||
|
||||
return computeFullPaymentInterest(
|
||||
rawPrincipalOutstanding,
|
||||
periodicRate,
|
||||
parentCloseTime,
|
||||
paymentInterval,
|
||||
prevPaymentDate,
|
||||
startDate,
|
||||
closeInterestRate);
|
||||
}
|
||||
|
||||
/* Calculates the theoretical loan state at maximum precision for a given point
|
||||
* in the amortization schedule.
|
||||
*
|
||||
@@ -1464,6 +1521,21 @@ computeRawLoanState(
|
||||
.managementFeeDue = rawManagementFeeOutstanding};
|
||||
};
|
||||
|
||||
LoanState
|
||||
computeRawLoanState(
|
||||
Number const& periodicPayment,
|
||||
TenthBips32 interestRate,
|
||||
std::uint32_t paymentInterval,
|
||||
std::uint32_t const paymentRemaining,
|
||||
TenthBips32 const managementFeeRate)
|
||||
{
|
||||
return computeRawLoanState(
|
||||
periodicPayment,
|
||||
loanPeriodicRate(interestRate, paymentInterval),
|
||||
paymentRemaining,
|
||||
managementFeeRate);
|
||||
}
|
||||
|
||||
/* Constructs a LoanState from rounded Loan ledger object values.
|
||||
*
|
||||
* This function creates a LoanState structure from the three tracked values
|
||||
|
||||
@@ -48,11 +48,6 @@ LoanBrokerCoverWithdraw::preclaim(PreclaimContext const& ctx)
|
||||
|
||||
auto const dstAcct = tx[~sfDestination].value_or(account);
|
||||
|
||||
if (isPseudoAccount(ctx.view, dstAcct))
|
||||
{
|
||||
JLOG(ctx.j.warn()) << "Trying to withdraw into a pseudo-account.";
|
||||
return tecPSEUDO_ACCOUNT;
|
||||
}
|
||||
auto const sleBroker = ctx.view.read(keylet::loanbroker(brokerID));
|
||||
if (!sleBroker)
|
||||
{
|
||||
|
||||
@@ -67,7 +67,7 @@ LoanBrokerDelete::preclaim(PreclaimContext const& ctx)
|
||||
JLOG(ctx.j.warn()) << "LoanBrokerDelete: Debt total is "
|
||||
<< debtTotal << ", which rounds to " << rounded;
|
||||
return tecHAS_OBLIGATIONS;
|
||||
// LCOV_EXCL_STOP
|
||||
// LCOV_EXCL_START
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,32 @@
|
||||
|
||||
namespace ripple {
|
||||
|
||||
using FunctionType = std::function<Expected<uint256, Json::Value>(
|
||||
Json::Value const&,
|
||||
Json::StaticString const,
|
||||
unsigned apiVersion)>;
|
||||
|
||||
static Expected<uint256, Json::Value>
|
||||
parseFixed(
|
||||
Keylet const& keylet,
|
||||
Json::Value const& params,
|
||||
Json::StaticString const& fieldName,
|
||||
unsigned apiVersion);
|
||||
|
||||
// Helper function to return FunctionType for objects that have a fixed
|
||||
// location. That is, they don't take parameters to compute the index.
|
||||
// e.g. amendments, fees, negative UNL, etc.
|
||||
static FunctionType
|
||||
fixed(Keylet const& keylet)
|
||||
{
|
||||
return [&keylet](
|
||||
Json::Value const& params,
|
||||
Json::StaticString const fieldName,
|
||||
unsigned apiVersion) -> Expected<uint256, Json::Value> {
|
||||
return parseFixed(keylet, params, fieldName, apiVersion);
|
||||
};
|
||||
}
|
||||
|
||||
static Expected<uint256, Json::Value>
|
||||
parseObjectID(
|
||||
Json::Value const& params,
|
||||
@@ -33,13 +59,33 @@ parseObjectID(
|
||||
}
|
||||
|
||||
static Expected<uint256, Json::Value>
|
||||
parseIndex(Json::Value const& params, Json::StaticString const fieldName)
|
||||
parseIndex(
|
||||
Json::Value const& params,
|
||||
Json::StaticString const fieldName,
|
||||
unsigned apiVersion)
|
||||
{
|
||||
if (apiVersion > 2u && params.isString())
|
||||
{
|
||||
std::string const index = params.asString();
|
||||
if (index == jss::amendments.c_str())
|
||||
return keylet::amendments().key;
|
||||
if (index == jss::fee.c_str())
|
||||
return keylet::fees().key;
|
||||
if (index == jss::nunl)
|
||||
return keylet::negativeUNL().key;
|
||||
if (index == jss::hashes)
|
||||
// Note this only finds the "short" skip list. Use "hashes":index to
|
||||
// get the long list.
|
||||
return keylet::skip().key;
|
||||
}
|
||||
return parseObjectID(params, fieldName, "hex string");
|
||||
}
|
||||
|
||||
static Expected<uint256, Json::Value>
|
||||
parseAccountRoot(Json::Value const& params, Json::StaticString const fieldName)
|
||||
parseAccountRoot(
|
||||
Json::Value const& params,
|
||||
Json::StaticString const fieldName,
|
||||
unsigned apiVersion)
|
||||
{
|
||||
if (auto const account = LedgerEntryHelpers::parse<AccountID>(params))
|
||||
{
|
||||
@@ -50,14 +96,13 @@ parseAccountRoot(Json::Value const& params, Json::StaticString const fieldName)
|
||||
"malformedAddress", fieldName, "AccountID");
|
||||
}
|
||||
|
||||
static Expected<uint256, Json::Value>
|
||||
parseAmendments(Json::Value const& params, Json::StaticString const fieldName)
|
||||
{
|
||||
return parseObjectID(params, fieldName, "hex string");
|
||||
}
|
||||
auto const parseAmendments = fixed(keylet::amendments());
|
||||
|
||||
static Expected<uint256, Json::Value>
|
||||
parseAMM(Json::Value const& params, Json::StaticString const fieldName)
|
||||
parseAMM(
|
||||
Json::Value const& params,
|
||||
Json::StaticString const fieldName,
|
||||
unsigned apiVersion)
|
||||
{
|
||||
if (!params.isObject())
|
||||
{
|
||||
@@ -84,7 +129,10 @@ parseAMM(Json::Value const& params, Json::StaticString const fieldName)
|
||||
}
|
||||
|
||||
static Expected<uint256, Json::Value>
|
||||
parseBridge(Json::Value const& params, Json::StaticString const fieldName)
|
||||
parseBridge(
|
||||
Json::Value const& params,
|
||||
Json::StaticString const fieldName,
|
||||
unsigned apiVersion)
|
||||
{
|
||||
if (!params.isMember(jss::bridge))
|
||||
{
|
||||
@@ -115,13 +163,19 @@ parseBridge(Json::Value const& params, Json::StaticString const fieldName)
|
||||
}
|
||||
|
||||
static Expected<uint256, Json::Value>
|
||||
parseCheck(Json::Value const& params, Json::StaticString const fieldName)
|
||||
parseCheck(
|
||||
Json::Value const& params,
|
||||
Json::StaticString const fieldName,
|
||||
unsigned apiVersion)
|
||||
{
|
||||
return parseObjectID(params, fieldName, "hex string");
|
||||
}
|
||||
|
||||
static Expected<uint256, Json::Value>
|
||||
parseCredential(Json::Value const& cred, Json::StaticString const fieldName)
|
||||
parseCredential(
|
||||
Json::Value const& cred,
|
||||
Json::StaticString const fieldName,
|
||||
unsigned apiVersion)
|
||||
{
|
||||
if (!cred.isObject())
|
||||
{
|
||||
@@ -152,7 +206,10 @@ parseCredential(Json::Value const& cred, Json::StaticString const fieldName)
|
||||
}
|
||||
|
||||
static Expected<uint256, Json::Value>
|
||||
parseDelegate(Json::Value const& params, Json::StaticString const fieldName)
|
||||
parseDelegate(
|
||||
Json::Value const& params,
|
||||
Json::StaticString const fieldName,
|
||||
unsigned apiVersion)
|
||||
{
|
||||
if (!params.isObject())
|
||||
{
|
||||
@@ -243,7 +300,10 @@ parseAuthorizeCredentials(Json::Value const& jv)
|
||||
}
|
||||
|
||||
static Expected<uint256, Json::Value>
|
||||
parseDepositPreauth(Json::Value const& dp, Json::StaticString const fieldName)
|
||||
parseDepositPreauth(
|
||||
Json::Value const& dp,
|
||||
Json::StaticString const fieldName,
|
||||
unsigned apiVersion)
|
||||
{
|
||||
if (!dp.isObject())
|
||||
{
|
||||
@@ -296,7 +356,10 @@ parseDepositPreauth(Json::Value const& dp, Json::StaticString const fieldName)
|
||||
}
|
||||
|
||||
static Expected<uint256, Json::Value>
|
||||
parseDID(Json::Value const& params, Json::StaticString const fieldName)
|
||||
parseDID(
|
||||
Json::Value const& params,
|
||||
Json::StaticString const fieldName,
|
||||
unsigned apiVersion)
|
||||
{
|
||||
auto const account = LedgerEntryHelpers::parse<AccountID>(params);
|
||||
if (!account)
|
||||
@@ -311,7 +374,8 @@ parseDID(Json::Value const& params, Json::StaticString const fieldName)
|
||||
static Expected<uint256, Json::Value>
|
||||
parseDirectoryNode(
|
||||
Json::Value const& params,
|
||||
Json::StaticString const fieldName)
|
||||
Json::StaticString const fieldName,
|
||||
unsigned apiVersion)
|
||||
{
|
||||
if (!params.isObject())
|
||||
{
|
||||
@@ -364,7 +428,10 @@ parseDirectoryNode(
|
||||
}
|
||||
|
||||
static Expected<uint256, Json::Value>
|
||||
parseEscrow(Json::Value const& params, Json::StaticString const fieldName)
|
||||
parseEscrow(
|
||||
Json::Value const& params,
|
||||
Json::StaticString const fieldName,
|
||||
unsigned apiVersion)
|
||||
{
|
||||
if (!params.isObject())
|
||||
{
|
||||
@@ -383,20 +450,53 @@ parseEscrow(Json::Value const& params, Json::StaticString const fieldName)
|
||||
return keylet::escrow(*id, *seq).key;
|
||||
}
|
||||
|
||||
auto const parseFeeSettings = fixed(keylet::fees());
|
||||
|
||||
static Expected<uint256, Json::Value>
|
||||
parseFeeSettings(Json::Value const& params, Json::StaticString const fieldName)
|
||||
parseFixed(
|
||||
Keylet const& keylet,
|
||||
Json::Value const& params,
|
||||
Json::StaticString const& fieldName,
|
||||
unsigned apiVersion)
|
||||
{
|
||||
return parseObjectID(params, fieldName, "hex string");
|
||||
if (!params.isBool())
|
||||
{
|
||||
return parseObjectID(params, fieldName, "hex string");
|
||||
}
|
||||
if (!params.asBool())
|
||||
{
|
||||
return LedgerEntryHelpers::invalidFieldError(
|
||||
"invalidParams", fieldName, "true");
|
||||
}
|
||||
|
||||
return keylet.key;
|
||||
}
|
||||
|
||||
static Expected<uint256, Json::Value>
|
||||
parseLedgerHashes(Json::Value const& params, Json::StaticString const fieldName)
|
||||
parseLedgerHashes(
|
||||
Json::Value const& params,
|
||||
Json::StaticString const fieldName,
|
||||
unsigned apiVersion)
|
||||
{
|
||||
return parseObjectID(params, fieldName, "hex string");
|
||||
if (params.isUInt() || params.isInt())
|
||||
{
|
||||
// If the index doesn't parse as a UInt, throw
|
||||
auto const index = params.asUInt();
|
||||
|
||||
// Return the "long" skip list for the given ledger index.
|
||||
auto const keylet = keylet::skip(index);
|
||||
return keylet.key;
|
||||
}
|
||||
// Return the key in `params` or the "short" skip list, which contains
|
||||
// hashes since the last flag ledger.
|
||||
return parseFixed(keylet::skip(), params, fieldName, apiVersion);
|
||||
}
|
||||
|
||||
static Expected<uint256, Json::Value>
|
||||
parseLoanBroker(Json::Value const& params, Json::StaticString const fieldName)
|
||||
parseLoanBroker(
|
||||
Json::Value const& params,
|
||||
Json::StaticString const fieldName,
|
||||
unsigned apiVersion)
|
||||
{
|
||||
if (!params.isObject())
|
||||
{
|
||||
@@ -416,7 +516,10 @@ parseLoanBroker(Json::Value const& params, Json::StaticString const fieldName)
|
||||
}
|
||||
|
||||
static Expected<uint256, Json::Value>
|
||||
parseLoan(Json::Value const& params, Json::StaticString const fieldName)
|
||||
parseLoan(
|
||||
Json::Value const& params,
|
||||
Json::StaticString const fieldName,
|
||||
unsigned apiVersion)
|
||||
{
|
||||
if (!params.isObject())
|
||||
{
|
||||
@@ -436,7 +539,10 @@ parseLoan(Json::Value const& params, Json::StaticString const fieldName)
|
||||
}
|
||||
|
||||
static Expected<uint256, Json::Value>
|
||||
parseMPToken(Json::Value const& params, Json::StaticString const fieldName)
|
||||
parseMPToken(
|
||||
Json::Value const& params,
|
||||
Json::StaticString const fieldName,
|
||||
unsigned apiVersion)
|
||||
{
|
||||
if (!params.isObject())
|
||||
{
|
||||
@@ -459,7 +565,8 @@ parseMPToken(Json::Value const& params, Json::StaticString const fieldName)
|
||||
static Expected<uint256, Json::Value>
|
||||
parseMPTokenIssuance(
|
||||
Json::Value const& params,
|
||||
Json::StaticString const fieldName)
|
||||
Json::StaticString const fieldName,
|
||||
unsigned apiVersion)
|
||||
{
|
||||
auto const mptIssuanceID = LedgerEntryHelpers::parse<uint192>(params);
|
||||
if (!mptIssuanceID)
|
||||
@@ -470,25 +577,30 @@ parseMPTokenIssuance(
|
||||
}
|
||||
|
||||
static Expected<uint256, Json::Value>
|
||||
parseNFTokenOffer(Json::Value const& params, Json::StaticString const fieldName)
|
||||
parseNFTokenOffer(
|
||||
Json::Value const& params,
|
||||
Json::StaticString const fieldName,
|
||||
unsigned apiVersion)
|
||||
{
|
||||
return parseObjectID(params, fieldName, "hex string");
|
||||
}
|
||||
|
||||
static Expected<uint256, Json::Value>
|
||||
parseNFTokenPage(Json::Value const& params, Json::StaticString const fieldName)
|
||||
parseNFTokenPage(
|
||||
Json::Value const& params,
|
||||
Json::StaticString const fieldName,
|
||||
unsigned apiVersion)
|
||||
{
|
||||
return parseObjectID(params, fieldName, "hex string");
|
||||
}
|
||||
|
||||
static Expected<uint256, Json::Value>
|
||||
parseNegativeUNL(Json::Value const& params, Json::StaticString const fieldName)
|
||||
{
|
||||
return parseObjectID(params, fieldName, "hex string");
|
||||
}
|
||||
auto const parseNegativeUNL = fixed(keylet::negativeUNL());
|
||||
|
||||
static Expected<uint256, Json::Value>
|
||||
parseOffer(Json::Value const& params, Json::StaticString const fieldName)
|
||||
parseOffer(
|
||||
Json::Value const& params,
|
||||
Json::StaticString const fieldName,
|
||||
unsigned apiVersion)
|
||||
{
|
||||
if (!params.isObject())
|
||||
{
|
||||
@@ -509,7 +621,10 @@ parseOffer(Json::Value const& params, Json::StaticString const fieldName)
|
||||
}
|
||||
|
||||
static Expected<uint256, Json::Value>
|
||||
parseOracle(Json::Value const& params, Json::StaticString const fieldName)
|
||||
parseOracle(
|
||||
Json::Value const& params,
|
||||
Json::StaticString const fieldName,
|
||||
unsigned apiVersion)
|
||||
{
|
||||
if (!params.isObject())
|
||||
{
|
||||
@@ -530,7 +645,10 @@ parseOracle(Json::Value const& params, Json::StaticString const fieldName)
|
||||
}
|
||||
|
||||
static Expected<uint256, Json::Value>
|
||||
parsePayChannel(Json::Value const& params, Json::StaticString const fieldName)
|
||||
parsePayChannel(
|
||||
Json::Value const& params,
|
||||
Json::StaticString const fieldName,
|
||||
unsigned apiVersion)
|
||||
{
|
||||
return parseObjectID(params, fieldName, "hex string");
|
||||
}
|
||||
@@ -538,7 +656,8 @@ parsePayChannel(Json::Value const& params, Json::StaticString const fieldName)
|
||||
static Expected<uint256, Json::Value>
|
||||
parsePermissionedDomain(
|
||||
Json::Value const& pd,
|
||||
Json::StaticString const fieldName)
|
||||
Json::StaticString const fieldName,
|
||||
unsigned apiVersion)
|
||||
{
|
||||
if (pd.isString())
|
||||
{
|
||||
@@ -567,7 +686,8 @@ parsePermissionedDomain(
|
||||
static Expected<uint256, Json::Value>
|
||||
parseRippleState(
|
||||
Json::Value const& jvRippleState,
|
||||
Json::StaticString const fieldName)
|
||||
Json::StaticString const fieldName,
|
||||
unsigned apiVersion)
|
||||
{
|
||||
Currency uCurrency;
|
||||
|
||||
@@ -617,13 +737,19 @@ parseRippleState(
|
||||
}
|
||||
|
||||
static Expected<uint256, Json::Value>
|
||||
parseSignerList(Json::Value const& params, Json::StaticString const fieldName)
|
||||
parseSignerList(
|
||||
Json::Value const& params,
|
||||
Json::StaticString const fieldName,
|
||||
unsigned apiVersion)
|
||||
{
|
||||
return parseObjectID(params, fieldName, "hex string");
|
||||
}
|
||||
|
||||
static Expected<uint256, Json::Value>
|
||||
parseTicket(Json::Value const& params, Json::StaticString const fieldName)
|
||||
parseTicket(
|
||||
Json::Value const& params,
|
||||
Json::StaticString const fieldName,
|
||||
unsigned apiVersion)
|
||||
{
|
||||
if (!params.isObject())
|
||||
{
|
||||
@@ -644,7 +770,10 @@ parseTicket(Json::Value const& params, Json::StaticString const fieldName)
|
||||
}
|
||||
|
||||
static Expected<uint256, Json::Value>
|
||||
parseVault(Json::Value const& params, Json::StaticString const fieldName)
|
||||
parseVault(
|
||||
Json::Value const& params,
|
||||
Json::StaticString const fieldName,
|
||||
unsigned apiVersion)
|
||||
{
|
||||
if (!params.isObject())
|
||||
{
|
||||
@@ -667,7 +796,8 @@ parseVault(Json::Value const& params, Json::StaticString const fieldName)
|
||||
static Expected<uint256, Json::Value>
|
||||
parseXChainOwnedClaimID(
|
||||
Json::Value const& claim_id,
|
||||
Json::StaticString const fieldName)
|
||||
Json::StaticString const fieldName,
|
||||
unsigned apiVersion)
|
||||
{
|
||||
if (!claim_id.isObject())
|
||||
{
|
||||
@@ -692,7 +822,8 @@ parseXChainOwnedClaimID(
|
||||
static Expected<uint256, Json::Value>
|
||||
parseXChainOwnedCreateAccountClaimID(
|
||||
Json::Value const& claim_id,
|
||||
Json::StaticString const fieldName)
|
||||
Json::StaticString const fieldName,
|
||||
unsigned apiVersion)
|
||||
{
|
||||
if (!claim_id.isObject())
|
||||
{
|
||||
@@ -716,10 +847,6 @@ parseXChainOwnedCreateAccountClaimID(
|
||||
return keylet.key;
|
||||
}
|
||||
|
||||
using FunctionType = Expected<uint256, Json::Value> (*)(
|
||||
Json::Value const&,
|
||||
Json::StaticString const);
|
||||
|
||||
struct LedgerEntry
|
||||
{
|
||||
Json::StaticString fieldName;
|
||||
@@ -752,7 +879,7 @@ doLedgerEntry(RPC::JsonContext& context)
|
||||
{jss::ripple_state, parseRippleState, ltRIPPLE_STATE},
|
||||
});
|
||||
|
||||
auto hasMoreThanOneMember = [&]() {
|
||||
auto const hasMoreThanOneMember = [&]() {
|
||||
int count = 0;
|
||||
|
||||
for (auto const& ledgerEntry : ledgerEntryParsers)
|
||||
@@ -796,8 +923,8 @@ doLedgerEntry(RPC::JsonContext& context)
|
||||
Json::Value const& params = ledgerEntry.fieldName == jss::bridge
|
||||
? context.params
|
||||
: context.params[ledgerEntry.fieldName];
|
||||
auto const result =
|
||||
ledgerEntry.parseFunction(params, ledgerEntry.fieldName);
|
||||
auto const result = ledgerEntry.parseFunction(
|
||||
params, ledgerEntry.fieldName, context.apiVersion);
|
||||
if (!result)
|
||||
return result.error();
|
||||
|
||||
@@ -828,9 +955,13 @@ doLedgerEntry(RPC::JsonContext& context)
|
||||
throw;
|
||||
}
|
||||
|
||||
// Return the computed index regardless of whether the node exists.
|
||||
jvResult[jss::index] = to_string(uNodeIndex);
|
||||
|
||||
if (uNodeIndex.isZero())
|
||||
{
|
||||
return RPC::make_error(rpcENTRY_NOT_FOUND);
|
||||
RPC::inject_error(rpcENTRY_NOT_FOUND, jvResult);
|
||||
return jvResult;
|
||||
}
|
||||
|
||||
auto const sleNode = lpLedger->read(keylet::unchecked(uNodeIndex));
|
||||
@@ -842,12 +973,14 @@ doLedgerEntry(RPC::JsonContext& context)
|
||||
if (!sleNode)
|
||||
{
|
||||
// Not found.
|
||||
return RPC::make_error(rpcENTRY_NOT_FOUND);
|
||||
RPC::inject_error(rpcENTRY_NOT_FOUND, jvResult);
|
||||
return jvResult;
|
||||
}
|
||||
|
||||
if ((expectedType != ltANY) && (expectedType != sleNode->getType()))
|
||||
{
|
||||
return RPC::make_error(rpcUNEXPECTED_LEDGER_TYPE);
|
||||
RPC::inject_error(rpcUNEXPECTED_LEDGER_TYPE, jvResult);
|
||||
return jvResult;
|
||||
}
|
||||
|
||||
if (bNodeBinary)
|
||||
@@ -857,12 +990,10 @@ doLedgerEntry(RPC::JsonContext& context)
|
||||
sleNode->add(s);
|
||||
|
||||
jvResult[jss::node_binary] = strHex(s.peekData());
|
||||
jvResult[jss::index] = to_string(uNodeIndex);
|
||||
}
|
||||
else
|
||||
{
|
||||
jvResult[jss::node] = sleNode->getJson(JsonOptions::none);
|
||||
jvResult[jss::index] = to_string(uNodeIndex);
|
||||
}
|
||||
|
||||
return jvResult;
|
||||
|
||||
Reference in New Issue
Block a user