Allow issuers to send LoanPay

- Implement a new helper accountCanSend, which is like accountHolds, but
  returns a meaningful value for issuers, and will include the available
  credit on the other side of a trust line. (The sfHighLimit or
  sfLowLimit as appropriate.)
- Use this new helper when checking the available balance in LoanPay.
This commit is contained in:
Ed Hennis
2025-10-23 01:46:43 -04:00
parent 6ad7d1c076
commit f78c5f65bc
3 changed files with 238 additions and 76 deletions

View File

@@ -361,6 +361,49 @@ accountHolds(
AuthHandling zeroIfUnauthorized,
beast::Journal j);
// Returns the amount an account can spend total.
//
// These functions use accountHolds, but unlike accountHolds:
// * The account can go into debt.
// * If the account is the asset issuer the only limit is defined by the asset /
// issuance.
//
// <-- saAmount: amount of currency held by account. May be negative.
[[nodiscard]] STAmount
accountCanSend(
ReadView const& view,
AccountID const& account,
Currency const& currency,
AccountID const& issuer,
FreezeHandling zeroIfFrozen,
beast::Journal j);
[[nodiscard]] STAmount
accountCanSend(
ReadView const& view,
AccountID const& account,
Issue const& issue,
FreezeHandling zeroIfFrozen,
beast::Journal j);
[[nodiscard]] STAmount
accountCanSend(
ReadView const& view,
AccountID const& account,
MPTIssue const& mptIssue,
FreezeHandling zeroIfFrozen,
AuthHandling zeroIfUnauthorized,
beast::Journal j);
[[nodiscard]] STAmount
accountCanSend(
ReadView const& view,
AccountID const& account,
Asset const& asset,
FreezeHandling zeroIfFrozen,
AuthHandling zeroIfUnauthorized,
beast::Journal j);
// Returns the amount an account can spend of the currency type saDefault, or
// returns saDefault if this account is the issuer of the currency in
// question. Should be used in favor of accountHolds when questioning how much

View File

@@ -383,6 +383,99 @@ isLPTokenFrozen(
isFrozen(view, account, asset2.currency, asset2.account);
}
static SLE::const_pointer
getLineIfUsable(
ReadView const& view,
AccountID const& account,
Currency const& currency,
AccountID const& issuer,
FreezeHandling zeroIfFrozen,
beast::Journal j)
{
auto const sle = view.read(keylet::line(account, issuer, currency));
if (!sle)
{
return nullptr;
}
if (zeroIfFrozen == fhZERO_IF_FROZEN)
{
if (isFrozen(view, account, currency, issuer) ||
isDeepFrozen(view, account, currency, issuer))
{
return nullptr;
}
// when fixFrozenLPTokenTransfer is enabled, if currency is lptoken,
// we need to check if the associated assets have been frozen
if (view.rules().enabled(fixFrozenLPTokenTransfer))
{
auto const sleIssuer = view.read(keylet::account(issuer));
if (!sleIssuer)
{
return nullptr; // LCOV_EXCL_LINE
}
else if (sleIssuer->isFieldPresent(sfAMMID))
{
auto const sleAmm =
view.read(keylet::amm((*sleIssuer)[sfAMMID]));
if (!sleAmm ||
isLPTokenFrozen(
view,
account,
(*sleAmm)[sfAsset].get<Issue>(),
(*sleAmm)[sfAsset2].get<Issue>()))
{
return nullptr;
}
}
}
}
return sle;
}
static STAmount
getTrustLineBalance(
ReadView const& view,
SLE::const_ref sle,
AccountID const& account,
Currency const& currency,
AccountID const& issuer,
bool includeOppositeLimit,
beast::Journal j)
{
STAmount amount;
if (sle)
{
amount = sle->getFieldAmount(sfBalance);
bool const accountHigh = account > issuer;
auto const& oppositeField = accountHigh ? sfLowLimit : sfHighLimit;
if (accountHigh)
{
// Put balance in account terms.
amount.negate();
}
if (includeOppositeLimit)
{
amount += sle->getFieldAmount(oppositeField);
}
amount.setIssuer(issuer);
}
else
{
amount.clear(Issue{currency, issuer});
}
JLOG(j.trace()) << "getTrustLineBalance:"
<< " account=" << to_string(account)
<< " amount=" << amount.getFullText();
return view.balanceHook(account, issuer, amount);
}
STAmount
accountHolds(
ReadView const& view,
@@ -399,71 +492,10 @@ accountHolds(
}
// IOU: Return balance on trust line modulo freeze
auto const sle = view.read(keylet::line(account, issuer, currency));
auto const allowBalance = [&]() {
if (!sle)
{
return false;
}
SLE::const_pointer const sle =
getLineIfUsable(view, account, currency, issuer, zeroIfFrozen, j);
if (zeroIfFrozen == fhZERO_IF_FROZEN)
{
if (isFrozen(view, account, currency, issuer) ||
isDeepFrozen(view, account, currency, issuer))
{
return false;
}
// when fixFrozenLPTokenTransfer is enabled, if currency is lptoken,
// we need to check if the associated assets have been frozen
if (view.rules().enabled(fixFrozenLPTokenTransfer))
{
auto const sleIssuer = view.read(keylet::account(issuer));
if (!sleIssuer)
{
return false; // LCOV_EXCL_LINE
}
else if (sleIssuer->isFieldPresent(sfAMMID))
{
auto const sleAmm =
view.read(keylet::amm((*sleIssuer)[sfAMMID]));
if (!sleAmm ||
isLPTokenFrozen(
view,
account,
(*sleAmm)[sfAsset].get<Issue>(),
(*sleAmm)[sfAsset2].get<Issue>()))
{
return false;
}
}
}
}
return true;
}();
if (allowBalance)
{
amount = sle->getFieldAmount(sfBalance);
if (account > issuer)
{
// Put balance in account terms.
amount.negate();
}
amount.setIssuer(issuer);
}
else
{
amount.clear(Issue{currency, issuer});
}
JLOG(j.trace()) << "accountHolds:"
<< " account=" << to_string(account)
<< " amount=" << amount.getFullText();
return view.balanceHook(account, issuer, amount);
return getTrustLineBalance(view, sle, account, currency, issuer, false, j);
}
STAmount
@@ -550,6 +582,96 @@ accountHolds(
asset.value());
}
STAmount
accountCanSend(
ReadView const& view,
AccountID const& account,
Currency const& currency,
AccountID const& issuer,
FreezeHandling zeroIfFrozen,
beast::Journal j)
{
if (isXRP(currency))
return accountHolds(view, account, currency, issuer, zeroIfFrozen, j);
if (account == issuer)
// If the account is the issuer, then their limit is effectively
// infinite
return STAmount{
Issue{currency, issuer}, STAmount::cMaxValue, STAmount::cMaxOffset};
// IOU: Return balance on trust line modulo freeze
SLE::const_pointer const sle =
getLineIfUsable(view, account, currency, issuer, zeroIfFrozen, j);
return getTrustLineBalance(view, sle, account, currency, issuer, true, j);
}
STAmount
accountCanSend(
ReadView const& view,
AccountID const& account,
Issue const& issue,
FreezeHandling zeroIfFrozen,
beast::Journal j)
{
return accountCanSend(
view, account, issue.currency, issue.account, zeroIfFrozen, j);
}
STAmount
accountCanSend(
ReadView const& view,
AccountID const& account,
MPTIssue const& mptIssue,
FreezeHandling zeroIfFrozen,
AuthHandling zeroIfUnauthorized,
beast::Journal j)
{
if (account == mptIssue.getIssuer())
{
// if the account is the issuer, and the issuance exists, their limit is
// the issuance limit minus the outstanding value
auto const issuance =
view.read(keylet::mptIssuance(mptIssue.getMptID()));
if (!issuance)
{
return STAmount{mptIssue};
}
return STAmount{
mptIssue,
issuance->at(~sfMaximumAmount).value_or(maxMPTokenAmount) -
issuance->at(sfOutstandingAmount)};
}
return accountHolds(
view, account, mptIssue, zeroIfFrozen, zeroIfUnauthorized, j);
}
[[nodiscard]] STAmount
accountCanSend(
ReadView const& view,
AccountID const& account,
Asset const& asset,
FreezeHandling zeroIfFrozen,
AuthHandling zeroIfUnauthorized,
beast::Journal j)
{
return std::visit(
[&](auto const& value) {
if constexpr (std::is_same_v<
std::remove_cvref_t<decltype(value)>,
Issue>)
{
return accountCanSend(view, account, value, zeroIfFrozen, j);
}
return accountCanSend(
view, account, value, zeroIfFrozen, zeroIfUnauthorized, j);
},
asset.value());
}
STAmount
accountFunds(
ReadView const& view,

View File

@@ -201,10 +201,7 @@ LoanPay::preclaim(PreclaimContext const& ctx)
// Do not support "partial payments" - if the transaction says to pay X,
// then the account must have X available, even if the loan payment takes
// less.
// Also assume that anybody taking loans is not using "community credit",
// which would let an IOU balance go negative up to the other side's limit.
// This may change in a later version.
if (auto const balance = accountHolds(
if (auto const balance = accountCanSend(
ctx.view,
account,
asset,
@@ -427,11 +424,11 @@ LoanPay::doApply()
}
#if !NDEBUG
auto const accountBalanceBefore =
accountHolds(view, account_, asset, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_);
auto const accountBalanceBefore = accountCanSend(
view, account_, asset, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_);
auto const vaultBalanceBefore = account_ == vaultPseudoAccount
? STAmount{asset, 0}
: accountHolds(
: accountCanSend(
view,
vaultPseudoAccount,
asset,
@@ -440,7 +437,7 @@ LoanPay::doApply()
j_);
auto const brokerBalanceBefore = account_ == brokerPayee
? STAmount{asset, 0}
: accountHolds(
: accountCanSend(
view, brokerPayee, asset, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_);
#endif
@@ -483,11 +480,11 @@ LoanPay::doApply()
return ter;
#if !NDEBUG
auto const accountBalanceAfter =
accountHolds(view, account_, asset, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_);
auto const accountBalanceAfter = accountCanSend(
view, account_, asset, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_);
auto const vaultBalanceAfter = account_ == vaultPseudoAccount
? STAmount{asset, 0}
: accountHolds(
: accountCanSend(
view,
vaultPseudoAccount,
asset,
@@ -496,7 +493,7 @@ LoanPay::doApply()
j_);
auto const brokerBalanceAfter = account_ == brokerPayee
? STAmount{asset, 0}
: accountHolds(
: accountCanSend(
view, brokerPayee, asset, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_);
XRPL_ASSERT_PARTS(