diff --git a/src/masternodes/mn_checks.cpp b/src/masternodes/mn_checks.cpp index 9be27c5de76..9001a05c5ba 100644 --- a/src/masternodes/mn_checks.cpp +++ b/src/masternodes/mn_checks.cpp @@ -815,7 +815,12 @@ class CCustomTxApplyVisitor : public CCustomTxVisitor } } - return mnview.UpdateToken(token.creationTx, obj.token, false); + auto updatedToken = obj.token; + if (height >= consensus.FortCanningHeight) { + updatedToken.symbol = trim_ws(updatedToken.symbol).substr(0, CToken::MAX_TOKEN_SYMBOL_LENGTH); + } + + return mnview.UpdateToken(token.creationTx, updatedToken, false); } Res operator()(const CMintTokensMessage& obj) const { @@ -872,10 +877,11 @@ class CCustomTxApplyVisitor : public CCustomTxVisitor return Res::Err("token %s does not exist!", poolPair.idTokenB.ToString()); } + const auto symbolLength = height >= consensus.FortCanningHeight ? CToken::MAX_TOKEN_POOLPAIR_LENGTH : CToken::MAX_TOKEN_SYMBOL_LENGTH; if (pairSymbol.empty()) { - pairSymbol = trim_ws(tokenA->symbol + "-" + tokenB->symbol).substr(0, CToken::MAX_TOKEN_SYMBOL_LENGTH); + pairSymbol = trim_ws(tokenA->symbol + "-" + tokenB->symbol).substr(0, symbolLength); } else { - pairSymbol = trim_ws(pairSymbol).substr(0, CToken::MAX_TOKEN_SYMBOL_LENGTH); + pairSymbol = trim_ws(pairSymbol).substr(0, symbolLength); } CTokenImplementation token; @@ -933,22 +939,7 @@ class CCustomTxApplyVisitor : public CCustomTxVisitor return Res::Err("tx must have at least one input from account owner"); } - auto poolPair = mnview.GetPoolPair(obj.idTokenFrom, obj.idTokenTo); - if (!poolPair) { - return Res::Err("can't find the poolpair!"); - } - - CPoolPair& pp = poolPair->second; - return pp.Swap({obj.idTokenFrom, obj.amountFrom}, obj.maxPrice, [&] (const CTokenAmount &tokenAmount) { - auto res = mnview.SetPoolPair(poolPair->first, height, pp); - if (!res) { - return res; - } - CalculateOwnerRewards(obj.from); - CalculateOwnerRewards(obj.to); - res = mnview.SubBalance(obj.from, {obj.idTokenFrom, obj.amountFrom}); - return !res ? res : mnview.AddBalance(obj.to, tokenAmount); - }, static_cast(height)); + return CPoolSwap(obj, height).ExecuteSwap(mnview, obj.poolIDs); } Res operator()(const CLiquidityMessage& obj) const { @@ -2109,3 +2100,201 @@ bool IsMempooledCustomTxCreate(const CTxMemPool & pool, const uint256 & txid) } return false; } + +std::vector CPoolSwap::CalculateSwaps(CCustomCSView& view) { + + // For tokens to be traded get all pairs and pool IDs + std::multimap fromPoolsID, toPoolsID; + view.ForEachPoolPair([&](DCT_ID const & id, const CPoolPair& pool) { + if (pool.idTokenA == obj.idTokenFrom) { + fromPoolsID.emplace(pool.idTokenB.v, id); + } else if (pool.idTokenB == obj.idTokenFrom) { + fromPoolsID.emplace(pool.idTokenA.v, id); + } + + if (pool.idTokenA == obj.idTokenTo) { + toPoolsID.emplace(pool.idTokenB.v, id); + } else if (pool.idTokenB == obj.idTokenTo) { + toPoolsID.emplace(pool.idTokenA.v, id); + } + return true; + }, {0}); + + if (fromPoolsID.empty() || toPoolsID.empty()) { + return {}; + } + + // Find intersection on key + std::map commonPairs; + set_intersection(fromPoolsID.begin(), fromPoolsID.end(), toPoolsID.begin(), toPoolsID.end(), + std::inserter(commonPairs, commonPairs.begin()), + [](std::pair a, std::pair b) { + return a.first < b.first; + }); + + // Loop through all common pairs and record direct pool to pool swaps + std::vector> poolPaths; + for (const auto& item : commonPairs) { + + // Loop through all source/intermediate pools matching common pairs + const auto poolFromIDs = fromPoolsID.equal_range(item.first); + for (auto fromID = poolFromIDs.first; fromID != poolFromIDs.second; ++fromID) { + + // Loop through all destination pools matching common pairs + const auto poolToIDs = toPoolsID.equal_range(item.first); + for (auto toID = poolToIDs.first; toID != poolToIDs.second; ++toID) { + + // Add to pool paths + poolPaths.push_back({fromID->second, toID->second}); + } + } + } + + // Look for pools that bridges token. Might be in addition to common token pairs paths. + view.ForEachPoolPair([&](DCT_ID const & id, const CPoolPair& pool) { + + // Loop through from pool multimap on unique keys only + for (auto fromIt = fromPoolsID.begin(); fromIt != fromPoolsID.end(); fromIt = fromPoolsID.equal_range(fromIt->first).second) { + + // Loop through to pool multimap on unique keys only + for (auto toIt = toPoolsID.begin(); toIt != toPoolsID.end(); toIt = toPoolsID.equal_range(toIt->first).second) { + + // If a pool pairs matches from pair and to pair add it to the pool paths + if ((fromIt->first == pool.idTokenA.v && toIt->first == pool.idTokenB.v) || + (fromIt->first == pool.idTokenB.v && toIt->first == pool.idTokenA.v)) { + poolPaths.push_back({fromIt->second, id, toIt->second}); + } + } + } + return true; + }, {0}); + + // Record best pair + std::pair, CAmount> bestPair{{}, 0}; + + // Loop through all common pairs + for (const auto& path : poolPaths) { + + // Test on copy of view + CCustomCSView dummy(view); + + // Execute pool path + auto res = ExecuteSwap(dummy, path); + + // Add error for RPC user feedback + if (!res) { + const auto token = dummy.GetToken(currentID); + if (token) { + errors.emplace_back(token->symbol, res.msg); + } + } + + // Record amount if more than previous or default value + if (res && result > bestPair.second) { + bestPair = {path, result}; + } + } + + return bestPair.first; +} + +Res CPoolSwap::ExecuteSwap(CCustomCSView& view, std::vector poolIDs) { + + CTokenAmount swapAmountResult{{},0}; + Res poolResult = Res::Ok(); + + // No composite swap allowed before Fort Canning + if (height < Params().GetConsensus().FortCanningHeight && !poolIDs.empty()) { + poolIDs.clear(); + } + + CCustomCSView intermediateView(view); + + // Single swap if no pool IDs provided + auto poolPrice = POOLPRICE_MAX; + boost::optional > poolPair; + if (poolIDs.empty()) { + poolPair = intermediateView.GetPoolPair(obj.idTokenFrom, obj.idTokenTo); + if (!poolPair) { + return Res::Err("Cannot find the pool pair."); + } + + // Add single swap pool to vector for loop + poolIDs.push_back(poolPair->first); + + // Get legacy max price + poolPrice = obj.maxPrice; + } + + for (size_t i{0}; i < poolIDs.size(); ++i) { + + // Also used to generate pool specific error messages for RPC users + currentID = poolIDs[i]; + + // Use single swap pool if already found + boost::optional pool; + if (poolPair) { + pool = poolPair->second; + } + else // Or get pools from IDs provided for composite swap + { + pool = intermediateView.GetPoolPair(currentID); + if (!pool) { + return Res::Err("Cannot find the pool pair."); + } + } + + // Set amount to be swapped in pool + CTokenAmount swapAmount{obj.idTokenFrom, obj.amountFrom}; + + // If set use amount from previous loop + if (swapAmountResult.nValue != 0) { + swapAmount = swapAmountResult; + } + + // Check if last pool swap + bool lastSwap = i + 1 == poolIDs.size(); + + // Perform swap + poolResult = pool->Swap(swapAmount, poolPrice, [&] (const CTokenAmount &tokenAmount) { + auto res = intermediateView.SetPoolPair(currentID, height, *pool); + if (!res) { + return res; + } + + // Update owner rewards if not being called from RPC + intermediateView.CalculateOwnerRewards(obj.from, height); + + if (lastSwap) { + intermediateView.CalculateOwnerRewards(obj.to, height); + } + + // Save swap amount for next loop + swapAmountResult = tokenAmount; + + // Update balances + res = intermediateView.SubBalance(obj.from, swapAmount); + return !res ? res : intermediateView.AddBalance(lastSwap ? obj.to : obj.from, tokenAmount); + }, static_cast(height)); + + if (!poolResult) { + return poolResult; + } + } + + // Reject if price paid post-swap above max price provided + if (height >= Params().GetConsensus().FortCanningHeight && obj.maxPrice != POOLPRICE_MAX) { + const CAmount userMaxPrice = obj.maxPrice.integer * COIN + obj.maxPrice.fraction; + if (arith_uint256(obj.amountFrom) * COIN / swapAmountResult.nValue > userMaxPrice) { + return Res::Err("Price is higher than indicated."); + } + } + + // Flush changes + intermediateView.Flush(); + + // Assign to result for loop testing best pool swap result + result = swapAmountResult.nValue; + + return poolResult; +} diff --git a/src/masternodes/mn_checks.h b/src/masternodes/mn_checks.h index f21bd4beedb..a8a573a2127 100644 --- a/src/masternodes/mn_checks.h +++ b/src/masternodes/mn_checks.h @@ -20,6 +20,7 @@ class CCoinsViewCache; class CCustomCSView; class CAccountsHistoryView; +class CCustomTxVisitor; static const std::vector DfTxMarker = {'D', 'f', 'T', 'x'}; // 44665478 @@ -352,4 +353,20 @@ inline CAmount GetNonMintedValueOut(const CTransaction & tx, DCT_ID tokenID) return tx.GetValueOut(mintingOutputsStart, tokenID); } +class CPoolSwap { + const CPoolSwapMessage& obj; + uint32_t height; + CAmount result{0}; + DCT_ID currentID; + +public: + std::vector> errors; + + CPoolSwap(const CPoolSwapMessage& obj, uint32_t height) + : obj(obj), height(height) {} + + std::vector CalculateSwaps(CCustomCSView& view); + Res ExecuteSwap(CCustomCSView& view, std::vector poolIDs); +}; + #endif // DEFI_MASTERNODES_MN_CHECKS_H diff --git a/src/masternodes/poolpairs.h b/src/masternodes/poolpairs.h index e9dd6973c18..882826a0dd4 100644 --- a/src/masternodes/poolpairs.h +++ b/src/masternodes/poolpairs.h @@ -39,13 +39,20 @@ struct PoolPrice { READWRITE(integer); READWRITE(fraction); } + + bool operator!=(const PoolPrice& rhs) const { + return integer != rhs.integer || fraction != rhs.fraction; + } }; +static constexpr auto POOLPRICE_MAX = PoolPrice{std::numeric_limits::max(), std::numeric_limits::max()}; + struct CPoolSwapMessage { CScript from, to; DCT_ID idTokenFrom, idTokenTo; CAmount amountFrom; PoolPrice maxPrice; + std::vector poolIDs{}; std::string ToString() const { return "(" + from.GetHex() + ":" + std::to_string(amountFrom) +"@"+ idTokenFrom.ToString() + "->" + to.GetHex() + ":?@" + idTokenTo.ToString() +")"; @@ -61,6 +68,11 @@ struct CPoolSwapMessage { READWRITE(to); READWRITE(idTokenTo); READWRITE(maxPrice); + + // Only available after FortCanning + if (!s.eof()) { + READWRITE(poolIDs); + } } }; diff --git a/src/masternodes/rpc_poolpair.cpp b/src/masternodes/rpc_poolpair.cpp index a5db2c09850..79b876f4ab1 100644 --- a/src/masternodes/rpc_poolpair.cpp +++ b/src/masternodes/rpc_poolpair.cpp @@ -118,8 +118,8 @@ void CheckAndFillPoolSwapMessage(const JSONRPCRequest& request, CPoolSwapMessage poolSwapMsg.maxPrice.fraction = maxPrice % COIN; } else { // There is no maxPrice calculation anymore - poolSwapMsg.maxPrice.integer = INT64_MAX; - poolSwapMsg.maxPrice.fraction = INT64_MAX; + poolSwapMsg.maxPrice.integer = std::numeric_limits::max(); + poolSwapMsg.maxPrice.fraction = std::numeric_limits::max(); } } } @@ -816,6 +816,32 @@ UniValue poolswap(const JSONRPCRequest& request) { CheckAndFillPoolSwapMessage(request, poolSwapMsg); int targetHeight = chainHeight(*pwallet->chain().lock()) + 1; + // If no direct swap found search for composite swap + if (!pcustomcsview->GetPoolPair(poolSwapMsg.idTokenFrom, poolSwapMsg.idTokenTo)) { + + // Base error message + std::string errorMsg{"Cannot find usable pool pair."}; + + if (targetHeight >= Params().GetConsensus().FortCanningHeight) { + auto compositeSwap = CPoolSwap(poolSwapMsg, targetHeight); + poolSwapMsg.poolIDs = compositeSwap.CalculateSwaps(*pcustomcsview); + + // Populate composite pool errors if any + if (poolSwapMsg.poolIDs.empty() && !compositeSwap.errors.empty()) { + errorMsg += " Details: ("; + for (size_t i{0}; i < compositeSwap.errors.size(); ++i) { + errorMsg += "\"" + compositeSwap.errors[i].first + "\":\"" + compositeSwap.errors[i].second + "\"" + (i + 1 < compositeSwap.errors.size() ? "," : ""); + } + errorMsg += ")"; + } + } + + // Bo composite or direct pools found + if (poolSwapMsg.poolIDs.empty()) { + throw JSONRPCError(RPC_INVALID_REQUEST, errorMsg); + } + } + CDataStream metadata(DfTxMarker, SER_NETWORK, PROTOCOL_VERSION); metadata << static_cast(CustomTxType::PoolSwap) << poolSwapMsg; @@ -825,7 +851,7 @@ UniValue poolswap(const JSONRPCRequest& request) { const auto txVersion = GetTransactionVersion(targetHeight); CMutableTransaction rawTx(txVersion); - rawTx.vout.push_back(CTxOut(0, scriptMeta)); + rawTx.vout.emplace_back(0, scriptMeta); UniValue const & txInputs = request.params[1]; CTransactionRef optAuthTx; diff --git a/src/masternodes/tokens.h b/src/masternodes/tokens.h index 540fcaa447f..52c0defcddf 100644 --- a/src/masternodes/tokens.h +++ b/src/masternodes/tokens.h @@ -23,6 +23,7 @@ class CToken public: static const uint8_t MAX_TOKEN_NAME_LENGTH = 128; static const uint8_t MAX_TOKEN_SYMBOL_LENGTH = 8; + static const uint8_t MAX_TOKEN_POOLPAIR_LENGTH = 16; enum class TokenFlags : uint8_t { None = 0, diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp index d58f632e3fb..d04c30127e8 100644 --- a/src/rpc/blockchain.cpp +++ b/src/rpc/blockchain.cpp @@ -592,6 +592,7 @@ static UniValue clearmempool(const JSONRPCRequest& request) std::vector vtxid; mempool.queryHashes(vtxid); + mempool.accountsView().Discard(); UniValue removed(UniValue::VARR); for (const uint256& hash : vtxid) diff --git a/test/functional/feature_poolswap.py b/test/functional/feature_poolswap.py index 5cdb3fba661..33827d65a62 100755 --- a/test/functional/feature_poolswap.py +++ b/test/functional/feature_poolswap.py @@ -14,6 +14,7 @@ from test_framework.util import ( assert_equal, connect_nodes_bi, + disconnect_nodes, assert_raises_rpc_error, ) @@ -27,10 +28,10 @@ def set_test_params(self): # node2: Non Foundation self.setup_clean_chain = True self.extra_args = [ - ['-txnotokens=0', '-amkheight=50', '-bayfrontheight=50', '-bayfrontgardensheight=0', '-dakotaheight=160', '-acindex=1'], - ['-txnotokens=0', '-amkheight=50', '-bayfrontheight=50', '-bayfrontgardensheight=0', '-dakotaheight=160', '-acindex=1'], - ['-txnotokens=0', '-amkheight=50', '-bayfrontheight=50', '-bayfrontgardensheight=0', '-dakotaheight=160',], - ['-txnotokens=0', '-amkheight=50', '-bayfrontheight=50', '-bayfrontgardensheight=0', '-dakotaheight=160',]] + ['-txnotokens=0', '-amkheight=50', '-bayfrontheight=50', '-bayfrontgardensheight=0', '-dakotaheight=160', '-fortcanningheight=163', '-acindex=1'], + ['-txnotokens=0', '-amkheight=50', '-bayfrontheight=50', '-bayfrontgardensheight=0', '-dakotaheight=160', '-fortcanningheight=163', '-acindex=1'], + ['-txnotokens=0', '-amkheight=50', '-bayfrontheight=50', '-bayfrontgardensheight=0', '-dakotaheight=160', '-fortcanningheight=163',], + ['-txnotokens=0', '-amkheight=50', '-bayfrontheight=50', '-bayfrontgardensheight=0', '-dakotaheight=160', '-fortcanningheight=163',]] def run_test(self): @@ -291,6 +292,57 @@ def run_test(self): }) self.nodes[0].generate(1) + # Test fort canning max price change + disconnect_nodes(self.nodes[0], 1) + disconnect_nodes(self.nodes[0], 2) + print(self.nodes[0].getconnectioncount()) + destination = self.nodes[0].getnewaddress("", "legacy") + swap_from = 200 + coin = 100000000 + + self.nodes[0].poolswap({ + "from": accountGN0, + "tokenFrom": symbolGOLD, + "amountFrom": swap_from, + "to": destination, + "tokenTo": symbolSILVER + }) + self.nodes[0].generate(1) + + silver_received = self.nodes[0].getaccount(destination, {}, True)[idSilver] + silver_per_gold = round((swap_from * coin) / (silver_received * coin), 8) + + # Reset swap and try again with max price set to expected amount + self.nodes[0].invalidateblock(self.nodes[0].getblockhash(self.nodes[0].getblockcount())) + self.nodes[0].clearmempool() + + self.nodes[0].poolswap({ + "from": accountGN0, + "tokenFrom": symbolGOLD, + "amountFrom": swap_from, + "to": destination, + "tokenTo": symbolSILVER, + "maxPrice": silver_per_gold, + }) + self.nodes[0].generate(1) + + # Reset swap and try again with max price set to one Satoshi below + self.nodes[0].invalidateblock(self.nodes[0].getblockhash(self.nodes[0].getblockcount())) + self.nodes[0].clearmempool() + + try: + self.nodes[0].poolswap({ + "from": accountGN0, + "tokenFrom": symbolGOLD, + "amountFrom": swap_from, + "to": destination, + "tokenTo": symbolSILVER, + "maxPrice": silver_per_gold - Decimal('0.00000001'), + }) + except JSONRPCException as e: + errorString = e.error['message'] + assert("Price is higher than indicated" in errorString) + # REVERTING: #======================== print ("Reverting...") @@ -299,6 +351,8 @@ def run_test(self): self.nodes[3].generate(30) connect_nodes_bi(self.nodes, 0, 3) + connect_nodes_bi(self.nodes, 1, 3) + connect_nodes_bi(self.nodes, 2, 3) self.sync_blocks() assert_equal(len(self.nodes[0].listpoolpairs()), 0) diff --git a/test/functional/feature_poolswap_composite.py b/test/functional/feature_poolswap_composite.py new file mode 100644 index 00000000000..3ede53258a8 --- /dev/null +++ b/test/functional/feature_poolswap_composite.py @@ -0,0 +1,408 @@ +#!/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 poolpair composite swap RPC.""" + +from test_framework.test_framework import DefiTestFramework + +from test_framework.authproxy import JSONRPCException +from test_framework.util import ( + assert_equal, + disconnect_nodes, +) + +from decimal import Decimal + +class PoolPairCompositeTest(DefiTestFramework): + def set_test_params(self): + self.num_nodes = 2 + self.setup_clean_chain = True + self.extra_args = [ + ['-txnotokens=0', '-amkheight=1', '-bayfrontheight=106', '-bayfrontgardensheight=107', '-dakotaheight=108', '-eunosheight=109', '-fortcanningheight=110'], + ['-txnotokens=0', '-amkheight=1', '-bayfrontheight=106', '-bayfrontgardensheight=107', '-dakotaheight=108', '-eunosheight=109', '-fortcanningheight=110']] + + def run_test(self): + + # Create tokens + collateral = self.nodes[0].get_genesis_keys().ownerAuthAddress + tokens = [ + { + "wallet": self.nodes[0], + "symbol": "dUSD", + "name": "DFI USD", + "collateralAddress": collateral, + "amount": 1000000 + }, + { + "wallet": self.nodes[0], + "symbol": "DOGE", + "name": "Dogecoin", + "collateralAddress": collateral, + "amount": 1000000 + }, + { + "wallet": self.nodes[0], + "symbol": "TSLA", + "name": "Tesla", + "collateralAddress": collateral, + "amount": 1000000 + }, + { + "wallet": self.nodes[0], + "symbol": "LTC", + "name": "Litecoin", + "collateralAddress": collateral, + "amount": 1000000 + }, + { + "wallet": self.nodes[0], + "symbol": "USDC", + "name": "USD Coin", + "collateralAddress": collateral, + "amount": 1000000 + }, + ] + self.setup_tokens(tokens) + disconnect_nodes(self.nodes[0], 1) + + symbolDOGE = "DOGE#" + self.get_id_token("DOGE") + symbolTSLA = "TSLA#" + self.get_id_token("TSLA") + symbolDUSD = "dUSD#" + self.get_id_token("dUSD") + symbolLTC = "LTC#" + self.get_id_token("LTC") + symbolUSDC = "USDC#" + self.get_id_token("USDC") + + idDOGE = list(self.nodes[0].gettoken(symbolDOGE).keys())[0] + idTSLA = list(self.nodes[0].gettoken(symbolTSLA).keys())[0] + idLTC = list(self.nodes[0].gettoken(symbolLTC).keys())[0] + + coin = 100000000 + + # Creating poolpairs + owner = self.nodes[0].getnewaddress("", "legacy") + + self.nodes[0].createpoolpair({ + "tokenA": symbolDOGE, + "tokenB": "DFI", + "commission": 0.1, + "status": True, + "ownerAddress": owner + }, []) + self.nodes[0].generate(1) + + self.nodes[0].createpoolpair({ + "tokenA": symbolTSLA, + "tokenB": symbolDUSD, + "commission": 0.1, + "status": True, + "ownerAddress": owner + }, []) + self.nodes[0].generate(1) + + self.nodes[0].createpoolpair({ + "tokenA": symbolLTC, + "tokenB": "DFI", + "commission": 0.1, + "status": True, + "ownerAddress": owner + }, []) + self.nodes[0].generate(1) + + self.nodes[0].createpoolpair({ + "tokenA": symbolDOGE, + "tokenB": symbolUSDC, + "commission": 0.1, + "status": True, + "ownerAddress": owner + }, []) + self.nodes[0].generate(1) + + self.nodes[0].createpoolpair({ + "tokenA": symbolLTC, + "tokenB": symbolUSDC, + "commission": 0.1, + "status": True, + "ownerAddress": owner + }, []) + self.nodes[0].generate(1) + + # Tokenise DFI + self.nodes[0].utxostoaccount({collateral: "900@0"}) + self.nodes[0].generate(1) + + # Set up addresses for swapping + source = self.nodes[0].getnewaddress("", "legacy") + destination = self.nodes[0].getnewaddress("", "legacy") + self.nodes[0].accounttoaccount(collateral, {source: "100@" + symbolLTC}) + self.nodes[0].generate(1) + + # Try a swap before liquidity added + ltc_to_doge_from = 10 + try: + self.nodes[0].poolswap({ + "from": source, + "tokenFrom": symbolLTC, + "amountFrom": ltc_to_doge_from, + "to": destination, + "tokenTo": symbolDOGE, + }, []) + except JSONRPCException as e: + errorString = e.error['message'] + assert('"LTC-DFI":"Lack of liquidity."' in errorString) + assert('"LTC-USDC":"Lack of liquidity."' in errorString) + + # Add pool liquidity + self.nodes[0].addpoolliquidity({ + collateral: ["1000@" + symbolDOGE, "200@DFI"] + }, collateral, []) + self.nodes[0].generate(1) + + self.nodes[0].addpoolliquidity({ + collateral: ["100@" + symbolTSLA, "30000@" + symbolDUSD] + }, collateral, []) + self.nodes[0].generate(1) + + self.nodes[0].addpoolliquidity({ + collateral: ["100@" + symbolLTC, "500@DFI"] + }, collateral, []) + self.nodes[0].generate(1) + + self.nodes[0].poolswap({ + "from": source, + "tokenFrom": symbolLTC, + "amountFrom": ltc_to_doge_from, + "to": destination, + "tokenTo": symbolDOGE, + }, []) + self.nodes[0].generate(1) + + # Check source + source_balance = self.nodes[0].getaccount(source, {}, True) + assert_equal(source_balance[idLTC], Decimal('90.00000000')) + assert_equal(len(source_balance), 1) + + # Check destination + dest_balance = self.nodes[0].getaccount(destination, {}, True) + doge_received = dest_balance[idDOGE] + ltc_per_doge = round((ltc_to_doge_from * coin) / (doge_received * coin), 8) + assert_equal(dest_balance[idDOGE], doge_received) + assert_equal(len(dest_balance), 1) + + # Reset swap and try again with max price as expected + self.nodes[0].invalidateblock(self.nodes[0].getblockhash(self.nodes[0].getblockcount())) + self.nodes[0].clearmempool() + + self.nodes[0].poolswap({ + "from": source, + "tokenFrom": symbolLTC, + "amountFrom": ltc_to_doge_from, + "to": destination, + "tokenTo": symbolDOGE, + "maxPrice": ltc_per_doge + }, []) + self.nodes[0].generate(1) + + # Check source + source_balance = self.nodes[0].getaccount(source, {}, True) + assert_equal(source_balance[idLTC], Decimal('90.00000000')) + assert_equal(len(source_balance), 1) + + # Check destination + dest_balance = self.nodes[0].getaccount(destination, {}, True) + assert_equal(dest_balance[idDOGE], doge_received) + assert_equal(len(dest_balance), 1) + + # Reset swap and try again with max price as expected less one Satoshi + self.nodes[0].invalidateblock(self.nodes[0].getblockhash(self.nodes[0].getblockcount())) + self.nodes[0].clearmempool() + + try: + self.nodes[0].poolswap({ + "from": source, + "tokenFrom": symbolLTC, + "amountFrom": ltc_to_doge_from, + "to": destination, + "tokenTo": symbolDOGE, + "maxPrice": ltc_per_doge - Decimal('0.00000001'), + }, []) + except JSONRPCException as e: + errorString = e.error['message'] + assert('"DOGE-DFI":"Price is higher than indicated."' in errorString) + assert('"LTC-USDC":"Lack of liquidity."' in errorString) + + # Add better route for swap with double amount + self.nodes[0].addpoolliquidity({ + collateral: ["100@" + symbolLTC, "500@" + symbolUSDC] + }, collateral, []) + self.nodes[0].generate(1) + + self.nodes[0].addpoolliquidity({ + collateral: ["2000@" + symbolDOGE, "200@" + symbolUSDC] + }, collateral, []) + self.nodes[0].generate(1) + + self.nodes[0].poolswap({ + "from": source, + "tokenFrom": symbolLTC, + "amountFrom": ltc_to_doge_from, + "to": destination, + "tokenTo": symbolDOGE, + "maxPrice": ltc_per_doge + }, []) + self.nodes[0].generate(1) + + # Check source + source_balance = self.nodes[0].getaccount(source, {}, True) + assert_equal(source_balance[idLTC], Decimal('90.00000000')) + assert_equal(len(source_balance), 1) + + # Check destination + dest_balance = self.nodes[0].getaccount(destination, {}, True) + assert_equal(dest_balance[idDOGE], doge_received * 2) + assert_equal(len(dest_balance), 1) + + # Set up addresses for swapping + source = self.nodes[0].getnewaddress("", "legacy") + destination = self.nodes[0].getnewaddress("", "legacy") + self.nodes[0].accounttoaccount(collateral, {source: "10@" + symbolTSLA}) + self.nodes[0].generate(1) + + # Let's move from TSLA to LTC + tsla_to_ltc_from = 1 + errorString = "" + try: + self.nodes[0].poolswap({ + "from": source, + "tokenFrom": symbolTSLA, + "amountFrom": tsla_to_ltc_from, + "to": destination, + "tokenTo": symbolLTC + }, []) + except JSONRPCException as e: + errorString = e.error['message'] + assert('Cannot find usable pool pair.' in errorString) + + # Let's add a pool to bridge TSLA-DUSD and LTC-DFI + self.nodes[0].createpoolpair({ + "tokenA": symbolDUSD, + "tokenB": "DFI", + "commission": 0.1, + "status": True, + "ownerAddress": owner + }, []) + self.nodes[0].generate(1) + + # Now swap TSLA to + errorString = "" + try: + self.nodes[0].poolswap({ + "from": source, + "tokenFrom": symbolTSLA, + "amountFrom": tsla_to_ltc_from, + "to": destination, + "tokenTo": symbolLTC + }, []) + except JSONRPCException as e: + errorString = e.error['message'] + assert('"dUSD-DFI":"Lack of liquidity."' in errorString) + + # Add some liquidity + self.nodes[0].addpoolliquidity({ + collateral: ["1000@" + symbolDUSD, "200@" + "DFI"] + }, collateral, []) + self.nodes[0].generate(1) + + # Test max price + try: + self.nodes[0].poolswap({ + "from": source, + "tokenFrom": symbolTSLA, + "amountFrom": tsla_to_ltc_from, + "to": destination, + "tokenTo": symbolLTC, + "maxPrice": "0.15311841" + }, []) + except JSONRPCException as e: + errorString = e.error['message'] + assert('"LTC-DFI":"Price is higher than indicated."' in errorString) + + self.nodes[0].poolswap({ + "from": source, + "tokenFrom": symbolTSLA, + "amountFrom": tsla_to_ltc_from, + "to": destination, + "tokenTo": symbolLTC, + "maxPrice": "0.15311842" + }, []) + self.nodes[0].generate(1) + + # Check source + source_balance = self.nodes[0].getaccount(source, {}, True) + assert_equal(source_balance[idTSLA], Decimal('9.00000000')) + assert_equal(len(source_balance), 1) + + # Check destination + dest_balance = self.nodes[0].getaccount(destination, {}, True) + assert_equal(dest_balance[idLTC], Decimal('6.53089259')) + assert_equal(len(dest_balance), 1) + + # Add another route to TSLA + self.nodes[0].createpoolpair({ + "tokenA": symbolDUSD, + "tokenB": symbolUSDC, + "commission": 0.1, + "status": True, + "ownerAddress": owner + }, []) + self.nodes[0].generate(1) + + # Add some liquidity + self.nodes[0].addpoolliquidity({ + collateral: ["1000@" + symbolDUSD, "1000@" + symbolUSDC] + }, collateral, []) + self.nodes[0].generate(1) + + # Set up addresses for swapping + source = self.nodes[0].getnewaddress("", "legacy") + destination = self.nodes[0].getnewaddress("", "legacy") + self.nodes[0].accounttoaccount(collateral, {source: "10@" + symbolTSLA}) + self.nodes[0].generate(1) + + # Test max price + try: + self.nodes[0].poolswap({ + "from": source, + "tokenFrom": symbolTSLA, + "amountFrom": tsla_to_ltc_from, + "to": destination, + "tokenTo": symbolLTC, + "maxPrice": "0.03361577" + }, []) + except JSONRPCException as e: + errorString = e.error['message'] + assert('"LTC-DFI":"Price is higher than indicated."' in errorString) + assert('"LTC-USDC":"Price is higher than indicated."' in errorString) + + self.nodes[0].poolswap({ + "from": source, + "tokenFrom": symbolTSLA, + "amountFrom": tsla_to_ltc_from, + "to": destination, + "tokenTo": symbolLTC, + "maxPrice": "0.03361578" + }, []) + self.nodes[0].generate(1) + + # Check source + source_balance = self.nodes[0].getaccount(source, {}, True) + assert_equal(source_balance[idTSLA], Decimal('9.00000000')) + assert_equal(len(source_balance), 1) + + # Check destination + dest_balance = self.nodes[0].getaccount(destination, {}, True) + assert_equal(dest_balance[idLTC], Decimal('29.74793123')) + assert_equal(len(dest_balance), 1) + +if __name__ == '__main__': + PoolPairCompositeTest().main() diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index dcbee34d741..e4acbe2ea03 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -160,6 +160,7 @@ 'feature_any_accounts_to_accounts.py', 'feature_sendtokenstoaddress.py', 'feature_poolswap.py', + 'feature_poolswap_composite.py', 'feature_poolswap_mechanism.py', 'feature_prevent_bad_tx_propagation.py', 'feature_masternode_operator.py',