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:
Ed Hennis
2025-04-24 21:12:57 -04:00
parent d6353e593f
commit 7823c8b186
11 changed files with 744 additions and 29 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -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);
});
}
}

View File

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

View File

@@ -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)...}}
{
}

View File

@@ -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)
{
}

View File

@@ -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.
*/

View File

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