From 28d23a683a5e2b339ca9d1ba8a17783877a154ef Mon Sep 17 00:00:00 2001 From: Anthony Fieroni Date: Wed, 27 Apr 2022 09:59:08 +0300 Subject: [PATCH] Live dex statistics (#1192) Signed-off-by: Anthony Fieroni --- src/masternodes/govvariables/attributes.cpp | 19 ++++++-- src/masternodes/govvariables/attributes.h | 53 +++++++++++++++++++-- src/masternodes/gv.cpp | 24 +++++++++- src/masternodes/mn_checks.cpp | 39 +++++++++++++-- src/validation.cpp | 10 ++-- test/functional/feature_poolswap.py | 43 +++++++++++++++-- 6 files changed, 170 insertions(+), 18 deletions(-) diff --git a/src/masternodes/govvariables/attributes.cpp b/src/masternodes/govvariables/attributes.cpp index cbf62b89101..d809a138393 100644 --- a/src/masternodes/govvariables/attributes.cpp +++ b/src/masternodes/govvariables/attributes.cpp @@ -152,6 +152,7 @@ const std::map>& ATTRIBUTES::displayKeys {EconomyKeys::DFIP2203Current, "dfip2203_current"}, {EconomyKeys::DFIP2203Burned, "dfip2203_burned"}, {EconomyKeys::DFIP2203Minted, "dfip2203_minted"}, + {EconomyKeys::DexTokens, "dex"}, } }, }; @@ -425,7 +426,7 @@ Res ATTRIBUTES::RefundFuturesContracts(CCustomCSView &mnview, const uint32_t hei } } - attributes[liveKey] = balances; + SetValue(liveKey, std::move(balances)); return Res::Ok(); } @@ -453,11 +454,11 @@ Res ATTRIBUTES::Import(const UniValue & val) { } else { newAttr.key = TokenKeys::PaybackDFIFeePCT; } - attributes[newAttr] = value; + SetValue(newAttr, value); return Res::Ok(); } } - attributes[attribute] = value; + SetValue(attribute, value); return Res::Ok(); } ); @@ -506,6 +507,18 @@ UniValue ATTRIBUTES::Export() const { result.pushKV("paybackfees", AmountsToJSON(paybacks->tokensFee.balances)); result.pushKV("paybacktokens", AmountsToJSON(paybacks->tokensPayback.balances)); ret.pushKV(key, result); + } else if (auto balances = boost::get(&attribute.second)) { + for (const auto& pool : *balances) { + auto& dexTokenA = pool.second.totalTokenA; + auto& dexTokenB = pool.second.totalTokenB; + auto poolkey = KeyBuilder(key, pool.first.v); + ret.pushKV(KeyBuilder(poolkey, "total_commission_a"), ValueFromUint(dexTokenA.commissions)); + ret.pushKV(KeyBuilder(poolkey, "total_commission_b"), ValueFromUint(dexTokenB.commissions)); + ret.pushKV(KeyBuilder(poolkey, "fee_burn_a"), ValueFromUint(dexTokenA.feeburn)); + ret.pushKV(KeyBuilder(poolkey, "fee_burn_b"), ValueFromUint(dexTokenB.feeburn)); + ret.pushKV(KeyBuilder(poolkey, "total_swap_a"), ValueFromUint(dexTokenA.swaps)); + ret.pushKV(KeyBuilder(poolkey, "total_swap_b"), ValueFromUint(dexTokenB.swaps)); + } } } catch (const std::out_of_range&) { // Should not get here, that's mean maps are mismatched diff --git a/src/masternodes/govvariables/attributes.h b/src/masternodes/govvariables/attributes.h index 604b3f2dd27..0a3a318b42b 100644 --- a/src/masternodes/govvariables/attributes.h +++ b/src/masternodes/govvariables/attributes.h @@ -32,6 +32,7 @@ enum EconomyKeys : uint8_t { DFIP2203Current = 'c', DFIP2203Burned = 'd', DFIP2203Minted = 'e', + DexTokens = 'f', }; enum DFIPKeys : uint8_t { @@ -113,8 +114,38 @@ struct CTokenPayback { ResVal GetFutureSwapContractAddress(); +struct CDexTokenInfo { + + struct CTokenInfo { + uint64_t swaps; + uint64_t feeburn; + uint64_t commissions; + + ADD_SERIALIZE_METHODS; + + template + inline void SerializationOp(Stream& s, Operation ser_action) { + READWRITE(swaps); + READWRITE(feeburn); + READWRITE(commissions); + } + }; + + CTokenInfo totalTokenA; + CTokenInfo totalTokenB; + + ADD_SERIALIZE_METHODS; + + template + inline void SerializationOp(Stream& s, Operation ser_action) { + READWRITE(totalTokenA); + READWRITE(totalTokenB); + } +}; + +using CDexBalances = std::map; using CAttributeType = boost::variant; -using CAttributeValue = boost::variant; +using CAttributeValue = boost::variant; class ATTRIBUTES : public GovVariable, public AutoRegistrator { @@ -144,6 +175,21 @@ class ATTRIBUTES : public GovVariable, public AutoRegistrator + void SetValue(const K& key, T&& value) { + static_assert(std::is_convertible_v); + static_assert(std::is_convertible_v); + changed.insert(key); + attributes[key] = std::forward(value); + } + + template + void EraseKey(const K& key) { + static_assert(std::is_convertible_v); + changed.insert(key); + attributes.erase(key); + } + template [[nodiscard]] bool CheckKey(const K& key) const { static_assert(std::is_convertible_v); @@ -158,10 +204,11 @@ class ATTRIBUTES : public GovVariable, public AutoRegistrator attributes; - private: + friend class CGovView; bool futureBlockUpdated{}; + std::set changed; + std::map attributes; // Defined allowed arguments static const std::map& allowedVersions(); diff --git a/src/masternodes/gv.cpp b/src/masternodes/gv.cpp index d1eb43ac3f1..55201edb66b 100644 --- a/src/masternodes/gv.cpp +++ b/src/masternodes/gv.cpp @@ -15,7 +15,29 @@ Res CGovView::SetVariable(GovVariable const & var) { - return WriteBy(var.GetName(), var) ? Res::Ok() : Res::Err("can't write to DB"); + auto WriteVar = [this](GovVariable const & var) { + return WriteBy(var.GetName(), var) ? Res::Ok() : Res::Err("can't write to DB"); + }; + if (var.GetName() != "ATTRIBUTES") { + return WriteVar(var); + } + auto attributes = GetAttributes(); + if (!attributes) { + return WriteVar(var); + } + auto& current = dynamic_cast(var); + if (current.changed.empty()) { + return Res::Ok(); + } + for (auto& key : current.changed) { + auto it = current.attributes.find(key); + if (it == current.attributes.end()) { + attributes->attributes.erase(key); + } else { + attributes->attributes[key] = it->second; + } + } + return WriteVar(*attributes); } std::shared_ptr CGovView::GetVariable(std::string const & name) const diff --git a/src/masternodes/mn_checks.cpp b/src/masternodes/mn_checks.cpp index e7167c133b1..9802848f968 100644 --- a/src/masternodes/mn_checks.cpp +++ b/src/masternodes/mn_checks.cpp @@ -1584,7 +1584,7 @@ class CCustomTxApplyVisitor : public CCustomTxVisitor balances.Add(obj.source); } - attributes->attributes[liveKey] = balances; + attributes->SetValue(liveKey, balances); mnview.SetVariable(*attributes); @@ -3175,7 +3175,7 @@ class CCustomTxApplyVisitor : public CCustomTxVisitor balances.Add(CTokenAmount{loanTokenId, subAmount}); balances.Add(CTokenAmount{paybackTokenId, penalty}); - attributes->attributes[liveKey] = balances; + attributes->SetValue(liveKey, balances); LogPrint(BCLog::LOAN, "CLoanPaybackLoanMessage(): Burning interest and loan in %s directly - total loan %lld (%lld %s), height - %d\n", paybackToken->symbol, subLoan + subInterest, subInToken, paybackToken->symbol, height); @@ -3188,7 +3188,7 @@ class CCustomTxApplyVisitor : public CCustomTxVisitor balances.tokensPayback.Add(CTokenAmount{loanTokenId, subAmount}); balances.tokensFee.Add(CTokenAmount{paybackTokenId, penalty}); - attributes->attributes[liveKey] = balances; + attributes->SetValue(liveKey, balances); LogPrint(BCLog::LOAN, "CLoanPaybackLoanMessage(): Swapping %s to DFI and burning it - total loan %lld (%lld %s), height - %d\n", paybackToken->symbol, subLoan + subInterest, subInToken, paybackToken->symbol, height); @@ -4064,6 +4064,14 @@ Res CPoolSwap::ExecuteSwap(CCustomCSView& view, std::vector poolIDs, boo mnview.Flush(); } + auto attributes = view.GetAttributes(); + if (!attributes) { + attributes = std::make_shared(); + } + + CDataStructureV0 dexKey{AttributeTypes::Live, ParamIDs::Economy, EconomyKeys::DexTokens}; + auto dexBalances = attributes->GetValue(dexKey, CDexBalances{}); + // Set amount to be swapped in pool CTokenAmount swapAmountResult{obj.idTokenFrom, obj.amountFrom}; @@ -4101,6 +4109,18 @@ Res CPoolSwap::ExecuteSwap(CCustomCSView& view, std::vector poolIDs, boo auto dexfeeInPct = view.GetDexFeeInPct(currentID, swapAmount.nTokenId); + auto& balances = dexBalances[currentID]; + auto forward = swapAmount.nTokenId == pool->idTokenA; + + auto& totalTokenA = forward ? balances.totalTokenA : balances.totalTokenB; + auto& totalTokenB = forward ? balances.totalTokenB : balances.totalTokenA; + + const auto& reserveAmount = forward ? pool->reserveA : pool->reserveB; + const auto& blockCommission = forward ? pool->blockCommissionA : pool->blockCommissionB; + + const auto initReserveAmount = reserveAmount; + const auto initBlockCommission = blockCommission; + // Perform swap poolResult = pool->Swap(swapAmount, dexfeeInPct, poolPrice, [&] (const CTokenAmount& dexfeeInAmount, const CTokenAmount& tokenAmount) { // Save swap amount for next loop @@ -4148,6 +4168,7 @@ Res CPoolSwap::ExecuteSwap(CCustomCSView& view, std::vector poolIDs, boo if (!res) { return res; } + totalTokenA.feeburn += dexfeeInAmount.nValue; } // burn the dex out amount @@ -4156,6 +4177,14 @@ Res CPoolSwap::ExecuteSwap(CCustomCSView& view, std::vector poolIDs, boo if (!res) { return res; } + totalTokenB.feeburn += dexfeeOutAmount.nValue; + } + + totalTokenA.swaps += (reserveAmount - initReserveAmount); + totalTokenA.commissions += (blockCommission - initBlockCommission); + + if (lastSwap && obj.to == Params().GetConsensus().burnAddress) { + totalTokenB.feeburn += swapAmountResult.nValue; } return res; @@ -4176,6 +4205,10 @@ Res CPoolSwap::ExecuteSwap(CCustomCSView& view, std::vector poolIDs, boo } } + if (!testOnly) { + attributes->SetValue(dexKey, std::move(dexBalances)); + view.SetVariable(*attributes); + } // Assign to result for loop testing best pool swap result result = swapAmountResult.nValue; diff --git a/src/validation.cpp b/src/validation.cpp index 978997671a2..2b29e9fd67b 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -2833,8 +2833,8 @@ bool CChainState::ConnectBlock(const CBlock& block, CValidationState& state, CBl CCustomCSView govCache(cache); // Add to existing ATTRIBUTES instead of overwriting. if (var->GetName() == "ATTRIBUTES") { - auto govVar = mnview.GetVariable(var->GetName()); - if (govVar->Import(var->Export()) && govVar->Validate(govCache) && govVar->Apply(govCache, pindex->nHeight) && govCache.SetVariable(*var)) { + auto govVar = cache.GetVariable(var->GetName()); + if (govVar->Import(var->Export()) && govVar->Validate(govCache) && govVar->Apply(govCache, pindex->nHeight) && govCache.SetVariable(*govVar)) { govCache.Flush(); } } else if (var->Validate(govCache) && var->Apply(govCache, pindex->nHeight) && govCache.SetVariable(*var)) { @@ -3435,11 +3435,11 @@ void CChainState::ProcessFutures(const CBlockIndex* pindex, CCustomCSView& cache cache.EraseFuturesUserValues(key); } - attributes->attributes[burnKey] = burned; - attributes->attributes[mintedKey] = minted; + attributes->SetValue(burnKey, std::move(burned)); + attributes->SetValue(mintedKey, std::move(minted)); if (!unpaidContracts.empty()) { - attributes->attributes[liveKey] = balances; + attributes->SetValue(liveKey, std::move(balances)); } cache.SetVariable(*attributes); diff --git a/test/functional/feature_poolswap.py b/test/functional/feature_poolswap.py index 7566a205ff2..dbe1cb31975 100755 --- a/test/functional/feature_poolswap.py +++ b/test/functional/feature_poolswap.py @@ -196,6 +196,11 @@ def run_test(self): # 7 Sync self.sync_blocks([self.nodes[0], self.nodes[2]]) + attributes = self.nodes[0].getgov('ATTRIBUTES')['ATTRIBUTES'] + # silver is tokenB + assert_equal(attributes['v0/live/economy/dex/%s/total_swap_b'%(idGS)], Decimal('9.0')) + assert_equal(attributes['v0/live/economy/dex/%s/total_commission_b'%(idGS)], Decimal('1.0')) + # 8 Checking that poolswap is correct goldCheckN0 = self.nodes[2].getaccount(accountGN0, {}, True)[idGold] silverCheckN0 = self.nodes[2].getaccount(accountGN0, {}, True)[idSilver] @@ -253,6 +258,10 @@ def run_test(self): ) self.nodes[0].generate(1) + attributes = self.nodes[0].getgov('ATTRIBUTES')['ATTRIBUTES'] + assert_equal(attributes['v0/live/economy/dex/%s/total_swap_b'%(idGS)], Decimal('189.0')) + assert_equal(attributes['v0/live/economy/dex/%s/total_commission_b'%(idGS)], Decimal('21.0')) + maxPrice = self.nodes[0].listpoolpairs()['1']['reserveB/reserveA'] # exchange tokens each other should work self.nodes[0].poolswap({ @@ -274,6 +283,12 @@ def run_test(self): }) self.nodes[0].generate(1) + attributes = self.nodes[0].getgov('ATTRIBUTES')['ATTRIBUTES'] + assert_equal(attributes['v0/live/economy/dex/%s/total_swap_a'%(idGS)], Decimal('180.0')) + assert_equal(attributes['v0/live/economy/dex/%s/total_commission_a'%(idGS)], Decimal('20.0')) + assert_equal(attributes['v0/live/economy/dex/%s/total_swap_b'%(idGS)], Decimal('369.0')) + assert_equal(attributes['v0/live/economy/dex/%s/total_commission_b'%(idGS)], Decimal('41.0')) + # Test fort canning max price change disconnect_nodes(self.nodes[0], 1) disconnect_nodes(self.nodes[0], 2) @@ -390,6 +405,12 @@ def run_test(self): }) self.nodes[0].generate(1) + attributes = self.nodes[0].getgov('ATTRIBUTES')['ATTRIBUTES'] + assert_equal(attributes['v0/live/economy/dex/%s/total_swap_a'%(idBL)], Decimal('0.0')) + assert_equal(attributes['v0/live/economy/dex/%s/total_commission_a'%(idBL)], Decimal('0.0')) + assert_equal(attributes['v0/live/economy/dex/%s/total_swap_b'%(idBL)], Decimal('0.00000189')) + assert_equal(attributes['v0/live/economy/dex/%s/total_commission_b'%(idBL)], Decimal('1E-8')) + assert_equal(self.nodes[0].getaccount(new_dest, {}, True)[idBTC], Decimal('0.00000002')) # Reset swap and move to Fort Canning Park Height and try swap again @@ -428,7 +449,9 @@ def run_test(self): self.nodes[0].setgov({"ATTRIBUTES":{'v0/poolpairs/%s/token_a_fee_pct'%(idGS): '0.05', 'v0/poolpairs/%s/token_b_fee_pct'%(idGS): '0.08'}}) self.nodes[0].generate(1) - assert_equal(self.nodes[0].getgov('ATTRIBUTES')['ATTRIBUTES'], {'v0/poolpairs/%s/token_a_fee_pct'%(idGS): '0.05', 'v0/poolpairs/%s/token_b_fee_pct'%(idGS): '0.08'}) + attributes = self.nodes[0].getgov('ATTRIBUTES')['ATTRIBUTES'] + assert_equal(attributes['v0/poolpairs/%s/token_a_fee_pct'%(idGS)], '0.05') + assert_equal(attributes['v0/poolpairs/%s/token_b_fee_pct'%(idGS)], '0.08') result = self.nodes[0].getpoolpair(idGS) assert_equal(result[idGS]['dexFeePctTokenA'], Decimal('0.05')) @@ -461,11 +484,17 @@ def run_test(self): assert_equal(self.nodes[0].getburninfo()['dexfeetokens'].sort(), ['%.8f'%(dexinfee)+symbolGOLD, '%.8f'%(dexoutfee)+symbolSILVER].sort()) + attributes = self.nodes[0].getgov('ATTRIBUTES')['ATTRIBUTES'] + assert_equal(attributes['v0/live/economy/dex/%s/fee_burn_a'%(idGS)], dexinfee) + assert_equal(attributes['v0/live/economy/dex/%s/fee_burn_b'%(idGS)], dexoutfee) + # set 1% token dex fee and commission self.nodes[0].setgov({"ATTRIBUTES":{'v0/poolpairs/%s/token_a_fee_pct'%(idGS): '0.01', 'v0/poolpairs/%s/token_b_fee_pct'%(idGS): '0.01'}}) self.nodes[0].generate(1) - assert_equal(self.nodes[0].getgov('ATTRIBUTES')['ATTRIBUTES'], {'v0/poolpairs/%s/token_a_fee_pct'%(idGS): '0.01', 'v0/poolpairs/%s/token_b_fee_pct'%(idGS): '0.01'}) + attributes = self.nodes[0].getgov('ATTRIBUTES')['ATTRIBUTES'] + assert_equal(attributes['v0/poolpairs/%s/token_a_fee_pct'%(idGS)], '0.01') + assert_equal(attributes['v0/poolpairs/%s/token_b_fee_pct'%(idGS)], '0.01') self.nodes[0].updatepoolpair({"pool": "GS", "commission": 0.01}) self.nodes[0].generate(1) @@ -489,7 +518,11 @@ def run_test(self): self.nodes[0].setgov({"ATTRIBUTES":{'v0/poolpairs/%s/token_a_fee_pct'%(idBL): '0.01', 'v0/poolpairs/%s/token_b_fee_pct'%(idBL): '0.01'}}) self.nodes[0].generate(1) - assert_equal(self.nodes[0].getgov('ATTRIBUTES')['ATTRIBUTES'], {'v0/poolpairs/%s/token_a_fee_pct'%(idGS): '0.01', 'v0/poolpairs/%s/token_b_fee_pct'%(idGS): '0.01', 'v0/poolpairs/%s/token_a_fee_pct'%(idBL): '0.01', 'v0/poolpairs/%s/token_b_fee_pct'%(idBL): '0.01'}) + attributes = self.nodes[0].getgov('ATTRIBUTES')['ATTRIBUTES'] + assert_equal(attributes['v0/poolpairs/%s/token_a_fee_pct'%(idGS)], '0.01') + assert_equal(attributes['v0/poolpairs/%s/token_b_fee_pct'%(idGS)], '0.01') + assert_equal(attributes['v0/poolpairs/%s/token_a_fee_pct'%(idBL)], '0.01') + assert_equal(attributes['v0/poolpairs/%s/token_b_fee_pct'%(idBL)], '0.01') self.nodes[0].invalidateblock(self.nodes[0].getblockhash(self.nodes[0].getblockcount())) self.nodes[0].clearmempool() @@ -528,6 +561,10 @@ def run_test(self): dexoutfee = round(trunc(amountA * Decimal(0.05) * coin) / coin, 8) assert_equal(round(amountA - Decimal(dexoutfee), 8), round(swapped, 8)) + attributes = self.nodes[0].getgov('ATTRIBUTES')['ATTRIBUTES'] + assert_equal(attributes['v0/live/economy/dex/%s/fee_burn_b'%(idBL)], round(dexinfee, 8)) + assert_equal(attributes['v0/live/economy/dex/%s/fee_burn_a'%(idBL)], Decimal(str(round(dexoutfee, 8)))) + # REVERTING: #======================== print ("Reverting...")