905 lines
32 KiB
C++
905 lines
32 KiB
C++
|
// Copyright (c) 2012-2014, The CryptoNote developers, The Bytecoin developers
|
||
|
//
|
||
|
// This file is part of Bytecoin.
|
||
|
//
|
||
|
// Bytecoin is free software: you can redistribute it and/or modify
|
||
|
// it under the terms of the GNU Lesser General Public License as published by
|
||
|
// the Free Software Foundation, either version 3 of the License, or
|
||
|
// (at your option) any later version.
|
||
|
//
|
||
|
// Bytecoin is distributed in the hope that it will be useful,
|
||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
|
// GNU Lesser General Public License for more details.
|
||
|
//
|
||
|
// You should have received a copy of the GNU Lesser General Public License
|
||
|
// along with Bytecoin. If not, see <http://www.gnu.org/licenses/>.
|
||
|
|
||
|
#include "gtest/gtest.h"
|
||
|
|
||
|
#include "cryptonote_core/TransactionApi.h"
|
||
|
#include "transfers/TransfersConsumer.h"
|
||
|
|
||
|
#include <algorithm>
|
||
|
#include <limits>
|
||
|
#include <transfers/CommonTypes.h>
|
||
|
#include <cryptonote_core/TransactionApi.h>
|
||
|
|
||
|
#include "INodeStubs.h"
|
||
|
#include "TransactionApiHelpers.h"
|
||
|
#include "TransfersObserver.h"
|
||
|
#include "TestBlockchainGenerator.h"
|
||
|
|
||
|
using namespace CryptoNote;
|
||
|
|
||
|
AccountSubscription getAccountSubscription(const AccountKeys& accountKeys) {
|
||
|
AccountSubscription subscription;
|
||
|
subscription.keys = accountKeys;
|
||
|
|
||
|
return subscription;
|
||
|
}
|
||
|
|
||
|
AccountKeys getAccountKeysWithViewKey(const PublicKey& publicViewKey, const SecretKey& secretViewKey) {
|
||
|
KeyPair viewKp;
|
||
|
viewKp.publicKey = publicViewKey;
|
||
|
viewKp.secretKey = secretViewKey;
|
||
|
AccountKeys accountKeys = accountKeysFromKeypairs(viewKp, generateKeys());
|
||
|
|
||
|
return accountKeys;
|
||
|
}
|
||
|
|
||
|
cryptonote::Transaction convertTx(ITransactionReader& tx) {
|
||
|
auto blob = tx.getTransactionData();
|
||
|
cryptonote::blobdata data(reinterpret_cast<const char*>(blob.data()), blob.size());
|
||
|
cryptonote::Transaction oldTx;
|
||
|
cryptonote::parse_and_validate_tx_from_blob(data, oldTx); // ignore return code
|
||
|
return oldTx;
|
||
|
}
|
||
|
|
||
|
class TransfersConsumerTest : public ::testing::Test {
|
||
|
public:
|
||
|
TransfersConsumerTest();
|
||
|
|
||
|
protected:
|
||
|
|
||
|
ITransfersSubscription& addSubscription(TransfersConsumer& consumer, const AccountKeys& acc, uint64_t height = 0,
|
||
|
uint64_t timestamp = 0, size_t age = 0)
|
||
|
{
|
||
|
AccountSubscription subscription = getAccountSubscription(acc);
|
||
|
subscription.syncStart.height = height;
|
||
|
subscription.syncStart.timestamp = timestamp;
|
||
|
subscription.transactionSpendableAge = age;
|
||
|
return consumer.addSubscription(subscription);
|
||
|
}
|
||
|
|
||
|
ITransfersSubscription& addSubscription(const AccountKeys& acc, uint64_t height = 0, uint64_t timestamp = 0, size_t age = 0) {
|
||
|
return addSubscription(m_consumer, acc, height, timestamp, age);
|
||
|
}
|
||
|
|
||
|
ITransfersSubscription& addSubscription(uint64_t height = 0, uint64_t timestamp = 0, size_t age = 0) {
|
||
|
return addSubscription(m_consumer, m_accountKeys, height, timestamp, age);
|
||
|
}
|
||
|
|
||
|
ITransfersSubscription& addSubscription(TransfersConsumer& consumer, uint64_t height = 0, uint64_t timestamp = 0, size_t age = 0) {
|
||
|
return addSubscription(consumer, m_accountKeys, height, timestamp, age);
|
||
|
}
|
||
|
|
||
|
AccountKeys generateAccount() {
|
||
|
return getAccountKeysWithViewKey(m_accountKeys.address.viewPublicKey, m_accountKeys.viewSecretKey);
|
||
|
}
|
||
|
|
||
|
cryptonote::Currency m_currency;
|
||
|
TestBlockchainGenerator m_generator;
|
||
|
INodeTrivialRefreshStub m_node;
|
||
|
AccountKeys m_accountKeys;
|
||
|
TransfersConsumer m_consumer;
|
||
|
};
|
||
|
|
||
|
TransfersConsumerTest::TransfersConsumerTest() :
|
||
|
m_currency(cryptonote::CurrencyBuilder().currency()),
|
||
|
m_generator(m_currency),
|
||
|
m_node(m_generator),
|
||
|
m_accountKeys(generateAccountKeys()),
|
||
|
m_consumer(m_currency, m_node, m_accountKeys.viewSecretKey)
|
||
|
{
|
||
|
}
|
||
|
|
||
|
bool amountFound(const std::vector<TransactionOutputInformation>& outs, uint64_t amount) {
|
||
|
return std::find_if(outs.begin(), outs.end(), [amount] (const TransactionOutputInformation& inf) { return inf.amount == amount; }) != outs.end();
|
||
|
}
|
||
|
|
||
|
AccountSubscription getAccountSubscriptionWithSyncStart(const AccountKeys& keys, uint64_t timestamp, uint64_t height) {
|
||
|
AccountSubscription subscription = getAccountSubscription(keys);
|
||
|
subscription.syncStart.timestamp = timestamp;
|
||
|
subscription.syncStart.height = height;
|
||
|
|
||
|
return subscription;
|
||
|
}
|
||
|
|
||
|
TEST_F(TransfersConsumerTest, addSubscription_Success) {
|
||
|
AccountSubscription subscription;
|
||
|
subscription.keys = m_accountKeys;
|
||
|
|
||
|
ITransfersSubscription& accountSubscription = m_consumer.addSubscription(subscription);
|
||
|
ASSERT_EQ(subscription.keys.address, accountSubscription.getAddress());
|
||
|
}
|
||
|
|
||
|
TEST_F(TransfersConsumerTest, addSubscription_WrongViewKey) {
|
||
|
AccountKeys accountKeys = generateAccountKeys();
|
||
|
AccountSubscription subscription = getAccountSubscription(accountKeys);
|
||
|
|
||
|
ASSERT_ANY_THROW(m_consumer.addSubscription(subscription));
|
||
|
}
|
||
|
|
||
|
TEST_F(TransfersConsumerTest, addSubscription_SameSubscription) {
|
||
|
AccountSubscription subscription = getAccountSubscription(m_accountKeys);
|
||
|
ITransfersSubscription* ts1 = &m_consumer.addSubscription(subscription);
|
||
|
ITransfersSubscription* ts2 = &m_consumer.addSubscription(subscription);
|
||
|
|
||
|
ASSERT_EQ(ts1, ts2);
|
||
|
}
|
||
|
|
||
|
TEST_F(TransfersConsumerTest, removeSubscription_Success) {
|
||
|
AccountSubscription subscription = getAccountSubscription(m_accountKeys);
|
||
|
m_consumer.addSubscription(subscription);
|
||
|
|
||
|
ITransfersSubscription* ts = m_consumer.getSubscription(m_accountKeys.address);
|
||
|
ASSERT_NE(nullptr, ts);
|
||
|
|
||
|
m_consumer.removeSubscription(m_accountKeys.address);
|
||
|
ts = m_consumer.getSubscription(m_accountKeys.address);
|
||
|
ASSERT_EQ(nullptr, ts);
|
||
|
}
|
||
|
|
||
|
TEST_F(TransfersConsumerTest, removeSubscription_OneAddressLeft) {
|
||
|
AccountSubscription subscription1 = getAccountSubscription(m_accountKeys);
|
||
|
m_consumer.addSubscription(subscription1);
|
||
|
|
||
|
AccountKeys accountKeys = getAccountKeysWithViewKey(m_accountKeys.address.viewPublicKey, m_accountKeys.viewSecretKey);
|
||
|
AccountSubscription subscription2 = getAccountSubscription(accountKeys);
|
||
|
|
||
|
m_consumer.addSubscription(subscription2);
|
||
|
|
||
|
ASSERT_FALSE(m_consumer.removeSubscription(subscription1.keys.address));
|
||
|
}
|
||
|
|
||
|
TEST_F(TransfersConsumerTest, removeSubscription_RemoveAllAddresses) {
|
||
|
AccountSubscription subscription1 = getAccountSubscription(m_accountKeys);
|
||
|
m_consumer.addSubscription(subscription1);
|
||
|
|
||
|
ASSERT_TRUE(m_consumer.removeSubscription(subscription1.keys.address));
|
||
|
}
|
||
|
|
||
|
TEST_F(TransfersConsumerTest, getSubscription_ReturnSameValueForSameAddress) {
|
||
|
AccountSubscription subscription = getAccountSubscription(m_accountKeys);
|
||
|
m_consumer.addSubscription(subscription);
|
||
|
|
||
|
ITransfersSubscription* ts1 = m_consumer.getSubscription(m_accountKeys.address);
|
||
|
ITransfersSubscription* ts2 = m_consumer.getSubscription(m_accountKeys.address);
|
||
|
|
||
|
ASSERT_EQ(ts1, ts2);
|
||
|
}
|
||
|
|
||
|
TEST_F(TransfersConsumerTest, getSubscription_ReturnNullForNonExistentAddr) {
|
||
|
AccountSubscription subscription1 = getAccountSubscription(m_accountKeys);
|
||
|
m_consumer.addSubscription(subscription1);
|
||
|
|
||
|
AccountKeys accountKeys = getAccountKeysWithViewKey(m_accountKeys.address.viewPublicKey, m_accountKeys.viewSecretKey);
|
||
|
|
||
|
ASSERT_EQ(nullptr, m_consumer.getSubscription(accountKeys.address));
|
||
|
}
|
||
|
|
||
|
TEST_F(TransfersConsumerTest, getSubscriptions_Empty) {
|
||
|
std::vector<AccountAddress> subscriptions;
|
||
|
m_consumer.getSubscriptions(subscriptions);
|
||
|
|
||
|
ASSERT_TRUE(subscriptions.empty());
|
||
|
}
|
||
|
|
||
|
TEST_F(TransfersConsumerTest, getSubscriptions_TwoSubscriptions) {
|
||
|
AccountSubscription subscription1 = getAccountSubscription(m_accountKeys);
|
||
|
m_consumer.addSubscription(subscription1);
|
||
|
|
||
|
AccountKeys accountKeys = getAccountKeysWithViewKey(m_accountKeys.address.viewPublicKey, m_accountKeys.viewSecretKey);
|
||
|
AccountSubscription subscription2 = getAccountSubscription(accountKeys);
|
||
|
m_consumer.addSubscription(subscription2);
|
||
|
|
||
|
std::vector<AccountAddress> subscriptions;
|
||
|
m_consumer.getSubscriptions(subscriptions);
|
||
|
|
||
|
ASSERT_EQ(2, subscriptions.size());
|
||
|
ASSERT_NE(subscriptions.end(), std::find(subscriptions.begin(), subscriptions.end(), subscription1.keys.address));
|
||
|
ASSERT_NE(subscriptions.end(), std::find(subscriptions.begin(), subscriptions.end(), subscription2.keys.address));
|
||
|
}
|
||
|
|
||
|
TEST_F(TransfersConsumerTest, getSyncStart_Empty) {
|
||
|
auto syncStart = m_consumer.getSyncStart();
|
||
|
|
||
|
EXPECT_EQ(std::numeric_limits<uint64_t>::max(), syncStart.height);
|
||
|
EXPECT_EQ(std::numeric_limits<uint64_t>::max(), syncStart.timestamp);
|
||
|
}
|
||
|
|
||
|
TEST_F(TransfersConsumerTest, getSyncStart_OneSubscription) {
|
||
|
const uint64_t height = 1209384;
|
||
|
const uint64_t timestamp = 99284512;
|
||
|
|
||
|
AccountSubscription subscription = getAccountSubscription(m_accountKeys);
|
||
|
subscription.syncStart.height = height;
|
||
|
subscription.syncStart.timestamp = timestamp;
|
||
|
|
||
|
m_consumer.addSubscription(subscription);
|
||
|
|
||
|
auto sync = m_consumer.getSyncStart();
|
||
|
ASSERT_EQ(height, sync.height);
|
||
|
ASSERT_EQ(timestamp, sync.timestamp);
|
||
|
}
|
||
|
|
||
|
TEST_F(TransfersConsumerTest, getSyncStart_MinSyncSameSubscription) {
|
||
|
const uint64_t height = 1209384;
|
||
|
const uint64_t timestamp = 99284512;
|
||
|
const uint64_t minHeight = 120984;
|
||
|
const uint64_t minTimestamp = 9984512;
|
||
|
|
||
|
AccountSubscription subscription1 = getAccountSubscription(m_accountKeys);
|
||
|
subscription1.syncStart.height = height;
|
||
|
subscription1.syncStart.timestamp = timestamp;
|
||
|
|
||
|
AccountKeys accountKeys = getAccountKeysWithViewKey(m_accountKeys.address.viewPublicKey, m_accountKeys.viewSecretKey);
|
||
|
AccountSubscription subscription2 = getAccountSubscription(accountKeys);
|
||
|
|
||
|
subscription2.syncStart.height = minHeight;
|
||
|
subscription2.syncStart.timestamp = minTimestamp;
|
||
|
|
||
|
m_consumer.addSubscription(subscription1);
|
||
|
m_consumer.addSubscription(subscription2);
|
||
|
|
||
|
auto sync = m_consumer.getSyncStart();
|
||
|
ASSERT_EQ(minHeight, sync.height);
|
||
|
ASSERT_EQ(minTimestamp, sync.timestamp);
|
||
|
}
|
||
|
|
||
|
TEST_F(TransfersConsumerTest, getSyncStart_MinSyncDifferentSubscriptions) {
|
||
|
const uint64_t height = 1209384;
|
||
|
const uint64_t timestamp = 99284512;
|
||
|
const uint64_t minHeight = 120984;
|
||
|
const uint64_t minTimestamp = 9984512;
|
||
|
|
||
|
AccountSubscription subscription1 = getAccountSubscription(m_accountKeys);
|
||
|
subscription1.syncStart.height = minHeight;
|
||
|
subscription1.syncStart.timestamp = timestamp;
|
||
|
|
||
|
AccountKeys accountKeys = getAccountKeysWithViewKey(m_accountKeys.address.viewPublicKey, m_accountKeys.viewSecretKey);
|
||
|
AccountSubscription subscription2 = getAccountSubscription(accountKeys);
|
||
|
|
||
|
subscription2.syncStart.height = height;
|
||
|
subscription2.syncStart.timestamp = minTimestamp;
|
||
|
|
||
|
m_consumer.addSubscription(subscription1);
|
||
|
m_consumer.addSubscription(subscription2);
|
||
|
|
||
|
auto sync = m_consumer.getSyncStart();
|
||
|
ASSERT_EQ(minHeight, sync.height);
|
||
|
ASSERT_EQ(minTimestamp, sync.timestamp);
|
||
|
}
|
||
|
|
||
|
TEST_F(TransfersConsumerTest, getSyncStart_RemoveMinSyncSubscription) {
|
||
|
const uint64_t height = 1209384;
|
||
|
const uint64_t timestamp = 99284512;
|
||
|
const uint64_t minHeight = 120984;
|
||
|
const uint64_t minTimestamp = 9984512;
|
||
|
|
||
|
AccountSubscription subscription1 = getAccountSubscription(m_accountKeys);
|
||
|
subscription1.syncStart.height = height;
|
||
|
subscription1.syncStart.timestamp = timestamp;
|
||
|
|
||
|
AccountKeys accountKeys = getAccountKeysWithViewKey(m_accountKeys.address.viewPublicKey, m_accountKeys.viewSecretKey);
|
||
|
AccountSubscription subscription2 = getAccountSubscription(accountKeys);
|
||
|
|
||
|
subscription2.syncStart.height = minHeight;
|
||
|
subscription2.syncStart.timestamp = minTimestamp;
|
||
|
|
||
|
m_consumer.addSubscription(subscription1);
|
||
|
m_consumer.addSubscription(subscription2);
|
||
|
m_consumer.removeSubscription(subscription2.keys.address);
|
||
|
|
||
|
auto sync = m_consumer.getSyncStart();
|
||
|
ASSERT_EQ(height, sync.height);
|
||
|
ASSERT_EQ(timestamp, sync.timestamp);
|
||
|
}
|
||
|
|
||
|
TEST_F(TransfersConsumerTest, onBlockchainDetach) {
|
||
|
auto& container1 = addSubscription().getContainer();
|
||
|
auto keys = generateAccount();
|
||
|
auto& container2 = addSubscription(keys).getContainer();
|
||
|
|
||
|
std::shared_ptr<ITransaction> tx1 = createTransaction();
|
||
|
addTestInput(*tx1, 100);
|
||
|
addTestKeyOutput(*tx1, 50, 1, m_accountKeys);
|
||
|
|
||
|
std::shared_ptr<ITransaction> tx2 = createTransaction();
|
||
|
addTestInput(*tx1, 100);
|
||
|
addTestKeyOutput(*tx1, 50, 1, keys);
|
||
|
|
||
|
CompleteBlock blocks[3];
|
||
|
blocks[0].block = cryptonote::Block();
|
||
|
blocks[0].block->timestamp = 1233;
|
||
|
|
||
|
blocks[1].block = cryptonote::Block();
|
||
|
blocks[1].block->timestamp = 1234;
|
||
|
blocks[1].transactions.push_back(tx1);
|
||
|
|
||
|
blocks[2].block = cryptonote::Block();
|
||
|
blocks[2].block->timestamp = 1235;
|
||
|
blocks[2].transactions.push_back(tx2);
|
||
|
|
||
|
ASSERT_TRUE(m_consumer.onNewBlocks(&blocks[0], 0, 3));
|
||
|
|
||
|
m_consumer.onBlockchainDetach(0);
|
||
|
std::vector<TransactionOutputInformation> trs;
|
||
|
container1.getOutputs(trs, ITransfersContainer::IncludeAll);
|
||
|
ASSERT_EQ(0, trs.size());
|
||
|
|
||
|
container2.getOutputs(trs, ITransfersContainer::IncludeAll);
|
||
|
ASSERT_EQ(0, trs.size());
|
||
|
}
|
||
|
|
||
|
TEST_F(TransfersConsumerTest, onNewBlocks_OneEmptyBlockOneFilled) {
|
||
|
AccountSubscription subscription = getAccountSubscription(m_accountKeys);
|
||
|
subscription.syncStart.height = 1;
|
||
|
subscription.syncStart.timestamp = 1234;
|
||
|
|
||
|
std::shared_ptr<ITransaction> ignoredTx(createTransaction());
|
||
|
addTestInput(*ignoredTx, 1000);
|
||
|
addTestKeyOutput(*ignoredTx, 123, 1, m_accountKeys);
|
||
|
|
||
|
std::shared_ptr<ITransaction> tx(createTransaction());
|
||
|
addTestInput(*tx, 10000);
|
||
|
addTestKeyOutput(*tx, 900, 2, m_accountKeys);
|
||
|
addTestKeyOutput(*tx, 850, 3, m_accountKeys);
|
||
|
|
||
|
CompleteBlock blocks[2];
|
||
|
blocks[0].transactions.push_back(ignoredTx);
|
||
|
blocks[1].block = cryptonote::Block();
|
||
|
blocks[1].block->timestamp = 1235;
|
||
|
blocks[1].transactions.push_back(tx);
|
||
|
|
||
|
ITransfersContainer& container = m_consumer.addSubscription(subscription).getContainer();
|
||
|
ASSERT_TRUE(m_consumer.onNewBlocks(&blocks[0], 1, 2));
|
||
|
|
||
|
auto outs = container.getTransactionOutputs(tx->getTransactionHash(), ITransfersContainer::IncludeAll);
|
||
|
ASSERT_TRUE(amountFound(outs, 850));
|
||
|
ASSERT_TRUE(amountFound(outs, 900));
|
||
|
|
||
|
auto ignoredOuts = container.getTransactionOutputs(ignoredTx->getTransactionHash(), ITransfersContainer::IncludeAll);
|
||
|
ASSERT_EQ(0, ignoredOuts.size());
|
||
|
}
|
||
|
|
||
|
TEST_F(TransfersConsumerTest, onNewBlocks_DifferentTimestamps) {
|
||
|
AccountSubscription subscription = getAccountSubscription(m_accountKeys);
|
||
|
subscription.syncStart.timestamp = 12345;
|
||
|
subscription.syncStart.height = 12;
|
||
|
|
||
|
std::shared_ptr<ITransaction> ignoredTx(createTransaction());
|
||
|
addTestInput(*ignoredTx, 1000);
|
||
|
addTestKeyOutput(*ignoredTx, 123, 1, m_accountKeys);
|
||
|
|
||
|
std::shared_ptr<ITransaction> tx(createTransaction());
|
||
|
addTestInput(*tx, 10000);
|
||
|
addTestKeyOutput(*tx, 900, 2, m_accountKeys);
|
||
|
addTestKeyOutput(*tx, 850, 3, m_accountKeys);
|
||
|
|
||
|
CompleteBlock blocks[2];
|
||
|
blocks[0].transactions.push_back(ignoredTx);
|
||
|
blocks[0].block = cryptonote::Block();
|
||
|
blocks[0].block->timestamp = subscription.syncStart.timestamp - 1;
|
||
|
|
||
|
blocks[1].block = cryptonote::Block();
|
||
|
blocks[1].block->timestamp = subscription.syncStart.timestamp;
|
||
|
blocks[1].transactions.push_back(tx);
|
||
|
|
||
|
ITransfersContainer& container = m_consumer.addSubscription(subscription).getContainer();
|
||
|
ASSERT_TRUE(m_consumer.onNewBlocks(&blocks[0], 2, 2));
|
||
|
|
||
|
auto ignoredOuts = container.getTransactionOutputs(ignoredTx->getTransactionHash(), ITransfersContainer::IncludeAll);
|
||
|
ASSERT_EQ(0, ignoredOuts.size());
|
||
|
|
||
|
auto outs = container.getTransactionOutputs(tx->getTransactionHash(), ITransfersContainer::IncludeAll);
|
||
|
ASSERT_TRUE(amountFound(outs, 850));
|
||
|
ASSERT_TRUE(amountFound(outs, 900));
|
||
|
}
|
||
|
|
||
|
TEST_F(TransfersConsumerTest, onNewBlocks_getTransactionOutsGlobalIndicesError) {
|
||
|
class INodeGlobalIndicesStub: public INodeDummyStub {
|
||
|
public:
|
||
|
virtual void getTransactionOutsGlobalIndices(const crypto::hash& transactionHash,
|
||
|
std::vector<uint64_t>& outsGlobalIndices, const Callback& callback) override {
|
||
|
callback(std::make_error_code(std::errc::operation_canceled));
|
||
|
};
|
||
|
};
|
||
|
|
||
|
INodeGlobalIndicesStub node;
|
||
|
|
||
|
TransfersConsumer consumer(m_currency, node, m_accountKeys.viewSecretKey);
|
||
|
|
||
|
auto subscription = getAccountSubscriptionWithSyncStart(m_accountKeys, 1234, 10);
|
||
|
|
||
|
std::shared_ptr<ITransaction> tx(createTransaction());
|
||
|
addTestInput(*tx, 10000);
|
||
|
addTestKeyOutput(*tx, 900, 2, m_accountKeys);
|
||
|
|
||
|
CompleteBlock block;
|
||
|
block.block = cryptonote::Block();
|
||
|
block.block->timestamp = subscription.syncStart.timestamp;
|
||
|
block.transactions.push_back(tx);
|
||
|
|
||
|
consumer.addSubscription(subscription);
|
||
|
ASSERT_FALSE(consumer.onNewBlocks(&block, subscription.syncStart.height, 1));
|
||
|
}
|
||
|
|
||
|
TEST_F(TransfersConsumerTest, onNewBlocks_updateHeight) {
|
||
|
AccountSubscription subscription = getAccountSubscription(m_accountKeys);
|
||
|
subscription.syncStart.timestamp = 2131;
|
||
|
subscription.syncStart.height = 32;
|
||
|
subscription.transactionSpendableAge = 5;
|
||
|
|
||
|
auto& container = m_consumer.addSubscription(subscription).getContainer();
|
||
|
|
||
|
std::shared_ptr<ITransaction> tx(createTransaction());
|
||
|
addTestInput(*tx, 10000);
|
||
|
addTestKeyOutput(*tx, 900, 0, m_accountKeys);
|
||
|
|
||
|
CompleteBlock block;
|
||
|
block.block = cryptonote::Block();
|
||
|
block.block->timestamp = subscription.syncStart.timestamp;
|
||
|
block.transactions.push_back(tx);
|
||
|
|
||
|
ASSERT_TRUE(m_consumer.onNewBlocks(&block, subscription.syncStart.height, 1));
|
||
|
ASSERT_EQ(900, container.balance(ITransfersContainer::IncludeAllLocked));
|
||
|
|
||
|
std::unique_ptr<CompleteBlock[]> blocks(new CompleteBlock[subscription.transactionSpendableAge]);
|
||
|
for (size_t i = 0; i < subscription.transactionSpendableAge; ++i) {
|
||
|
blocks[i].block = cryptonote::Block();
|
||
|
auto tr = createTransaction();
|
||
|
addTestInput(*tr, 1000);
|
||
|
addTestKeyOutput(*tr, 100, i + 1, generateAccountKeys());
|
||
|
}
|
||
|
|
||
|
ASSERT_TRUE(m_consumer.onNewBlocks(blocks.get(), subscription.syncStart.height + 1, subscription.transactionSpendableAge));
|
||
|
ASSERT_EQ(0, container.balance(ITransfersContainer::IncludeAllLocked));
|
||
|
ASSERT_EQ(900, container.balance(ITransfersContainer::IncludeAllUnlocked));
|
||
|
}
|
||
|
|
||
|
TEST_F(TransfersConsumerTest, onNewBlocks_DifferentSubscribers) {
|
||
|
auto& container1 = addSubscription().getContainer();
|
||
|
|
||
|
auto keys = generateAccount();
|
||
|
auto& container2 = addSubscription(keys).getContainer();
|
||
|
|
||
|
uint64_t amount1 = 900;
|
||
|
uint64_t amount2 = 850;
|
||
|
|
||
|
std::shared_ptr<ITransaction> tx(createTransaction());
|
||
|
addTestInput(*tx, 10000);
|
||
|
addTestKeyOutput(*tx, amount1, 0, m_accountKeys);
|
||
|
addTestKeyOutput(*tx, amount2, 1, keys);
|
||
|
|
||
|
CompleteBlock block;
|
||
|
block.block = cryptonote::Block();
|
||
|
block.block->timestamp = 0;
|
||
|
block.transactions.push_back(tx);
|
||
|
|
||
|
ASSERT_TRUE(m_consumer.onNewBlocks(&block, 0, 1));
|
||
|
auto outs1 = container1.getTransactionOutputs(tx->getTransactionHash(), ITransfersContainer::IncludeAll);
|
||
|
ASSERT_EQ(1, outs1.size());
|
||
|
ASSERT_EQ(amount1, outs1[0].amount);
|
||
|
|
||
|
auto outs2 = container2.getTransactionOutputs(tx->getTransactionHash(), ITransfersContainer::IncludeAll);
|
||
|
ASSERT_EQ(1, outs2.size());
|
||
|
ASSERT_EQ(amount2, outs2[0].amount);
|
||
|
}
|
||
|
|
||
|
TEST_F(TransfersConsumerTest, onNewBlocks_MultisignatureTransaction) {
|
||
|
auto& container1 = addSubscription().getContainer();
|
||
|
|
||
|
auto keys = generateAccount();
|
||
|
|
||
|
auto keys2 = generateAccount();
|
||
|
auto keys3 = generateAccount();
|
||
|
|
||
|
uint64_t amount = 900;
|
||
|
|
||
|
std::shared_ptr<ITransaction> tx(createTransaction());
|
||
|
addTestInput(*tx, 10000);
|
||
|
tx->addOutput(amount, { m_accountKeys.address, keys.address, keys2.address } , 3);
|
||
|
tx->addOutput(800, { keys.address, keys2.address, keys3.address }, 3);
|
||
|
|
||
|
CompleteBlock block;
|
||
|
block.block = cryptonote::Block();
|
||
|
block.block->timestamp = 0;
|
||
|
block.transactions.push_back(tx);
|
||
|
|
||
|
ASSERT_TRUE(m_consumer.onNewBlocks(&block, 0, 1));
|
||
|
auto outs1 = container1.getTransactionOutputs(tx->getTransactionHash(), ITransfersContainer::IncludeAll);
|
||
|
ASSERT_EQ(1, outs1.size());
|
||
|
ASSERT_EQ(amount, outs1[0].amount);
|
||
|
}
|
||
|
|
||
|
TEST_F(TransfersConsumerTest, onNewBlocks_getTransactionOutsGlobalIndicesIsProperlyCalled) {
|
||
|
class INodeGlobalIndicesStub: public INodeDummyStub {
|
||
|
public:
|
||
|
virtual void getTransactionOutsGlobalIndices(const crypto::hash& transactionHash,
|
||
|
std::vector<uint64_t>& outsGlobalIndices, const Callback& callback) override {
|
||
|
outsGlobalIndices.push_back(3);
|
||
|
hash = transactionHash;
|
||
|
callback(std::error_code());
|
||
|
};
|
||
|
|
||
|
crypto::hash hash;
|
||
|
};
|
||
|
|
||
|
INodeGlobalIndicesStub node;
|
||
|
TransfersConsumer consumer(m_currency, node, m_accountKeys.viewSecretKey);
|
||
|
|
||
|
AccountSubscription subscription = getAccountSubscription(m_accountKeys);
|
||
|
subscription.syncStart.height = 0;
|
||
|
subscription.syncStart.timestamp = 0;
|
||
|
consumer.addSubscription(subscription);
|
||
|
|
||
|
std::shared_ptr<ITransaction> tx(createTransaction());
|
||
|
addTestInput(*tx, 10000);
|
||
|
addTestKeyOutput(*tx, 900, 2, m_accountKeys);
|
||
|
|
||
|
CompleteBlock block;
|
||
|
block.block = cryptonote::Block();
|
||
|
block.block->timestamp = 0;
|
||
|
block.transactions.push_back(tx);
|
||
|
|
||
|
ASSERT_TRUE(consumer.onNewBlocks(&block, 1, 1));
|
||
|
const CryptoNote::Hash &hash = tx->getTransactionHash();
|
||
|
const crypto::hash expectedHash = *reinterpret_cast<const crypto::hash*>(&hash);
|
||
|
ASSERT_EQ(expectedHash, node.hash);
|
||
|
}
|
||
|
|
||
|
TEST_F(TransfersConsumerTest, onNewBlocks_getTransactionOutsGlobalIndicesIsNotCalled) {
|
||
|
class INodeGlobalIndicesStub: public INodeDummyStub {
|
||
|
public:
|
||
|
INodeGlobalIndicesStub() : called(false) {};
|
||
|
|
||
|
virtual void getTransactionOutsGlobalIndices(const crypto::hash& transactionHash,
|
||
|
std::vector<uint64_t>& outsGlobalIndices, const Callback& callback) override {
|
||
|
outsGlobalIndices.push_back(3);
|
||
|
called = true;
|
||
|
callback(std::error_code());
|
||
|
};
|
||
|
|
||
|
bool called;
|
||
|
};
|
||
|
|
||
|
INodeGlobalIndicesStub node;
|
||
|
TransfersConsumer consumer(m_currency, node, m_accountKeys.viewSecretKey);
|
||
|
|
||
|
AccountSubscription subscription = getAccountSubscription(m_accountKeys);
|
||
|
subscription.syncStart.height = 0;
|
||
|
subscription.syncStart.timestamp = 0;
|
||
|
consumer.addSubscription(subscription);
|
||
|
|
||
|
std::shared_ptr<ITransaction> tx(createTransaction());
|
||
|
addTestInput(*tx, 10000);
|
||
|
addTestKeyOutput(*tx, 900, 2, generateAccount());
|
||
|
|
||
|
CompleteBlock block;
|
||
|
block.block = cryptonote::Block();
|
||
|
block.block->timestamp = 0;
|
||
|
block.transactions.push_back(tx);
|
||
|
ASSERT_TRUE(consumer.onNewBlocks(&block, 1, 1));
|
||
|
|
||
|
ASSERT_FALSE(node.called);
|
||
|
}
|
||
|
|
||
|
TEST_F(TransfersConsumerTest, onNewBlocks_markTransactionConfirmed) {
|
||
|
auto& container = addSubscription().getContainer();
|
||
|
|
||
|
std::shared_ptr<ITransaction> tx(createTransaction());
|
||
|
addTestInput(*tx, 10000);
|
||
|
addTestKeyOutput(*tx, 10000, UNCONFIRMED_TRANSACTION_GLOBAL_OUTPUT_INDEX, m_accountKeys);
|
||
|
|
||
|
m_consumer.onPoolUpdated({convertTx(*tx)}, {});
|
||
|
|
||
|
auto lockedOuts = container.getTransactionOutputs(tx->getTransactionHash(),
|
||
|
ITransfersContainer::IncludeStateLocked | ITransfersContainer::IncludeTypeKey);
|
||
|
ASSERT_EQ(1, lockedOuts.size());
|
||
|
ASSERT_EQ(10000, lockedOuts[0].amount);
|
||
|
|
||
|
CompleteBlock blocks[2];
|
||
|
blocks[0].block = cryptonote::Block();
|
||
|
blocks[0].block->timestamp = 0;
|
||
|
blocks[0].transactions.push_back(tx);
|
||
|
blocks[1].block = cryptonote::Block();
|
||
|
blocks[1].block->timestamp = 0;
|
||
|
blocks[1].transactions.push_back(createTransaction());
|
||
|
ASSERT_TRUE(m_consumer.onNewBlocks(&blocks[0], 0, 2));
|
||
|
|
||
|
auto softLockedOuts = container.getTransactionOutputs(tx->getTransactionHash(), ITransfersContainer::IncludeKeyUnlocked);
|
||
|
ASSERT_EQ(1, softLockedOuts.size());
|
||
|
ASSERT_EQ(10000, softLockedOuts[0].amount);
|
||
|
}
|
||
|
|
||
|
class INodeGlobalIndexStub: public INodeDummyStub {
|
||
|
public:
|
||
|
|
||
|
virtual void getTransactionOutsGlobalIndices(const crypto::hash& transactionHash,
|
||
|
std::vector<uint64_t>& outsGlobalIndices, const Callback& callback) override {
|
||
|
outsGlobalIndices.push_back(globalIndex);
|
||
|
callback(std::error_code());
|
||
|
};
|
||
|
|
||
|
uint64_t globalIndex;
|
||
|
};
|
||
|
|
||
|
TEST_F(TransfersConsumerTest, onNewBlocks_checkTransactionOutputInformation) {
|
||
|
const uint64_t index = 2;
|
||
|
|
||
|
INodeGlobalIndexStub node;
|
||
|
TransfersConsumer consumer(m_currency, node, m_accountKeys.viewSecretKey);
|
||
|
|
||
|
node.globalIndex = index;
|
||
|
|
||
|
auto& container = addSubscription(consumer).getContainer();
|
||
|
|
||
|
std::shared_ptr<ITransaction> tx(createTransaction());
|
||
|
addTestInput(*tx, 10000);
|
||
|
auto out = addTestKeyOutput(*tx, 10000, index, m_accountKeys);
|
||
|
|
||
|
CompleteBlock block;
|
||
|
block.block = cryptonote::Block();
|
||
|
block.block->timestamp = 0;
|
||
|
block.transactions.push_back(tx);
|
||
|
ASSERT_TRUE(consumer.onNewBlocks(&block, 0, 1));
|
||
|
|
||
|
auto outs = container.getTransactionOutputs(tx->getTransactionHash(), ITransfersContainer::IncludeAll);
|
||
|
ASSERT_EQ(1, outs.size());
|
||
|
|
||
|
auto& o = outs[0];
|
||
|
|
||
|
ASSERT_EQ(out.type, o.type);
|
||
|
ASSERT_EQ(out.amount, o.amount);
|
||
|
ASSERT_EQ(out.outputKey, o.outputKey);
|
||
|
ASSERT_EQ(out.globalOutputIndex, o.globalOutputIndex);
|
||
|
ASSERT_EQ(out.outputInTransaction, o.outputInTransaction);
|
||
|
ASSERT_EQ(out.transactionPublicKey, o.transactionPublicKey);
|
||
|
}
|
||
|
|
||
|
TEST_F(TransfersConsumerTest, onNewBlocks_checkTransactionOutputInformationMultisignature) {
|
||
|
const uint64_t index = 2;
|
||
|
|
||
|
INodeGlobalIndexStub node;
|
||
|
TransfersConsumer consumer(m_currency, node, m_accountKeys.viewSecretKey);
|
||
|
|
||
|
node.globalIndex = index;
|
||
|
|
||
|
auto& container = addSubscription(consumer).getContainer();
|
||
|
|
||
|
std::shared_ptr<ITransaction> tx(createTransaction());
|
||
|
addTestInput(*tx, 10000);
|
||
|
uint32_t txIndex = tx->addOutput(300, { m_accountKeys.address, generateAccountKeys().address}, 2);
|
||
|
|
||
|
TransactionOutputInformation expectedOut;
|
||
|
expectedOut.type = TransactionTypes::OutputType::Multisignature;
|
||
|
expectedOut.amount = 300;
|
||
|
expectedOut.globalOutputIndex = index;
|
||
|
expectedOut.outputInTransaction = txIndex;
|
||
|
expectedOut.transactionPublicKey = tx->getTransactionPublicKey();
|
||
|
expectedOut.requiredSignatures = 2;
|
||
|
|
||
|
CompleteBlock block;
|
||
|
block.block = cryptonote::Block();
|
||
|
block.block->timestamp = 0;
|
||
|
block.transactions.push_back(tx);
|
||
|
ASSERT_TRUE(consumer.onNewBlocks(&block, 0, 1));
|
||
|
|
||
|
auto outs = container.getTransactionOutputs(tx->getTransactionHash(), ITransfersContainer::IncludeAll);
|
||
|
ASSERT_EQ(1, outs.size());
|
||
|
|
||
|
auto& o = outs[0];
|
||
|
ASSERT_EQ(expectedOut.type, o.type);
|
||
|
ASSERT_EQ(expectedOut.amount, o.amount);
|
||
|
ASSERT_EQ(expectedOut.requiredSignatures, o.requiredSignatures);
|
||
|
ASSERT_EQ(expectedOut.globalOutputIndex, o.globalOutputIndex);
|
||
|
ASSERT_EQ(expectedOut.outputInTransaction, o.outputInTransaction);
|
||
|
ASSERT_EQ(expectedOut.transactionPublicKey, o.transactionPublicKey);
|
||
|
}
|
||
|
|
||
|
TEST_F(TransfersConsumerTest, onNewBlocks_checkTransactionInformation) {
|
||
|
auto& container = addSubscription().getContainer();
|
||
|
|
||
|
std::shared_ptr<ITransaction> tx(createTransaction());
|
||
|
addTestInput(*tx, 10000);
|
||
|
addTestKeyOutput(*tx, 1000, 2, m_accountKeys);
|
||
|
Hash paymentId = crypto::rand<Hash>();
|
||
|
uint64_t unlockTime = 10;
|
||
|
tx->setPaymentId(paymentId);
|
||
|
tx->setUnlockTime(unlockTime);
|
||
|
|
||
|
CompleteBlock blocks[2];
|
||
|
blocks[0].block = cryptonote::Block();
|
||
|
blocks[0].block->timestamp = 0;
|
||
|
blocks[0].transactions.push_back(createTransaction());
|
||
|
|
||
|
blocks[1].block = cryptonote::Block();
|
||
|
blocks[1].block->timestamp = 11;
|
||
|
blocks[1].transactions.push_back(tx);
|
||
|
|
||
|
ASSERT_TRUE(m_consumer.onNewBlocks(&blocks[0], 0, 2));
|
||
|
|
||
|
TransactionInformation info;
|
||
|
int64_t balance;
|
||
|
ASSERT_TRUE(container.getTransactionInformation(tx->getTransactionHash(), info, balance));
|
||
|
|
||
|
ASSERT_EQ(tx->getTransactionHash(), info.transactionHash);
|
||
|
ASSERT_EQ(tx->getTransactionPublicKey(), info.publicKey);
|
||
|
ASSERT_EQ(1, info.blockHeight);
|
||
|
ASSERT_EQ(11, info.timestamp);
|
||
|
ASSERT_EQ(unlockTime, info.unlockTime);
|
||
|
ASSERT_EQ(10000, info.totalAmountIn);
|
||
|
ASSERT_EQ(1000, info.totalAmountOut);
|
||
|
ASSERT_EQ(paymentId, info.paymentId);
|
||
|
}
|
||
|
|
||
|
TEST_F(TransfersConsumerTest, onNewBlocks_manyBlocks) {
|
||
|
const size_t blocksCount = 1000;
|
||
|
const size_t txPerBlock = 10;
|
||
|
|
||
|
auto& container = addSubscription().getContainer();
|
||
|
|
||
|
std::vector<CompleteBlock> blocks(blocksCount);
|
||
|
|
||
|
uint64_t timestamp = 10000;
|
||
|
uint64_t expectedAmount = 0;
|
||
|
size_t expectedTransactions = 0;
|
||
|
uint64_t globalOut = 0;
|
||
|
size_t blockIdx = 0;
|
||
|
|
||
|
for (auto& b : blocks) {
|
||
|
b.block = cryptonote::Block();
|
||
|
b.block->timestamp = timestamp++;
|
||
|
|
||
|
if (++blockIdx % 10 == 0) {
|
||
|
for (size_t i = 0; i < txPerBlock; ++i) {
|
||
|
auto tx = createTransaction();
|
||
|
addTestInput(*tx, 10000);
|
||
|
if ((i % 3) == 0) {
|
||
|
addTestKeyOutput(*tx, 1000, ++globalOut, m_accountKeys);
|
||
|
addTestKeyOutput(*tx, 2000, ++globalOut, m_accountKeys);
|
||
|
expectedAmount += 3000;
|
||
|
++expectedTransactions;
|
||
|
}
|
||
|
|
||
|
b.transactions.push_back(std::move(tx));
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
ASSERT_TRUE(m_consumer.onNewBlocks(&blocks[0], 0, blocks.size()));
|
||
|
|
||
|
ASSERT_EQ(expectedTransactions, container.transactionsCount());
|
||
|
ASSERT_EQ(expectedAmount, container.balance(ITransfersContainer::IncludeAll));
|
||
|
}
|
||
|
|
||
|
|
||
|
|
||
|
TEST_F(TransfersConsumerTest, onPoolUpdated_addTransaction) {
|
||
|
auto& sub = addSubscription();
|
||
|
|
||
|
// construct tx
|
||
|
auto tx = createTransaction();
|
||
|
addTestInput(*tx, 10000);
|
||
|
auto out = addTestKeyOutput(*tx, 10000, UNCONFIRMED_TRANSACTION_GLOBAL_OUTPUT_INDEX, m_accountKeys);
|
||
|
|
||
|
m_consumer.onPoolUpdated({convertTx(*tx)}, {});
|
||
|
|
||
|
auto outputs = sub.getContainer().getTransactionOutputs(tx->getTransactionHash(), ITransfersContainer::IncludeAll);
|
||
|
|
||
|
ASSERT_EQ(1, outputs.size());
|
||
|
|
||
|
auto& o = outputs[0];
|
||
|
|
||
|
ASSERT_EQ(out.type, o.type);
|
||
|
ASSERT_EQ(out.amount, o.amount);
|
||
|
ASSERT_EQ(out.outputKey, o.outputKey);
|
||
|
ASSERT_EQ(UNCONFIRMED_TRANSACTION_GLOBAL_OUTPUT_INDEX, o.globalOutputIndex);
|
||
|
}
|
||
|
|
||
|
TEST_F(TransfersConsumerTest, onPoolUpdated_addTransactionMultisignature) {
|
||
|
auto& sub = addSubscription();
|
||
|
|
||
|
// construct tx with multisignature output
|
||
|
auto tx = createTransaction();
|
||
|
addTestInput(*tx, 10000);
|
||
|
auto addresses = { m_accountKeys.address, generateAccountKeys().address };
|
||
|
tx->addOutput(10000, addresses, 1);
|
||
|
|
||
|
m_consumer.onPoolUpdated({ convertTx(*tx) }, {});
|
||
|
|
||
|
auto outputs = sub.getContainer().getTransactionOutputs(tx->getTransactionHash(), ITransfersContainer::IncludeAll);
|
||
|
|
||
|
ASSERT_EQ(1, outputs.size());
|
||
|
|
||
|
auto& o = outputs[0];
|
||
|
|
||
|
TransactionTypes::OutputMultisignature out;
|
||
|
tx->getOutput(0, out);
|
||
|
|
||
|
ASSERT_EQ(TransactionTypes::OutputType::Multisignature, o.type);
|
||
|
ASSERT_EQ(out.amount, o.amount);
|
||
|
ASSERT_EQ(out.requiredSignatures, o.requiredSignatures);
|
||
|
ASSERT_EQ(UNCONFIRMED_TRANSACTION_GLOBAL_OUTPUT_INDEX, o.globalOutputIndex);
|
||
|
}
|
||
|
|
||
|
|
||
|
TEST_F(TransfersConsumerTest, onPoolUpdated_addTransactionDoesNotGetsGlobalIndices) {
|
||
|
auto& sub = addSubscription();
|
||
|
// construct tx
|
||
|
auto tx = createTransaction();
|
||
|
addTestInput(*tx, 10000);
|
||
|
auto out = addTestKeyOutput(*tx, 10000, UNCONFIRMED_TRANSACTION_GLOBAL_OUTPUT_INDEX, m_accountKeys);
|
||
|
m_consumer.onPoolUpdated({ convertTx(*tx) }, {});
|
||
|
|
||
|
ASSERT_TRUE(m_node.calls_getTransactionOutsGlobalIndices.empty());
|
||
|
}
|
||
|
|
||
|
TEST_F(TransfersConsumerTest, onPoolUpdated_deleteTransaction) {
|
||
|
auto& sub = addSubscription();
|
||
|
TransfersObserver observer;
|
||
|
sub.addObserver(&observer);
|
||
|
|
||
|
std::vector<crypto::hash> deleted = {
|
||
|
crypto::rand<crypto::hash>(),
|
||
|
crypto::rand<crypto::hash>()
|
||
|
};
|
||
|
|
||
|
m_consumer.onPoolUpdated({}, deleted);
|
||
|
|
||
|
ASSERT_EQ(deleted.size(), observer.deleted.size());
|
||
|
ASSERT_EQ(reinterpret_cast<const Hash&>(deleted[0]), observer.deleted[0]);
|
||
|
ASSERT_EQ(reinterpret_cast<const Hash&>(deleted[1]), observer.deleted[1]);
|
||
|
}
|
||
|
|
||
|
TEST_F(TransfersConsumerTest, getKnownPoolTxIds_empty) {
|
||
|
auto& sub = addSubscription();
|
||
|
std::vector<crypto::hash> ids;
|
||
|
m_consumer.getKnownPoolTxIds(ids);
|
||
|
ASSERT_TRUE(ids.empty());
|
||
|
}
|
||
|
|
||
|
std::unique_ptr<ITransaction> createTransactionTo(const AccountKeys& to, uint64_t amountIn, uint64_t amountOut) {
|
||
|
auto tx = createTransaction();
|
||
|
addTestInput(*tx, amountIn);
|
||
|
addTestKeyOutput(*tx, amountOut, UNCONFIRMED_TRANSACTION_GLOBAL_OUTPUT_INDEX, to);
|
||
|
return tx;
|
||
|
}
|
||
|
|
||
|
TEST_F(TransfersConsumerTest, getKnownPoolTxIds_returnsUnconfirmed) {
|
||
|
auto acc1 = generateAccount();
|
||
|
auto acc2 = generateAccount();
|
||
|
|
||
|
auto& sub1 = addSubscription(acc1);
|
||
|
auto& sub2 = addSubscription(acc2);
|
||
|
|
||
|
std::vector<std::unique_ptr<ITransaction>> txs;
|
||
|
txs.push_back(createTransactionTo(acc1, 10000, 10000));
|
||
|
txs.push_back(createTransactionTo(acc1, 20000, 20000));
|
||
|
txs.push_back(createTransactionTo(acc2, 30000, 30000));
|
||
|
|
||
|
m_consumer.onPoolUpdated({ convertTx(*txs[0]), convertTx(*txs[1]), convertTx(*txs[2])}, {});
|
||
|
|
||
|
std::vector<crypto::hash> ids;
|
||
|
m_consumer.getKnownPoolTxIds(ids);
|
||
|
|
||
|
ASSERT_EQ(3, ids.size());
|
||
|
|
||
|
for (int i = 0; i < 3; ++i) {
|
||
|
auto txhash = txs[i]->getTransactionHash();
|
||
|
ASSERT_TRUE(std::find(ids.begin(), ids.end(), reinterpret_cast<const crypto::hash&>(txhash)) != ids.end());
|
||
|
}
|
||
|
}
|