diff --git a/src/masternodes/masternodes.cpp b/src/masternodes/masternodes.cpp index a7c34fded7..a9be5ab7bc 100644 --- a/src/masternodes/masternodes.cpp +++ b/src/masternodes/masternodes.cpp @@ -926,6 +926,20 @@ CAmount CCollateralLoans::precisionRatio() const return ratio > maxRatio / precision ? -COIN : CAmount(ratio * precision); } +ResVal CCustomCSView::GetAmountInCurrency(CAmount amount, CTokenCurrencyPair priceFeedId, bool useNextPrice, bool requireLivePrice) +{ + auto priceResult = GetValidatedIntervalPrice(priceFeedId, useNextPrice, requireLivePrice); + if (!priceResult) + return std::move(priceResult); + + auto price = priceResult.val.get(); + auto amountInCurrency = MultiplyAmounts(price, amount); + if (price > COIN && amountInCurrency < amount) + return Res::Err("Value/price too high (%s/%s)", GetDecimaleString(amount), GetDecimaleString(price)); + + return ResVal(amountInCurrency, Res::Ok()); +} + ResVal CCustomCSView::GetLoanCollaterals(CVaultId const& vaultId, CBalances const& collaterals, uint32_t height, int64_t blockTime, bool useNextPrice, bool requireLivePrice) { @@ -992,22 +1006,15 @@ Res CCustomCSView::PopulateLoansData(CCollateralLoans& result, CVaultId const& v if (rate->height > height) return Res::Err("Trying to read loans in the past"); - auto priceResult = GetValidatedIntervalPrice(token->fixedIntervalPriceId, useNextPrice, requireLivePrice); - if (!priceResult) - return std::move(priceResult); - - auto price = priceResult.val.get(); - LogPrint(BCLog::LOAN,"\t\t%s()->for_loans->%s->", __func__, token->symbol); /* Continued */ - auto value = loanTokenAmount + TotalInterest(*rate, height); - auto amountInCurrency = MultiplyAmounts(price, value); - - if (price > COIN && amountInCurrency < value) - return Res::Err("Value/price too high (%s/%s)", GetDecimaleString(value), GetDecimaleString(price)); + auto totalAmount = loanTokenAmount + TotalInterest(*rate, height); + auto amountInCurrency = GetAmountInCurrency(totalAmount, token->fixedIntervalPriceId, useNextPrice, requireLivePrice); + if (!amountInCurrency) + return std::move(amountInCurrency); auto prevLoans = result.totalLoans; - result.totalLoans += amountInCurrency; + result.totalLoans += *amountInCurrency.val; if (prevLoans > result.totalLoans) return Res::Err("Exceeded maximum loans"); @@ -1028,20 +1035,15 @@ Res CCustomCSView::PopulateCollateralData(CCollateralLoans& result, CVaultId con if (!token) return Res::Err("Collateral token with id (%s) does not exist!", tokenId.ToString()); - auto priceResult = GetValidatedIntervalPrice(token->fixedIntervalPriceId, useNextPrice, requireLivePrice); - if (!priceResult) - return std::move(priceResult); - - auto price = priceResult.val.get(); + auto amountInCurrency = GetAmountInCurrency(tokenAmount, token->fixedIntervalPriceId, useNextPrice, requireLivePrice); + if (!amountInCurrency) + return std::move(amountInCurrency); - auto amountInCurrency = MultiplyAmounts(price, tokenAmount); - if (price > COIN && amountInCurrency < tokenAmount) - return Res::Err("Value/price too high (%s/%s)", GetDecimaleString(tokenAmount), GetDecimaleString(price)); - - amountInCurrency = MultiplyAmounts(token->factor, amountInCurrency); + auto amountFactor = MultiplyAmounts(token->factor, *amountInCurrency.val); auto prevCollaterals = result.totalCollaterals; - result.totalCollaterals += amountInCurrency; + result.totalCollaterals += amountFactor; + if (prevCollaterals > result.totalCollaterals) return Res::Err("Exceeded maximum collateral"); diff --git a/src/masternodes/masternodes.h b/src/masternodes/masternodes.h index b946d7d62e..35ca2b27ee 100644 --- a/src/masternodes/masternodes.h +++ b/src/masternodes/masternodes.h @@ -429,6 +429,8 @@ class CCustomCSView bool CalculateOwnerRewards(CScript const & owner, uint32_t height); + ResVal GetAmountInCurrency(CAmount amount, CTokenCurrencyPair priceFeedId, bool useNextPrice = false, bool requireLivePrice = true); + ResVal GetLoanCollaterals(CVaultId const & vaultId, CBalances const & collaterals, uint32_t height, int64_t blockTime, bool useNextPrice = false, bool requireLivePrice = true); void SetDbVersion(int version); diff --git a/src/masternodes/rpc_vault.cpp b/src/masternodes/rpc_vault.cpp index ba3772c5e7..b934258f73 100644 --- a/src/masternodes/rpc_vault.cpp +++ b/src/masternodes/rpc_vault.cpp @@ -1491,6 +1491,73 @@ UniValue listvaulthistory(const JSONRPCRequest& request) { return slice; } +UniValue estimatevault(const JSONRPCRequest& request) { + auto pwallet = GetWallet(request); + + RPCHelpMan{"estimatevault", + "Returns estimated vault for given collateral and loan amounts.\n", + { + {"collateralAmounts", RPCArg::Type::STR, RPCArg::Optional::NO, + "Collateral amounts as json string, or array. Example: '[ \"amount@token\" ]'" + }, + {"loanAmounts", RPCArg::Type::STR, RPCArg::Optional::NO, + "Loan amounts as json string, or array. Example: '[ \"amount@token\" ]'" + }, + }, + RPCResult{ + "{\n" + " \"collateralValue\" : n.nnnnnnnn, (amount) The total collateral value in USD\n" + " \"loanValue\" : n.nnnnnnnn, (amount) The total loan value in USD\n" + " \"informativeRatio\" : n.nnnnnnnn, (amount) Informative ratio with 8 digit precision\n" + " \"collateralRatio\" : n, (uint) Ratio as unsigned int\n" + "}\n" + }, + RPCExamples{ + HelpExampleCli("estimatevault", R"('["1000.00000000@DFI"]' '["0.65999990@GOOGL"]')") + + HelpExampleRpc("estimatevault", R"(["1000.00000000@DFI"], ["0.65999990@GOOGL"])") + }, + }.Check(request); + + CBalances collateralBalances = DecodeAmounts(pwallet->chain(), request.params[0], ""); + CBalances loanBalances = DecodeAmounts(pwallet->chain(), request.params[1], ""); + + LOCK(cs_main); + auto height = (uint32_t) ::ChainActive().Height(); + + CCollateralLoans result{}; + + for (const auto& collateral : collateralBalances.balances) { + auto collateralToken = pcustomcsview->HasLoanCollateralToken({collateral.first, height}); + if (!collateralToken || !collateralToken->factor) { + throw JSONRPCError(RPC_DATABASE_ERROR, strprintf("Token with id (%s) is not a valid collateral!", collateral.first.ToString())); + } + + auto amountInCurrency = pcustomcsview->GetAmountInCurrency(collateral.second, collateralToken->fixedIntervalPriceId); + if (!amountInCurrency) { + throw JSONRPCError(RPC_DATABASE_ERROR, amountInCurrency.msg); + } + result.totalCollaterals += MultiplyAmounts(collateralToken->factor, *amountInCurrency.val);; + } + + for (const auto& loan : loanBalances.balances) { + auto loanToken = pcustomcsview->GetLoanTokenByID(loan.first); + if (!loanToken) throw JSONRPCError(RPC_INVALID_PARAMETER, "Token with id (" + loan.first.ToString() + ") is not a loan token!"); + + auto amountInCurrency = pcustomcsview->GetAmountInCurrency(loan.second, loanToken->fixedIntervalPriceId); + if (!amountInCurrency) { + throw JSONRPCError(RPC_DATABASE_ERROR, amountInCurrency.msg); + } + result.totalLoans += *amountInCurrency.val; + } + + UniValue ret(UniValue::VOBJ); + ret.pushKV("collateralValue", ValueFromUint(result.totalCollaterals)); + ret.pushKV("loanValue", ValueFromUint(result.totalLoans)); + ret.pushKV("informativeRatio", ValueFromAmount(result.precisionRatio())); + ret.pushKV("collateralRatio", int(result.ratio())); + return ret; +} + static const CRPCCommand commands[] = { // category name actor (function) params @@ -1506,6 +1573,7 @@ static const CRPCCommand commands[] = {"vault", "placeauctionbid", &placeauctionbid, {"id", "index", "from", "amount", "inputs"}}, {"vault", "listauctions", &listauctions, {"pagination"}}, {"vault", "listauctionhistory", &listauctionhistory, {"owner", "pagination"}}, + {"vault", "estimatevault", &estimatevault, {"collateralAmounts", "loanAmounts"}}, }; void RegisterVaultRPCCommands(CRPCTable& tableRPC) { diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp index 22b9c98fb6..e1f477abc0 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -261,6 +261,8 @@ static const CRPCConvertParam vRPCConvertParams[] = { "listvaults", 1, "pagination" }, { "listauctions", 0, "pagination" }, { "listauctionhistory", 1, "pagination" }, + { "estimatevault", 0, "collateralAmounts" }, + { "estimatevault", 1, "loanAmounts" }, { "spv_sendrawtx", 0, "rawtx" }, { "spv_createanchor", 0, "inputs" }, diff --git a/test/functional/feature_loan_vault.py b/test/functional/feature_loan_vault.py index 4a6032741f..e02821b503 100755 --- a/test/functional/feature_loan_vault.py +++ b/test/functional/feature_loan_vault.py @@ -3,7 +3,7 @@ # Copyright (c) DeFi Blockchain Developers # Distributed under the MIT software license, see the accompanying # file LICENSE or http://www.opensource.org/licenses/mit-license.php. -"""Test Loan Scheme.""" +"""Test vault.""" from decimal import Decimal from test_framework.test_framework import DefiTestFramework @@ -12,6 +12,7 @@ from test_framework.util import assert_equal, assert_raises_rpc_error import calendar import time + class VaultTest (DefiTestFramework): def set_test_params(self): self.num_nodes = 2 @@ -247,7 +248,7 @@ def run_test(self): self.nodes[0].setcollateraltoken({ 'token': idBTC, - 'factor': 1, + 'factor': 0.8, 'fixedIntervalPriceId': "BTC/USD"}) self.nodes[0].generate(7) @@ -336,7 +337,7 @@ def run_test(self): vault1 = self.nodes[0].getvault(vaultId1) assert_equal(vault1['loanAmounts'], ['0.50000047@TSLA']) - assert_equal(vault1['collateralValue'], Decimal('2.00000000')) + assert_equal(vault1['collateralValue'], Decimal('1.800000000')) assert_equal(vault1['loanValue'],Decimal('0.50000047')) assert_equal(vault1['interestValue'],Decimal('0.00000047')) assert_equal(vault1['interestAmounts'],['0.00000047@TSLA']) @@ -418,6 +419,41 @@ def run_test(self): # collaterals 2.5 + 0.5 fee assert_equal(self.nodes[0].getaccount(ownerAddress2)[0], '3.00000000@DFI') + # Invalid loan token + try: + estimatevault = self.nodes[0].estimatevault('3.00000000@DFI', '3.00000000@TSLAA') + except JSONRPCException as e: + errorString = e.error['message'] + print("errorString", errorString) + assert("Invalid Defi token: TSLAA" in errorString) + # Invalid collateral token + try: + estimatevault = self.nodes[0].estimatevault('3.00000000@DFII', '3.00000000@TSLA') + except JSONRPCException as e: + errorString = e.error['message'] + print("errorString", errorString) + assert("Invalid Defi token: DFII" in errorString) + # Token not set as a collateral + try: + estimatevault = self.nodes[0].estimatevault('3.00000000@TSLA', '3.00000000@TSLA') + except JSONRPCException as e: + errorString = e.error['message'] + print("errorString", errorString) + assert("Token with id (2) is not a valid collateral!" in errorString) + # Token not set as loan token + try: + estimatevault = self.nodes[0].estimatevault('3.00000000@DFI', '3.00000000@DFI') + except JSONRPCException as e: + errorString = e.error['message'] + print("errorString", errorString) + assert("Token with id (0) is not a loan token!" in errorString) + + vault = self.nodes[0].getvault(vaultId2) + estimatevault = self.nodes[0].estimatevault(vault["collateralAmounts"], vault["loanAmounts"]) + assert_equal(estimatevault["collateralValue"], vault["collateralValue"]) + assert_equal(estimatevault["loanValue"], vault["loanValue"]) + assert_equal(estimatevault["informativeRatio"], vault["informativeRatio"]) + assert_equal(estimatevault["collateralRatio"], vault["collateralRatio"]) if __name__ == '__main__': VaultTest().main()