diff --git a/src/masternodes/rpc_accounts.cpp b/src/masternodes/rpc_accounts.cpp index 9840c289a27..08cf854e77f 100644 --- a/src/masternodes/rpc_accounts.cpp +++ b/src/masternodes/rpc_accounts.cpp @@ -963,46 +963,74 @@ class CScopeAccountReverter { UniValue listaccounthistory(const JSONRPCRequest& request) { auto pwallet = GetWallet(request); - RPCHelpMan{"listaccounthistory", - "\nReturns information about account history.\n", - { - {"owner", RPCArg::Type::STR, RPCArg::Optional::OMITTED, - "Single account ID (CScript or address) or reserved words: \"mine\" - to list history for all owned accounts or \"all\" to list whole DB (default = \"mine\")."}, - {"options", RPCArg::Type::OBJ, RPCArg::Optional::OMITTED, "", - { - {"maxBlockHeight", RPCArg::Type::NUM, RPCArg::Optional::OMITTED, - "Optional height to iterate from (downto genesis block), (default = chaintip)."}, - {"depth", RPCArg::Type::NUM, RPCArg::Optional::OMITTED, - "Maximum depth, from the genesis block is the default"}, - {"no_rewards", RPCArg::Type::BOOL, RPCArg::Optional::OMITTED, - "Filter out rewards"}, - {"token", RPCArg::Type::STR, RPCArg::Optional::OMITTED, - "Filter by token"}, - {"txtype", RPCArg::Type::STR, RPCArg::Optional::OMITTED, - "Filter by transaction type, supported letter from {CustomTxType}"}, - {"limit", RPCArg::Type::NUM, RPCArg::Optional::OMITTED, - "Maximum number of records to return, 100 by default"}, - {"txn", RPCArg::Type::NUM, RPCArg::Optional::OMITTED, - "Order in block, unlimited by default"}, - {"format", RPCArg::Type::STR, RPCArg::Optional::OMITTED, - "Return amounts with the following: 'id' -> @id; (default)'symbol' -> @symbol"}, - - }, - }, - }, - RPCResult{ - "[{},{}...] (array) Objects with account history information\n" - }, - RPCExamples{ - HelpExampleCli("listaccounthistory", "all '{\"maxBlockHeight\":160,\"depth\":10}'") - + HelpExampleRpc("listaccounthistory", "address false") - }, - }.Check(request); + RPCHelpMan{ + "listaccounthistory", + "\nReturns information about account history.\n", + { + {"owner", + RPCArg::Type::STR, + RPCArg::Optional::OMITTED, + "Single account ID (CScript or address) or reserved words: \"mine\" - to list history for all owned " + "accounts or \"all\" to list whole DB (default = \"mine\")."}, + { + "options", + RPCArg::Type::OBJ, + RPCArg::Optional::OMITTED, + "", + { + {"maxBlockHeight", + RPCArg::Type::NUM, + RPCArg::Optional::OMITTED, + "Optional height to iterate from (downto genesis block), (default = chaintip)."}, + {"depth", + RPCArg::Type::NUM, + RPCArg::Optional::OMITTED, + "Maximum depth, from the genesis block is the default"}, + {"no_rewards", RPCArg::Type::BOOL, RPCArg::Optional::OMITTED, "Filter out rewards"}, + {"token", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "Filter by token"}, + { + "txtype", + RPCArg::Type::STR, + RPCArg::Optional::OMITTED, + "Filter by transaction type, supported letter from {CustomTxType}. Ignored if txtypes is provided", + }, + {"txtypes", + RPCArg::Type::ARR, + RPCArg::Optional::OMITTED, + "Filter multiple transaction types, supported letter from {CustomTxType}", + { + { + "Transaction Type", + RPCArg::Type::STR, + RPCArg::Optional::OMITTED, + "letter from {CustomTxType}", + }, + }}, + {"limit", + RPCArg::Type::NUM, + RPCArg::Optional::OMITTED, + "Maximum number of records to return, 100 by default"}, + {"start", + RPCArg::Type::NUM, + RPCArg::Optional::OMITTED, + "Number of entries to skip"}, + {"including_start", + RPCArg::Type::BOOL, + RPCArg::Optional::OMITTED, + "If true, then iterate including starting position. False by default"}, + {"txn", RPCArg::Type::NUM, RPCArg::Optional::OMITTED, "Order in block, unlimited by default"}, + {"format", + RPCArg::Type::STR, + RPCArg::Optional::OMITTED, + "Return amounts with the following: 'id' -> @id; (default)'symbol' -> @symbol"}, - std::string accounts = "mine"; - if (request.params.size() > 0) { - accounts = request.params[0].getValStr(); + }, + }, }, + RPCResult{"[{},{}...] (array) Objects with account history information\n"}, + RPCExamples{HelpExampleCli("listaccounthistory", "all '{\"maxBlockHeight\":160,\"depth\":10}'") + + HelpExampleRpc("listaccounthistory", "address false")}, } + .Check(request); if (!paccountHistoryDB) { throw JSONRPCError(RPC_INVALID_REQUEST, "-acindex is needed for account history"); @@ -1015,7 +1043,10 @@ UniValue listaccounthistory(const JSONRPCRequest& request) { bool noRewards = false; std::string tokenFilter; uint32_t limit = 100; - auto txType = CustomTxType::None; + std::set txTypes{}; + bool hasTxFilter = false; + uint32_t start{0}; + bool includingStart = true; uint32_t txn = std::numeric_limits::max(); AmountFormat format = AmountFormat::Symbol; @@ -1028,7 +1059,10 @@ UniValue listaccounthistory(const JSONRPCRequest& request) { {"no_rewards", UniValueType(UniValue::VBOOL)}, {"token", UniValueType(UniValue::VSTR)}, {"txtype", UniValueType(UniValue::VSTR)}, + {"txtypes", UniValueType(UniValue::VARR)}, {"limit", UniValueType(UniValue::VNUM)}, + {"start", UniValueType(UniValue::VNUM)}, + {"including_start", UniValueType(UniValue::VBOOL)}, {"txn", UniValueType(UniValue::VNUM)}, {"format", UniValueType(UniValue::VSTR)} }, true, true); @@ -1047,18 +1081,39 @@ UniValue listaccounthistory(const JSONRPCRequest& request) { if (!optionsObj["token"].isNull()) { tokenFilter = optionsObj["token"].get_str(); } - - if (!optionsObj["txtype"].isNull()) { + if (!optionsObj["txtypes"].isNull()) { + hasTxFilter = true; + const auto types = optionsObj["txtypes"].get_array().getValues(); + if (!types.empty()) { + for (const auto& type: types) { + auto str = type.get_str(); + if (str.size() == 1) { + txTypes.insert(CustomTxCodeToType(str[0])); + } else { + txTypes.insert(FromString(str)); + } + } + } + } + else if (!optionsObj["txtype"].isNull()) { + hasTxFilter = true; const auto str = optionsObj["txtype"].get_str(); if (str.size() == 1) { - txType = CustomTxCodeToType(str[0]); + txTypes.insert(CustomTxCodeToType(str[0])); } else { - txType = FromString(str); + txTypes.insert(FromString(str)); } } if (!optionsObj["limit"].isNull()) { limit = (uint32_t) optionsObj["limit"].get_int64(); } + if (!optionsObj["start"].isNull()) { + start = (uint32_t) optionsObj["start"].get_int64(); + includingStart = false; + } + if (!optionsObj["including_start"].isNull()) { + includingStart = (uint32_t) optionsObj["including_start"].get_bool(); + } if (limit == 0) { limit = std::numeric_limits::max(); } @@ -1079,28 +1134,42 @@ UniValue listaccounthistory(const JSONRPCRequest& request) { throw JSONRPCError(RPC_INVALID_REQUEST, "format must be one of the following: \"id\", \"symbol\""); } } + + if (!includingStart) + start++; } std::function isMatchOwner = [](CScript const &) { return true; }; - CScript account; + std::string accounts = "mine"; + if (request.params.size() > 0) { + accounts = request.params[0].getValStr(); + } + bool isMine = false; isminetype filter = ISMINE_ALL; + std::set accountSet{CScript{}}; + if (accounts == "mine") { isMine = true; filter = ISMINE_SPENDABLE; } else if (accounts != "all") { - account = DecodeScript(accounts); - isMatchOwner = [&account](CScript const & owner) { - return owner == account; - }; + if (request.params[0].isArray()) { + accountSet.clear(); + for (const auto &acc : request.params[0].get_array().getValues()) { + accountSet.emplace(DecodeScript(acc.get_str())); + } + } else { + accountSet.clear(); + accountSet.emplace(DecodeScript(accounts)); + } } std::set txs; - const bool shouldSearchInWallet = (tokenFilter.empty() || tokenFilter == "DFI") && CustomTxType::None == txType; + const bool shouldSearchInWallet = (tokenFilter.empty() || tokenFilter == "DFI") && !hasTxFilter; auto hasToken = [&tokenFilter](TAmounts const & diffs) { for (auto const & diff : diffs) { @@ -1121,128 +1190,148 @@ UniValue listaccounthistory(const JSONRPCRequest& request) { maxBlockHeight = std::min(maxBlockHeight, uint32_t(::ChainActive().Height())); depth = std::min(depth, maxBlockHeight); - const auto startBlock = maxBlockHeight - depth; - auto shouldSkipBlock = [startBlock, maxBlockHeight](uint32_t blockHeight) { - return startBlock > blockHeight || blockHeight > maxBlockHeight; - }; - - CScript lastOwner; - auto count = limit; - auto lastHeight = maxBlockHeight; + for (const auto &account : accountSet) { + const auto startBlock = maxBlockHeight - depth; + auto shouldSkipBlock = [startBlock, maxBlockHeight](uint32_t blockHeight) { + return startBlock > blockHeight || blockHeight > maxBlockHeight; + }; - auto shouldContinueToNextAccountHistory = [&](AccountHistoryKey const & key, AccountHistoryValue value) -> bool { - if (!isMatchOwner(key.owner)) { - return false; - } + CScript lastOwner; + auto count = limit + start; + auto lastHeight = maxBlockHeight; - std::unique_ptr reverter; - if (!noRewards) { - reverter = std::make_unique(view, key.owner, value.diff); - } + if (!account.empty()) + isMatchOwner = [&account](const CScript &owner) { return owner == account; }; + else + isMatchOwner = [](const CScript &owner) { return true; }; - bool accountRecord = true; - auto workingHeight = key.blockHeight; + auto shouldContinueToNextAccountHistory = [&](AccountHistoryKey const &key, AccountHistoryValue value) -> bool { + if (!isMatchOwner(key.owner)) { + return false; + } - if (shouldSkipBlock(key.blockHeight)) { - // show rewards in interval [startBlock, lastHeight) - if (!noRewards && startBlock > workingHeight) { - accountRecord = false; - workingHeight = startBlock; - } else { - return true; + std::unique_ptr reverter; + if (!noRewards) { + reverter = std::make_unique(view, key.owner, value.diff); } - } - if (isMine && !(IsMineCached(*pwallet, key.owner) & filter)) { - return true; - } + bool accountRecord = true; + auto workingHeight = key.blockHeight; - if (CustomTxType::None != txType && value.category != uint8_t(txType)) { - return true; - } + if (shouldSkipBlock(key.blockHeight)) { + // show rewards in interval [startBlock, lastHeight) + if (!noRewards && startBlock > workingHeight) { + accountRecord = false; + workingHeight = startBlock; + } else { + return true; + } + } - if (isMine) { - // starts new account owned by the wallet - if (lastOwner != key.owner) { - count = limit; - } else if (count == 0) { + if (isMine && !(IsMineCached(*pwallet, key.owner) & filter)) { return true; } - } - - // starting new account - if (account.empty() && lastOwner != key.owner) { - view.Discard(); - lastOwner = key.owner; - lastHeight = maxBlockHeight; - } - if (accountRecord && (tokenFilter.empty() || hasToken(value.diff))) { - auto& array = ret.emplace(workingHeight, UniValue::VARR).first->second; - array.push_back(accounthistoryToJSON(key, value, format)); - if (shouldSearchInWallet) { - txs.insert(value.txid); + if (hasTxFilter && txTypes.find(CustomTxCodeToType(value.category)) == txTypes.end()) { + return true; } - --count; - } - if (!noRewards && count && lastHeight > workingHeight) { - onPoolRewards(view, key.owner, workingHeight, lastHeight, - [&](int32_t height, DCT_ID poolId, RewardType type, CTokenAmount amount) { - if (tokenFilter.empty() || hasToken({{amount.nTokenId, amount.nValue}})) { - auto& array = ret.emplace(height, UniValue::VARR).first->second; - array.push_back(rewardhistoryToJSON(key.owner, height, poolId, type, amount, format)); - count ? --count : 0; - } + if (isMine) { + // starts new account owned by the wallet + if (lastOwner != key.owner) { + count = limit + start; + } else if (count == 0) { + return true; } - ); - } - - lastHeight = workingHeight; + } - return count != 0 || isMine; - }; + // starting new account + if (account.empty() && lastOwner != key.owner) { + view.Discard(); + lastOwner = key.owner; + lastHeight = maxBlockHeight; + } - if (!noRewards && !account.empty()) { - // revert previous tx to restore account balances to maxBlockHeight - paccountHistoryDB->ForEachAccountHistory([&](AccountHistoryKey const & key, AccountHistoryValue const & value) { - if (maxBlockHeight > key.blockHeight) { - return false; + if (accountRecord && (tokenFilter.empty() || hasToken(value.diff))) { + auto &array = ret.emplace(workingHeight, UniValue::VARR).first->second; + array.push_back(accounthistoryToJSON(key, value, format)); + if (shouldSearchInWallet) { + txs.insert(value.txid); + } + --count; } - if (!isMatchOwner(key.owner)) { - return false; + + if (!noRewards && count && lastHeight > workingHeight) { + onPoolRewards( + view, + key.owner, + workingHeight, + lastHeight, + [&](int32_t height, DCT_ID poolId, RewardType type, CTokenAmount amount) { + if (tokenFilter.empty() || hasToken({ + {amount.nTokenId, amount.nValue} + })) { + auto &array = ret.emplace(height, UniValue::VARR).first->second; + array.push_back(rewardhistoryToJSON(key.owner, height, poolId, type, amount, format)); + count ? --count : 0; + } + }); } - CScopeAccountReverter(view, key.owner, value.diff); - return true; - }, account); - } - paccountHistoryDB->ForEachAccountHistory(shouldContinueToNextAccountHistory, account, maxBlockHeight, txn); + lastHeight = workingHeight; - if (shouldSearchInWallet) { - count = limit; - searchInWallet(pwallet, account, filter, - [&](CBlockIndex const * index, CWalletTx const * pwtx) { - uint32_t height = index->nHeight; - return txs.count(pwtx->GetHash()) || startBlock > height || height > maxBlockHeight; - }, - [&](COutputEntry const & entry, CBlockIndex const * index, CWalletTx const * pwtx) { - uint32_t height = index->nHeight; - uint32_t nIndex = pwtx->nIndex; - if (txn != std::numeric_limits::max() && height == maxBlockHeight && nIndex > txn ) { + return count != 0 || isMine; + }; + + if (!noRewards && !account.empty()) { + // revert previous tx to restore account balances to maxBlockHeight + paccountHistoryDB->ForEachAccountHistory( + [&](AccountHistoryKey const &key, AccountHistoryValue const &value) { + if (maxBlockHeight > key.blockHeight) { + return false; + } + if (!isMatchOwner(key.owner)) { + return false; + } + CScopeAccountReverter(view, key.owner, value.diff); return true; - } - auto& array = ret.emplace(index->nHeight, UniValue::VARR).first->second; - array.push_back(outputEntryToJSON(entry, index, pwtx, format)); - return --count != 0; - } - ); + }, + account); + } + + paccountHistoryDB->ForEachAccountHistory(shouldContinueToNextAccountHistory, account, maxBlockHeight, txn); + + if (shouldSearchInWallet) { + count = limit + start; + searchInWallet( + pwallet, + account, + filter, + [&](CBlockIndex const *index, CWalletTx const *pwtx) { + uint32_t height = index->nHeight; + return txs.count(pwtx->GetHash()) || startBlock > height || height > maxBlockHeight; + }, + [&](COutputEntry const &entry, CBlockIndex const *index, CWalletTx const *pwtx) { + uint32_t height = index->nHeight; + uint32_t nIndex = pwtx->nIndex; + if (txn != std::numeric_limits::max() && height == maxBlockHeight && nIndex > txn) { + return true; + } + auto &array = ret.emplace(index->nHeight, UniValue::VARR).first->second; + array.push_back(outputEntryToJSON(entry, index, pwtx, format)); + return --count != 0; + }); + } } UniValue slice(UniValue::VARR); for (auto it = ret.cbegin(); limit != 0 && it != ret.cend(); ++it) { const auto& array = it->second.get_array(); for (size_t i = 0; limit != 0 && i < array.size(); ++i) { + if (start != 0) { + --start; + continue; + } slice.push_back(array[i]); --limit; } diff --git a/test/functional/feature_listaccounthistory_multiaccountquery.py b/test/functional/feature_listaccounthistory_multiaccountquery.py new file mode 100644 index 00000000000..dd9ec493b02 --- /dev/null +++ b/test/functional/feature_listaccounthistory_multiaccountquery.py @@ -0,0 +1,73 @@ +#!/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 multi-account listaccounthistory query.""" + +from test_framework.test_framework import DefiTestFramework +from test_framework.util import assert_equal + + +class MultiAccountListAccountHistory(DefiTestFramework): + def set_test_params(self): + self.num_nodes = 2 + self.setup_clean_chain = True + self.extra_args = [ + ['-acindex=1', '-txnotokens=0', '-amkheight=50', '-bayfrontheight=50', '-bayfrontgardensheight=50', + '-grandcentralheight=51'], + ['-acindex=1', '-txnotokens=0', '-amkheight=50', '-bayfrontheight=50', '-bayfrontgardensheight=50', + '-grandcentralheight=51'], + ] + + def run_test(self): + self.nodes[0].generate(101) + self.sync_blocks() + self.nodes[1].generate(101) + self.sync_blocks() + + collateral_a = self.nodes[0].getnewaddress("", "legacy") + collateral_b = self.nodes[0].getnewaddress("", "legacy") + + self.nodes[0].createtoken({ + "symbol": "T1", + "name": "T1", + "collateralAddress": collateral_a + }) + self.nodes[1].createtoken({ + "symbol": "T2", + "name": "T2", + "collateralAddress": collateral_b + }) + self.sync_mempools() + self.nodes[0].generate(1) + self.sync_blocks() + + list_tokens = self.nodes[0].listtokens() + for idx, token in list_tokens.items(): + if token["symbol"] == "T1": + token1 = idx + if token["symbol"] == "T2": + token2 = idx + + self.nodes[0].minttokens(["300@" + token1]) + self.nodes[1].minttokens(["500@" + token2]) + self.sync_mempools() + self.nodes[0].generate(1) + self.sync_blocks() + + self.nodes[0].minttokens(["300@" + token1]) + self.nodes[1].minttokens(["500@" + token2]) + self.sync_mempools() + self.nodes[0].generate(1) + self.sync_blocks() + + combined = self.nodes[0].listaccounthistory([collateral_a, collateral_b]) + a = self.nodes[0].listaccounthistory([collateral_a]) + b = self.nodes[0].listaccounthistory([collateral_b]) + + assert_equal(len(combined), len(a) + len(b)) + + +if __name__ == '__main__': + MultiAccountListAccountHistory().main() diff --git a/test/functional/rpc_listaccounthistory.py b/test/functional/rpc_listaccounthistory.py index 579695fc0a7..f0c66ce3ab4 100755 --- a/test/functional/rpc_listaccounthistory.py +++ b/test/functional/rpc_listaccounthistory.py @@ -12,14 +12,18 @@ connect_nodes_bi ) + class TokensRPCListAccountHistory(DefiTestFramework): def set_test_params(self): self.num_nodes = 3 self.setup_clean_chain = True self.extra_args = [ - ['-acindex=1', '-txnotokens=0', '-amkheight=50', '-bayfrontheight=50', '-bayfrontgardensheight=50'], - ['-acindex=1', '-txnotokens=0', '-amkheight=50', '-bayfrontheight=50', '-bayfrontgardensheight=50'], - ['-acindex=1', '-txnotokens=0', '-amkheight=50', '-bayfrontheight=50', '-bayfrontgardensheight=50'], + ['-acindex=1', '-txnotokens=0', '-amkheight=50', '-bayfrontheight=50', '-bayfrontgardensheight=50', + '-grandcentralheight=51'], + ['-acindex=1', '-txnotokens=0', '-amkheight=50', '-bayfrontheight=50', '-bayfrontgardensheight=50', + '-grandcentralheight=51'], + ['-acindex=1', '-txnotokens=0', '-amkheight=50', '-bayfrontheight=50', '-bayfrontgardensheight=50', + '-grandcentralheight=51'], ] def run_test(self): @@ -71,28 +75,28 @@ def run_test(self): # check amounts field is type of array for txs in results: - assert(hasattr(txs['amounts'], '__len__') and (not isinstance(txs['amounts'], str))) + assert (hasattr(txs['amounts'], '__len__') and (not isinstance(txs['amounts'], str))) # list {"maxBlockHeight":103, "txn":1}, should list without blockheight = 103, txn=2. i.e without MintToken - results = self.nodes[0].listaccounthistory(collateral_a, {"maxBlockHeight":103, "txn":1}) + results = self.nodes[0].listaccounthistory(collateral_a, {"maxBlockHeight": 103, "txn": 1}) for txs in results: self.log.info("test 1: block %d, txn is %d", txs['blockHeight'], txs['txn']) assert_equal(txs['owner'], collateral_a) assert_equal(txs['blockHeight'] <= 103, True) if txs['blockHeight'] == 103: - assert_equal(txs['txn'] <= 1 , True) # for block 103 txn:1 applies. + assert_equal(txs['txn'] <= 1, True) # for block 103 txn:1 applies. # list {"maxBlockHeight":103, "txn":0}, should list without blockheight = 103, txn=1,2. i.e without any txs from 103 block - results = self.nodes[0].listaccounthistory(collateral_a, {"maxBlockHeight":103, "txn":0}) + results = self.nodes[0].listaccounthistory(collateral_a, {"maxBlockHeight": 103, "txn": 0}) for txs in results: self.log.info("test 2: block %d, txn is %d", txs['blockHeight'], txs['txn']) assert_equal(txs['owner'], collateral_a) assert_equal(txs['blockHeight'] <= 103, True) if txs['blockHeight'] == 103: - assert_equal(txs['txn'] <= 0 , True) + assert_equal(txs['txn'] <= 0, True) else: - assert_equal(txs['txn'] >= 0 , True) # means txn:0 only applicable to block 103 only + assert_equal(txs['txn'] >= 0, True) # means txn:0 only applicable to block 103 only # Get node 1 results results = self.nodes[1].listaccounthistory(collateral_a) @@ -106,7 +110,7 @@ def run_test(self): assert_equal(results[0]['type'], 'MintToken') # check amounts field is type of array - assert(hasattr(results[0]['amounts'], '__len__') and (not isinstance(results[0]['amounts'], str))) + assert (hasattr(results[0]['amounts'], '__len__') and (not isinstance(results[0]['amounts'], str))) result = self.nodes[0].listaccounthistory() assert 'blockReward' in [res['type'] for res in result] @@ -114,12 +118,51 @@ def run_test(self): result = self.nodes[1].listaccounthistory() assert_equal(result, []) - assert_equal(self.nodes[0].listaccounthistory('all', {"txtype": "MintToken"}), self.nodes[0].listaccounthistory('all', {"txtype": "M"})) + assert_equal(self.nodes[0].listaccounthistory('all', {"txtype": "MintToken"}), + self.nodes[0].listaccounthistory('all', {"txtype": "M"})) + + # test multiple transaction type filter + self.nodes[0].burntokens({ + 'amounts': "1@" + token_a, + 'from': collateral_a, + }) + self.nodes[0].generate(1) + + self.nodes[0].burntokens({ + 'amounts': "1@" + token_a, + 'from': collateral_a, + }) + self.nodes[0].generate(1) + + assert_equal(self.nodes[0].listaccounthistory(collateral_a, {"txtypes": ["MintToken", "BurnToken"]}), + self.nodes[0].listaccounthistory(collateral_a, {"txtype": "BurnToken"}) + + self.nodes[0].listaccounthistory(collateral_a, {"txtype": "MintToken"})) + + # txtype should be ignored if txtypes is passed + assert_equal(self.nodes[0].listaccounthistory(collateral_a, {"txtypes": ["MintToken", "BurnToken"]}), + self.nodes[0].listaccounthistory(collateral_a, {"txtype": "BurnToken", "txtypes": ["MintToken", + "BurnToken"]})) + + # test pagination + res0 = self.nodes[0].listaccounthistory(collateral_a, {"start": 0, "including_start": True}) + res1 = self.nodes[0].listaccounthistory(collateral_a, {"start": 1, "including_start": True}) + res2 = self.nodes[0].listaccounthistory(collateral_a, {"start": 1, "including_start": False}) + res3 = self.nodes[0].listaccounthistory(collateral_a, {"start": 2, "including_start": False}) + + # check if entries line up + assert_equal(res0[1], res1[0]) + assert_equal(res0[2], res2[0]) + assert_equal(res0[3], res3[0]) + + # check if lengths add up + assert_equal(len(res0), len(res1) + 1) + assert_equal(len(res0), len(res2) + 2) + assert_equal(len(res0), len(res3) + 3) # REVERTING: - #======================== + # ======================== self.start_node(2) - self.nodes[2].generate(2) + self.nodes[2].generate(4) connect_nodes_bi(self.nodes, 1, 2) self.sync_blocks() @@ -131,5 +174,6 @@ def run_test(self): assert_equal(len(results), 0) assert_equal(self.nodes[1].accounthistorycount(collateral_a), 0) + if __name__ == '__main__': - TokensRPCListAccountHistory().main () + TokensRPCListAccountHistory().main() diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index a28121b27a3..2c75c87fa12 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -272,6 +272,7 @@ 'feature_filelock.py', 'p2p_unrequested_blocks.py', 'rpc_listaccounthistory.py', + 'feature_listaccounthistory_multiaccountquery.py', 'rpc_getaccounthistory.py', 'feature_includeconf.py', 'rpc_deriveaddresses.py',