diff --git a/include/xrpl/protocol/detail/sfields.macro b/include/xrpl/protocol/detail/sfields.macro index 41269e6d44..64386aa82c 100644 --- a/include/xrpl/protocol/detail/sfields.macro +++ b/include/xrpl/protocol/detail/sfields.macro @@ -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 diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp new file mode 100644 index 0000000000..86bda14391 --- /dev/null +++ b/src/test/app/Loan_test.cpp @@ -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 +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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 modifyJTx, + std::function checkBroker, + std::function changeBroker, + std::function 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() ? "IOU " + : asset.holds() ? "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 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 diff --git a/src/test/jtx/JTx.h b/src/test/jtx/JTx.h index 1c3ea0a47b..a8cce10bd7 100644 --- a/src/test/jtx/JTx.h +++ b/src/test/jtx/JTx.h @@ -54,8 +54,6 @@ struct JTx bool fill_sig = true; bool fill_netid = true; std::shared_ptr stx; - // TODO: Remove - std::function signer; std::vector> signers; JTx() = default; diff --git a/src/test/jtx/impl/Env.cpp b/src/test/jtx/impl/Env.cpp index 2b9394cfd9..1674d51bb0 100644 --- a/src/test/jtx/impl/Env.cpp +++ b/src/test/jtx/impl/Env.cpp @@ -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; } diff --git a/src/test/jtx/impl/multisign.cpp b/src/test/jtx/impl/multisign.cpp index a802528247..90989f81c6 100644 --- a/src/test/jtx/impl/multisign.cpp +++ b/src/test/jtx/impl/multisign.cpp @@ -65,7 +65,8 @@ signers(Account const& account, none_t) //------------------------------------------------------------------------------ -msig::msig(std::vector signers_) : signers(std::move(signers_)) +msig::msig(SField const* subField_, std::vector 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 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 diff --git a/src/test/jtx/impl/sig.cpp b/src/test/jtx/impl/sig.cpp index fa1977fe08..6e8e1d75b7 100644 --- a/src/test/jtx/impl/sig.cpp +++ b/src/test/jtx/impl/sig.cpp @@ -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); + }); } } diff --git a/src/test/jtx/impl/utility.cpp b/src/test/jtx/impl/utility.cpp index afa7ee8f35..c2bbceb9f2 100644 --- a/src/test/jtx/impl/utility.cpp +++ b/src/test/jtx/impl/utility.cpp @@ -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 diff --git a/src/test/jtx/multisign.h b/src/test/jtx/multisign.h index 6bcb1a671c..a364d07ca6 100644 --- a/src/test/jtx/multisign.h +++ b/src/test/jtx/multisign.h @@ -96,16 +96,53 @@ public: }; std::vector signers; + SField const* const subField = nullptr; + static constexpr SField* const topLevel = nullptr; public: - msig(std::vector signers_); + msig(SField const* subField_, std::vector signers_); + + msig(SField const& subField_, std::vector signers_) + : msig{&subField_, signers_} + { + } + + msig(std::vector signers_) : msig(topLevel, signers_) + { + } template requires std::convertible_to + explicit msig(SField const* subField_, AccountType&& a0, Accounts&&... aN) + : msig{ + subField_, + std::vector{ + std::forward(a0), + std::forward(aN)...}} + { + } + + template + requires std::convertible_to + explicit msig(SField const& subField_, AccountType&& a0, Accounts&&... aN) + : msig{ + &subField_, + std::vector{ + std::forward(a0), + std::forward(aN)...}} + { + } + + template + requires( + std::convertible_to && + !std::is_same_v) explicit msig(AccountType&& a0, Accounts&&... aN) - : msig{std::vector{ - std::forward(a0), - std::forward(aN)...}} + : msig{ + topLevel, + std::vector{ + std::forward(a0), + std::forward(aN)...}} { } diff --git a/src/test/jtx/sig.h b/src/test/jtx/sig.h index aa65a4f697..6a73038c42 100644 --- a/src/test/jtx/sig.h +++ b/src/test/jtx/sig.h @@ -36,6 +36,9 @@ class sig private: bool manual_ = true; std::optional 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) { } diff --git a/src/test/jtx/utility.h b/src/test/jtx/utility.h index 2c21fbd3ff..9e89c3bb93 100644 --- a/src/test/jtx/utility.h +++ b/src/test/jtx/utility.h @@ -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. */ diff --git a/src/xrpld/app/tx/detail/LoanSet.cpp b/src/xrpld/app/tx/detail/LoanSet.cpp index 34285b966c..e678a0e4e6 100644 --- a/src/xrpld/app/tx/detail/LoanSet.cpp +++ b/src/xrpld/app/tx/detail/LoanSet.cpp @@ -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; }