You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
323 lines
10 KiB
323 lines
10 KiB
/* |
|
* 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; |
|
} |