Moved data sources out of wallet class.

Added Blockchair support for BTC/BCH.
Support for multiple operations for each ETH tx, introducing support for ERC-20.
Separated DB data to "raw" and "processed". The goal is to be able to wipe processed data (so we can process again with updated rules) without requiring to analyze again input files.
Updated SQL schema to match changes.
This commit is contained in:
evilny0 2021-02-21 22:50:11 +01:00
parent 71cfe71f9d
commit a41b13f3b2
39 changed files with 2897 additions and 1302 deletions

View File

@ -38,6 +38,7 @@ set(LIBS ${LIBS} "-lcpprest")
include_directories(src)
include_directories(src/wallets)
include_directories(src/exchanges)
include_directories(src/datasources)
set(CMAKE_CXX_STANDARD 11)

View File

@ -29,7 +29,7 @@ if (SQLPP11_MYSQL_MAIN_HEADER)
)
if("${check_result}" STREQUAL "")
string(APPEND SQLPP11_NOT_FOUND_MESSAGE "\nRejecting found '${SQLPP11_MYSQL_MAIN_HEADER}', it seems to be invalid.")
string(APPEND SQLPP11_MYSQL_NOT_FOUND_MESSAGE "\nRejecting found '${SQLPP11_MYSQL_MAIN_HEADER}', it seems to be invalid.")
unset(SQLPP11_MYSQL_INCLUDE_DIR CACHE)
else()
# Check succeeded, create target

View File

@ -7,6 +7,37 @@ SET time_zone = "+00:00";
/*!40101 SET NAMES utf8mb4 */;
CREATE TABLE `blockchain_btc_raw_tx` (
`raw_tx_id` int(11) NOT NULL,
`blockchain_id` int(11) NOT NULL,
`hash` varchar(200) NOT NULL,
`unix_time` int(11) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `blockchain_btc_raw_tx_details` (
`raw_tx_detail_id` int(11) NOT NULL,
`raw_tx_id` int(11) NOT NULL,
`amount` decimal(40,18) NOT NULL,
`address` varchar(100) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `blockchain_eth_raw_tx` (
`raw_tx_id` int(11) NOT NULL,
`blockchain_id` int(11) NOT NULL,
`hash` varchar(200) NOT NULL,
`unix_time` int(11) NOT NULL,
`fee` decimal(40,18) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `blockchain_eth_raw_tx_details` (
`raw_tx_detail_id` int(11) NOT NULL,
`raw_tx_id` int(11) NOT NULL,
`address_from` varchar(200) NOT NULL,
`address_to` varchar(200) NOT NULL,
`amount` decimal(40,18) NOT NULL,
`amount_coin_id` int(11) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `coins` (
`coin_id` int(11) NOT NULL,
`coin_name` varchar(50) NOT NULL,
@ -32,7 +63,7 @@ CREATE TABLE `exchanges_ledgers` (
`account_id` int(11) NOT NULL,
`exchange_ledger_id` varchar(50) NOT NULL,
`exchange_reference_id` varchar(50) NOT NULL,
`timestamp` int(11) NOT NULL,
`unix_time` int(11) NOT NULL,
`operation_type` int(11) NOT NULL,
`coin_id` int(11) NOT NULL,
`amount` decimal(40,18) NOT NULL,
@ -46,7 +77,7 @@ CREATE TABLE `exchanges_trades` (
`exchange_order_id` varchar(50) NOT NULL,
`base_coin_id` int(11) NOT NULL,
`quote_coin_id` int(11) NOT NULL,
`timestamp` int(11) NOT NULL,
`unix_time` int(11) NOT NULL,
`type` int(11) NOT NULL,
`order_type` int(11) NOT NULL,
`price` decimal(40,18) NOT NULL,
@ -60,7 +91,8 @@ CREATE TABLE `wallets` (
`wallet_id` int(11) NOT NULL,
`user_id` int(11) NOT NULL,
`type_id` int(11) NOT NULL,
`wallet_name` varchar(50) NOT NULL
`wallet_name` varchar(50) NOT NULL,
`active` tinyint(1) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `wallets_addresses` (
@ -77,16 +109,40 @@ CREATE TABLE `wallets_balances` (
`balance` decimal(48,18) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `wallets_btc_raw_tx` (
`raw_tx_id` int(11) NOT NULL,
`wallet_id` int(11) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `wallets_eth_raw_tx` (
`raw_tx_id` int(11) NOT NULL,
`wallet_id` int(11) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `wallets_tx` (
`tx_id` int(11) NOT NULL,
`blockchain_tx_id` varchar(200) NOT NULL,
`wallet_id` int(11) NOT NULL,
`amount` decimal(40,18) NOT NULL,
`fee` decimal(40,18) NOT NULL,
`timestamp` int(11) NOT NULL
`amount_coin_id` int(11) NOT NULL,
`fee_coin_id` int(11) NOT NULL,
`unix_time` int(11) NOT NULL,
`operation_type` int(11) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
ALTER TABLE `blockchain_btc_raw_tx`
ADD PRIMARY KEY (`raw_tx_id`);
ALTER TABLE `blockchain_btc_raw_tx_details`
ADD PRIMARY KEY (`raw_tx_detail_id`);
ALTER TABLE `blockchain_eth_raw_tx`
ADD PRIMARY KEY (`raw_tx_id`);
ALTER TABLE `blockchain_eth_raw_tx_details`
ADD PRIMARY KEY (`raw_tx_detail_id`);
ALTER TABLE `exchanges_accounts`
ADD PRIMARY KEY (`account_id`);
@ -103,12 +159,20 @@ ALTER TABLE `wallets_addresses`
ADD PRIMARY KEY (`address_id`);
ALTER TABLE `wallets_balances`
ADD PRIMARY KEY (`wallet_id`);
ADD PRIMARY KEY (`wallet_id`,`coin_id`);
ALTER TABLE `wallets_tx`
ADD PRIMARY KEY (`tx_id`);
ALTER TABLE `blockchain_btc_raw_tx`
MODIFY `raw_tx_id` int(11) NOT NULL AUTO_INCREMENT;
ALTER TABLE `blockchain_btc_raw_tx_details`
MODIFY `raw_tx_detail_id` int(11) NOT NULL AUTO_INCREMENT;
ALTER TABLE `blockchain_eth_raw_tx`
MODIFY `raw_tx_id` int(11) NOT NULL AUTO_INCREMENT;
ALTER TABLE `blockchain_eth_raw_tx_details`
MODIFY `raw_tx_detail_id` int(11) NOT NULL AUTO_INCREMENT;
ALTER TABLE `exchanges_accounts`
MODIFY `account_id` int(11) NOT NULL AUTO_INCREMENT;
ALTER TABLE `exchanges_ledgers`
@ -123,4 +187,4 @@ ALTER TABLE `wallets_tx`
MODIFY `tx_id` int(11) NOT NULL AUTO_INCREMENT;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;

View File

@ -1,6 +1,7 @@
aux_source_directory(. SOURCE_LIST)
aux_source_directory(wallets SOURCE_LIST_WALLETS)
aux_source_directory(exchanges SOURCE_LIST_EXCHANGES)
aux_source_directory(datasources SOURCE_LIST_DATASOURCES)
add_executable(${EXECUTABLE} ${SOURCE_LIST} ${SOURCE_LIST_WALLETS} ${SOURCE_LIST_EXCHANGES})
add_executable(${EXECUTABLE} ${SOURCE_LIST} ${SOURCE_LIST_WALLETS} ${SOURCE_LIST_EXCHANGES} ${SOURCE_LIST_DATASOURCES})
target_link_libraries(${EXECUTABLE} ${LIBS})

View File

@ -0,0 +1,137 @@
/*
* Copyright (c) 2021, evilny0
*
* This file is part of cpfm.
*
* cpfm is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* cpm 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 General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with cpfm. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "datasources/blockchain_info.h"
list<BlockchainTxDetailsTypeBTC> BlockchainDataSourceBTC_BlockchainInfo::getTxDetailsListForAddress(string address)
{
linfo << "Blockchain.info : Analyzing address: " << address;
list<BlockchainTxDetailsTypeBTC> l;
if (!bfs::exists(getCacheFilenameForAddress(address)))
{
saveBlockchainAddressDataToCacheFile(address);
}
if (!bfs::exists(getCacheFilenameForAddress(address)))
{
lerr << "Blockchain.info : cache file could not be found. Address data was not retrieved.";
return l;
}
ifstream f;
f.open(getCacheFilenameForAddress(address));
json::value jvalue = json::value::parse(f);
f.close();
linfo << "Blockchain.info : analyzing address: " << jvalue["address"].as_string();
for (int i=0;i<jvalue["txs"].size();i++)
{
BlockchainTxDetailsTypeBTC tx;
tx.hash = jvalue["txs"][i]["hash"].as_string();
tx.time.setFromUnixTime(jvalue["txs"][i]["time"].as_integer());
for (int j=0;j<jvalue["txs"][i]["inputs"].size();j++)
{
string addr = jvalue["txs"][i]["inputs"][j]["prev_out"]["addr"].as_string();
__int64 amount = jvalue["txs"][i]["inputs"][j]["prev_out"]["value"].as_integer();
Money m = amount;
Money mdecimals = m/100000000;
// The same address can be multiple time in the inputs, so we need to sum the amounts.
tx.inputs[addr] += mdecimals;
}
for (int j=0;j<jvalue["txs"][i]["out"].size();j++)
{
string addr = jvalue["txs"][i]["out"][j]["addr"].as_string();
__int64 amount = jvalue["txs"][i]["out"][j]["value"].as_integer();
Money m = amount;
Money mdecimals = m/100000000;
// The same address can only be once in the outputs.
tx.outputs[addr] = mdecimals;
}
l.push_back(tx);
}
return l;
}
string BlockchainDataSourceBTC_BlockchainInfo::getCacheFilenameForAddress(string address)
{
string cacheFilename("data/cache/blockchain.info/btc/" + address);
return cacheFilename;
}
void BlockchainDataSourceBTC_BlockchainInfo::saveBlockchainAddressDataToCacheFile(string address)
{
try
{
linfo << "Blockchain.info : querying API about " << address;
string sRequestURL = "/rawaddr/";
sRequestURL += address;
http_client apiclient("https://blockchain.info/");
m_currentRequestCacheFilename = getCacheFilenameForAddress(address);
apiclient.request(methods::GET,sRequestURL).then([](http_response response)
{
if (response.status_code() == status_codes::OK)
{
ldebug << "Blockchain.info : response OK.";
return response.extract_json();
}
else if (response.status_code() == status_codes::TooManyRequests)
{
lwarn << "Blockchain.info : too many queries! We are being rate limited.";
pplx::task_from_result(json::value());
}
return pplx::task_from_result(json::value());
})
.then([this](pplx::task<json::value> previousTask)
{
if (previousTask.get() != json::value::null())
{
linfo << "Blockchain.info : saving query result to " << m_currentRequestCacheFilename;
ofstream f;
f.open(m_currentRequestCacheFilename);
f << previousTask.get();
f.close();
}
else
{
lerr << "Blockchain.info : query result is empty. Nothing will be saved to the cache file.";
}
})
.wait();
}
catch(const http::http_exception& e)
{
lerr << "Blockchain.info : failed to query API about " << address;
}
}

View File

@ -0,0 +1,39 @@
/*
* Copyright (c) 2021, evilny0
*
* This file is part of cpfm.
*
* cpfm is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* cpm 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 General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with cpfm. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef CPFM_DATASOURCE_BLOCKCHAIN_INFO_H_INCLUDED
#define CPFM_DATASOURCE_BLOCKCHAIN_INFO_H_INCLUDED
#include "datasources/datasource.h"
class BlockchainDataSourceBTC_BlockchainInfo
{
public:
list<BlockchainTxDetailsTypeBTC> getTxDetailsListForAddress(string address);
private:
string getCacheFilenameForAddress(string address);
void saveBlockchainAddressDataToCacheFile(string address);
string m_currentRequestCacheFilename;
};
#endif // CPFM_DATASOURCE_BLOCKCHAIN_INFO_H_INCLUDED

View File

@ -0,0 +1,245 @@
/*
* Copyright (c) 2021, evilny0
*
* This file is part of cpfm.
*
* cpfm is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* cpm 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 General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with cpfm. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "datasources/blockchair.h"
Time BlockchainDataSourceTypeBTC_Blockchair::getTimeFromString(string s)
{
std::istringstream in{s};
date::sys_time<std::chrono::seconds> tp;
in >> date::parse("%F %T", tp);
Time t;
t.setFromUnixTime(std::chrono::system_clock::to_time_t(tp));
return t;
}
list<BlockchainTxDetailsTypeBTC> BlockchainDataSourceTypeBTC_Blockchair::getTxDetailsListForAddresses(list<string> addresses)
{
linfo << "Blockchair : analyzing address list";
list<BlockchainTxDetailsTypeBTC> l;
if (!bfs::exists(getCacheFilenameForAddresses(addresses)))
{
saveBlockchainAddressesDataToCacheFile(addresses);
}
if (!bfs::exists(getCacheFilenameForAddresses(addresses)))
{
lerr << "Blockchair : cache file could not be found. Addresses data was not retrieved.";
return l;
}
ifstream f;
f.open(getCacheFilenameForAddresses(addresses));
json::value jvalue = json::value::parse(f);
f.close();
for (int i=0;i<jvalue["data"]["transactions"].size();i++)
{
string txHash = jvalue["data"]["transactions"][i].as_string();
linfo << "Blockchair : found tx " << txHash << ". Starting analysis.";
if (!bfs::exists(getCacheFilenameForTx(txHash)))
{
saveBlockchainTxDataToCacheFile(txHash);
}
if (!bfs::exists(getCacheFilenameForTx(txHash)))
{
lerr << "Blockchair : cache file for tx " << txHash << " could not be found. Tx data was not retrieved.";
}
ifstream fTx;
fTx.open(getCacheFilenameForTx(txHash));
json::value jvalueTx = json::value::parse(fTx);
fTx.close();
BlockchainTxDetailsTypeBTC tx;
tx.hash = txHash;
tx.time = getTimeFromString (jvalueTx["data"][txHash]["transaction"]["time"].as_string());
for (int j=0;j<jvalueTx["data"][txHash]["inputs"].size();j++)
{
string addr = jvalueTx["data"][txHash]["inputs"][j]["recipient"].as_string();
__int64 amount = jvalueTx["data"][txHash]["inputs"][j]["value"].as_integer();
Money m = amount;
Money mdecimals = m/100000000;
// The same address can be multiple time in the inputs, so we need to sum the amounts.
tx.inputs[addr] += mdecimals;
}
for (int j=0;j<jvalueTx["data"][txHash]["outputs"].size();j++)
{
string addr = jvalueTx["data"][txHash]["outputs"][j]["recipient"].as_string();
__int64 amount = jvalueTx["data"][txHash]["outputs"][j]["value"].as_integer();
Money m = amount;
Money mdecimals = m/100000000;
// The same address can only be once in the outputs.
tx.outputs[addr] = mdecimals;
}
l.push_back(tx);
linfo << "Blockchair : finished analysis for tx " << txHash << ".";
}
linfo << "Blockchair : finished analysis for address list.";
return l;
}
string BlockchainDataSourceTypeBTC_Blockchair::getCacheFilenameForAddresses(list<string> addresses)
{
string s;
for (const auto a: addresses)
s = s+a;
string cacheFilename("data/cache/blockchair/"+m_blockchainName+"/addresses/" + s);
return cacheFilename;
}
string BlockchainDataSourceTypeBTC_Blockchair::getCacheFilenameForTx(string txHash)
{
string cacheFilename("data/cache/blockchair/"+m_blockchainName+"/tx/" + txHash);
return cacheFilename;
}
void BlockchainDataSourceTypeBTC_Blockchair::saveBlockchainAddressesDataToCacheFile(list<string> addresses)
{
try
{
string s;
for (const auto a: addresses)
{
if (s.length())
s = s+","+a;
else
s = a;
}
linfo << "Blockchair : querying API about addresses : " << s;
string sRequestURL = "/"+m_blockchainName+"/dashboards/addresses/";
sRequestURL += s;
http_client apiclient("https://api.blockchair.com/");
m_currentRequestCacheFilename = getCacheFilenameForAddresses(addresses);
ltrace << "Blockchair : query : " << sRequestURL;
apiclient.request(methods::GET,sRequestURL).then([](http_response response)
{
if (response.status_code() == status_codes::OK)
{
ldebug << "Blockchair : response OK.";
return response.extract_json();
}
else if (response.status_code() == status_codes::TooManyRequests)
{
lwarn << "Blockchair : too many queries! We are being rate limited.";
pplx::task_from_result(json::value());
}
return pplx::task_from_result(json::value());
})
.then([this](pplx::task<json::value> previousTask)
{
if (previousTask.get() != json::value::null())
{
linfo << "Blockchair : saving query result to " << m_currentRequestCacheFilename;
ofstream f;
f.open(m_currentRequestCacheFilename);
f << previousTask.get();
f.close();
}
else
{
lerr << "Blockchair : query result is empty. Nothing will be saved to the cache file.";
}
})
.wait();
}
catch(const http::http_exception& e)
{
lerr << "Blockchair : failed to query API";
}
}
void BlockchainDataSourceTypeBTC_Blockchair::saveBlockchainTxDataToCacheFile(string txHash)
{
try
{
linfo << "Blockchair : querying API about tx : " << txHash;
string sRequestURL = "/"+m_blockchainName+"/dashboards/transaction/";
sRequestURL += txHash;
http_client apiclient("https://api.blockchair.com/");
m_currentRequestCacheFilename = getCacheFilenameForTx(txHash);
ltrace << "Blockchair : query : " << sRequestURL;
apiclient.request(methods::GET,sRequestURL).then([](http_response response)
{
if (response.status_code() == status_codes::OK)
{
ldebug << "Blockchair : response OK.";
return response.extract_json();
}
else if (response.status_code() == status_codes::TooManyRequests)
{
lwarn << "Blockchair : too many queries! We are being rate limited.";
pplx::task_from_result(json::value());
}
return pplx::task_from_result(json::value());
})
.then([this](pplx::task<json::value> previousTask)
{
if (previousTask.get() != json::value::null())
{
linfo << "Blockchair : saving query result to " << m_currentRequestCacheFilename;
ofstream f;
f.open(m_currentRequestCacheFilename);
f << previousTask.get();
f.close();
}
else
{
lerr << "Blockchair : query result is empty. Nothing will be saved to the cache file.";
}
})
.wait();
}
catch(const http::http_exception& e)
{
lerr << "Blockchair : failed to query API about " << txHash;
}
}

View File

@ -0,0 +1,67 @@
/*
* Copyright (c) 2021, evilny0
*
* This file is part of cpfm.
*
* cpfm is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* cpm 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 General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with cpfm. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef CPFM_DATASOURCE_BLOCKCHAIR_H_INCLUDED
#define CPFM_DATASOURCE_BLOCKCHAIR_H_INCLUDED
#include "datasources/datasource.h"
class BlockchainDataSourceTypeBTC_Blockchair
{
public:
list<BlockchainTxDetailsTypeBTC> getTxDetailsListForAddresses(list<string> addresses);
private:
string getCacheFilenameForAddresses(list<string> address);
string getCacheFilenameForTx(string txHash);
void saveBlockchainAddressesDataToCacheFile(list<string> address);
void saveBlockchainTxDataToCacheFile(string txHash);
Time getTimeFromString(string s);
string m_currentRequestCacheFilename;
protected:
string m_blockchainName;
virtual void dummyMakeAbstract() = 0;
};
class BlockchainDataSourceBTC_Blockchair : public BlockchainDataSourceTypeBTC_Blockchair
{
public:
BlockchainDataSourceBTC_Blockchair() { m_blockchainName = "bitcoin"; }
protected:
virtual void dummyMakeAbstract(){};
};
class BlockchainDataSourceBCH_Blockchair : public BlockchainDataSourceTypeBTC_Blockchair
{
public:
BlockchainDataSourceBCH_Blockchair() { m_blockchainName = "bitcoin-cash"; }
protected:
virtual void dummyMakeAbstract(){};
};
#endif // CPFM_DATASOURCE_BLOCKCHAIR_H_INCLUDED

View File

@ -0,0 +1,77 @@
/*
* Copyright (c) 2021, evilny0
*
* This file is part of cpfm.
*
* cpfm is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* cpm 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 General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with cpfm. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "datasources/datasource.h"
#include "datasources/blockchain_info.h"
#include "datasources/blockchair.h"
#include "datasources/etherscan.h"
list<BlockchainTxDetailsTypeBTC> BlockchainDataSourceBTC::getTxDetailsListForAddresses(list<string> addresses)
{
list<BlockchainTxDetailsTypeBTC> l;
// Blockchain.info
/*
BlockchainDataSourceBTC_BlockchainInfo bci;
list<string> hashList;
for (auto const address: addresses)
{
list<BlockchainTxDetailsTypeBTC> listForSingleAddress = bci.getTxDetailsListForAddress(address);
for (auto const tx: listForSingleAddress)
{
if (std::find(hashList.begin(), hashList.end(), tx.hash) == hashList.end())
{
hashList.push_back(tx.hash);
l.push_back(tx);
}
}
}
*/
// Blockchair
BlockchainDataSourceBTC_Blockchair bc;
l = bc.getTxDetailsListForAddresses(addresses);
return l;
}
list<BlockchainTxDetailsTypeBTC> BlockchainDataSourceBCH::getTxDetailsListForAddresses(list<string> addresses)
{
list<BlockchainTxDetailsTypeBTC> l;
// Blockchair
BlockchainDataSourceBCH_Blockchair bc;
l = bc.getTxDetailsListForAddresses(addresses);
return l;
}
list<BlockchainTxDetailsTypeETH> BlockchainDataSourceETH::getTxDetailsListForAddress(string address)
{
list<BlockchainTxDetailsTypeETH> l;
// Etherscan
BlockchainDataSourceETH_Etherscan bc;
l = bc.getTxDetailsListForAddress(address);
return l;
}

View File

@ -0,0 +1,71 @@
/*
* Copyright (c) 2021, evilny0
*
* This file is part of cpfm.
*
* cpfm is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* cpm 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 General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with cpfm. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef CPFM_DATASOURCE_H_INCLUDED
#define CPFM_DATASOURCE_H_INCLUDED
#include "pf.h"
class BlockchainTxDetailsTypeBTC
{
public:
string hash;
Time time;
map <string,Money> inputs;
map <string,Money> outputs;
};
class BlockchainTxOperationTypeETH
{
public:
string addressFrom;
string addressTo;
Money amount;
int amountCoinId;
};
class BlockchainTxDetailsTypeETH
{
public:
string hash;
Time time;
list<BlockchainTxOperationTypeETH> operations;
Money fee;
};
class BlockchainDataSourceBTC
{
public:
list<BlockchainTxDetailsTypeBTC> getTxDetailsListForAddresses(list<string> addresses);
};
class BlockchainDataSourceBCH
{
public:
list<BlockchainTxDetailsTypeBTC> getTxDetailsListForAddresses(list<string> addresses);
};
class BlockchainDataSourceETH
{
public:
list<BlockchainTxDetailsTypeETH> getTxDetailsListForAddress(string address);
};
#endif // CPFM_DATASOURCE_H_INCLUDED

View File

@ -0,0 +1,323 @@
/*
* Copyright (c) 2021, evilny0
*
* This file is part of cpfm.
*
* cpfm is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* cpm 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 General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with cpfm. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "datasources/etherscan.h"
#define TXLIST_NORMAL "txlist"
#define TXLIST_INTERNAL "txlistinternal"
#define TXLIST_TOKENS "tokentx"
list<BlockchainTxDetailsTypeETH> BlockchainDataSourceETH_Etherscan::getTxDetailsListForAddress(string address)
{
linfo << "Etherscan : Analyzing address: " << address;
if (!retrieveCacheFilesForAddress(address))
{
list<BlockchainTxDetailsTypeETH> l;
lerr << "Etherscan : cache files could not be found. Address data was not retrieved.";
return l;
}
list<string> hashListNormal;
list<string> hashListInternal;
list<string> hashListToken;
list<BlockchainTxDetailsTypeETH> lNormal = getTxDetailsListForAddressTxType(address,TXLIST_NORMAL,hashListNormal);
list<BlockchainTxDetailsTypeETH> lInternal = getTxDetailsListForAddressTxType(address,TXLIST_INTERNAL,hashListInternal);
list<BlockchainTxDetailsTypeETH> lToken = getTxDetailsListForAddressTxType(address,TXLIST_TOKENS,hashListToken);
list<BlockchainTxDetailsTypeETH> l = lNormal;
// Add internals tx to the list
for (auto const tx: lInternal)
{
BlockchainTxDetailsTypeETH txNew = tx;
list<BlockchainTxDetailsTypeETH>::iterator it;
for (it=l.begin();it!=l.end();it++)
{
if (it->hash == tx.hash)
{
txNew = (*it);
for (const auto op: tx.operations)
{
txNew.operations.push_back(op);
}
l.erase(it);
break;
}
}
l.push_back(txNew);
}
// Add tokens tx to the list
for (auto const tx: lToken)
{
BlockchainTxDetailsTypeETH txNew = tx;
list<BlockchainTxDetailsTypeETH>::iterator it;
for (it=l.begin();it!=l.end();it++)
{
if (it->hash == tx.hash)
{
txNew = (*it);
for (const auto op: tx.operations)
{
txNew.operations.push_back(op);
}
l.erase(it);
break;
}
}
l.push_back(txNew);
}
return l;
}
list<BlockchainTxDetailsTypeETH> BlockchainDataSourceETH_Etherscan::getTxDetailsListForAddressTxType(string address, string type, list<string>& hashList)
{
list<BlockchainTxDetailsTypeETH> l;
ifstream f;
f.open(getCacheFilenameForAddress(address,type));
json::value jvalue = json::value::parse(f);
f.close();
linfo << "Etherscan : analyzing " << type << " for address: " << address;
if (!jvalue["status"].as_string().compare("1"))
{
for (int i=0;i<jvalue["result"].size();i++)
{
ltrace << "Found tx " << jvalue["result"][i]["hash"].as_string();
BlockchainTxDetailsTypeETH tx;
// Search if the hash is already in the list. If yes, remove the element.
// It will be added after the operation list is updated.
list<BlockchainTxDetailsTypeETH>::iterator it;
for (it=l.begin();it!=l.end();it++)
{
if (it->hash == jvalue["result"][i]["hash"].as_string())
{
ltrace << "Tx already existed. Removing from list so it can be updated with new values.";
tx = (*it);
l.erase(it);
break;
}
}
tx.hash = jvalue["result"][i]["hash"].as_string();
tx.time.setFromUnixTime(strtol(jvalue["result"][i]["timeStamp"].as_string().c_str(),NULL,10));
Money gasUsed (jvalue["result"][i]["gasUsed"]);
Money gasPrice (jvalue["result"][i]["gasPrice"]);
Money fee = gasPrice*gasUsed;
Money feeDivider("1000000000000000000");
fee /= feeDivider;
tx.fee = fee;
if (type == TXLIST_NORMAL)
{
BlockchainTxOperationTypeETH op;
op.addressFrom = jvalue["result"][i]["from"].as_string();
op.addressTo = jvalue["result"][i]["to"].as_string();
op.amountCoinId = CPFM_COIN_ID_ETH;
Money divider("1000000000000000000");
Money m(jvalue["result"][i]["value"]);
m /= divider;
op.amount = m;
ltrace << "Operation. Amount: " << op.amount << ". Coin : " << getCoinName(op.amountCoinId);
tx.operations.push_back(op);
}
else if (type == TXLIST_TOKENS)
{
BlockchainTxOperationTypeETH op;
op.addressFrom = jvalue["result"][i]["from"].as_string();
op.addressTo = jvalue["result"][i]["to"].as_string();
op.amountCoinId = GetCoinIdFromContractAddress(jvalue["result"][i]["contractAddress"].as_string());
if (!op.amountCoinId)
{
lerr << "Unknown token : " << jvalue["result"][i]["tokenSymbol"].as_string() << ". Tx will be ignored.";
continue;
}
Money divider = 1;
int multiplier = strtol(jvalue["result"][i]["tokenDecimal"].as_string().c_str(),NULL,10);
for (int i=0;i<multiplier;i++)
{
divider*=10;
}
Money m(jvalue["result"][i]["value"]);
m /= divider;
op.amount = m;
ltrace << "Operation. Amount: " << op.amount << ". Coin : " << getCoinName(op.amountCoinId);
tx.operations.push_back(op);
}
else if (type == TXLIST_TOKENS)
{
lerr << "Internal tx are not supported.";
continue;
}
else
{
lerr << "Unknown tx type : " << type << ". Tx will be ignored.";
continue;
}
hashList.push_back(tx.hash);
l.push_back(tx);
}
}
return l;
}
bool BlockchainDataSourceETH_Etherscan::retrieveCacheFilesForAddress(string address)
{
if (!bfs::exists(getCacheFilenameForAddress(address,TXLIST_NORMAL)))
{
saveBlockchainAddressDataToCacheFile(address,TXLIST_NORMAL);
}
if (!bfs::exists(getCacheFilenameForAddress(address,TXLIST_NORMAL)))
{
lerr << "Etherscan : cache file for " << TXLIST_NORMAL << " could not be found.";
return false;
}
if (!bfs::exists(getCacheFilenameForAddress(address,TXLIST_INTERNAL)))
{
saveBlockchainAddressDataToCacheFile(address,TXLIST_INTERNAL);
}
if (!bfs::exists(getCacheFilenameForAddress(address,TXLIST_INTERNAL)))
{
lerr << "Etherscan : cache file for " << TXLIST_INTERNAL << " could not be found.";
return false;
}
if (!bfs::exists(getCacheFilenameForAddress(address,TXLIST_TOKENS)))
{
saveBlockchainAddressDataToCacheFile(address,TXLIST_TOKENS);
}
if (!bfs::exists(getCacheFilenameForAddress(address,TXLIST_TOKENS)))
{
lerr << "Etherscan : cache file for " << TXLIST_TOKENS << " could not be found.";
return false;
}
return true;
}
string BlockchainDataSourceETH_Etherscan::getCacheFilenameForAddress(string address, string type)
{
string cacheFilename("data/cache/etherscan.io/" + type + "/" + address);
return cacheFilename;
}
void BlockchainDataSourceETH_Etherscan::saveBlockchainAddressDataToCacheFile(string address, string type)
{
try
{
sleep (5);
linfo << "Etherscan : querying API about " << type << " for " << address;
string sRequestURL = "/api?module=account&action="+type+"&address=";
sRequestURL += address;
http_client apiclient("https://api.etherscan.io");
m_currentRequestCacheFilename = getCacheFilenameForAddress(address,type);
apiclient.request(methods::GET,sRequestURL).then([](http_response response)
{
if (response.status_code() == status_codes::OK)
{
ldebug << "Etherscan : response OK.";
return response.extract_json();
}
return pplx::task_from_result(json::value());
})
.then([this](pplx::task<json::value> previousTask)
{
if (previousTask.get() != json::value::null())
{
linfo << "Etherscan : saving query result to " << m_currentRequestCacheFilename;
ofstream f;
f.open(m_currentRequestCacheFilename);
f << previousTask.get();
f.close();
}
else
{
lerr << "Etherscan : query result is empty. Nothing will be saved to the cache file.";
}
})
.wait();
}
catch(const http::http_exception& e)
{
lerr << "Etherscan : failed to query API about " << address;
}
}
int BlockchainDataSourceETH_Etherscan::GetCoinIdFromContractAddress(string contractAddress)
{
// LPT - Livepeer Token
if (contractAddress == "0x58b6a8a3302369daec383334672404ee733ab239")
return CPFM_COIN_ID_LPT;
// LEND - EthLend - Migrated to AAVE.
if (contractAddress == "0x80fb784b7ed66730e8b1dbd9820afd29931aab03")
return CPFM_COIN_ID_LEND;
// AAVE
if (contractAddress == "0x7fc66500c84a76ad7e9c93437bfc5ac33e2ddae9")
return CPFM_COIN_ID_AAVE;
// BCDT - Blockchain Certified Data Token.
if (contractAddress == "0xacfa209fb73bf3dd5bbfb1101b9bc999c49062a5")
return CPFM_COIN_ID_BCDT;
// KNC - Kyber Network
if (contractAddress == "0xdd974d5c2e2928dea5f71b9825b8b646686bd200")
return CPFM_COIN_ID_KNC;
// AIR - AirToken
if (contractAddress == "0x27dce1ec4d3f72c3e457cc50354f1f975ddef488")
return CPFM_COIN_ID_AIR;
lerr << "Unsupported contact address " << contractAddress;
return 0;
}

View File

@ -0,0 +1,42 @@
/*
* Copyright (c) 2021, evilny0
*
* This file is part of cpfm.
*
* cpfm is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* cpm 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 General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with cpfm. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef CPFM_DATASOURCE_ETHERSCAN_H_INCLUDED
#define CPFM_DATASOURCE_ETHERSCAN_H_INCLUDED
#include "datasources/datasource.h"
class BlockchainDataSourceETH_Etherscan
{
public:
list<BlockchainTxDetailsTypeETH> getTxDetailsListForAddress(string address);
private:
list<BlockchainTxDetailsTypeETH> getTxDetailsListForAddressTxType(string address, string type, list<string>& hashList);
void saveBlockchainAddressDataToCacheFile(string address, string type);
string getCacheFilenameForAddress(string address, string type);
bool retrieveCacheFilesForAddress(string address);
int GetCoinIdFromContractAddress(string contractAddress);
string m_currentRequestCacheFilename;
};
#endif // CPFM_DATASOURCE_ETHERSCAN_H_INCLUDED

View File

@ -47,10 +47,10 @@ void ExchangeHandlerKraken::analyzeUserData()
getAccountBalance();
analyzeAccountBalance();
//getAccountTrades();
getAccountTrades();
//analyzeAccountTrades();
//getAccountLedgers();
getAccountLedgers();
//analyzeAccountLedgers();
}

View File

@ -23,7 +23,7 @@
#include <chrono>
#include "log.h"
using namespace farm;
using namespace cpfm;
using namespace log;
const char* ErrorLogger::name() { return "ERROR"; }
@ -34,6 +34,8 @@ const char* InfoLogger::name() { return "INFO "; }
const char* InfoLogger::color() { return L_White; }
const char* DebugLogger::name() { return "DEBUG"; }
const char* DebugLogger::color() { return L_Teal; }
const char* TraceLogger::name() { return "TRACE"; }
const char* TraceLogger::color() { return L_Violet; }
LoggerBase::LoggerBase()
{

View File

@ -28,13 +28,14 @@
using namespace std;
#define llog(X) farm::log::Logger<X>()
#define lerr llog(farm::log::ErrorLogger)
#define linfo llog(farm::log::InfoLogger)
#define lwarn llog(farm::log::WarningLogger)
#define ldebug llog(farm::log::DebugLogger)
#define llog(X) cpfm::log::Logger<X>()
#define lerr llog(cpfm::log::ErrorLogger)
#define linfo llog(cpfm::log::InfoLogger)
#define lwarn llog(cpfm::log::WarningLogger)
#define ldebug llog(cpfm::log::DebugLogger)
#define ltrace llog(cpfm::log::TraceLogger)
namespace farm { namespace log {
namespace cpfm { namespace log {
#define L_Reset "\x1b[0m" // Text Reset
@ -107,6 +108,7 @@ struct ErrorLogger: public DefaultLogger { static const char* name(); static con
struct WarningLogger: public DefaultLogger { static const char* name(); static const char* color(); };
struct InfoLogger: public DefaultLogger { static const char* name(); static const char* color(); };
struct DebugLogger: public DefaultLogger { static const char* name(); static const char* color(); };
struct TraceLogger: public DefaultLogger { static const char* name(); static const char* color(); };
class LoggerBase
{

View File

@ -23,7 +23,7 @@
Money::Money ()
{
m_amount = 0;
}
Money::Money (bmp::mpf_float_50 x)

View File

@ -54,7 +54,7 @@ public:
Money& operator/=(const Money& x);
friend Money operator/(Money a, const Money& b) { a /= b; return a; }
Money operator-() { return Money(-m_amount); }
//Money operator-() { return Money(-m_amount); }
friend bool operator==(const Money& a, const int& b);
friend bool operator!=(const Money& a, const int& b) { return !operator==(a,b); }

View File

@ -40,8 +40,8 @@ void PortfolioManager::doTestStuff()
// Test user 1
emptyUserBalances(1);
ExchangesManager exchangesManager(1);
exchangesManager.analyzeUserAccounts();
// ExchangesManager exchangesManager(1);
// exchangesManager.analyzeUserAccounts();
WalletsManager walletsManager(1);
walletsManager.analyzeUserWallets();
@ -49,6 +49,7 @@ void PortfolioManager::doTestStuff()
displayUserBalances(1);
// Test user 2
/*
emptyUserBalances(2);
ExchangesManager exchangesManagerTwo(2);
@ -58,6 +59,7 @@ void PortfolioManager::doTestStuff()
walletsManagerTwo.analyzeUserWallets();
displayUserBalances(2);
*/
}
void PortfolioManager::emptyUserBalances(int userId)
@ -155,6 +157,24 @@ string getCoinName(int coinId)
case CPFM_COIN_ID_BTG:
s = "BTG";
break;
case CPFM_COIN_ID_LPT:
s = "LPT";
break;
case CPFM_COIN_ID_BCDT:
s = "BCDT";
break;
case CPFM_COIN_ID_AIR:
s = "AIR";
break;
case CPFM_COIN_ID_AAVE:
s = "AAVE";
break;
case CPFM_COIN_ID_LEND:
s = "LEND";
break;
case CPFM_COIN_ID_KNC:
s = "KNC";
break;
default:
s = "Error!";
break;

View File

@ -39,14 +39,31 @@
#include "log.h"
#include "money.h"
#define CPFM_COIN_ID_BTC 1
#define CPFM_COIN_ID_LTC 2
#define CPFM_COIN_ID_EUR 3
#define CPFM_COIN_ID_ICN 4
#define CPFM_COIN_ID_ETC 5
#define CPFM_COIN_ID_ETH 6
#define CPFM_COIN_ID_BCH 7
#define CPFM_COIN_ID_BTG 8
#define CPFM_COIN_ID_BTC 1
#define CPFM_COIN_ID_LTC 2
#define CPFM_COIN_ID_EUR 3
#define CPFM_COIN_ID_ICN 4
#define CPFM_COIN_ID_ETC 5
#define CPFM_COIN_ID_ETH 6
#define CPFM_COIN_ID_BCH 7
#define CPFM_COIN_ID_BTG 8
#define CPFM_COIN_ID_KNC 9
#define CPFM_COIN_ID_AIR 10
#define CPFM_COIN_ID_LEND 11
#define CPFM_COIN_ID_AAVE 12
#define CPFM_COIN_ID_LPT 13
#define CPFM_COIN_ID_BCDT 14
class Time
{
public:
void setFromUnixTime(time_t unixTime) { m_unixTime = unixTime; }
const time_t toUnixTime() const { return m_unixTime; }
private:
time_t m_unixTime;
//date::sys_time<std::chrono::milliseconds>
};
namespace bmp = boost::multiprecision;
namespace bfs = boost::filesystem;

View File