From 5bd203148c14a302d6f97233f544e1476366216d Mon Sep 17 00:00:00 2001 From: jouzo Date: Tue, 23 Nov 2021 15:28:39 +0100 Subject: [PATCH 1/4] Adds estimatecollateral RPC --- src/masternodes/rpc_vault.cpp | 100 +++++++++ src/rpc/client.cpp | 2 + .../feature_loan_estimatecollateral.py | 204 ++++++++++++++++++ test/functional/test_runner.py | 1 + 4 files changed, 307 insertions(+) create mode 100644 test/functional/feature_loan_estimatecollateral.py diff --git a/src/masternodes/rpc_vault.cpp b/src/masternodes/rpc_vault.cpp index 529459e6d42..2df5efd1e9b 100644 --- a/src/masternodes/rpc_vault.cpp +++ b/src/masternodes/rpc_vault.cpp @@ -1146,6 +1146,105 @@ 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 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 collateralSplits; + if (request.params.size() > 2) { + request.params[2].getObjMap(collateralSplits); + } else { + collateralSplits["DFI"] = 1; + } + + LOCK(cs_main); + + CAmount totalLoanValue{0}; + for (const auto& [tokenId, tokenAmount] : loanAmounts.balances) { + auto loanToken = pcustomcsview->GetLoanTokenByID(tokenId); + 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.v)); + } + totalLoanValue += MultiplyAmounts(tokenAmount, price); + } + + uint32_t height = ::ChainActive().Height(); + CBalances collateralBalances; + CAmount totalSplit{0}; + for (const auto& [tokenId, splitValue] : collateralSplits) { + CAmount split = AmountFromValue(splitValue); + + auto token = pcustomcsview->GetToken(tokenId); + if (!token) { + throw JSONRPCError(RPC_DATABASE_ERROR, strprintf("Token %d does not exist!", tokenId)); + } + + auto collateralToken = pcustomcsview->HasLoanCollateralToken({token->first, height}); + if (!collateralToken || !collateralToken->factor) { + throw JSONRPCError(RPC_DATABASE_ERROR, strprintf("(%s) is not a valid collateral!", tokenId)); + } + + 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", tokenId)); + } + + 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 = %d vs expected %d", totalSplit, COIN)); + } + + return AmountsToJSON(collateralBalances.balances); +} + static const CRPCCommand commands[] = { // category name actor (function) params @@ -1160,6 +1259,7 @@ static const CRPCCommand commands[] = {"vault", "placeauctionbid", &placeauctionbid, {"id", "index", "from", "amount", "inputs"}}, {"vault", "listauctions", &listauctions, {"pagination"}}, {"vault", "listauctionhistory", &listauctionhistory, {"owner", "pagination"}}, + {"vault", "estimatecollateral", &estimatecollateral, {"loanAmounts", "targetRatio", "tokens"}}, }; void RegisterVaultRPCCommands(CRPCTable& tableRPC) { diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp index 951c81fc8e5..dee47ed90f4 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -260,6 +260,8 @@ static const CRPCConvertParam vRPCConvertParams[] = { "listvaults", 1, "pagination" }, { "listauctions", 0, "pagination" }, { "listauctionhistory", 1, "pagination" }, + { "estimatecollateral", 1, "targetRatio" }, + { "estimatecollateral", 2, "tokens" }, { "spv_sendrawtx", 0, "rawtx" }, { "spv_createanchor", 0, "inputs" }, diff --git a/test/functional/feature_loan_estimatecollateral.py b/test/functional/feature_loan_estimatecollateral.py new file mode 100644 index 00000000000..f587c79ad00 --- /dev/null +++ b/test/functional/feature_loan_estimatecollateral.py @@ -0,0 +1,204 @@ +#!/usr/bin/env python3 +# Copyright (c) 2014-2019 The Bitcoin Core developers +# 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 - estimatecollateral.""" + +from test_framework.test_framework import DefiTestFramework + +from test_framework.authproxy import JSONRPCException +from test_framework.util import assert_equal +import time + +class EstimateCollateralTest (DefiTestFramework): + def set_test_params(self): + self.num_nodes = 1 + self.setup_clean_chain = True + self.extra_args = [ + ['-txnotokens=0', '-amkheight=1', '-bayfrontheight=1', '-eunosheight=1', '-txindex=1', '-fortcanningheight=1'], + ] + + def run_test(self): + self.nodes[0].generate(150) + + self.nodes[0].createtoken({ + "symbol": "BTC", + "name": "BTC token", + "isDAT": True, + "collateralAddress": self.nodes[0].get_genesis_keys().ownerAuthAddress + }) + self.nodes[0].generate(1) + + symbolDFI = "DFI" + symbolBTC = "BTC" + idDFI = list(self.nodes[0].gettoken(symbolDFI).keys())[0] + idBTC = list(self.nodes[0].gettoken(symbolBTC).keys())[0] + + self.nodes[0].minttokens("100@" + symbolBTC) + self.nodes[0].generate(1) + + account = self.nodes[0].get_genesis_keys().ownerAuthAddress + + self.nodes[0].utxostoaccount({account: "500@" + symbolDFI}) + self.nodes[0].generate(1) + + oracle_address1 = self.nodes[0].getnewaddress("", "legacy") + price_feeds1 = [ + {"currency": "USD", "token": "DFI"}, + {"currency": "USD", "token": "BTC"}, + {"currency": "USD", "token": "TSLA"}, + {"currency": "USD", "token": "TWTR"}, + ] + oracle_id1 = self.nodes[0].appointoracle(oracle_address1, price_feeds1, 10) + self.nodes[0].generate(1) + + oracle1_prices = [ + {"currency": "USD", "tokenAmount": "1@DFI"}, + {"currency": "USD", "tokenAmount": "100@BTC"}, + {"currency": "USD", "tokenAmount": "5@TSLA"}, + {"currency": "USD", "tokenAmount": "10@TWTR"}, + ] + mock_time = int(time.time()) + self.nodes[0].setmocktime(mock_time) + self.nodes[0].setoracledata(oracle_id1, mock_time, oracle1_prices) + + self.nodes[0].generate(1) + + self.nodes[0].setcollateraltoken({ + 'token': idDFI, + 'factor': 1, + 'fixedIntervalPriceId': "DFI/USD"}) + + self.nodes[0].setcollateraltoken({ + 'token': idBTC, + 'factor': 0.8, + 'fixedIntervalPriceId': "BTC/USD"}) + + self.nodes[0].generate(7) + + loanSchemeRatio = 200 + self.nodes[0].createloanscheme(loanSchemeRatio, 1, 'LOAN0001') + self.nodes[0].generate(1) + + ownerAddress1 = self.nodes[0].getnewaddress('', 'legacy') + vaultId1 = self.nodes[0].createvault(ownerAddress1) # default loan scheme + self.nodes[0].generate(1) + + self.nodes[0].setloantoken({ + 'symbol': "TSLA", + 'name': "Tesla Token", + 'fixedIntervalPriceId': "TSLA/USD", + 'mintable': True, + 'interest': 0.01}) + self.nodes[0].setloantoken({ + 'symbol': "TWTR", + 'name': "Twitter Token", + 'fixedIntervalPriceId': "TWTR/USD", + 'mintable': True, + 'interest': 0.01}) + self.nodes[0].generate(1) + + # Token that does not exists + try: + self.nodes[0].estimatecollateral("10@TSLAA", 200) + except JSONRPCException as e: + errorString = e.error['message'] + assert("Invalid Defi token: TSLAA" in errorString) + # Token not set as loan token + try: + self.nodes[0].estimatecollateral("10@DFI", 200) + except JSONRPCException as e: + errorString = e.error['message'] + assert("not a loan token!" in errorString) + # Token without live price + try: + self.nodes[0].estimatecollateral("10@TSLA", 200) + except JSONRPCException as e: + errorString = e.error['message'] + assert("No live fixed price" in errorString) + + oracle1_prices = [ + {"currency": "USD", "tokenAmount": "1@DFI"}, + {"currency": "USD", "tokenAmount": "100@BTC"}, + {"currency": "USD", "tokenAmount": "5@TSLA"}, + {"currency": "USD", "tokenAmount": "10@TWTR"}, + ] + mock_time = int(time.time()) + self.nodes[0].setmocktime(mock_time) + self.nodes[0].setoracledata(oracle_id1, mock_time, oracle1_prices) + + self.nodes[0].generate(8) # activate prices + + # Negative split value + try: + self.nodes[0].estimatecollateral("10@TSLA", 200, {"DFI": -1}) + except JSONRPCException as e: + errorString = e.error['message'] + assert("Amount out of range" in errorString) + # Token not set as collateral + try: + self.nodes[0].estimatecollateral("10@TSLA", 200, {"TSLA": 1}) + except JSONRPCException as e: + errorString = e.error['message'] + assert("(TSLA) is not a valid collateral!" in errorString) + # Total split should be equal to 1 + try: + self.nodes[0].estimatecollateral("10@TSLA", 200, {"DFI": 0.8}) + except JSONRPCException as e: + errorString = e.error['message'] + assert("total split between collateral tokens = 80000000 vs expected 100000000" in errorString) + + estimatecollateral = self.nodes[0].estimatecollateral("10@TSLA", 200) + + self.nodes[0].deposittovault(vaultId1, account, estimatecollateral[0]) + self.nodes[0].generate(1) + # Cannot take more loan than estimated + try: + self.nodes[0].takeloan({ "vaultId": vaultId1, "amounts": "10.1@TSLA" }) + except JSONRPCException as e: + errorString = e.error['message'] + assert("Vault does not have enough collateralization ratio" in errorString) + + self.nodes[0].takeloan({ "vaultId": vaultId1, "amounts": "10@TSLA" }) # should be able to take loan amount from estimatecollateral + self.nodes[0].generate(1) + + vault1 = self.nodes[0].getvault(vaultId1) + assert_equal(vault1["collateralRatio"], 200) # vault collateral ratio should be equal to estimatecollateral targetRatio + + vaultId2 = self.nodes[0].createvault(ownerAddress1) + estimatecollateral = self.nodes[0].estimatecollateral("10@TSLA", 200, {"BTC":0.5, "DFI": 0.5}) + + amountDFI = next(x for x in estimatecollateral if "DFI" in x) + amountBTC = next(x for x in estimatecollateral if "BTC" in x) + self.nodes[0].generate(1) + self.nodes[0].deposittovault(vaultId2, account, amountDFI) + self.nodes[0].generate(1) + self.nodes[0].deposittovault(vaultId2, account, amountBTC) + self.nodes[0].generate(1) + + self.nodes[0].takeloan({ "vaultId": vaultId2, "amounts": "10@TSLA" }) + self.nodes[0].generate(1) + + vault2 = self.nodes[0].getvault(vaultId2) + assert_equal(vault2["collateralRatio"], 200) + + vaultId3 = self.nodes[0].createvault(ownerAddress1) + estimatecollateral = self.nodes[0].estimatecollateral(["10@TSLA", "10@TWTR"], 200, {"BTC":0.5, "DFI": 0.5}) + + amountDFI = next(x for x in estimatecollateral if "DFI" in x) + amountBTC = next(x for x in estimatecollateral if "BTC" in x) + self.nodes[0].generate(1) + self.nodes[0].deposittovault(vaultId3, account, amountDFI) + self.nodes[0].generate(1) + self.nodes[0].deposittovault(vaultId3, account, amountBTC) + self.nodes[0].generate(1) + + self.nodes[0].takeloan({ "vaultId": vaultId3, "amounts": ["10@TSLA", "10@TWTR"] }) + self.nodes[0].generate(1) + + vault3 = self.nodes[0].getvault(vaultId3) + assert_equal(vault3["collateralRatio"], 200) + +if __name__ == '__main__': + EstimateCollateralTest().main() diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index 98c60dd544f..7abd3329608 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -256,6 +256,7 @@ 'feature_loan_priceupdate.py', 'feature_loan_vaultstate.py', 'feature_loan.py', + 'feature_loan_estimatecollateral.py', 'p2p_node_network_limited.py', 'p2p_permissions.py', 'feature_blocksdir.py', From 97e1726cc767ec0289493ad4a400d655e7903ff2 Mon Sep 17 00:00:00 2001 From: jouzo Date: Tue, 23 Nov 2021 15:45:04 +0100 Subject: [PATCH 2/4] Fix error messages --- src/masternodes/rpc_vault.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/masternodes/rpc_vault.cpp b/src/masternodes/rpc_vault.cpp index 2df5efd1e9b..d808ccbebd4 100644 --- a/src/masternodes/rpc_vault.cpp +++ b/src/masternodes/rpc_vault.cpp @@ -1189,7 +1189,7 @@ UniValue estimatecollateral(const JSONRPCRequest& request) { for (const auto& [tokenId, tokenAmount] : loanAmounts.balances) { auto loanToken = pcustomcsview->GetLoanTokenByID(tokenId); if (!loanToken) { - throw JSONRPCError(RPC_DATABASE_ERROR, strprintf("(%s) is not a loan token!", tokenId)); + throw JSONRPCError(RPC_DATABASE_ERROR, strprintf("(%s) is not a loan token!", tokenId.v)); } auto priceFeed = pcustomcsview->GetFixedIntervalPrice(loanToken->fixedIntervalPriceId); @@ -1212,7 +1212,7 @@ UniValue estimatecollateral(const JSONRPCRequest& request) { auto token = pcustomcsview->GetToken(tokenId); if (!token) { - throw JSONRPCError(RPC_DATABASE_ERROR, strprintf("Token %d does not exist!", tokenId)); + throw JSONRPCError(RPC_DATABASE_ERROR, strprintf("Token %s does not exist!", tokenId)); } auto collateralToken = pcustomcsview->HasLoanCollateralToken({token->first, height}); From 69e27a13cfe06240f9ecfb355373ff2c4d6e85d6 Mon Sep 17 00:00:00 2001 From: jouzo Date: Tue, 23 Nov 2021 17:51:33 +0100 Subject: [PATCH 3/4] Improve error messages --- src/masternodes/rpc_vault.cpp | 6 +++--- test/functional/feature_loan_estimatecollateral.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/masternodes/rpc_vault.cpp b/src/masternodes/rpc_vault.cpp index d808ccbebd4..d926ac8762e 100644 --- a/src/masternodes/rpc_vault.cpp +++ b/src/masternodes/rpc_vault.cpp @@ -1189,7 +1189,7 @@ UniValue estimatecollateral(const JSONRPCRequest& request) { for (const auto& [tokenId, tokenAmount] : loanAmounts.balances) { auto loanToken = pcustomcsview->GetLoanTokenByID(tokenId); if (!loanToken) { - throw JSONRPCError(RPC_DATABASE_ERROR, strprintf("(%s) is not a loan token!", tokenId.v)); + throw JSONRPCError(RPC_DATABASE_ERROR, strprintf("(%d) is not a loan token!", tokenId.v)); } auto priceFeed = pcustomcsview->GetFixedIntervalPrice(loanToken->fixedIntervalPriceId); @@ -1199,7 +1199,7 @@ UniValue estimatecollateral(const JSONRPCRequest& request) { 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.v)); + throw JSONRPCError(RPC_MISC_ERROR, strprintf("No live fixed price for %s", loanToken->symbol)); } totalLoanValue += MultiplyAmounts(tokenAmount, price); } @@ -1239,7 +1239,7 @@ UniValue estimatecollateral(const JSONRPCRequest& request) { totalSplit += split; } if (totalSplit != COIN) { - throw JSONRPCError(RPC_MISC_ERROR, strprintf("total split between collateral tokens = %d vs expected %d", 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); diff --git a/test/functional/feature_loan_estimatecollateral.py b/test/functional/feature_loan_estimatecollateral.py index f587c79ad00..78c2e33583f 100644 --- a/test/functional/feature_loan_estimatecollateral.py +++ b/test/functional/feature_loan_estimatecollateral.py @@ -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" in errorString) + assert("No live fixed price for TSLA" in errorString) oracle1_prices = [ {"currency": "USD", "tokenAmount": "1@DFI"}, @@ -147,7 +147,7 @@ def run_test(self): self.nodes[0].estimatecollateral("10@TSLA", 200, {"DFI": 0.8}) except JSONRPCException as e: errorString = e.error['message'] - assert("total split between collateral tokens = 80000000 vs expected 100000000" in errorString) + assert("total split between collateral tokens = 0.80000000 vs expected 1.00000000" in errorString) estimatecollateral = self.nodes[0].estimatecollateral("10@TSLA", 200) From 49b8094de4faeb6cc62c6884e3466162a10baf72 Mon Sep 17 00:00:00 2001 From: jouzo Date: Wed, 24 Nov 2021 09:45:38 +0100 Subject: [PATCH 4/4] Remove use of structured bindings --- src/masternodes/rpc_vault.cpp | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/masternodes/rpc_vault.cpp b/src/masternodes/rpc_vault.cpp index d926ac8762e..6b064d54d8f 100644 --- a/src/masternodes/rpc_vault.cpp +++ b/src/masternodes/rpc_vault.cpp @@ -1186,10 +1186,10 @@ UniValue estimatecollateral(const JSONRPCRequest& request) { LOCK(cs_main); CAmount totalLoanValue{0}; - for (const auto& [tokenId, tokenAmount] : loanAmounts.balances) { - auto loanToken = pcustomcsview->GetLoanTokenByID(tokenId); + 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!", tokenId.v)); + throw JSONRPCError(RPC_DATABASE_ERROR, strprintf("(%d) is not a loan token!", balance.first.v)); } auto priceFeed = pcustomcsview->GetFixedIntervalPrice(loanToken->fixedIntervalPriceId); @@ -1201,23 +1201,23 @@ UniValue estimatecollateral(const JSONRPCRequest& request) { if (!priceFeed.val->isLive(pcustomcsview->GetPriceDeviation())) { throw JSONRPCError(RPC_MISC_ERROR, strprintf("No live fixed price for %s", loanToken->symbol)); } - totalLoanValue += MultiplyAmounts(tokenAmount, price); + totalLoanValue += MultiplyAmounts(balance.second, price); } uint32_t height = ::ChainActive().Height(); CBalances collateralBalances; CAmount totalSplit{0}; - for (const auto& [tokenId, splitValue] : collateralSplits) { - CAmount split = AmountFromValue(splitValue); + for (const auto& collateralSplit : collateralSplits) { + CAmount split = AmountFromValue(collateralSplit.second); - auto token = pcustomcsview->GetToken(tokenId); + auto token = pcustomcsview->GetToken(collateralSplit.first); if (!token) { - throw JSONRPCError(RPC_DATABASE_ERROR, strprintf("Token %s does not exist!", tokenId)); + 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!", tokenId)); + throw JSONRPCError(RPC_DATABASE_ERROR, strprintf("(%s) is not a valid collateral!", collateralSplit.first)); } auto priceFeed = pcustomcsview->GetFixedIntervalPrice(collateralToken->fixedIntervalPriceId); @@ -1227,7 +1227,7 @@ UniValue estimatecollateral(const JSONRPCRequest& request) { 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)); + throw JSONRPCError(RPC_MISC_ERROR, strprintf("No live fixed price for %s", collateralSplit.first)); } auto requiredValue = MultiplyAmounts(totalLoanValue, split);