Skip to content

Commit

Permalink
Feature composite swap (#676)
Browse files Browse the repository at this point in the history
* 16 char pool pair

* Composite Swap

* Fix lint errors

* Execute CalculateOwnerRewards via view

* Add intermediate view

* Use intermediate view inside swap loop
  • Loading branch information
Bushstar authored Aug 23, 2021
1 parent ead30df commit 6c36d77
Show file tree
Hide file tree
Showing 9 changed files with 735 additions and 26 deletions.
227 changes: 208 additions & 19 deletions src/masternodes/mn_checks.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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<int>(height));
return CPoolSwap(obj, height).ExecuteSwap(mnview, obj.poolIDs);
}

Res operator()(const CLiquidityMessage& obj) const {
Expand Down Expand Up @@ -2109,3 +2100,201 @@ bool IsMempooledCustomTxCreate(const CTxMemPool & pool, const uint256 & txid)
}
return false;
}

std::vector<DCT_ID> CPoolSwap::CalculateSwaps(CCustomCSView& view) {

// For tokens to be traded get all pairs and pool IDs
std::multimap<uint32_t, DCT_ID> 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<uint32_t, DCT_ID> commonPairs;
set_intersection(fromPoolsID.begin(), fromPoolsID.end(), toPoolsID.begin(), toPoolsID.end(),
std::inserter(commonPairs, commonPairs.begin()),
[](std::pair<uint32_t, DCT_ID> a, std::pair<uint32_t, DCT_ID> b) {
return a.first < b.first;
});

// Loop through all common pairs and record direct pool to pool swaps
std::vector<std::vector<DCT_ID>> 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<std::vector<DCT_ID>, 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<DCT_ID> 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<std::pair<DCT_ID, CPoolPair> > 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<CPoolPair> 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<int>(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;
}
17 changes: 17 additions & 0 deletions src/masternodes/mn_checks.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class CCoinsViewCache;

class CCustomCSView;
class CAccountsHistoryView;
class CCustomTxVisitor;

static const std::vector<unsigned char> DfTxMarker = {'D', 'f', 'T', 'x'}; // 44665478

Expand Down Expand Up @@ -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<std::pair<std::string, std::string>> errors;

CPoolSwap(const CPoolSwapMessage& obj, uint32_t height)
: obj(obj), height(height) {}

std::vector<DCT_ID> CalculateSwaps(CCustomCSView& view);
Res ExecuteSwap(CCustomCSView& view, std::vector<DCT_ID> poolIDs);
};

#endif // DEFI_MASTERNODES_MN_CHECKS_H
12 changes: 12 additions & 0 deletions src/masternodes/poolpairs.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<CAmount>::max(), std::numeric_limits<CAmount>::max()};

struct CPoolSwapMessage {
CScript from, to;
DCT_ID idTokenFrom, idTokenTo;
CAmount amountFrom;
PoolPrice maxPrice;
std::vector<DCT_ID> poolIDs{};

std::string ToString() const {
return "(" + from.GetHex() + ":" + std::to_string(amountFrom) +"@"+ idTokenFrom.ToString() + "->" + to.GetHex() + ":?@" + idTokenTo.ToString() +")";
Expand All @@ -61,6 +68,11 @@ struct CPoolSwapMessage {
READWRITE(to);
READWRITE(idTokenTo);
READWRITE(maxPrice);

// Only available after FortCanning
if (!s.eof()) {
READWRITE(poolIDs);
}
}
};

Expand Down
32 changes: 29 additions & 3 deletions src/masternodes/rpc_poolpair.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<CAmount>::max();
poolSwapMsg.maxPrice.fraction = std::numeric_limits<CAmount>::max();
}
}
}
Expand Down Expand Up @@ -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<unsigned char>(CustomTxType::PoolSwap)
<< poolSwapMsg;
Expand All @@ -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;
Expand Down
1 change: 1 addition & 0 deletions src/masternodes/tokens.h
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions src/rpc/blockchain.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,7 @@ static UniValue clearmempool(const JSONRPCRequest& request)

std::vector<uint256> vtxid;
mempool.queryHashes(vtxid);
mempool.accountsView().Discard();

UniValue removed(UniValue::VARR);
for (const uint256& hash : vtxid)
Expand Down
Loading

0 comments on commit 6c36d77

Please sign in to comment.