mirror of
https://github.com/XRPLF/rippled.git
synced 2026-06-02 16:26:48 +00:00
Start writing Loan tests
- Required adding support for counterparty signature to jtx framework: arbitrary signature field destination, multiple signer callbacks
This commit is contained in:
@@ -394,7 +394,7 @@ UNTYPED_SFIELD(sfXChainClaimAttestationCollectionElement, OBJECT, 30)
|
||||
UNTYPED_SFIELD(sfXChainCreateAccountAttestationCollectionElement, OBJECT, 31)
|
||||
UNTYPED_SFIELD(sfPriceData, OBJECT, 32)
|
||||
UNTYPED_SFIELD(sfCredential, OBJECT, 33)
|
||||
UNTYPED_SFIELD(sfCounterpartySignature, OBJECT, 34)
|
||||
UNTYPED_SFIELD(sfCounterpartySignature, OBJECT, 34, SField::sMD_Default, SField::notSigning)
|
||||
|
||||
// array of objects (common)
|
||||
// ARRAY/1 is reserved for end of array
|
||||
|
||||
631
src/test/app/Loan_test.cpp
Normal file
631
src/test/app/Loan_test.cpp
Normal file
@@ -0,0 +1,631 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2025 Ripple Labs Inc.
|
||||
|
||||
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/Account.h>
|
||||
#include <test/jtx/Env.h>
|
||||
#include <test/jtx/TestHelpers.h>
|
||||
#include <test/jtx/mpt.h>
|
||||
#include <test/jtx/vault.h>
|
||||
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/beast/unit_test/suite.h>
|
||||
#include <xrpl/json/json_forwards.h>
|
||||
#include <xrpl/protocol/Asset.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/STNumber.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
|
||||
class Loan_test : public beast::unit_test::suite
|
||||
{
|
||||
// Ensure that all the features needed for Lending Protocol are included,
|
||||
// even if they are set to unsupported.
|
||||
FeatureBitset const all{
|
||||
jtx::supported_amendments() | featureMPTokensV1 |
|
||||
featureSingleAssetVault | featureLendingProtocol};
|
||||
|
||||
void
|
||||
testDisabled()
|
||||
{
|
||||
testcase("Disabled");
|
||||
// Lending Protocol depends on Single Asset Vault (SAV). Test
|
||||
// combinations of the two amendments.
|
||||
// Single Asset Vault depends on MPTokensV1, but don't test every combo
|
||||
// of that.
|
||||
using namespace jtx;
|
||||
auto failAll = [this](FeatureBitset features, bool goodVault = false) {
|
||||
Env env(*this, features);
|
||||
|
||||
Account const alice{"alice"};
|
||||
env.fund(XRP(10000), alice);
|
||||
|
||||
auto const keylet = keylet::loanbroker(alice, env.seq(alice));
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
using namespace loan;
|
||||
|
||||
// counter party signature is required on LoanSet
|
||||
env(set(alice, keylet.key, Number(10000), env.now() + 720h),
|
||||
ter(temMALFORMED));
|
||||
|
||||
// All loan transactions are disabled.
|
||||
// 1. LoanSet
|
||||
env(set(alice, keylet.key, Number(10000), env.now() + 720h),
|
||||
sig(sfCounterpartySignature, alice),
|
||||
ter(temDISABLED));
|
||||
auto const loanKeylet =
|
||||
keylet::loan(alice.id(), keylet.key, env.seq(alice));
|
||||
#if 0
|
||||
// Other Loan transactions are disabled, too.
|
||||
// 2. LoanDelete
|
||||
env(delete(alice, loanKeylet.key),
|
||||
ter(temDISABLED));
|
||||
// 3. LoanManage
|
||||
env(manage(alice, loanKeylet.key, tfLoanImpair),
|
||||
ter(temDISABLED));
|
||||
// 4. LoanDraw
|
||||
env(draw(alice, loanKeylet.key, Number(500)), ter(temDISABLED));
|
||||
// 5. LoanPay
|
||||
env(pay(alice, loanKeylet.key, Number(500)), ter(temDISABLED));
|
||||
#endif
|
||||
};
|
||||
failAll(all - featureMPTokensV1);
|
||||
failAll(all - featureSingleAssetVault - featureLendingProtocol);
|
||||
failAll(all - featureSingleAssetVault);
|
||||
failAll(all - featureLendingProtocol, true);
|
||||
}
|
||||
|
||||
struct BrokerInfo
|
||||
{
|
||||
jtx::PrettyAsset asset;
|
||||
uint256 brokerID;
|
||||
BrokerInfo(jtx::PrettyAsset const& asset_, uint256 const& brokerID_)
|
||||
: asset(asset_), brokerID(brokerID_)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
void
|
||||
lifecycle(
|
||||
const char* label,
|
||||
jtx::Env& env,
|
||||
jtx::Account const& alice,
|
||||
jtx::Account const& evan,
|
||||
BrokerInfo const& vault,
|
||||
std::function<jtx::JTx(jtx::JTx const&)> modifyJTx,
|
||||
std::function<void(SLE::const_ref)> checkBroker,
|
||||
std::function<void(SLE::const_ref)> changeBroker,
|
||||
std::function<void(SLE::const_ref)> checkChangedBroker)
|
||||
{
|
||||
#if 0
|
||||
auto const keylet = keylet::loanbroker(alice.id(), env.seq(alice));
|
||||
{
|
||||
auto const& asset = vault.asset.raw();
|
||||
testcase << "Lifecycle: "
|
||||
<< (asset.native() ? "XRP "
|
||||
: asset.holds<Issue>() ? "IOU "
|
||||
: asset.holds<MPTIssue>() ? "MPT "
|
||||
: "Unknown ")
|
||||
<< label;
|
||||
}
|
||||
|
||||
using namespace jtx;
|
||||
using namespace loanBroker;
|
||||
|
||||
{
|
||||
auto const keylet = keylet::loanbroker(alice.id(), env.seq(alice));
|
||||
// Start with default values
|
||||
auto jtx = env.jt(set(alice, vault.vaultID), fee(increment));
|
||||
// Modify as desired
|
||||
if (modifyJTx)
|
||||
jtx = modifyJTx(jtx);
|
||||
// Successfully create a Loan Broker
|
||||
env(jtx);
|
||||
}
|
||||
|
||||
env.close();
|
||||
if (auto broker = env.le(keylet); BEAST_EXPECT(broker))
|
||||
{
|
||||
// log << "Broker after create: " << to_string(broker->getJson())
|
||||
// << std::endl;
|
||||
BEAST_EXPECT(broker->at(sfVaultID) == vault.vaultID);
|
||||
BEAST_EXPECT(broker->at(sfAccount) != alice.id());
|
||||
BEAST_EXPECT(broker->at(sfOwner) == alice.id());
|
||||
BEAST_EXPECT(broker->at(sfFlags) == 0);
|
||||
BEAST_EXPECT(broker->at(sfSequence) == env.seq(alice) - 1);
|
||||
BEAST_EXPECT(broker->at(sfOwnerCount) == 0);
|
||||
BEAST_EXPECT(broker->at(sfDebtTotal) == 0);
|
||||
BEAST_EXPECT(broker->at(sfCoverAvailable) == 0);
|
||||
if (checkBroker)
|
||||
checkBroker(broker);
|
||||
|
||||
// if (auto const vaultSLE = env.le(keylet::vault(vault.vaultID)))
|
||||
//{
|
||||
// log << "Vault: " << to_string(vaultSLE->getJson()) <<
|
||||
// std::endl;
|
||||
// }
|
||||
// Load the pseudo-account
|
||||
Account const pseudoAccount{
|
||||
"Broker pseudo-account", broker->at(sfAccount)};
|
||||
auto const pseudoKeylet = keylet::account(pseudoAccount);
|
||||
if (auto const pseudo = env.le(pseudoKeylet); BEAST_EXPECT(pseudo))
|
||||
{
|
||||
// log << "Pseudo-account after create: "
|
||||
// << to_string(pseudo->getJson()) << std::endl
|
||||
// << std::endl;
|
||||
BEAST_EXPECT(
|
||||
pseudo->at(sfFlags) ==
|
||||
(lsfDisableMaster | lsfDefaultRipple | lsfDepositAuth));
|
||||
BEAST_EXPECT(pseudo->at(sfSequence) == 0);
|
||||
BEAST_EXPECT(pseudo->at(sfBalance) == beast::zero);
|
||||
BEAST_EXPECT(
|
||||
pseudo->at(sfOwnerCount) ==
|
||||
(vault.asset.raw().native() ? 0 : 1));
|
||||
BEAST_EXPECT(!pseudo->isFieldPresent(sfAccountTxnID));
|
||||
BEAST_EXPECT(!pseudo->isFieldPresent(sfRegularKey));
|
||||
BEAST_EXPECT(!pseudo->isFieldPresent(sfEmailHash));
|
||||
BEAST_EXPECT(!pseudo->isFieldPresent(sfWalletLocator));
|
||||
BEAST_EXPECT(!pseudo->isFieldPresent(sfWalletSize));
|
||||
BEAST_EXPECT(!pseudo->isFieldPresent(sfMessageKey));
|
||||
BEAST_EXPECT(!pseudo->isFieldPresent(sfTransferRate));
|
||||
BEAST_EXPECT(!pseudo->isFieldPresent(sfDomain));
|
||||
BEAST_EXPECT(!pseudo->isFieldPresent(sfTickSize));
|
||||
BEAST_EXPECT(!pseudo->isFieldPresent(sfTicketCount));
|
||||
BEAST_EXPECT(!pseudo->isFieldPresent(sfNFTokenMinter));
|
||||
BEAST_EXPECT(!pseudo->isFieldPresent(sfMintedNFTokens));
|
||||
BEAST_EXPECT(!pseudo->isFieldPresent(sfBurnedNFTokens));
|
||||
BEAST_EXPECT(!pseudo->isFieldPresent(sfFirstNFTokenSequence));
|
||||
BEAST_EXPECT(!pseudo->isFieldPresent(sfAMMID));
|
||||
BEAST_EXPECT(!pseudo->isFieldPresent(sfVaultID));
|
||||
BEAST_EXPECT(pseudo->at(sfLoanBrokerID) == keylet.key);
|
||||
}
|
||||
|
||||
auto verifyCoverAmount =
|
||||
[&env, &vault, &broker, &pseudoAccount, this](auto n) {
|
||||
auto const amount = vault.asset(n);
|
||||
BEAST_EXPECT(
|
||||
broker->at(sfCoverAvailable) == amount.number());
|
||||
env.require(balance(pseudoAccount, amount));
|
||||
};
|
||||
|
||||
// Test Cover funding before allowing alterations
|
||||
env(coverDeposit(alice, uint256(0), vault.asset(10)),
|
||||
ter(temINVALID));
|
||||
env(coverDeposit(evan, keylet.key, vault.asset(10)),
|
||||
ter(tecNO_PERMISSION));
|
||||
env(coverDeposit(evan, keylet.key, vault.asset(0)),
|
||||
ter(temBAD_AMOUNT));
|
||||
env(coverDeposit(evan, keylet.key, vault.asset(-10)),
|
||||
ter(temBAD_AMOUNT));
|
||||
env(coverDeposit(alice, vault.vaultID, vault.asset(10)),
|
||||
ter(tecNO_ENTRY));
|
||||
|
||||
verifyCoverAmount(0);
|
||||
|
||||
// Fund the cover deposit
|
||||
env(coverDeposit(alice, keylet.key, vault.asset(10)));
|
||||
if (BEAST_EXPECT(broker = env.le(keylet)))
|
||||
{
|
||||
verifyCoverAmount(10);
|
||||
}
|
||||
|
||||
// Test withdrawal failure cases
|
||||
env(coverWithdraw(alice, uint256(0), vault.asset(10)),
|
||||
ter(temINVALID));
|
||||
env(coverWithdraw(evan, keylet.key, vault.asset(10)),
|
||||
ter(tecNO_PERMISSION));
|
||||
env(coverWithdraw(evan, keylet.key, vault.asset(0)),
|
||||
ter(temBAD_AMOUNT));
|
||||
env(coverWithdraw(evan, keylet.key, vault.asset(-10)),
|
||||
ter(temBAD_AMOUNT));
|
||||
env(coverWithdraw(alice, vault.vaultID, vault.asset(10)),
|
||||
ter(tecNO_ENTRY));
|
||||
env(coverWithdraw(alice, keylet.key, vault.asset(900)),
|
||||
ter(tecINSUFFICIENT_FUNDS));
|
||||
|
||||
// Withdraw some of the cover amount
|
||||
env(coverWithdraw(alice, keylet.key, vault.asset(7)));
|
||||
if (BEAST_EXPECT(broker = env.le(keylet)))
|
||||
{
|
||||
verifyCoverAmount(3);
|
||||
}
|
||||
|
||||
// Add some more cover
|
||||
env(coverDeposit(alice, keylet.key, vault.asset(5)));
|
||||
if (BEAST_EXPECT(broker = env.le(keylet)))
|
||||
{
|
||||
verifyCoverAmount(8);
|
||||
}
|
||||
|
||||
// Withdraw some more
|
||||
env(coverWithdraw(alice, keylet.key, vault.asset(2)));
|
||||
if (BEAST_EXPECT(broker = env.le(keylet)))
|
||||
{
|
||||
verifyCoverAmount(6);
|
||||
}
|
||||
|
||||
env.close();
|
||||
|
||||
// no-op
|
||||
env(set(alice, vault.vaultID), loanBrokerID(keylet.key));
|
||||
|
||||
// Make modifications to the broker
|
||||
if (changeBroker)
|
||||
changeBroker(broker);
|
||||
|
||||
env.close();
|
||||
|
||||
// Check the results of modifications
|
||||
if (BEAST_EXPECT(broker = env.le(keylet)) && checkChangedBroker)
|
||||
checkChangedBroker(broker);
|
||||
|
||||
// Verify that fields get removed when set to default values
|
||||
// Debt maximum: explicit 0
|
||||
// Data: explicit empty
|
||||
env(set(alice, vault.vaultID),
|
||||
loanBrokerID(broker->key()),
|
||||
debtMaximum(Number(0)),
|
||||
data(""));
|
||||
|
||||
// Check the updated fields
|
||||
if (BEAST_EXPECT(broker = env.le(keylet)))
|
||||
{
|
||||
BEAST_EXPECT(!broker->isFieldPresent(sfDebtMaximum));
|
||||
BEAST_EXPECT(!broker->isFieldPresent(sfData));
|
||||
}
|
||||
|
||||
/////////////////////////////////////
|
||||
// try to delete the wrong broker object
|
||||
env(del(alice, vault.vaultID), ter(tecNO_ENTRY));
|
||||
// evan tries to delete the broker
|
||||
env(del(evan, keylet.key), ter(tecNO_PERMISSION));
|
||||
|
||||
// TODO: test deletion with an active loan
|
||||
|
||||
// Note alice's balance of the asset and the broker account's cover
|
||||
// funds
|
||||
auto const aliceBalance = env.balance(alice, vault.asset);
|
||||
auto const coverFunds = env.balance(pseudoAccount, vault.asset);
|
||||
BEAST_EXPECT(coverFunds.number() == broker->at(sfCoverAvailable));
|
||||
BEAST_EXPECT(coverFunds != beast::zero);
|
||||
verifyCoverAmount(6);
|
||||
|
||||
// delete the broker
|
||||
// log << "Broker before delete: " << to_string(broker->getJson())
|
||||
// << std::endl;
|
||||
// if (auto const pseudo = env.le(pseudoKeylet);
|
||||
// BEAST_EXPECT(pseudo))
|
||||
//{
|
||||
// log << "Pseudo-account before delete: "
|
||||
// << to_string(pseudo->getJson()) << std::endl
|
||||
// << std::endl;
|
||||
//}
|
||||
|
||||
env(del(alice, keylet.key));
|
||||
env.close();
|
||||
{
|
||||
broker = env.le(keylet);
|
||||
BEAST_EXPECT(!broker);
|
||||
auto pseudo = env.le(pseudoKeylet);
|
||||
BEAST_EXPECT(!pseudo);
|
||||
}
|
||||
auto const expectedBalance = aliceBalance + coverFunds -
|
||||
(aliceBalance.value().native()
|
||||
? STAmount(env.current()->fees().base.value())
|
||||
: vault.asset(0));
|
||||
env.require(balance(alice, expectedBalance));
|
||||
env.require(balance(pseudoAccount, None(vault.asset.raw())));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void
|
||||
testLifecycle()
|
||||
{
|
||||
testcase("Lifecycle");
|
||||
using namespace jtx;
|
||||
|
||||
// Create 3 loan brokers: one for XRP, one for an IOU, and one for an
|
||||
// MPT. That'll require three corresponding SAVs.
|
||||
Env env(*this, all);
|
||||
|
||||
Account issuer{"issuer"};
|
||||
// For simplicity, alice will be the sole actor for the vault & brokers.
|
||||
Account alice{"alice"};
|
||||
// Evan will attempt to be naughty
|
||||
Account evan{"evan"};
|
||||
Vault vault{env};
|
||||
|
||||
// Fund the accounts and trust lines with the same amount so that tests
|
||||
// can use the same values regardless of the asset.
|
||||
env.fund(XRP(100'000), issuer, noripple(alice, evan));
|
||||
env.close();
|
||||
|
||||
// Create assets
|
||||
PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
|
||||
PrettyAsset const iouAsset = issuer["IOU"];
|
||||
env(trust(alice, iouAsset(1'000'000)));
|
||||
env(trust(evan, iouAsset(1'000'000)));
|
||||
env(pay(issuer, evan, iouAsset(100'000)));
|
||||
env(pay(issuer, alice, iouAsset(100'000)));
|
||||
env.close();
|
||||
|
||||
MPTTester mptt{env, issuer, mptInitNoFund};
|
||||
mptt.create(
|
||||
{.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock});
|
||||
PrettyAsset const mptAsset = mptt.issuanceID();
|
||||
mptt.authorize({.account = alice});
|
||||
mptt.authorize({.account = evan});
|
||||
env(pay(issuer, alice, mptAsset(100'000)));
|
||||
env(pay(issuer, evan, mptAsset(100'000)));
|
||||
env.close();
|
||||
|
||||
std::array const assets{xrpAsset, iouAsset, mptAsset};
|
||||
|
||||
// Create vaults and loan brokers
|
||||
std::vector<BrokerInfo> brokers;
|
||||
for (auto const& asset : assets)
|
||||
{
|
||||
auto [tx, vaultKeylet] =
|
||||
vault.create({.owner = alice, .asset = asset});
|
||||
env(tx);
|
||||
env.close();
|
||||
BEAST_EXPECT(env.le(vaultKeylet));
|
||||
|
||||
env(vault.deposit(
|
||||
{.depositor = alice,
|
||||
.id = vaultKeylet.key,
|
||||
.amount = asset(5000)}));
|
||||
env.close();
|
||||
|
||||
auto const keylet = keylet::loanbroker(alice.id(), env.seq(alice));
|
||||
auto const testData = "spam spam spam spam";
|
||||
|
||||
using namespace loanBroker;
|
||||
env(set(alice, vaultKeylet.key),
|
||||
fee(increment),
|
||||
data(testData),
|
||||
managementFeeRate(TenthBips16(123)),
|
||||
debtMaximum(Number(9)),
|
||||
coverRateMinimum(TenthBips32(100)),
|
||||
coverRateLiquidation(TenthBips32(200)));
|
||||
|
||||
brokers.emplace_back(asset, keylet.key);
|
||||
}
|
||||
|
||||
#if 0
|
||||
// Create and update Loan Brokers
|
||||
for (auto const& vault : vaults)
|
||||
{
|
||||
using namespace loanBroker;
|
||||
|
||||
auto badKeylet = keylet::vault(alice.id(), env.seq(alice));
|
||||
// Try some failure cases
|
||||
// insufficient fee
|
||||
env(set(evan, vault.vaultID), ter(telINSUF_FEE_P));
|
||||
// not the vault owner
|
||||
env(set(evan, vault.vaultID),
|
||||
fee(increment),
|
||||
ter(tecNO_PERMISSION));
|
||||
// not a vault
|
||||
env(set(alice, badKeylet.key), fee(increment), ter(tecNO_ENTRY));
|
||||
// flags are checked first
|
||||
env(set(evan, vault.vaultID, ~tfUniversal),
|
||||
fee(increment),
|
||||
ter(temINVALID_FLAG));
|
||||
// field length validation
|
||||
// sfData: good length, bad account
|
||||
env(set(evan, vault.vaultID),
|
||||
fee(increment),
|
||||
data(std::string(maxDataPayloadLength, 'X')),
|
||||
ter(tecNO_PERMISSION));
|
||||
// sfData: too long
|
||||
env(set(evan, vault.vaultID),
|
||||
fee(increment),
|
||||
data(std::string(maxDataPayloadLength + 1, 'Y')),
|
||||
ter(temINVALID));
|
||||
// sfManagementFeeRate: good value, bad account
|
||||
env(set(evan, vault.vaultID),
|
||||
managementFeeRate(maxManagementFeeRate),
|
||||
fee(increment),
|
||||
ter(tecNO_PERMISSION));
|
||||
// sfManagementFeeRate: too big
|
||||
env(set(evan, vault.vaultID),
|
||||
managementFeeRate(maxManagementFeeRate + TenthBips16(10)),
|
||||
fee(increment),
|
||||
ter(temINVALID));
|
||||
// sfCoverRateMinimum: good value, bad account
|
||||
env(set(evan, vault.vaultID),
|
||||
coverRateMinimum(maxCoverRate),
|
||||
fee(increment),
|
||||
ter(tecNO_PERMISSION));
|
||||
// sfCoverRateMinimum: too big
|
||||
env(set(evan, vault.vaultID),
|
||||
coverRateMinimum(maxCoverRate + 1),
|
||||
fee(increment),
|
||||
ter(temINVALID));
|
||||
// sfCoverRateLiquidation: good value, bad account
|
||||
env(set(evan, vault.vaultID),
|
||||
coverRateLiquidation(maxCoverRate),
|
||||
fee(increment),
|
||||
ter(tecNO_PERMISSION));
|
||||
// sfCoverRateLiquidation: too big
|
||||
env(set(evan, vault.vaultID),
|
||||
coverRateLiquidation(maxCoverRate + 1),
|
||||
fee(increment),
|
||||
ter(temINVALID));
|
||||
// sfDebtMaximum: good value, bad account
|
||||
env(set(evan, vault.vaultID),
|
||||
debtMaximum(Number(0)),
|
||||
fee(increment),
|
||||
ter(tecNO_PERMISSION));
|
||||
// sfDebtMaximum: overflow
|
||||
env(set(evan, vault.vaultID),
|
||||
debtMaximum(Number(1, 100)),
|
||||
fee(increment),
|
||||
ter(temINVALID));
|
||||
// sfDebtMaximum: negative
|
||||
env(set(evan, vault.vaultID),
|
||||
debtMaximum(Number(-1)),
|
||||
fee(increment),
|
||||
ter(temINVALID));
|
||||
|
||||
std::string testData;
|
||||
lifecycle(
|
||||
"default fields",
|
||||
env,
|
||||
alice,
|
||||
evan,
|
||||
vault,
|
||||
// No modifications
|
||||
{},
|
||||
[&](SLE::const_ref broker) {
|
||||
// Extra checks
|
||||
BEAST_EXPECT(!broker->isFieldPresent(sfManagementFeeRate));
|
||||
BEAST_EXPECT(!broker->isFieldPresent(sfCoverRateMinimum));
|
||||
BEAST_EXPECT(
|
||||
!broker->isFieldPresent(sfCoverRateLiquidation));
|
||||
BEAST_EXPECT(!broker->isFieldPresent(sfData));
|
||||
BEAST_EXPECT(!broker->isFieldPresent(sfDebtMaximum));
|
||||
BEAST_EXPECT(broker->at(sfDebtMaximum) == 0);
|
||||
BEAST_EXPECT(broker->at(sfCoverRateMinimum) == 0);
|
||||
BEAST_EXPECT(broker->at(sfCoverRateLiquidation) == 0);
|
||||
},
|
||||
[&](SLE::const_ref broker) {
|
||||
// Modifications
|
||||
|
||||
// Update the fields
|
||||
auto const nextKeylet =
|
||||
keylet::loanbroker(alice.id(), env.seq(alice));
|
||||
|
||||
// fields that can't be changed
|
||||
// LoanBrokerID
|
||||
env(set(alice, vault.vaultID),
|
||||
loanBrokerID(nextKeylet.key),
|
||||
ter(tecNO_ENTRY));
|
||||
// VaultID
|
||||
env(set(alice, nextKeylet.key),
|
||||
loanBrokerID(broker->key()),
|
||||
ter(tecNO_PERMISSION));
|
||||
// Owner
|
||||
env(set(evan, vault.vaultID),
|
||||
loanBrokerID(broker->key()),
|
||||
ter(tecNO_PERMISSION));
|
||||
// ManagementFeeRate
|
||||
env(set(alice, vault.vaultID),
|
||||
loanBrokerID(broker->key()),
|
||||
managementFeeRate(maxManagementFeeRate),
|
||||
ter(temINVALID));
|
||||
// CoverRateMinimum
|
||||
env(set(alice, vault.vaultID),
|
||||
loanBrokerID(broker->key()),
|
||||
coverRateMinimum(maxManagementFeeRate),
|
||||
ter(temINVALID));
|
||||
// CoverRateLiquidation
|
||||
env(set(alice, vault.vaultID),
|
||||
loanBrokerID(broker->key()),
|
||||
coverRateLiquidation(maxManagementFeeRate),
|
||||
ter(temINVALID));
|
||||
|
||||
// fields that can be changed
|
||||
testData = "Test Data 1234";
|
||||
// Bad data: too long
|
||||
env(set(alice, vault.vaultID),
|
||||
loanBrokerID(broker->key()),
|
||||
data(std::string(maxDataPayloadLength + 1, 'W')),
|
||||
ter(temINVALID));
|
||||
|
||||
// Bad debt maximum
|
||||
env(set(alice, vault.vaultID),
|
||||
loanBrokerID(broker->key()),
|
||||
debtMaximum(Number(-175, -1)),
|
||||
ter(temINVALID));
|
||||
// Data & Debt maximum
|
||||
env(set(alice, vault.vaultID),
|
||||
loanBrokerID(broker->key()),
|
||||
data(testData),
|
||||
debtMaximum(Number(175, -1)));
|
||||
},
|
||||
[&](SLE::const_ref broker) {
|
||||
// Check the updated fields
|
||||
BEAST_EXPECT(checkVL(broker->at(sfData), testData));
|
||||
BEAST_EXPECT(broker->at(sfDebtMaximum) == Number(175, -1));
|
||||
});
|
||||
|
||||
lifecycle(
|
||||
"non-default fields",
|
||||
env,
|
||||
alice,
|
||||
evan,
|
||||
vault,
|
||||
[&](jtx::JTx const& jv) {
|
||||
testData = "spam spam spam spam";
|
||||
// Finally, create another Loan Broker with none of the
|
||||
// values at default
|
||||
return env.jt(
|
||||
jv,
|
||||
data(testData),
|
||||
managementFeeRate(TenthBips16(123)),
|
||||
debtMaximum(Number(9)),
|
||||
coverRateMinimum(TenthBips32(100)),
|
||||
coverRateLiquidation(TenthBips32(200)));
|
||||
},
|
||||
[&](SLE::const_ref broker) {
|
||||
// Extra checks
|
||||
BEAST_EXPECT(broker->at(sfManagementFeeRate) == 123);
|
||||
BEAST_EXPECT(broker->at(sfCoverRateMinimum) == 100);
|
||||
BEAST_EXPECT(broker->at(sfCoverRateLiquidation) == 200);
|
||||
BEAST_EXPECT(broker->at(sfDebtMaximum) == Number(9));
|
||||
BEAST_EXPECT(checkVL(broker->at(sfData), testData));
|
||||
},
|
||||
[&](SLE::const_ref broker) {
|
||||
// Reset Data & Debt maximum to default values
|
||||
env(set(alice, vault.vaultID),
|
||||
loanBrokerID(broker->key()),
|
||||
data(""),
|
||||
debtMaximum(Number(0)));
|
||||
},
|
||||
[&](SLE::const_ref broker) {
|
||||
// Check the updated fields
|
||||
BEAST_EXPECT(!broker->isFieldPresent(sfData));
|
||||
BEAST_EXPECT(!broker->isFieldPresent(sfDebtMaximum));
|
||||
});
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testDisabled();
|
||||
testLifecycle();
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(Loan, tx, ripple);
|
||||
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
@@ -54,8 +54,6 @@ struct JTx
|
||||
bool fill_sig = true;
|
||||
bool fill_netid = true;
|
||||
std::shared_ptr<STTx const> stx;
|
||||
// TODO: Remove
|
||||
std::function<void(Env&, JTx&)> signer;
|
||||
std::vector<std::function<void(Env&, JTx&)>> signers;
|
||||
|
||||
JTx() = default;
|
||||
|
||||
@@ -481,8 +481,11 @@ void
|
||||
Env::autofill_sig(JTx& jt)
|
||||
{
|
||||
auto& jv = jt.jv;
|
||||
if (jt.signer)
|
||||
return jt.signer(*this, jt);
|
||||
if (!jt.signers.empty())
|
||||
{
|
||||
for (auto const& signer : jt.signers)
|
||||
signer(*this, jt);
|
||||
}
|
||||
if (!jt.fill_sig)
|
||||
return;
|
||||
auto const account = lookup(jv[jss::Account].asString());
|
||||
@@ -549,8 +552,9 @@ Env::st(JTx const& jt)
|
||||
{
|
||||
return sterilize(STTx{std::move(*obj)});
|
||||
}
|
||||
catch (std::exception const&)
|
||||
catch (std::exception const& e)
|
||||
{
|
||||
test.log << e.what() << std::endl;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -65,7 +65,8 @@ signers(Account const& account, none_t)
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
msig::msig(std::vector<msig::Reg> signers_) : signers(std::move(signers_))
|
||||
msig::msig(SField const* subField_, std::vector<msig::Reg> signers_)
|
||||
: signers(std::move(signers_)), subField(subField_)
|
||||
{
|
||||
// Signatures must be applied in sorted order.
|
||||
std::sort(
|
||||
@@ -80,8 +81,14 @@ void
|
||||
msig::operator()(Env& env, JTx& jt) const
|
||||
{
|
||||
auto const mySigners = signers;
|
||||
jt.signer = [mySigners, &env](Env&, JTx& jtx) {
|
||||
jtx[sfSigningPubKey.getJsonName()] = "";
|
||||
jt.signers.emplace_back([subField = subField, mySigners, &env](
|
||||
Env&, JTx& jtx) {
|
||||
// Where to put the signature. Supports sfCounterPartySignature.
|
||||
auto& sigObject = subField ? jtx[*subField] : jtx.jv;
|
||||
if (jtx.fill_sig && !subField)
|
||||
jtx.fill_sig = false;
|
||||
|
||||
sigObject[sfSigningPubKey] = "";
|
||||
std::optional<STObject> st;
|
||||
try
|
||||
{
|
||||
@@ -92,7 +99,7 @@ msig::operator()(Env& env, JTx& jt) const
|
||||
env.test.log << pretty(jtx.jv) << std::endl;
|
||||
Rethrow();
|
||||
}
|
||||
auto& js = jtx[sfSigners.getJsonName()];
|
||||
auto& js = sigObject[sfSigners];
|
||||
for (std::size_t i = 0; i < mySigners.size(); ++i)
|
||||
{
|
||||
auto const& e = mySigners[i];
|
||||
@@ -106,7 +113,7 @@ msig::operator()(Env& env, JTx& jt) const
|
||||
jo[sfTxnSignature.getJsonName()] =
|
||||
strHex(Slice{sig.data(), sig.size()});
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace jtx
|
||||
|
||||
@@ -29,12 +29,18 @@ sig::operator()(Env&, JTx& jt) const
|
||||
{
|
||||
if (!manual_)
|
||||
return;
|
||||
jt.fill_sig = false;
|
||||
if (account_)
|
||||
{
|
||||
// VFALCO Inefficient pre-C++14
|
||||
auto const account = *account_;
|
||||
jt.signer = [account](Env&, JTx& jtx) { jtx::sign(jtx.jv, account); };
|
||||
jt.signers.emplace_back([subField = subField, account](Env&, JTx& jtx) {
|
||||
// Where to put the signature. Supports sfCounterPartySignature.
|
||||
auto& sigObject = subField ? jtx[*subField] : jtx.jv;
|
||||
if (jtx.fill_sig && !subField)
|
||||
jtx.fill_sig = false;
|
||||
|
||||
jtx::sign(jtx.jv, account, sigObject);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -44,14 +44,20 @@ parse(Json::Value const& jv)
|
||||
}
|
||||
|
||||
void
|
||||
sign(Json::Value& jv, Account const& account)
|
||||
sign(Json::Value& jv, Account const& account, Json::Value& sigObject)
|
||||
{
|
||||
jv[jss::SigningPubKey] = strHex(account.pk().slice());
|
||||
sigObject[jss::SigningPubKey] = strHex(account.pk().slice());
|
||||
Serializer ss;
|
||||
ss.add32(HashPrefix::txSign);
|
||||
parse(jv).addWithoutSigningFields(ss);
|
||||
auto const sig = ripple::sign(account.pk(), account.sk(), ss.slice());
|
||||
jv[jss::TxnSignature] = strHex(Slice{sig.data(), sig.size()});
|
||||
sigObject[jss::TxnSignature] = strHex(Slice{sig.data(), sig.size()});
|
||||
}
|
||||
|
||||
void
|
||||
sign(Json::Value& jv, Account const& account)
|
||||
{
|
||||
sign(jv, account, jv);
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
@@ -96,16 +96,53 @@ public:
|
||||
};
|
||||
|
||||
std::vector<Reg> signers;
|
||||
SField const* const subField = nullptr;
|
||||
static constexpr SField* const topLevel = nullptr;
|
||||
|
||||
public:
|
||||
msig(std::vector<Reg> signers_);
|
||||
msig(SField const* subField_, std::vector<Reg> signers_);
|
||||
|
||||
msig(SField const& subField_, std::vector<Reg> signers_)
|
||||
: msig{&subField_, signers_}
|
||||
{
|
||||
}
|
||||
|
||||
msig(std::vector<Reg> signers_) : msig(topLevel, signers_)
|
||||
{
|
||||
}
|
||||
|
||||
template <class AccountType, class... Accounts>
|
||||
requires std::convertible_to<AccountType, Reg>
|
||||
explicit msig(SField const* subField_, AccountType&& a0, Accounts&&... aN)
|
||||
: msig{
|
||||
subField_,
|
||||
std::vector<Reg>{
|
||||
std::forward<AccountType>(a0),
|
||||
std::forward<Accounts>(aN)...}}
|
||||
{
|
||||
}
|
||||
|
||||
template <class AccountType, class... Accounts>
|
||||
requires std::convertible_to<AccountType, Reg>
|
||||
explicit msig(SField const& subField_, AccountType&& a0, Accounts&&... aN)
|
||||
: msig{
|
||||
&subField_,
|
||||
std::vector<Reg>{
|
||||
std::forward<AccountType>(a0),
|
||||
std::forward<Accounts>(aN)...}}
|
||||
{
|
||||
}
|
||||
|
||||
template <class AccountType, class... Accounts>
|
||||
requires(
|
||||
std::convertible_to<AccountType, Reg> &&
|
||||
!std::is_same_v<AccountType, SField*>)
|
||||
explicit msig(AccountType&& a0, Accounts&&... aN)
|
||||
: msig{std::vector<Reg>{
|
||||
std::forward<AccountType>(a0),
|
||||
std::forward<Accounts>(aN)...}}
|
||||
: msig{
|
||||
topLevel,
|
||||
std::vector<Reg>{
|
||||
std::forward<AccountType>(a0),
|
||||
std::forward<Accounts>(aN)...}}
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -36,6 +36,9 @@ class sig
|
||||
private:
|
||||
bool manual_ = true;
|
||||
std::optional<Account> account_;
|
||||
/// subField only supported with explicit account
|
||||
SField const* const subField = nullptr;
|
||||
static constexpr SField* const topLevel = nullptr;
|
||||
|
||||
public:
|
||||
explicit sig(autofill_t) : manual_(false)
|
||||
@@ -46,7 +49,17 @@ public:
|
||||
{
|
||||
}
|
||||
|
||||
explicit sig(Account const& account) : account_(account)
|
||||
explicit sig(SField const* subField_, Account const& account)
|
||||
: subField(subField_), account_(account)
|
||||
{
|
||||
}
|
||||
|
||||
explicit sig(SField const& subField_, Account const& account)
|
||||
: sig(&subField_, account)
|
||||
{
|
||||
}
|
||||
|
||||
explicit sig(Account const& account) : sig(topLevel, account)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -51,6 +51,12 @@ struct parse_error : std::logic_error
|
||||
STObject
|
||||
parse(Json::Value const& jv);
|
||||
|
||||
/** Sign automatically into a specific Json field of the jv object.
|
||||
@note This only works on accounts with multi-signing off.
|
||||
*/
|
||||
void
|
||||
sign(Json::Value& jv, Account const& account, Json::Value& sigObject);
|
||||
|
||||
/** Sign automatically.
|
||||
@note This only works on accounts with multi-signing off.
|
||||
*/
|
||||
|
||||
@@ -276,20 +276,21 @@ LoanSet::doApply()
|
||||
|
||||
// Account for the origination fee using two payments
|
||||
//
|
||||
// 1. Transfer (principalRequested - originationFee) from vault
|
||||
// pseudo-account to LoanBroker pseudo-account.
|
||||
// 1. Transfer loanAssetsAvailable (principalRequested - originationFee)
|
||||
// from vault pseudo-account to LoanBroker pseudo-account.
|
||||
//
|
||||
// Create the holding if it doesn't already exist
|
||||
// Create the holding if it doesn't already exist (necessary for MPTs)
|
||||
if (auto const ter = addEmptyHolding(
|
||||
view,
|
||||
brokerPseudo,
|
||||
brokerPseudoSle->at(sfBalance).value().xrp(),
|
||||
vaultAsset,
|
||||
j_);
|
||||
ter != tesSUCCESS && ter != tecDUPLICATE)
|
||||
!isTesSuccess(ter) && ter != tecDUPLICATE)
|
||||
// ignore tecDUPLICATE. That means the holding already exists, and is
|
||||
// fine here
|
||||
return ter;
|
||||
// 1a. Transfer the loanAssetsAvailable to the pseudo-account
|
||||
if (auto const ter = accountSend(
|
||||
view,
|
||||
vaultPseudo,
|
||||
@@ -302,7 +303,7 @@ LoanSet::doApply()
|
||||
// LoanBroker owner.
|
||||
if (originationFee)
|
||||
{
|
||||
// Create the holding if it doesn't already exist
|
||||
// Create the holding if it doesn't already exist (necessary for MPTs)
|
||||
if (auto const ter = addEmptyHolding(
|
||||
view,
|
||||
brokerOwner,
|
||||
@@ -324,8 +325,11 @@ LoanSet::doApply()
|
||||
}
|
||||
|
||||
auto const managementFeeRate = brokerSle->at(sfManagementFeeRate);
|
||||
// The total amount if interest the loan is expected to generate
|
||||
auto const loanInterest =
|
||||
tenthBipsOfValue(principalRequested, interestRate);
|
||||
// The portion of the loan interest that will go to the vault (total
|
||||
// interest minus the management fee)
|
||||
auto const loanInterestToVault = loanInterest -
|
||||
tenthBipsOfValue(loanInterest, managementFeeRate.value());
|
||||
auto const loanSequence = tx.getSeqValue();
|
||||
@@ -370,6 +374,7 @@ LoanSet::doApply()
|
||||
tx[~sfPaymentTotal].value_or(defaultPaymentTotal);
|
||||
loan->at(sfAssetsAvailable) = loanAssetsAvailable;
|
||||
loan->at(sfPrincipalOutstanding) = principalRequested;
|
||||
view.insert(loan);
|
||||
|
||||
// Update the balances in the vault
|
||||
vaultSle->at(sfAssetsAvailable) -= principalRequested;
|
||||
@@ -378,16 +383,18 @@ LoanSet::doApply()
|
||||
|
||||
// Update the balances in the loan broker
|
||||
brokerSle->at(sfDebtTotal) += principalRequested + loanInterestToVault;
|
||||
// The broker's owner count is solely for the number of outstanding loans,
|
||||
// and is distinct from the broker's pseudo-account's owner count
|
||||
adjustOwnerCount(view, brokerSle, 1, j_);
|
||||
view.update(brokerSle);
|
||||
|
||||
// Put the loan into the pseudo-account's directory
|
||||
if (auto const ter = dirLink(view, brokerPseudo, loan, sfLoanBrokerNode))
|
||||
return ter;
|
||||
// Borrower is effectively the owner of the loan
|
||||
// Borrower is the owner of the loan
|
||||
if (auto const ter = dirLink(view, borrower, loan, sfOwnerNode))
|
||||
return ter;
|
||||
|
||||
view.update(brokerSle);
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user