|
|
|
//
|
|
|
|
// Created by mwo on 8/01/17.
|
|
|
|
//
|
|
|
|
|
|
|
|
#define MYSQLPP_SSQLS_NO_STATICS 1
|
|
|
|
|
|
|
|
#include "TxSearch.h"
|
|
|
|
|
|
|
|
#include "db/MySqlAccounts.h"
|
|
|
|
|
|
|
|
#include "db/ssqlses.h"
|
|
|
|
|
|
|
|
#include "CurrentBlockchainStatus.h"
|
|
|
|
#include "src/UniversalIdentifier.hpp"
|
|
|
|
|
|
|
|
namespace xmreg
|
|
|
|
{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
TxSearch::TxSearch(XmrAccount const& _acc,
|
|
|
|
std::shared_ptr<CurrentBlockchainStatus> _current_bc_status)
|
|
|
|
: current_bc_status {_current_bc_status}
|
|
|
|
{
|
|
|
|
acc = make_shared<XmrAccount>(_acc);
|
|
|
|
|
|
|
|
// creates an mysql connection for this thread
|
|
|
|
xmr_accounts = make_shared<MySqlAccounts>(current_bc_status);
|
|
|
|
|
|
|
|
network_type net_type = current_bc_status->get_bc_setup().net_type;
|
|
|
|
|
|
|
|
|
|
|
|
if (!xmreg::parse_str_address(acc->address, address, net_type))
|
|
|
|
{
|
|
|
|
OMERROR << "Cant parse address: " + acc->address;
|
|
|
|
throw TxSearchException("Cant parse address: " + acc->address);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!xmreg::parse_str_secret_key(acc->viewkey, viewkey))
|
|
|
|
{
|
|
|
|
OMERROR << "Cant parse private view key: " + acc->address;
|
|
|
|
throw TxSearchException("Cant parse private view key: "
|
|
|
|
+ acc->viewkey);
|
|
|
|
}
|
|
|
|
|
|
|
|
populate_known_outputs();
|
|
|
|
|
|
|
|
// start searching from last block that we searched for
|
|
|
|
// this accont
|
|
|
|
searched_blk_no = acc->scanned_block_height;
|
|
|
|
|
|
|
|
last_ping_timestamp = 0s;
|
|
|
|
|
|
|
|
address_prefix = acc->address.substr(0, 6);
|
|
|
|
|
|
|
|
ping();
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
TxSearch::operator()()
|
|
|
|
{
|
|
|
|
|
|
|
|
seconds current_timestamp = get_current_timestamp();
|
|
|
|
|
|
|
|
last_ping_timestamp = current_timestamp;
|
|
|
|
|
|
|
|
uint64_t blocks_lookahead
|
|
|
|
= current_bc_status->get_bc_setup().blocks_search_lookahead;
|
|
|
|
|
|
|
|
|
|
|
|
auto current_bc_status_ptr = current_bc_status.get();
|
|
|
|
|
|
|
|
uint64_t account_id = acc->id.data;
|
|
|
|
|
|
|
|
searching_is_ongoing = true;
|
|
|
|
|
|
|
|
MicroCoreAdapter mcore_addapter {current_bc_status_ptr};
|
|
|
|
|
|
|
|
// we put everything in massive catch, as there are plenty ways in which
|
|
|
|
// an exceptions can be thrown here. Mostly from mysql.
|
|
|
|
// but because this is detatch thread, we cant catch them in main thread.
|
|
|
|
// program will blow up if exception is thrown. need to handle exceptions
|
|
|
|
// here.
|
|
|
|
try
|
|
|
|
{
|
|
|
|
while(continue_search)
|
|
|
|
{
|
|
|
|
|
|
|
|
seconds loop_timestamp {current_timestamp};
|
|
|
|
|
|
|
|
uint64_t last_block_height = current_bc_status->current_height;
|
|
|
|
|
|
|
|
uint64_t h1 = searched_blk_no;
|
|
|
|
uint64_t h2 = std::min(h1 + blocks_lookahead - 1, last_block_height);
|
|
|
|
|
|
|
|
vector<block> blocks;
|
|
|
|
|
|
|
|
blocks = current_bc_status->get_blocks_range(h1, h2);
|
|
|
|
|
|
|
|
if (blocks.empty())
|
|
|
|
{
|
|
|
|
|
|
|
|
if (h1 <= h2)
|
|
|
|
{
|
|
|
|
OMERROR << address_prefix << ": cant get blocks from " << h1
|
|
|
|
<< " to " << h2;
|
|
|
|
stop();
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
OMVLOG1 << address_prefix << ": waiting for new block. "
|
|
|
|
<< "Last scanned was " << h2;
|
|
|
|
}
|
|
|
|
|
|
|
|
std::this_thread::sleep_for(
|
|
|
|
std::chrono::seconds(
|
|
|
|
current_bc_status->get_bc_setup()
|
|
|
|
.refresh_block_status_every)
|
|
|
|
);
|
|
|
|
|
|
|
|
loop_timestamp = get_current_timestamp();
|
|
|
|
|
|
|
|
// if thread has lived longer than thread_search_li
|
|
|
|
// without last_ping_timestamp being updated,
|
|
|
|
// stop the thread
|
|
|
|
if (loop_timestamp - last_ping_timestamp > thread_search_life)
|
|
|
|
{
|
|
|
|
OMINFO << address_prefix
|
|
|
|
+ ": search thread stopped.";
|
|
|
|
stop();
|
|
|
|
}
|
|
|
|
|
|
|
|
// update current_height of blockchain, as maybe top block(s)
|
|
|
|
// were dropped due to reorganization.
|
|
|
|
//CurrentBlockchainStatus::update_current_blockchain_height();
|
|
|
|
|
|
|
|
// if any txs that we already indexed got orphaned as a
|
|
|
|
// consequence of this
|
|
|
|
// MySqlAccounts::select_txs_for_account_spendability_check
|
|
|
|
// should
|
|
|
|
// update database accordingly when get_address_txs is executed.
|
|
|
|
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
OMVLOG2 << address_prefix + ": analyzing "
|
|
|
|
<< blocks.size() << " blocks from "
|
|
|
|
<< h1 << " to " << h2
|
|
|
|
<< " out of " << last_block_height << " blocks";
|
|
|
|
|
|
|
|
vector<crypto::hash> txs_hashes_from_blocks;
|
|
|
|
vector<transaction> txs_in_blocks;
|
|
|
|
vector<CurrentBlockchainStatus::txs_tuple_t> txs_data;
|
|
|
|
|
|
|
|
if (!current_bc_status->get_txs_in_blocks(blocks,
|
|
|
|
txs_hashes_from_blocks,
|
|
|
|
txs_in_blocks,
|
|
|
|
txs_data))
|
|
|
|
{
|
|
|
|
OMERROR << address_prefix
|
|
|
|
+ ": cant get tx in blocks from "
|
|
|
|
<< h1 << " to " << h2;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// we will only create mysql DateTime object once, anything is found
|
|
|
|
// in a given block;
|
|
|
|
unique_ptr<DateTime> blk_timestamp_mysql_format;
|
|
|
|
|
|
|
|
// searching for our incoming and outgoing xmr has two components.
|
|
|
|
//
|
|
|
|
// FIRST. to search for the incoming xmr, we use address, viewkey and
|
|
|
|
// outputs public key. Its straight forward, as this is what viewkey was
|
|
|
|
// designed to do.
|
|
|
|
//
|
|
|
|
// SECOND. Searching for spendings (i.e., key images) is more difficult,
|
|
|
|
// because we dont have spendkey. But what we can do is, we can look for
|
|
|
|
// candidate key images. And this can be achieved by checking if any mixin
|
|
|
|
// in associated with the given key image, is our output. If it is our output,
|
|
|
|
// then we assume its our key image (i.e. we spend this output).
|
|
|
|
// Off course this is only
|
|
|
|
// assumption as our outputs can be used in key images of others for their
|
|
|
|
// mixin purposes. Thus, we sent to the front end the list of key images
|
|
|
|
// that we think are yours, and the frontend, because it has spend key,
|
|
|
|
// can filter out false positives.
|
|
|
|
|
|
|
|
size_t tx_idx {0};
|
|
|
|
|
|
|
|
for (auto const& tx_tuple: txs_data)
|
|
|
|
{
|
|
|
|
crypto::hash const& tx_hash = txs_hashes_from_blocks[tx_idx];
|
|
|
|
transaction const& tx = txs_in_blocks[tx_idx++];
|
|
|
|
uint64_t blk_height = std::get<0>(tx_tuple);
|
|
|
|
uint64_t blk_timestamp = std::get<1>(tx_tuple);
|
|
|
|
bool is_coinbase = std::get<2>(tx_tuple);
|
|
|
|
bool is_rct = (tx.version == 2);
|
|
|
|
uint8_t rct_type = (is_rct ? tx.rct_signatures.type : 0);
|
|
|
|
|
|
|
|
|
|
|
|
// Class that is responsible for identification of our outputs
|
|
|
|
// and inputs in a given tx.
|
|
|
|
|
|
|
|
auto identifier = make_identifier(tx,
|
|
|
|
make_unique<Output>(&address, &viewkey),
|
|
|
|
make_unique<Input>(&address, &viewkey,
|
|
|
|
&known_outputs_keys,
|
|
|
|
&mcore_addapter));
|
|
|
|
identifier.identify();
|
|
|
|
|
|
|
|
// flag indicating whether the txs in the given block are
|
|
|
|
// spendable.
|
|
|
|
// this is true when block number is more than 10 blocks
|
|
|
|
// from current blockchain height.
|
|
|
|
// if tx.unlock_time is not given (its value is 0), we set it
|
|
|
|
// here. For coinbase its always given, so no need to check
|
|
|
|
// for that
|
|
|
|
|
|
|
|
uint64_t tx_unlock_time = tx.unlock_time;
|
|
|
|
|
|
|
|
if (tx_unlock_time == 0)
|
|
|
|
tx_unlock_time = blk_height
|
|
|
|
+ CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE;
|
|
|
|
|
|
|
|
bool is_spendable = current_bc_status->is_tx_unlocked(
|
|
|
|
tx_unlock_time, blk_height);
|
|
|
|
|
|
|
|
// this is id of txs in lmdb blockchain table.
|
|
|
|
// it will be used mostly to sort txs in the frontend.
|
|
|
|
uint64_t blockchain_tx_id {0};
|
|
|
|
|
|
|
|
// FIRSt step.
|
|
|
|
//
|
|
|
|
//oi_identification.identify_outputs();
|
|
|
|
auto const& outputs_identified
|
|
|
|
= identifier.get<Output>()->get();
|
|
|
|
|
|
|
|
auto total_received = calc_total_xmr(outputs_identified);
|
|
|
|
|
|
|
|
vector<uint64_t> amount_specific_indices;
|
|
|
|
|
|
|
|
uint64_t tx_mysql_id {0};
|
|
|
|
|
|
|
|
// create pointer to mysql transaction object
|
|
|
|
// that we will initilize if we find something.
|
|
|
|
unique_ptr<mysqlpp::Transaction> mysql_transaction;
|
|
|
|
|
|
|
|
// if we identified some outputs as ours,
|
|
|
|
// save them into mysql.
|
|
|
|
if (!outputs_identified.empty())
|
|
|
|
{
|
|
|
|
auto tx_hash_str = pod_to_hex(tx_hash);
|
|
|
|
auto tx_hash_prefix_str
|
|
|
|
= pod_to_hex(get_transaction_prefix_hash(tx));
|
|
|
|
auto tx_pub_key_str
|
|
|
|
= pod_to_hex(identifier.get_tx_pub_key());
|
|
|
|
uint64_t mixin_no {0};
|
|
|
|
if (!is_coinbase)
|
|
|
|
mixin_no = xmreg::get_mixin_no(tx);
|
|
|
|
|
|
|
|
|
|
|
|
if (!blk_timestamp_mysql_format)
|
|
|
|
{
|
|
|
|
blk_timestamp_mysql_format
|
|
|
|
= unique_ptr<DateTime>(
|
|
|
|
new DateTime(
|
|
|
|
static_cast<time_t>(blk_timestamp)));
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!mysql_transaction)
|
|
|
|
{
|
|
|
|
// start mysql transaction here
|
|
|
|
mysql_transaction
|
|
|
|
= unique_ptr<mysqlpp::Transaction>(
|
|
|
|
new mysqlpp::Transaction(
|
|
|
|
xmr_accounts->get_connection()
|
|
|
|
->get_connection()));
|
|
|
|
|
|
|
|
// when we rescan blockchain some txs can already
|
|
|
|
// be present in the mysql. So remove them, and their
|
|
|
|
// associated data in that case to repopulate fresh
|
|
|
|
// tx data
|
|
|
|
if (!delete_existing_tx_if_exists(tx_hash_str))
|
|
|
|
throw TxSearchException("Cant delete tx "
|
|
|
|
+ tx_hash_str);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!current_bc_status->tx_exist(tx_hash, blockchain_tx_id))
|
|
|
|
{
|
|
|
|
OMERROR << "Tx " << tx_hash_str
|
|
|
|
<< " not found in blockchain!";
|
|
|
|
throw TxSearchException("Cant get tx from blockchain: "
|
|
|
|
+ tx_hash_str);
|
|
|
|
}
|
|
|
|
|
|
|
|
OMVLOG1 << address_prefix + ": found some outputs in block "
|
|
|
|
<< blk_height << ", tx: " << tx_hash_str;
|
|
|
|
|
|
|
|
|
|
|
|
XmrTransaction tx_data;
|
|
|
|
|
|
|
|
tx_data.id = mysqlpp::null;
|
|
|
|
tx_data.hash = tx_hash_str;
|
|
|
|
tx_data.prefix_hash = tx_hash_prefix_str;
|
|
|
|
tx_data.tx_pub_key = tx_pub_key_str;
|
|
|
|
tx_data.account_id = account_id;
|
|
|
|
tx_data.blockchain_tx_id = blockchain_tx_id;
|
|
|
|
tx_data.total_received = total_received;
|
|
|
|
tx_data.total_sent = 0; // at this stage we don't have
|
|
|
|
// anyinfo about spendings
|
|
|
|
|
|
|
|
// this is current block
|
|
|
|
// + unlock time
|
|
|
|
// for regular tx,
|
|
|
|
// the unlock time is
|
|
|
|
// default of 10 blocks.
|
|
|
|
// for coinbase tx it is 60 blocks
|
|
|
|
tx_data.unlock_time = tx_unlock_time;
|
|
|
|
|
|
|
|
tx_data.height = blk_height;
|
|
|
|
tx_data.coinbase = is_coinbase;
|
|
|
|
tx_data.is_rct = is_rct;
|
|
|
|
tx_data.rct_type = rct_type;
|
|
|
|
tx_data.spendable = is_spendable;
|
|
|
|
tx_data.payment_id = current_bc_status
|
|
|
|
->get_payment_id_as_string(tx);
|
|
|
|
tx_data.mixin = mixin_no;
|
|
|
|
tx_data.timestamp = *blk_timestamp_mysql_format;
|
|
|
|
|
|
|
|
|
|
|
|
// insert tx_data into mysql's Transactions table
|
|
|
|
tx_mysql_id = xmr_accounts->insert(tx_data);
|
|
|
|
|
|
|
|
// get amount specific (i.e., global) indices of outputs
|
|
|
|
if (!current_bc_status->get_amount_specific_indices(
|
|
|
|
tx_hash, amount_specific_indices))
|
|
|
|
{
|
|
|
|
OMERROR << address_prefix
|
|
|
|
+ ": cant get_amount_specific_indices!";
|
|
|
|
throw TxSearchException(
|
|
|
|
"cant get_amount_specific_indices!");
|
|
|
|
}
|
|
|
|
|
|
|
|
if (tx_mysql_id == 0)
|
|
|
|
{
|
|
|
|
OMERROR << address_prefix
|
|
|
|
+ ": tx_mysql_id is zero!"
|
|
|
|
<< tx_data;
|
|
|
|
throw TxSearchException("tx_mysql_id is zero!");
|
|
|
|
}
|
|
|
|
|
|
|
|
vector<XmrOutput> outputs_found;
|
|
|
|
|
|
|
|
// now add the found outputs into Outputs tables
|
|
|
|
for (auto&& out_info: outputs_identified)
|
|
|
|
{
|
|
|
|
XmrOutput out_data;
|
|
|
|
|
|
|
|
out_data.id = mysqlpp::null;
|
|
|
|
out_data.account_id = account_id;
|
|
|
|
out_data.tx_id = tx_mysql_id;
|
|
|
|
out_data.out_pub_key = pod_to_hex(out_info.pub_key);
|
|
|
|
out_data.tx_pub_key = tx_pub_key_str;
|
|
|
|
out_data.amount = out_info.amount;
|
|
|
|
out_data.out_index = out_info.idx_in_tx;
|
|
|
|
out_data.rct_outpk = pod_to_hex(out_info.rtc_outpk);
|
|
|
|
out_data.rct_mask = pod_to_hex(out_info.rtc_mask);
|
|
|
|
out_data.rct_amount = pod_to_hex(out_info.rtc_amount);
|
|
|
|
out_data.global_index = amount_specific_indices
|
|
|
|
.at(out_data.out_index);
|
|
|
|
out_data.mixin = tx_data.mixin;
|
|
|
|
out_data.timestamp = tx_data.timestamp;
|
|
|
|
|
|
|
|
outputs_found.push_back(std::move(out_data));
|
|
|
|
|
|
|
|
{
|
|
|
|
// add the outputs found into known_outputs_keys map
|
|
|
|
std::lock_guard<std::mutex> lck (
|
|
|
|
getting_known_outputs_keys);
|
|
|
|
known_outputs_keys.insert(
|
|
|
|
{out_info.pub_key, out_info.amount});
|
|
|
|
}
|
|
|
|
|
|
|
|
} // for (auto& out_info: outputs_identified)
|
|
|
|
|
|
|
|
|
|
|
|
// insert all outputs found into mysql's outputs table
|
|
|
|
uint64_t no_rows_inserted
|
|
|
|
= xmr_accounts->insert(outputs_found);
|
|
|
|
|
|
|
|
if (no_rows_inserted == 0)
|
|
|
|
{
|
|
|
|
OMERROR << address_prefix
|
|
|
|
<< ": insert outputs_found: no_rows_inserted is zero!"
|
|
|
|
<< outputs_found;
|
|
|
|
|
|
|
|
throw TxSearchException("insert output_found: no_rows_inserted is zero!");
|
|
|
|
}
|
|
|
|
|
|
|
|
} // if (!found_mine_outputs.empty())
|
|
|
|
|
|
|
|
// SECOND component: Checking for our key images, i.e., inputs.
|
|
|
|
|
|
|
|
// no need mutex here, as this will be exectued only after
|
|
|
|
// the above. there is no threads here.
|
|
|
|
|
|
|
|
auto const& inputs_identfied = identifier.get<Input>()->get();
|
|
|
|
|
|
|
|
|
|
|
|
if (!inputs_identfied.empty())
|
|
|
|
{
|
|
|
|
// some inputs were identified as ours in a given tx.
|
|
|
|
// so now, go over those inputs, and check
|
|
|
|
// get detail info for each found mixin output from database
|
|
|
|
|
|
|
|
auto tx_hash_str = pod_to_hex(tx_hash);
|
|
|
|
auto tx_hash_prefix_str
|
|
|
|
= pod_to_hex(get_transaction_prefix_hash(tx));
|
|
|
|
auto tx_pub_key_str
|
|
|
|
= pod_to_hex(identifier.get_tx_pub_key());
|
|
|
|
uint64_t mixin_no {0};
|
|
|
|
if (mixin_no == 0 && !is_coinbase)
|
|
|
|
mixin_no = xmreg::get_mixin_no(tx);
|
|
|
|
|
|
|
|
if (!blk_timestamp_mysql_format)
|
|
|
|
{
|
|
|
|
blk_timestamp_mysql_format
|
|
|
|
= unique_ptr<DateTime>(
|
|
|
|
new DateTime(
|
|
|
|
static_cast<time_t>(blk_timestamp)));
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!mysql_transaction)
|
|
|
|
{
|
|
|
|
// start mysql transaction here if not already present
|
|
|
|
mysql_transaction
|
|
|
|
= unique_ptr<mysqlpp::Transaction>(
|
|
|
|
new mysqlpp::Transaction(
|
|
|
|
xmr_accounts->get_connection()
|
|
|
|
->get_connection()));
|
|
|
|
|
|
|
|
// when we rescan blockchain some txs can already
|
|
|
|
// be present in the mysql. So remove them, and their
|
|
|
|
// associated data in that case to repopulate fresh tx data
|
|
|
|
|
|
|
|
// this will only execture if no outputs were found
|
|
|
|
// above. So there is no risk of deleting same tx twice
|
|
|
|
if (!delete_existing_tx_if_exists(tx_hash_str))
|
|
|
|
{
|
|
|
|
throw TxSearchException(
|
|
|
|
"Cant delete tx " + tx_hash_str);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (blockchain_tx_id == 0)
|
|
|
|
{
|
|
|
|
if (!current_bc_status
|
|
|
|
->tx_exist(tx_hash, blockchain_tx_id))
|
|
|
|
{
|
|
|
|
OMERROR << address_prefix + ": tx "
|
|
|
|
<< tx_hash_str
|
|
|
|
<< "not found in blockchain !";
|
|
|
|
throw TxSearchException(
|
|
|
|
"Cant get tx from blockchain: "
|
|
|
|
+ tx_hash_str);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
OMVLOG1 << address_prefix + ": found some possible "
|
|
|
|
<< "inputs in block " << blk_height << ", tx: "
|
|
|
|
<< tx_hash_str;
|
|
|
|
|
|
|
|
vector<XmrInput> inputs_found;
|
|
|
|
|
|
|
|
for (auto& in_info: inputs_identfied)
|
|
|
|
{
|
|
|
|
XmrOutput out;
|
|
|
|
|
|
|
|
if (xmr_accounts->output_exists(
|
|
|
|
pod_to_hex(in_info.out_pub_key), out))
|
|
|
|
{
|
|
|
|
|
|
|
|
// seems that this key image is ours.
|
|
|
|
// so get it information from database into XmrInput
|
|
|
|
// database structure that will be written later
|
|
|
|
// on into database.
|
|
|
|
|
|
|
|
XmrInput in_data;
|
|
|
|
|
|
|
|
in_data.id = mysqlpp::null;
|
|
|
|
in_data.account_id = acc->id.data;
|
|
|
|
in_data.tx_id = 0; // later we set it
|
|
|
|
in_data.output_id = out.id.data;
|
|
|
|
in_data.key_image = pod_to_hex(in_info.key_img);
|
|
|
|
in_data.amount = out.amount;
|
|
|
|
// must match corresponding
|
|
|
|
// output's amount
|
|
|
|
in_data.timestamp = *blk_timestamp_mysql_format;
|
|
|
|
|
|
|
|
inputs_found.push_back(in_data);
|
|
|
|
|
|
|
|
} // if (xmr_accounts->output_exists(o
|
|
|
|
|
|
|
|
} // for (auto& in_info: inputs_identfied)
|
|
|
|
|
|
|
|
|
|
|
|
if (!inputs_found.empty())
|
|
|
|
{
|
|
|
|
// seems we have some inputs found. time
|
|
|
|
// to write it to mysql. But first,
|
|
|
|
// check if this tx is written in mysql.
|
|
|
|
|
|
|
|
// calculate how much we preasumply spent.
|
|
|
|
uint64_t total_sent {0};
|
|
|
|
|
|
|
|
for (const XmrInput& in_data: inputs_found)
|
|
|
|
total_sent += in_data.amount;
|
|
|
|
|
|
|
|
if (tx_mysql_id == 0)
|
|
|
|
{
|
|
|
|
// this txs hasnt been seen in step first.
|
|
|
|
// it means that it only contains potentially our
|
|
|
|
// key images. It does not have our outputs.
|
|
|
|
// so write it to mysql as ours, with
|
|
|
|
// total received of 0.
|
|
|
|
|
|
|
|
XmrTransaction tx_data;
|
|
|
|
|
|
|
|
tx_data.id = mysqlpp::null;
|
|
|
|
tx_data.hash = tx_hash_str;
|
|
|
|
tx_data.prefix_hash = tx_hash_prefix_str;
|
|
|
|
tx_data.tx_pub_key = tx_pub_key_str;
|
|
|
|
tx_data.account_id = acc->id.data;
|
|
|
|
tx_data.blockchain_tx_id = blockchain_tx_id;
|
|
|
|
tx_data.total_received = 0; // because this is
|
|
|
|
//spending,
|
|
|
|
//total_recieved is 0
|
|
|
|
tx_data.total_sent = total_sent;
|
|
|
|
tx_data.unlock_time = tx_unlock_time;
|
|
|
|
tx_data.height = blk_height;
|
|
|
|
tx_data.coinbase = is_coinbase;
|
|
|
|
tx_data.is_rct = is_rct;
|
|
|
|
tx_data.rct_type = rct_type;
|
|
|
|
tx_data.spendable = is_spendable;
|
|
|
|
tx_data.payment_id = current_bc_status
|
|
|
|
->get_payment_id_as_string(tx);
|
|
|
|
tx_data.mixin = mixin_no;
|
|
|
|
tx_data.timestamp = *blk_timestamp_mysql_format;
|
|
|
|
|
|
|
|
// insert tx_data into mysql's Transactions table
|
|
|
|
tx_mysql_id = xmr_accounts->insert(tx_data);
|
|
|
|
|
|
|
|
if (tx_mysql_id == 0)
|
|
|
|
{
|
|
|
|
OMERROR << address_prefix
|
|
|
|
+ ": tx_mysql_id is zero!"
|
|
|
|
<< tx_data;
|
|
|
|
|
|
|
|
//cerr << "tx_mysql_id is zero!" << endl;
|
|
|
|
throw TxSearchException("tx_mysql_id is zero!");
|
|
|
|
// it did not insert this tx, because maybe
|
|
|
|
// it already
|
|
|
|
// exisits in the MySQL. So maybe can now
|
|
|
|
// check if we have it and get tx_mysql_id this
|
|
|
|
// way.
|
|
|
|
}
|
|
|
|
|
|
|
|
} // if (tx_mysql_id == 0)
|
|
|
|
|
|
|
|
// save all input found into database at once
|
|
|
|
// but first update tx_mysql_id for these inputs
|
|
|
|
for (XmrInput& in_data: inputs_found)
|
|
|
|
in_data.tx_id = tx_mysql_id; // set tx id now.
|
|
|
|
//before we made it 0
|
|
|
|
|
|
|
|
uint64_t no_rows_inserted
|
|
|
|
= xmr_accounts->insert(inputs_found);
|
|
|
|
|
|
|
|
if (no_rows_inserted == 0)
|
|
|
|
{
|
|
|
|
OMERROR << address_prefix
|
|
|
|
<< ": insert inputs_found: no_rows_inserted is zero!"
|
|
|
|
<< inputs_found;
|
|
|
|
|
|
|
|
throw TxSearchException(
|
|
|
|
"insert inputs_found: no_rows_inserted is zero!");
|
|
|
|
}
|
|
|
|
|
|
|
|
} // if (!inputs_found.empty())
|
|
|
|
|
|
|
|
} // if (!oi_identification.identified_inputs.empty())
|
|
|
|
|
|
|
|
// if we get to this point, we assume that all tx related
|
|
|
|
// tables are ready
|
|
|
|
// to be written, i.e., Transactions,
|
|
|
|
// Outputs and Inputs. If so, write
|
|
|
|
// all this into database.
|
|
|
|
|
|
|
|
if (mysql_transaction)
|
|
|
|
mysql_transaction->commit();
|
|
|
|
|
|
|
|
} // for (auto const& tx_pair: txs_map)
|
|
|
|
|
|
|
|
// update scanned_block_height every given interval
|
|
|
|
// or when we reached top of the blockchain
|
|
|
|
|
|
|
|
std::lock_guard<std::mutex> acc_lck(access_acc);
|
|
|
|
|
|
|
|
XmrAccount updated_acc = *acc;
|
|
|
|
|
|
|
|
updated_acc.scanned_block_height = h2;
|
|
|
|
updated_acc.scanned_block_timestamp
|
|
|
|
= DateTime(static_cast<time_t>(
|
|
|
|
blocks.back().timestamp));
|
|
|
|
|
|
|
|
if (xmr_accounts->update(*acc, updated_acc))
|
|
|
|
{
|
|
|
|
// iff success, set acc to updated_acc;
|
|
|
|
//cout << "scanned_block_height updated\n";
|
|
|
|
*acc = updated_acc;
|
|
|
|
}
|
|
|
|
|
|
|
|
//current_timestamp = loop_timestamp;
|
|
|
|
// update this only when this variable is false
|
|
|
|
// otherwise a new search block value can
|
|
|
|
// be overwritten to h2, instead of the new value
|
|
|
|
if (!searched_block_got_updated)
|
|
|
|
searched_blk_no = h2 + 1;
|
|
|
|
|
|
|
|
searched_block_got_updated = false;
|
|
|
|
|
|
|
|
} // while(continue_search)
|
|
|
|
|
|
|
|
}
|
|
|
|
catch(std::exception const& e)
|
|
|
|
{
|
|
|
|
OMERROR << address_prefix
|
|
|
|
+ ": exception in TxSearch: "
|
|
|
|
<< e.what();
|
|
|
|
set_exception_ptr();
|
|
|
|
}
|
|
|
|
catch(...)
|
|
|
|
{
|
|
|
|
OMERROR << address_prefix
|
|
|
|
+ ": exception in TxSearch!";
|
|
|
|
set_exception_ptr();
|
|
|
|
}
|
|
|
|
|
|
|
|
searching_is_ongoing = false;
|
|
|
|
|
|
|
|
// it will stop anyway, but just call it so we get info message pritened out
|
|
|
|
stop();
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
TxSearch::stop()
|
|
|
|
{
|
|
|
|
OMINFO << address_prefix + ": stopping the thread";
|
|
|
|
continue_search = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
TxSearch::~TxSearch()
|
|
|
|
{
|
|
|
|
OMINFO << address_prefix
|
|
|
|
+ ": txSearch thread destroyed";
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
TxSearch::set_searched_blk_no(uint64_t new_value)
|
|
|
|
{
|
|
|
|
searched_blk_no = new_value;
|
|
|
|
searched_block_got_updated = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
uint64_t
|
|
|
|
TxSearch::get_searched_blk_no() const
|
|
|
|
{
|
|
|
|
return searched_blk_no;
|
|
|
|
}
|
|
|
|
|
|
|
|
inline seconds
|
|
|
|
TxSearch::get_current_timestamp() const
|
|
|
|
{
|
|
|
|
return chrono::duration_cast<seconds>(
|
|
|
|
chrono::system_clock::now().time_since_epoch());
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
TxSearch::ping()
|
|
|
|
{
|
|
|
|
last_ping_timestamp = chrono::duration_cast<seconds>(
|
|
|
|
chrono::system_clock::now().time_since_epoch());
|
|
|
|
|
|
|
|
OMVLOG2 << address_prefix
|
|
|
|
<< ": last_ping_timestamp updated to: "
|
|
|
|
<< last_ping_timestamp.count();
|
|
|
|
}
|
|
|
|
|
|
|
|
bool
|
|
|
|
TxSearch::still_searching() const
|
|
|
|
{
|
|
|
|
return searching_is_ongoing;
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
TxSearch::populate_known_outputs()
|
|
|
|
{
|
|
|
|
vector<XmrOutput> outs;
|
|
|
|
|
|
|
|
if (xmr_accounts->select(acc->id.data, outs))
|
|
|
|
{
|
|
|
|
for (const XmrOutput& out: outs)
|
|
|
|
{
|
|
|
|
public_key out_pub_key;
|
|
|
|
|
|
|
|
hex_to_pod(out.out_pub_key, out_pub_key);
|
|
|
|
|
|
|
|
known_outputs_keys[out_pub_key] = out.amount;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
TxSearch::known_outputs_t
|
|
|
|
TxSearch::get_known_outputs_keys()
|
|
|
|
{
|
|
|
|
std::lock_guard<std::mutex> lck (getting_known_outputs_keys);
|
|
|
|
return known_outputs_keys;
|
|
|
|
};
|
|
|
|
|
|
|
|
void
|
|
|
|
TxSearch::find_txs_in_mempool(
|
|
|
|
TxSearch::pool_txs_t mempool_txs,
|
|
|
|
json* j_transactions)
|
|
|
|
{
|
|
|
|
|
|
|
|
*j_transactions = json::array();
|
|
|
|
|
|
|
|
uint64_t current_height = current_bc_status
|
|
|
|
->get_current_blockchain_height();
|
|
|
|
|
|
|
|
auto current_bc_status_ptr = current_bc_status.get();
|
|
|
|
|
|
|
|
known_outputs_t known_outputs_keys_copy = get_known_outputs_keys();
|
|
|
|
|
|
|
|
// since find_txs_in_mempool can be called outside of this thread,
|
|
|
|
// we need to use local connection. we cant use connection that the
|
|
|
|
// main search method is using, as we can end up wtih mysql errors
|
|
|
|
// mysql will blow up when two queries are done at the same
|
|
|
|
// time in a single connection.
|
|
|
|
// so we create local connection here, only to be used in this method.
|
|
|
|
|
|
|
|
auto local_xmr_accounts = make_shared<MySqlAccounts>(current_bc_status);
|
|
|
|
|
|
|
|
MicroCoreAdapter mcore_addapter {current_bc_status_ptr};
|
|
|
|
|
|
|
|
for (auto const& mtx: mempool_txs)
|
|
|
|
{
|
|
|
|
|
|
|
|
uint64_t recieve_time = mtx.first;
|
|
|
|
|
|
|
|
const transaction& tx = mtx.second;
|
|
|
|
|
|
|
|
const crypto::hash tx_hash = get_transaction_hash(tx);
|
|
|
|
bool is_rct = (tx.version == 2);
|
|
|
|
uint8_t rct_type = (is_rct ? tx.rct_signatures.type : 0);
|
|
|
|
|
|
|
|
// Class that is resposnible for idenficitaction of our outputs
|
|
|
|
// and inputs in a given tx.
|
|
|
|
|
|
|
|
auto identifier = make_identifier(tx,
|
|
|
|
make_unique<Output>(&address, &viewkey),
|
|
|
|
make_unique<Input>(&address, &viewkey,
|
|
|
|
&known_outputs_keys,
|
|
|
|
&mcore_addapter));
|
|
|
|
|
|
|
|
identifier.identify();
|
|
|
|
|
|
|
|
// FIRSt step. to search for the incoming xmr, we use address,
|
|
|
|
// viewkey and
|
|
|
|
// outputs public key.
|
|
|
|
|
|
|
|
auto const& outputs_identified
|
|
|
|
= identifier.get<Output>()->get();
|
|
|
|
|
|
|
|
auto total_received = calc_total_xmr(outputs_identified);
|
|
|
|
|
|
|
|
//vector<uint64_t> amount_specific_indices;
|
|
|
|
|
|
|
|
// if we identified some outputs as ours,
|
|
|
|
// save them into json to be returned.
|
|
|
|
if (!outputs_identified.empty())
|
|
|
|
{
|
|
|
|
auto tx_hash_str = pod_to_hex(tx_hash);
|
|
|
|
auto tx_hash_prefix_str
|
|
|
|
= pod_to_hex(get_transaction_prefix_hash(tx));
|
|
|
|
auto tx_pub_key_str
|
|
|
|
= pod_to_hex(identifier.get_tx_pub_key());
|
|
|
|
auto is_coinbase = cryptonote::is_coinbase(tx);
|
|
|
|
uint64_t mixin_no {0};
|
|
|
|
if (!is_coinbase)
|
|
|
|
mixin_no = xmreg::get_mixin_no(tx);
|
|
|
|
|
|
|
|
json j_tx;
|
|
|
|
|
|
|
|
j_tx["id"] = 0; // dont have any database id
|
|
|
|
//for tx in mempool
|
|
|
|
// this id is used for
|
|
|
|
// sorting txs in the frontend.
|
|
|
|
|
|
|
|
j_tx["hash"] = tx_hash_str;
|
|
|
|
j_tx["tx_pub_key"] = tx_pub_key_str;
|
|
|
|
j_tx["timestamp"] = recieve_time*1e3; // when it got into mempool
|
|
|
|
j_tx["total_received"] = std::to_string(total_received);
|
|
|
|
j_tx["total_sent"] = "0"; // to be set later when looking for key images
|
|
|
|
j_tx["unlock_time"] = "0"; // for mempool we set it to zero
|
|
|
|
// since we dont have block_height to work with
|
|
|
|
j_tx["height"] = current_height; // put current blockchain height,
|
|
|
|
// just to indicate to frontend that this
|
|
|
|
// tx is younger than 10 blocks so that
|
|
|
|
// it shows unconfirmed message.
|
|
|
|
j_tx["payment_id"] = current_bc_status->get_payment_id_as_string(tx);
|
|
|
|
j_tx["coinbase"] = false; // pool tx are not coinbase, so always false
|
|
|
|
j_tx["is_rct"] = is_rct;
|
|
|
|
j_tx["rct_type"] = rct_type;
|
|
|
|
j_tx["mixin"] = mixin_no;
|
|
|
|
j_tx["mempool"] = true;
|
|
|
|
|
|
|
|
j_transactions->push_back(j_tx);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// SECOND step: Checking for our key images, i.e., inputs.
|
|
|
|
|
|
|
|
// no need mutex here, as this will be exectued only after
|
|
|
|
// the above. there is no threads here.
|
|
|
|
|
|
|
|
auto const& inputs_identfied = identifier.get<Input>()->get();
|
|
|
|
|
|
|
|
if (!inputs_identfied.empty())
|
|
|
|
{
|
|
|
|
// if we find something we need to construct spent_outputs json
|
|
|
|
// array that will be appended into j_tx above. or in case
|
|
|
|
// this is
|
|
|
|
// only spending tx, i.e., no outputs were found, we need to
|
|
|
|
// construct new j_tx.
|
|
|
|
|
|
|
|
auto tx_hash_str = pod_to_hex(tx_hash);
|
|
|
|
auto tx_hash_prefix_str
|
|
|
|
= pod_to_hex(get_transaction_prefix_hash(tx));
|
|
|
|
auto tx_pub_key_str
|
|
|
|
= pod_to_hex(identifier.get_tx_pub_key());
|
|
|
|
uint64_t mixin_no {0};
|
|
|
|
auto is_coinbase = cryptonote::is_coinbase(tx);
|
|
|
|
if (!is_coinbase)
|
|
|
|
mixin_no = xmreg::get_mixin_no(tx);
|
|
|
|
|
|
|
|
json spend_keys;
|
|
|
|
uint64_t total_sent {0};
|
|
|
|
|
|
|
|
for (auto& in_info: inputs_identfied)
|
|
|
|
{
|
|
|
|
// need to get output info from mysql, as we need
|
|
|
|
// to know output's amount, its orginal
|
|
|
|
// tx public key and its index in that tx
|
|
|
|
XmrOutput out;
|
|
|
|
|
|
|
|
if (local_xmr_accounts->output_exists(
|
|
|
|
pod_to_hex(in_info.out_pub_key), out))
|
|
|
|
{
|
|
|
|
total_sent += out.amount;
|
|
|
|
|
|
|
|
spend_keys.push_back({
|
|
|
|
{"key_image" , pod_to_hex(in_info.key_img)},
|
|
|
|
{"amount" , std::to_string(out.amount)},
|
|
|
|
{"tx_pub_key", out.tx_pub_key},
|
|
|
|
{"out_index" , out.out_index},
|
|
|
|
{"mixin" , out.mixin},
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!spend_keys.empty())
|
|
|
|
{
|
|
|
|
// check if we got outputs in this tx. if yes
|
|
|
|
// we use exising j_tx. If not, we need to construct new
|
|
|
|
// j_tx object.
|
|
|
|
|
|
|
|
if (!outputs_identified.empty())
|
|
|
|
{
|
|
|
|
// we have outputs in this tx as well, so use
|
|
|
|
// exisiting j_tx. we add spending info
|
|
|
|
// to j_tx created before.
|
|
|
|
|
|
|
|
json& j_tx = j_transactions->back();
|
|
|
|
|
|
|
|
j_tx["total_sent"] = std::to_string(total_sent);
|
|
|
|
j_tx["spent_outputs"] = spend_keys;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
// we dont have any outputs in this tx as
|
|
|
|
// this is spend only tx, so we need to create new
|
|
|
|
// j_tx.
|
|
|
|
|
|
|
|
json j_tx;
|
|
|
|
|
|
|
|
j_tx["id"] = 0; // dont have any database
|
|
|
|
// id for tx in mempool
|
|
|
|
// this id is used for
|
|
|
|
// sorting txs in the
|
|
|
|
// frontend.
|
|
|
|
|
|
|
|
j_tx["hash"] = tx_hash_str;
|
|
|
|
j_tx["tx_pub_key"] = tx_pub_key_str;
|
|
|
|
j_tx["timestamp"] = recieve_time*1e3; // when it got into mempool
|
|
|
|
j_tx["total_received"] = "0"; // we did not recive any outputs/xmr
|
|
|
|
j_tx["total_sent"] = std::to_string(total_sent); // to be set later when looking for key images
|
|
|
|
j_tx["unlock_time"] = 0; // for mempool we set it to zero
|
|
|
|
// since we dont have block_height to work with
|
|
|
|
j_tx["height"] = current_height; // put current blockchain height,
|
|
|
|
// just to indicate to frontend that this
|
|
|
|
// tx is younger than 10 blocks so that
|
|
|
|
// it shows unconfirmed message.
|
|
|
|
j_tx["payment_id"] = current_bc_status
|
|
|
|
->get_payment_id_as_string(tx);
|
|
|
|
j_tx["coinbase"] = false; // mempool tx are not coinbase, so always false
|
|
|
|
j_tx["is_rct"] = is_rct;
|
|
|
|
j_tx["rct_type"] = rct_type;
|
|
|
|
j_tx["mixin"] = mixin_no;
|
|
|
|
j_tx["mempool"] = true;
|
|
|
|
j_tx["spent_outputs"] = spend_keys;
|
|
|
|
|
|
|
|
j_transactions->push_back(j_tx);
|
|
|
|
|
|
|
|
} // else of if (!identified_outputs.empty())
|
|
|
|
|
|
|
|
} // if (!spend_keys.empty())
|
|
|
|
|
|
|
|
} // if (!identified_inputs.empty())
|
|
|
|
|
|
|
|
} // for (const transaction& tx: txs_to_check)
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
TxSearch::addr_view_t
|
|
|
|
TxSearch::get_xmr_address_viewkey() const
|
|
|
|
{
|
|
|
|
return make_pair(address, viewkey);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
string
|
|
|
|
TxSearch::get_viewkey() const
|
|
|
|
{
|
|
|
|
static string viewkey = pod_to_hex(this->viewkey);
|
|
|
|
|
|
|
|
return viewkey;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void
|
|
|
|
TxSearch::set_search_thread_life(seconds life_seconds)
|
|
|
|
{
|
|
|
|
thread_search_life = life_seconds;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool
|
|
|
|
TxSearch::delete_existing_tx_if_exists(string const& tx_hash)
|
|
|
|
{
|
|
|
|
XmrTransaction tx_data_existing;
|
|
|
|
|
|
|
|
if (xmr_accounts->tx_exists(acc->id.data, tx_hash, tx_data_existing))
|
|
|
|
{
|
|
|
|
OMVLOG1 << '\n' << address_prefix
|
|
|
|
+ ": tx " << tx_hash
|
|
|
|
<< " already present in db, so remove it";
|
|
|
|
|
|
|
|
// if tx is already present for that user,
|
|
|
|
// we remove it, as we get it data from scrach
|
|
|
|
|
|
|
|
if (xmr_accounts->delete_tx(tx_data_existing.id.data) == 0)
|
|
|
|
{
|
|
|
|
OMERROR << address_prefix + ": cant remove tx "
|
|
|
|
<< tx_hash;
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
TxSearch::update_acc(XmrAccount const& _acc)
|
|
|
|
{
|
|
|
|
std::lock_guard<std::mutex> acc_lck(access_acc);
|
|
|
|
*acc = _acc;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// default value of static veriables
|
|
|
|
seconds TxSearch::thread_search_life {600};
|
|
|
|
|
|
|
|
}
|