Skip to content

Commit

Permalink
Merge pull request #924 from DeFiCh/feature/estimate_loan
Browse files Browse the repository at this point in the history
Adds estimateloan RPC
  • Loading branch information
Jouzo authored Dec 2, 2021
2 parents 1d1d272 + 8fc1173 commit ebae304
Show file tree
Hide file tree
Showing 6 changed files with 418 additions and 101 deletions.
288 changes: 189 additions & 99 deletions src/masternodes/rpc_vault.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1153,105 +1153,6 @@ UniValue listauctionhistory(const JSONRPCRequest& request) {
return ret;
}

UniValue estimatecollateral(const JSONRPCRequest& request) {
auto pwallet = GetWallet(request);

RPCHelpMan{"estimatecollateral",
"Returns amount of collateral tokens needed to take an amount of loan tokens for a target collateral ratio.\n",
{
{"loanAmounts", RPCArg::Type::STR, RPCArg::Optional::NO,
"Amount as json string, or array. Example: '[ \"amount@token\" ]'"
},
{"targetRatio", RPCArg::Type::NUM, RPCArg::Optional::NO, "Target collateral ratio."},
{"tokens", RPCArg::Type::OBJ, RPCArg::Optional::OMITTED, "Object with collateral token as key and their percent split as value. (defaults to { DFI: 1 }",
{
{"split", RPCArg::Type::NUM, RPCArg::Optional::NO, "The percent split"},
},
},
},
RPCResult{
"\"json\" (Array) Array of <amount@token> strings\n"
},
RPCExamples{
HelpExampleCli("estimatecollateral", R"(23.55311144@MSFT 150 '{"DFI": 0.8, "BTC":0.2}')") +
HelpExampleRpc("estimatecollateral", R"("23.55311144@MSFT" 150 {"DFI": 0.8, "BTC":0.2})")
},
}.Check(request);

RPCTypeCheck(request.params, {UniValueType(), UniValue::VNUM, UniValue::VOBJ}, false);

const CBalances loanAmounts = DecodeAmounts(pwallet->chain(), request.params[0], "");
auto ratio = request.params[1].get_int();

std::map<std::string, UniValue> collateralSplits;
if (request.params.size() > 2) {
request.params[2].getObjMap(collateralSplits);
} else {
collateralSplits["DFI"] = 1;
}

LOCK(cs_main);

CAmount totalLoanValue{0};
for (const auto& balance : loanAmounts.balances) {
auto loanToken = pcustomcsview->GetLoanTokenByID(balance.first);
if (!loanToken) {
throw JSONRPCError(RPC_DATABASE_ERROR, strprintf("(%d) is not a loan token!", balance.first.v));
}

auto priceFeed = pcustomcsview->GetFixedIntervalPrice(loanToken->fixedIntervalPriceId);
if (!priceFeed.ok) {
throw JSONRPCError(RPC_DATABASE_ERROR, priceFeed.msg);
}

auto price = priceFeed.val->priceRecord[0];
if (!priceFeed.val->isLive(pcustomcsview->GetPriceDeviation())) {
throw JSONRPCError(RPC_MISC_ERROR, strprintf("No live fixed price for %s", loanToken->symbol));
}
totalLoanValue += MultiplyAmounts(balance.second, price);
}

uint32_t height = ::ChainActive().Height();
CBalances collateralBalances;
CAmount totalSplit{0};
for (const auto& collateralSplit : collateralSplits) {
CAmount split = AmountFromValue(collateralSplit.second);

auto token = pcustomcsview->GetToken(collateralSplit.first);
if (!token) {
throw JSONRPCError(RPC_DATABASE_ERROR, strprintf("Token %s does not exist!", collateralSplit.first));
}

auto collateralToken = pcustomcsview->HasLoanCollateralToken({token->first, height});
if (!collateralToken || !collateralToken->factor) {
throw JSONRPCError(RPC_DATABASE_ERROR, strprintf("(%s) is not a valid collateral!", collateralSplit.first));
}

auto priceFeed = pcustomcsview->GetFixedIntervalPrice(collateralToken->fixedIntervalPriceId);
if (!priceFeed.ok) {
throw JSONRPCError(RPC_DATABASE_ERROR, priceFeed.msg);
}

auto price = priceFeed.val->priceRecord[0];
if (!priceFeed.val->isLive(pcustomcsview->GetPriceDeviation())) {
throw JSONRPCError(RPC_MISC_ERROR, strprintf("No live fixed price for %s", collateralSplit.first));
}

auto requiredValue = MultiplyAmounts(totalLoanValue, split);
auto collateralValue = DivideAmounts(requiredValue, price);
auto amountRatio = DivideAmounts(MultiplyAmounts(collateralValue, ratio), 100);
auto totalAmount = DivideAmounts(amountRatio, collateralToken->factor);

collateralBalances.Add({token->first, totalAmount});
totalSplit += split;
}
if (totalSplit != COIN) {
throw JSONRPCError(RPC_MISC_ERROR, strprintf("total split between collateral tokens = %s vs expected %s", GetDecimaleString(totalSplit), GetDecimaleString(COIN)));
}

return AmountsToJSON(collateralBalances.balances);
}

UniValue vaultToJSON(const uint256& vaultID, const std::string& address, const uint64_t blockHeight, const std::string& type,
const uint64_t txn, const std::string& txid, const TAmounts& amounts) {
UniValue obj(UniValue::VOBJ);
Expand Down Expand Up @@ -1596,6 +1497,194 @@ UniValue listvaulthistory(const JSONRPCRequest& request) {
return slice;
}

UniValue estimateloan(const JSONRPCRequest& request) {

RPCHelpMan{"estimateloan",
"Returns amount of loan tokens a vault can take depending on a target collateral ratio.\n",
{
{"vaultId", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "vault hex id",},
{"tokens", RPCArg::Type::OBJ, RPCArg::Optional::NO, "Object with loans token as key and their percent split as value",
{

{"split", RPCArg::Type::NUM, RPCArg::Optional::NO, "The percent split"},
},
},
{"targetRatio", RPCArg::Type::NUM, RPCArg::Optional::OMITTED, "Target collateral ratio. (defaults to vault's loan scheme ratio)"}
},
RPCResult{
"\"json\" (Array) Array of <amount@token> strings\n"
},
RPCExamples{
HelpExampleCli("estimateloan", R"(5474b2e9bfa96446e5ef3c9594634e1aa22d3a0722cb79084d61253acbdf87bf '{"TSLA":0.5, "FB": 0.4, "GOOGL":0.1}' 150)") +
HelpExampleRpc("estimateloan", R"("5474b2e9bfa96446e5ef3c9594634e1aa22d3a0722cb79084d61253acbdf87bf", {"TSLA":0.5, "FB": 0.4, "GOOGL":0.1}, 150)")
},
}.Check(request);

RPCTypeCheck(request.params, {UniValue::VSTR, UniValue::VOBJ, UniValue::VNUM}, false);

CVaultId vaultId = ParseHashV(request.params[0], "vaultId");

LOCK(cs_main);

auto vault = pcustomcsview->GetVault(vaultId);
if (!vault) {
throw JSONRPCError(RPC_DATABASE_ERROR, strprintf("Vault <%s> not found.", vaultId.GetHex()));
}

auto vaultState = GetVaultState(vaultId, *vault);
if (vaultState == VaultState::InLiquidation) {
throw JSONRPCError(RPC_MISC_ERROR, strprintf("Vault <%s> is in liquidation.", vaultId.GetHex()));
}

auto scheme = pcustomcsview->GetLoanScheme(vault->schemeId);
uint32_t ratio = scheme->ratio;
if (request.params.size() > 2) {
ratio = (size_t) request.params[2].get_int64();
}

auto collaterals = pcustomcsview->GetVaultCollaterals(vaultId);
if (!collaterals) {
throw JSONRPCError(RPC_MISC_ERROR, "Cannot estimate loan without collaterals.");
}

auto height = ::ChainActive().Height();
auto blockTime = ::ChainActive().Tip()->GetBlockTime();
auto rate = pcustomcsview->GetLoanCollaterals(vaultId, *collaterals, height + 1, blockTime, false, true);
if (!rate.ok) {
throw JSONRPCError(RPC_MISC_ERROR, rate.msg);
}

CBalances loanBalances;
CAmount totalSplit{0};
if (request.params.size() > 1 && request.params[1].isObject()) {
for (const auto& tokenId : request.params[1].getKeys()) {
CAmount split = AmountFromValue(request.params[1][tokenId]);

auto token = pcustomcsview->GetToken(tokenId);
if (!token) {
throw JSONRPCError(RPC_DATABASE_ERROR, strprintf("Token %d does not exist!", tokenId));
}

auto loanToken = pcustomcsview->GetLoanTokenByID(token->first);
if (!loanToken) {
throw JSONRPCError(RPC_DATABASE_ERROR, strprintf("(%s) is not a loan token!", tokenId));
}

auto priceFeed = pcustomcsview->GetFixedIntervalPrice(loanToken->fixedIntervalPriceId);
if (!priceFeed.ok) {
throw JSONRPCError(RPC_DATABASE_ERROR, priceFeed.msg);
}

auto price = priceFeed.val->priceRecord[0];
if (!priceFeed.val->isLive(pcustomcsview->GetPriceDeviation())) {
throw JSONRPCError(RPC_MISC_ERROR, strprintf("No live fixed price for %s", tokenId));
}

auto availableValue = MultiplyAmounts(rate.val->totalCollaterals, split);
auto loanAmount = DivideAmounts(availableValue, price);
auto amountRatio = MultiplyAmounts(DivideAmounts(loanAmount, ratio), 100);

loanBalances.Add({token->first, amountRatio});
totalSplit += split;
}
if (totalSplit != COIN)
throw JSONRPCError(RPC_MISC_ERROR, strprintf("total split between loan tokens = %s vs expected %s", GetDecimaleString(totalSplit), GetDecimaleString(COIN)));
}
return AmountsToJSON(loanBalances.balances);
}

UniValue estimatecollateral(const JSONRPCRequest& request) {
auto pwallet = GetWallet(request);

RPCHelpMan{"estimatecollateral",
"Returns amount of collateral tokens needed to take an amount of loan tokens for a target collateral ratio.\n",
{
{"loanAmounts", RPCArg::Type::STR, RPCArg::Optional::NO,
"Amount as json string, or array. Example: '[ \"amount@token\" ]'"
},
{"targetRatio", RPCArg::Type::NUM, RPCArg::Optional::NO, "Target collateral ratio."},
{"tokens", RPCArg::Type::OBJ, RPCArg::Optional::OMITTED, "Object with collateral token as key and their percent split as value. (defaults to { DFI: 1 }",
{
{"split", RPCArg::Type::NUM, RPCArg::Optional::NO, "The percent split"},
},
},
},
RPCResult{
"\"json\" (Array) Array of <amount@token> strings\n"
},
RPCExamples{
HelpExampleCli("estimatecollateral", R"(23.55311144@MSFT 150 '{"DFI": 0.8, "BTC":0.2}')") +
HelpExampleRpc("estimatecollateral", R"("23.55311144@MSFT" 150 {"DFI": 0.8, "BTC":0.2})")
},
}.Check(request);

RPCTypeCheck(request.params, {UniValueType(), UniValue::VNUM, UniValue::VOBJ}, false);

const CBalances loanBalances = DecodeAmounts(pwallet->chain(), request.params[0], "");
auto ratio = request.params[1].get_int();

std::map<std::string, UniValue> collateralSplits;
if (request.params.size() > 2) {
request.params[2].getObjMap(collateralSplits);
} else {
collateralSplits["DFI"] = 1;
}

LOCK(cs_main);

CAmount totalLoanValue{0};
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);
}
totalLoanValue += *amountInCurrency.val;
}

uint32_t height = ::ChainActive().Height();
CBalances collateralBalances;
CAmount totalSplit{0};
for (const auto& collateralSplit : collateralSplits) {
CAmount split = AmountFromValue(collateralSplit.second);

auto token = pcustomcsview->GetToken(collateralSplit.first);
if (!token) {
throw JSONRPCError(RPC_DATABASE_ERROR, strprintf("Token %s does not exist!", collateralSplit.first));
}

auto collateralToken = pcustomcsview->HasLoanCollateralToken({token->first, height});
if (!collateralToken || !collateralToken->factor) {
throw JSONRPCError(RPC_DATABASE_ERROR, strprintf("(%s) is not a valid collateral!", collateralSplit.first));
}

auto priceFeed = pcustomcsview->GetFixedIntervalPrice(collateralToken->fixedIntervalPriceId);
if (!priceFeed.ok) {
throw JSONRPCError(RPC_DATABASE_ERROR, priceFeed.msg);
}

auto price = priceFeed.val->priceRecord[0];
if (!priceFeed.val->isLive(pcustomcsview->GetPriceDeviation())) {
throw JSONRPCError(RPC_MISC_ERROR, strprintf("No live fixed price for %s", collateralSplit.first));
}

auto requiredValue = MultiplyAmounts(totalLoanValue, split);
auto collateralValue = DivideAmounts(requiredValue, price);
auto amountRatio = DivideAmounts(MultiplyAmounts(collateralValue, ratio), 100);
auto totalAmount = DivideAmounts(amountRatio, collateralToken->factor);

collateralBalances.Add({token->first, totalAmount});
totalSplit += split;
}
if (totalSplit != COIN) {
throw JSONRPCError(RPC_MISC_ERROR, strprintf("total split between collateral tokens = %s vs expected %s", GetDecimaleString(totalSplit), GetDecimaleString(COIN)));
}

return AmountsToJSON(collateralBalances.balances);
}

UniValue estimatevault(const JSONRPCRequest& request) {
auto pwallet = GetWallet(request);

Expand Down Expand Up @@ -1678,6 +1767,7 @@ static const CRPCCommand commands[] =
{"vault", "placeauctionbid", &placeauctionbid, {"id", "index", "from", "amount", "inputs"}},
{"vault", "listauctions", &listauctions, {"pagination"}},
{"vault", "listauctionhistory", &listauctionhistory, {"owner", "pagination"}},
{"vault", "estimateloan", &estimateloan, {"vaultId", "tokens", "targetRatio"}},
{"vault", "estimatecollateral", &estimatecollateral, {"loanAmounts", "targetRatio", "tokens"}},
{"vault", "estimatevault", &estimatevault, {"collateralAmounts", "loanAmounts"}},
};
Expand Down
2 changes: 2 additions & 0 deletions src/rpc/client.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,8 @@ static const CRPCConvertParam vRPCConvertParams[] =
{ "listvaults", 1, "pagination" },
{ "listauctions", 0, "pagination" },
{ "listauctionhistory", 1, "pagination" },
{ "estimateloan", 1, "tokens" },
{ "estimateloan", 2, "targetRatio" },
{ "estimatecollateral", 1, "targetRatio" },
{ "estimatecollateral", 2, "tokens" },
{ "estimatevault", 0, "collateralAmounts" },
Expand Down
2 changes: 1 addition & 1 deletion test/functional/feature_loan_deposittovault.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 Loan - deposittovault."""

from decimal import Decimal
from test_framework.test_framework import DefiTestFramework
Expand Down
2 changes: 1 addition & 1 deletion test/functional/feature_loan_estimatecollateral.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ def run_test(self):
self.nodes[0].estimatecollateral("10@TSLA", 200)
except JSONRPCException as e:
errorString = e.error['message']
assert("No live fixed price for TSLA" in errorString)
assert("No live fixed prices for TSLA/USD" in errorString)

oracle1_prices = [
{"currency": "USD", "tokenAmount": "1@DFI"},
Expand Down
Loading

0 comments on commit ebae304

Please sign in to comment.