Files

891 lines
34 KiB
C++
Raw Permalink Normal View History

2019-09-23 11:07:45 +02:00
/*
* This file is part of the Flowee project
2021-02-22 16:27:57 +01:00
* Copyright (C) 2019-2021 Tom Zander <tom@flowee.org>
2019-09-23 11:07:45 +02:00
*
* This program 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.
*
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "BitcoreProxy.h"
#include <httpengine/socket.h>
#include <networkmanager/NetworkManager.h>
2019-11-13 15:11:39 +01:00
#include <primitives/script.h>
#include <primitives/Tx.h>
2023-11-24 18:01:36 +01:00
#include <primitives/PublicKey.h>
2019-09-23 11:07:45 +02:00
#include <uint256.h>
#include <utilstrencodings.h>
#include <cashaddr.h>
2019-11-13 15:11:39 +01:00
#include <streaming/BufferPool.h>
2019-09-23 11:07:45 +02:00
#include <QSettings>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QFile>
#include <QTimer>
2019-10-05 16:01:52 +02:00
#include <base58.h>
2025-01-13 23:41:23 +01:00
#include <QTimeZone>
2019-09-23 11:07:45 +02:00
namespace {
// for now assume cashaddress
static QString ripeToAddress(const std::vector<quint8> &in, CashAddress::AddressType type)
{
2026-02-09 15:23:48 +01:00
return QString::fromStdString(CashAddress::encode("bitcoincash", { type, in })).mid(12);
2019-09-23 11:07:45 +02:00
}
2019-10-16 22:43:16 +02:00
static QString addressAsString(const CashAddress::Content &c)
{
2026-02-09 15:23:48 +01:00
return QString::fromStdString(CashAddress::encode("bitcoincash", c)).mid(12);
2019-10-16 22:43:16 +02:00
}
2019-09-23 11:07:45 +02:00
Streaming::ConstBuffer hexStringToBuffer(const QString &hash)
{
assert(hash.size() == 64);
Streaming::BufferPool pool(32);
int i2 = 31;
for (int i = 0; i < 64; ++i) {
QChar k = hash.at(i);
uint8_t v = static_cast<uint8_t>(HexDigit(static_cast<int8_t>(k.unicode())));
if (k.unicode() > 'f' || v == 0xFF)
throw std::runtime_error("Not a hash");
if ((i % 2) == 0) {
pool.begin()[i2] = static_cast<char>(v << 4);
} else {
pool.begin()[i2--] += v;
}
}
return pool.commit(32);
}
QString uint256ToString(const uint256 &hash)
{
return QString::fromStdString(hash.ToString());
}
QChar hexChar(uint8_t k) {
assert(k < 16);
if (k < 10)
return QChar('0' + k);
return QChar('a' + k - 10);
}
QString uint256ToString(const Streaming::ConstBuffer &buf)
{
assert(buf.size() == 32);
QString answer;
answer.resize(64, QChar('0'));
QChar *string = answer.data();
for (int pos = buf.size() - 1; pos >= 0; --pos) {
uint8_t k = static_cast<uint8_t>(buf.begin()[pos]);
*string++ = hexChar((k >> 4) & 0xF).unicode();
*string++ = hexChar(k & 0xF).unicode();
}
return answer;
}
QJsonObject toJson(const Blockchain::Transaction &tx, const QJsonObject &templateMap) {
QJsonObject answer;
answer.insert("coinbase", tx.offsetInBlock > 0 && tx.offsetInBlock < 90);
if (!tx.txid.isEmpty())
answer.insert("txid", uint256ToString(tx.txid));
else if (templateMap.contains("txid"))
answer.insert("txid", templateMap.value("txid"));
answer.insert("blockHeight", tx.blockHeight);
answer.insert("coinbase", tx.offsetInBlock > 80 && tx.offsetInBlock < 100);
if (!tx.fullTxData.isEmpty()) {
answer.insert("size", tx.fullTxData.size());
Tx fullTx(tx.fullTxData);
Tx::Iterator iter(fullTx);
int inputCount = 0;
int outputCount = 0;
long value = 0;
while (iter.next() != Tx::End) {
if (iter.tag() == Tx::OutputValue) {
outputCount++;
value += iter.longData();
}
else if (iter.tag() == Tx::PrevTxHash)
inputCount++;
}
2019-10-05 18:17:12 +02:00
answer.insert("locktime", -1); // TODO not sure what this means
2019-09-23 11:07:45 +02:00
answer.insert("inputCount", inputCount);
answer.insert("outputCount", outputCount);
// obj.insert("fee", -1); // this one is tricky...
answer.insert("value", (qint64) value);
if (!answer.contains("txid"))
answer.insert("txid", QString::fromStdString(fullTx.createHash().ToString()));
}
return answer;
}
QJsonObject toJson(const Blockchain::BlockHeader &header, const QJsonObject &orig) {
QJsonObject answer = orig;
2025-01-13 23:41:23 +01:00
QDateTime dt = QDateTime::fromSecsSinceEpoch(static_cast<qint64>(header.time), QTimeZone::UTC);
2019-09-23 11:07:45 +02:00
QString date = dt.toString(Qt::ISODateWithMs);
answer.insert("blockTime", date);
answer.insert("blockTimeNormalized", date);
answer.insert("confirmations", header.confirmations);
if (!answer.contains("blockHash"))
answer.insert("blockHash", uint256ToString(header.hash));
return answer;
}
void addRequestId(QJsonObject &object) {
static QAtomicInt s_requestId = QAtomicInt(0);
object.insert("_id", QString::number(s_requestId.fetchAndAddRelaxed(1), 16));
}
void parseScriptAndAddress(QJsonObject &object, const Streaming::ConstBuffer &script)
{
CScript scriptPubKey(script);
std::vector<std::vector<unsigned char> > vSolutions;
Script::TxnOutType whichType;
bool recognizedTx = Script::solver(scriptPubKey, whichType, vSolutions);
2019-10-16 22:43:16 +02:00
if (recognizedTx && (whichType == Script::TX_PUBKEY
|| whichType == Script::TX_PUBKEYHASH || whichType == Script::TX_SCRIPTHASH)) {
if (whichType == Script::TX_SCRIPTHASH) {
Q_ASSERT(vSolutions[0].size() == 20);
2026-02-09 15:23:48 +01:00
object.insert("address", ripeToAddress(vSolutions[0], CashAddress::ScriptType));
2019-10-16 22:43:16 +02:00
} else if (whichType == Script::TX_PUBKEYHASH) {
2019-09-23 11:07:45 +02:00
Q_ASSERT(vSolutions[0].size() == 20);
2026-02-09 15:23:48 +01:00
object.insert("address", ripeToAddress(vSolutions[0], CashAddress::PubkeyType));
2019-09-23 11:07:45 +02:00
} else if (whichType == Script::TX_PUBKEY) {
2022-07-06 21:56:34 +02:00
PublicKey pubKey(vSolutions[0]);
2021-04-19 11:58:00 +02:00
Q_ASSERT (pubKey.isValid());
2022-07-06 21:52:47 +02:00
KeyId address = pubKey.getKeyId();
2019-09-23 11:07:45 +02:00
std::vector<quint8> id(address.begin(), address.end());
2026-02-09 15:23:48 +01:00
object.insert("address", ripeToAddress(id, CashAddress::PubkeyType));
2019-09-23 11:07:45 +02:00
}
}
object.insert("script", QString::fromLatin1(QByteArray(script.begin(), script.size()).toHex()));
}
}
static QJsonDocument::JsonFormat s_JsonFormat = QJsonDocument::Compact;
class UserInputException : public std::runtime_error
{
public:
// UserInputException() = default;
UserInputException(const char *error, const char *helpPage)
: std::runtime_error(error),
m_helpPage(helpPage)
{
}
const char *helpPage() const {
return m_helpPage;
}
private:
const char *m_helpPage = nullptr;
};
BitcoreProxy::BitcoreProxy()
{
}
void BitcoreProxy::onIncomingConnection(HttpEngine::WebRequest *request_)
{
Q_ASSERT(request_);
auto request = qobject_cast<BitcoreWebRequest*>(request_);
Q_ASSERT(request);
auto socket = request->socket();
QObject::connect(socket, SIGNAL(disconnected()), request, SLOT(deleteLater()));
if (socket->method() != HttpEngine::Socket::HEAD && socket->method() != HttpEngine::Socket::GET)
socket->close();
socket->setHeader("server", "Flowee");
if (socket->path() == QLatin1String("/api/status/enabled-chains")) {
returnEnabledChains(request);
return;
}
RequestString rs(socket->path());
if (rs.wholePath.isEmpty() || rs.request.isEmpty()) {
returnTemplatePath(socket, "index.html");
return;
}
if (rs.chain != "BCH" || rs.network != "mainnet") {
request->socket()->writeError(404);
return;
}
const QString now = QString("%1 GMT").arg(QDateTime::currentDateTimeUtc().toString("ddd, d MMM yyyy h:mm:ss"));
socket->setHeader("last-modified", now.toLatin1()); // no cashing.
if (socket->method() == HttpEngine::Socket::HEAD) {
socket->writeHeaders();
socket->close();
return;
}
logWarning().nospace() << "GET\t" << socket->peerAddress().toString() << "\t" << rs.anonPath()
2019-09-23 11:07:45 +02:00
<< "\t" << socket->headers().value("User-Agent").data();
try {
2019-10-05 16:01:52 +02:00
request->map().insert("network", rs.network);
request->map().insert("chain", rs.chain);
2019-09-23 11:07:45 +02:00
if (rs.request == "tx") {
requestTransactionInfo(rs, request);
} else if (rs.request == "address") {
requestAddressInfo(rs, request);
} else if (rs.request == "block") {
requestBlockInfo(rs, request);
} else if (rs.request == "wallet") {
socket->writeError(501);
} else if (rs.request == "fee") {
returnFeeSuggestion(rs, request);
} else if (rs.request == "stats/daily-transactions") {
returnDailyTransactions(rs, request);
}
if (request->answerType == BitcoreWebRequest::Unset) {
returnTemplatePath(socket, "index.html");
return;
}
start(request);
2020-08-28 15:45:03 +02:00
} catch (const Blockchain::ServiceUnavailableException &e) {
QString error("Config or backend issue: can't find upstream service: %1");
switch (e.service()) {
case Blockchain::TheHub:
error = error.arg("The Hub");
break;
case Blockchain::IndexerTxIdDb:
error = error.arg("TxID indexer");
break;
case Blockchain::IndexerAddressDb:
error = error.arg("Addresses indexer");
break;
case Blockchain::IndexerSpentDb:
error = error.arg("Spent-db indexer");
break;
}
2021-02-22 16:27:57 +01:00
returnTemplatePath(socket, e.temporarily() ? "error.json" : "setup.html", error);
2019-09-23 11:07:45 +02:00
} catch (const UserInputException &e) {
returnTemplatePath(socket, e.helpPage(), e.what());
} catch (const std::exception &e) {
logCritical() << "Failed to handle request because of" << e;
socket->writeError(503);
}
}
void BitcoreProxy::returnEnabledChains(HttpEngine::WebRequest *request) const
{
// TODO make sure we detect this or configure this instead of hard coding it.
QJsonObject chain;
chain.insert("chain", "BCH");
chain.insert("network", "mainnet");
QJsonArray top;
top.append(chain);
request->socket()->writeJson(QJsonDocument(top), s_JsonFormat);
}
void BitcoreProxy::parseConfig(const std::string &confFile)
2019-09-23 11:07:45 +02:00
{
QSettings conf(QString::fromStdString(confFile), QSettings::IniFormat);
2019-09-23 11:07:45 +02:00
conf.beginGroup("json");
s_JsonFormat = conf.value("compact", true).toBool() ? QJsonDocument::Compact : QJsonDocument::Indented;
conf.endGroup();
}
void BitcoreProxy::initializeHubConnection(NetworkConnection con, const std::string &)
2019-09-23 11:07:45 +02:00
{
con.send(Message(Api::BlockChainService, Api::BlockChain::GetBlockCount));
con.send(Message(Api::BlockNotificationService, Api::BlockNotification::Subscribe));
}
void BitcoreProxy::onReparseConfig()
{
reparseConfig();
}
2019-09-23 11:07:45 +02:00
void BitcoreProxy::requestTransactionInfo(const RequestString &rs, BitcoreWebRequest *request)
{
if (rs.post.isEmpty()) {
auto map = request->socket()->queryString();
QString strBlockHeight = map.value("blockHeight");
if (!strBlockHeight.isEmpty()) {
request->answerType = BitcoreWebRequest::TxForHeight;
bool ok;
Blockchain::Job job;
job.type = Blockchain::FetchBlockOfTx;
job.transactionFilters = Blockchain::IncludeFullTransactionData;
job.intData = strBlockHeight.toInt(&ok);
if (!ok)
throw UserInputException("blockchain not a number", "txHelp.html");
request->jobs.push_back(job);
request->map().insert("blockHeight", job.intData);
job.type = Blockchain::FetchBlockHeader;
request->jobs.push_back(job);
}
else {
QString strBlockHash = map.value("blockHash");
if (!strBlockHash.isEmpty()) {
if (strBlockHash.length() != 64 || QByteArray::fromHex(strBlockHash.toLatin1()).length() != 32)
throw UserInputException("blockHash not a hash", "txHelp.html");
request->map().insert("blockHash", strBlockHash);
request->answerType = BitcoreWebRequest::TxForBlockHash;
Blockchain::Job job;
job.type = Blockchain::FetchBlockOfTx;
job.transactionFilters = Blockchain::IncludeFullTransactionData | Blockchain::IncludeTxId;
job.data = hexStringToBuffer(strBlockHash);
request->jobs.push_back(job);
job.type = Blockchain::FetchBlockHeader;
request->jobs.push_back(job);
}
2019-10-06 13:07:28 +02:00
else
throw UserInputException("", "txHelp.html");
2019-09-23 11:07:45 +02:00
}
} else {
QString hashStr = rs.post.left(64);
if (hashStr.length() < 64 || QByteArray::fromHex(hashStr.toLatin1()).length() != 32)
throw UserInputException("No argument found", "txHelp.html");
request->map().insert("txid", hashStr);
Blockchain::Job job;
job.type = Blockchain::FetchTx;
job.data = hexStringToBuffer(hashStr);
job.transactionFilters = Blockchain::IncludeFullTransactionData;
if (rs.post.endsWith("authhead")) {
request->answerType = BitcoreWebRequest::TxForTxIdAuthHead;
// TODO Check the bugreport on what this is supposed to do. I suspect we need a new job.type
job.transactionFilters = Blockchain::IncludeFullTransactionData; // TODO
}
else if (rs.post.endsWith("coins")) {
request->answerType = BitcoreWebRequest::TxForTxIdCoins;
}
else {
request->answerType = BitcoreWebRequest::TxForTxId;
}
job.nextJobId = 1; // that would be the 'fetchBlockHeader'
request->jobs.push_back(job);
job = Blockchain::Job();
job.type = Blockchain::FetchBlockHeader;
request->jobs.push_back(job);
}
}
void BitcoreProxy::requestAddressInfo(const RequestString &rs, BitcoreWebRequest *request)
{
2019-10-05 16:01:52 +02:00
if (rs.post.isEmpty())
throw UserInputException("Missing address", "addressHelp.html");
2022-09-10 00:36:53 +02:00
auto args = rs.post.split("/", Qt::SkipEmptyParts);
2019-10-05 16:01:52 +02:00
Q_ASSERT(!args.isEmpty());
if (args.size() > 1) {
if (args.at(1) == "txs")
request->answerType = BitcoreWebRequest::AddressTxs;
else if (args.at(1) == "balance")
request->answerType = BitcoreWebRequest::AddressBalance;
}
else {
2019-10-06 13:07:28 +02:00
if (request->socket()->queryString().contains("unspent"))
2019-10-05 16:01:52 +02:00
request->answerType = BitcoreWebRequest::AddressUnspentOutputs;
}
if (request->answerType == BitcoreWebRequest::Unset)
throw UserInputException("Unknown request", "addressHelp.html");
2026-02-09 15:23:48 +01:00
CashAddress::Content c = CashAddress::decode(args.first().toStdString(), "bitcoincash");
2019-10-16 22:43:16 +02:00
bool ok = !c.hash.empty();
if (!ok) { // try to fall back to legacy address encoding (btc compatible)
2019-10-05 16:01:52 +02:00
CBase58Data old;
if (old.SetString(args.first().toStdString())) {
2019-10-16 22:43:16 +02:00
c.hash = old.data();
ok = true;
if (old.isMainnetPkh())
2026-02-09 15:23:48 +01:00
c.type = CashAddress::PubkeyType;
2019-10-16 22:43:16 +02:00
else if (old.isMainnetSh())
2026-02-09 15:23:48 +01:00
c.type = CashAddress::ScriptType;
2019-10-16 22:43:16 +02:00
else
ok = false;
2019-10-05 16:01:52 +02:00
}
}
2019-10-16 22:43:16 +02:00
Blockchain::Job job;
if (ok)
job.data = CashAddress::createHashedOutputScript(c);
2019-10-05 16:01:52 +02:00
if (job.data.isEmpty())
throw UserInputException("Address could not be parsed", "addressHelp.html");
job.type = Blockchain::LookupByAddress;
request->jobs.push_back(job);
2019-10-16 22:43:16 +02:00
request->map().insert("address", addressAsString(c));
2019-09-23 11:07:45 +02:00
}
void BitcoreProxy::requestBlockInfo(const RequestString &rs, BitcoreWebRequest *request)
{
// TODO
}
void BitcoreProxy::returnFeeSuggestion(const RequestString &rs, BitcoreWebRequest *request)
{
// TODO
}
void BitcoreProxy::returnDailyTransactions(const RequestString &rs, BitcoreWebRequest *request)
{
// TODO
2019-09-23 11:07:45 +02:00
}
// ------------------------------------------
RequestString::RequestString(const QString &path)
{
if (path.startsWith("/api/")) {
wholePath = path;
int slash = path.indexOf("/", 5);
chain = path.mid(5, slash - 5); // typically BCH
int slash2 = path.indexOf("/", ++slash);
if (slash2 > 0) {
network = path.mid(slash, slash2 - slash); // typically 'mainnet'
slash = path.indexOf("/", ++slash2);
request = path.mid(slash2, slash - slash2);
if (slash > 0)
post = path.mid(slash+1);
}
}
}
QString RequestString::anonPath() const
{
int i = post.indexOf('/');
return chain + "/" + network + "/" + request + "/" + (i > 0 ? (QString("{HASH}") + post.mid(i)) : "");
}
BitcoreWebRequest::BitcoreWebRequest(qintptr socketDescriptor, std::function<void (HttpEngine::WebRequest *)> &handler)
: HttpEngine::WebRequest(socketDescriptor, handler)
2019-10-08 10:26:22 +02:00
#ifdef BENCH
, startTime(QDateTime::currentDateTime())
#endif
2019-09-23 11:07:45 +02:00
{
}
2019-10-08 10:26:22 +02:00
BitcoreWebRequest::~BitcoreWebRequest()
{
#ifdef BENCH
RequestString rs(socket()->path());
logInfo().nospace() << "BENCH\t" << socket()->peerAddress().toString() << "\t" << rs.anonPath()
<< "\t" << startTime.msecsTo(QDateTime::currentDateTime()) << "ms";
#endif
}
2019-09-23 11:07:45 +02:00
QJsonObject &BitcoreWebRequest::map()
{
return m_map;
}
void BitcoreWebRequest::finished(int unfinishedJobs)
{
Q_UNUSED(unfinishedJobs)
// SearchEngine does everything in the threads that it uses for individual connections.
// Our http engine wants to use its own thread, so lets move threads.
QTimer::singleShot(0, this, SLOT(threadSafeFinished()));
// TODO maybe remember unfinishedJobs being non-zero so we can deal with not found items
}
2020-09-02 14:03:01 +02:00
void BitcoreWebRequest::transactionAdded(const Blockchain::Transaction &transaction, int resultIndex)
2019-09-23 11:07:45 +02:00
{
2019-10-05 16:01:52 +02:00
if ((answerType == TxForTxIdCoins
|| answerType == AddressTxs)
&& !transaction.fullTxData.isEmpty()) {
2019-10-06 13:07:28 +02:00
logDebug() << "Fetched Tx:" << transaction.blockHeight << transaction.offsetInBlock << "=>" << uint256ToString(transaction.txid);
2019-10-05 16:01:52 +02:00
auto txRef = txRefs.find(std::make_pair(transaction.blockHeight, transaction.offsetInBlock));
2019-09-23 11:07:45 +02:00
Tx tx(transaction.fullTxData);
2019-10-05 16:01:52 +02:00
if (answerType == TxForTxIdCoins) { // insert all outputs with 'spent' placeholders into txRefs to be updated later
assert(txRef == txRefs.end());
int outIndex = 0;
std::map<int, std::pair<int, int>> outputs;
Tx::Iterator iter(tx);
while (iter.next() != Tx::End) {
if (iter.tag() == Tx::OutputValue)
2020-01-17 19:44:29 +01:00
outputs.insert(std::make_pair(outIndex++, std::make_pair(-1, 0)));
2019-10-05 16:01:52 +02:00
}
txRefs.insert(std::make_pair(std::make_pair(transaction.blockHeight, transaction.offsetInBlock), outputs));
}
2019-09-23 11:07:45 +02:00
int outputIndex = 0;
Tx::Iterator iter(tx);
while (iter.next() != Tx::End) {
2019-10-05 16:01:52 +02:00
if (!transaction.isCoinbase() && iter.tag() == Tx::PrevTxHash && answerType == TxForTxIdCoins) {
2019-09-23 11:07:45 +02:00
logDebug() << "Finding prev output, location of txid:" << iter.uint256Data();
// I want to know what the block height was of this input.
2019-10-05 16:01:52 +02:00
// this is needed by TxFoTxIdCoins
2019-09-23 11:07:45 +02:00
Blockchain::Job job;
job.data = iter.byteData();
job.type = Blockchain::LookupTxById;
2019-10-05 16:01:52 +02:00
logDebug() << "additionally, fetch the outputs of that TX";
job.nextJobId = jobs.size() + 1;
jobs.push_back(job);
job = Blockchain::Job();
job.type = Blockchain::FetchTx;
job.transactionFilters = Blockchain::IncludeOutputs;
2019-09-23 11:07:45 +02:00
jobs.push_back(job);
}
else if (iter.tag() == Tx::OutputValue) {
// I want to know if it was spent, and if so, at what height
2019-10-05 16:01:52 +02:00
// this is needed by TxFoTxIdCoins and AddressTxs
if (answerType == TxForTxIdCoins
// for AddressTxs we only generate new jobs for the outputs we found in the FindAddress lookup
|| (answerType == AddressTxs && txRef != txRefs.end()
&& txRef->second.find(outputIndex) != txRef->second.end())) {
2019-10-06 13:07:28 +02:00
logDebug() << " for output, lets find who spent it." << transaction.blockHeight << transaction.offsetInBlock << "outIndex:" << outputIndex;
2019-10-05 16:01:52 +02:00
Blockchain::Job job;
job.data = transaction.txid;
assert(job.data.size() == 32);
job.intData = outputIndex;
job.type = Blockchain::LookupSpentTx;
2020-01-17 19:44:29 +01:00
job.intData2= transaction.blockHeight;
job.intData3 = transaction.offsetInBlock;
2019-09-23 11:07:45 +02:00
job.nextJobId = jobs.size() + 1;
jobs.push_back(job);
job = Blockchain::Job();
job.type = Blockchain::FetchTx;
job.transactionFilters = Blockchain::IncludeTxId;
2019-10-05 16:01:52 +02:00
jobs.push_back(job);
2019-09-23 11:07:45 +02:00
}
2019-10-05 16:01:52 +02:00
outputIndex++;
2019-09-23 11:07:45 +02:00
}
}
}
}
2019-10-06 13:07:28 +02:00
void BitcoreWebRequest::txIdResolved(int jobId, int blockHeight, int offsetInBlock)
2019-09-23 11:07:45 +02:00
{
assert(jobId >= 0);
assert(static_cast<int>(jobs.size()) > jobId);
assert(jobs.at(static_cast<size_t>(jobId)).data.size() == 32);
blockHeights.insert(std::make_pair(
uint256(jobs.at(static_cast<size_t>(jobId)).data.begin()), blockHeight));
2019-10-06 13:07:28 +02:00
logDebug().nospace() << "txid resolved "
<< uint256(jobs.at(static_cast<size_t>(jobId)).data.begin())
<< " is tx: (" << blockHeight << ", " << offsetInBlock << ")";
2019-09-23 11:07:45 +02:00
}
void BitcoreWebRequest::spentOutputResolved(int jobId, int blockHeight, int offsetInBlock)
{
assert(jobId >= 0);
assert(static_cast<int>(jobs.size()) > jobId);
2019-10-05 16:01:52 +02:00
// Job request is data = txid. intData = outIndex
Blockchain::Job &job = jobs.at(static_cast<size_t>(jobId));
const int outIndex = job.intData;
2020-01-17 19:44:29 +01:00
logDebug() << "output spent resolved" << outIndex << "->" << blockHeight << offsetInBlock;
2019-10-05 16:01:52 +02:00
assert(job.data.size() == 32);
2020-01-17 19:44:29 +01:00
assert(job.intData2 > 0 && job.intData3 > 0);
2019-10-05 16:01:52 +02:00
2020-01-17 19:44:29 +01:00
auto txIter = txRefs.find(std::make_pair(job.intData2, job.intData3));
2019-10-05 16:01:52 +02:00
assert (txIter != txRefs.end());
2020-01-17 19:44:29 +01:00
assert(outIndex >= 0);
2019-10-05 16:01:52 +02:00
auto row = txIter->second.find(outIndex);
assert(row != txIter->second.end());
2020-01-17 19:44:29 +01:00
// update placeholder inserted in addressUsedInOutput
if (blockHeight == -1) {
// Hub API states -1 means it is unspent.
// Bitcore decided that should be -2:
row->second.first = -2;
}
else {
row->second.first = blockHeight;
}
2019-10-05 16:01:52 +02:00
row->second.second = offsetInBlock;
}
void BitcoreWebRequest::addressUsedInOutput(int blockHeight, int offsetInBlock, int outIndex)
{
2019-10-06 13:07:28 +02:00
logDebug().nospace() << "FindByAddress returned tx:(" << blockHeight << ", " << offsetInBlock << ") outIndex: " << outIndex;
2019-10-05 16:01:52 +02:00
Q_ASSERT(blockHeight > 0);
Q_ASSERT(offsetInBlock > 0);
auto iter = txRefs.find(std::make_pair(blockHeight, offsetInBlock));
2020-01-17 19:44:29 +01:00
bool txPresent = false;
2019-10-05 16:01:52 +02:00
if (iter != txRefs.end()) {
// only fetch a tx once
2020-01-17 19:44:29 +01:00
txPresent = true;
// insert the outIndex
iter->second.insert(std::make_pair(outIndex, std::make_pair(-1, 0)));
logDebug().nospace() << " _ " << blockHeight << "|" << offsetInBlock << ", " << outIndex << " => " << -1 << "|" << 0;
2019-10-05 16:01:52 +02:00
}
else { // insert outindex
std::map<int, std::pair<int, int>> output;
2020-01-17 19:44:29 +01:00
output.insert(std::make_pair(outIndex, std::make_pair(-1, 0)));
2019-10-05 16:01:52 +02:00
txRefs.insert(std::make_pair(std::make_pair(blockHeight, offsetInBlock), std::move(output)));
2020-01-17 19:44:29 +01:00
logDebug().nospace() << " " << blockHeight << "|" << offsetInBlock << ", " << outIndex << " => " << -1 << "|" << 0;
2019-10-05 16:01:52 +02:00
}
2019-10-06 13:07:28 +02:00
Blockchain::Job job;
2020-01-17 19:44:29 +01:00
job.intData = blockHeight;
job.intData2 = offsetInBlock;
if (answerType == AddressTxs && !txPresent) {
2019-10-05 16:01:52 +02:00
job.type = Blockchain::FetchTx;
job.transactionFilters = Blockchain::IncludeFullTransactionData;
2020-01-17 19:44:29 +01:00
} else if (answerType == AddressBalance || answerType == AddressUnspentOutputs) {
2019-10-06 13:07:28 +02:00
job.type = Blockchain::FetchUTXOUnspent;
job.intData3 = outIndex;
2019-10-05 16:01:52 +02:00
}
2020-01-17 19:44:29 +01:00
else
return;
logDebug() << "Job created:" << jobs.size();
jobs.push_back(job);
2019-10-06 13:07:28 +02:00
}
2020-01-17 19:44:29 +01:00
void BitcoreWebRequest::utxoLookup(int jobId, int blockHeight, int offsetInBlock, int outIndex, bool unspent, int64_t, Streaming::ConstBuffer)
2019-10-06 13:07:28 +02:00
{
2020-01-17 19:44:29 +01:00
if (unspent) {
auto txIter = txRefs.find(std::make_pair(blockHeight, offsetInBlock));
if (txIter != txRefs.end()) {
auto row = txIter->second.find(outIndex);
// mark as unspent.
row->second.first = -2;
row->second.second = 0;
logDebug().nospace() << " = " << blockHeight << "|" << offsetInBlock << ", " << outIndex << " => " << -2 << "|" << 0;
}
}
if (unspent && (answerType == AddressUnspentOutputs || answerType == AddressBalance)) {
2019-10-06 13:07:28 +02:00
// TODO avoid requesting duplicate transactions.
2020-01-17 19:44:29 +01:00
logDebug() << "UTXO unspent, going to lookup:" << blockHeight << offsetInBlock << outIndex;
2019-10-06 13:07:28 +02:00
// fetch unspent tx
Blockchain::Job job;
job.type = Blockchain::FetchTx;
job.intData = blockHeight;
job.intData2 = offsetInBlock;
job.transactionFilters = answerType == AddressBalance ? Blockchain::IncludeOutputs : Blockchain::IncludeFullTransactionData;
2019-10-06 13:07:28 +02:00
jobs.push_back(job);
2019-10-05 16:01:52 +02:00
}
2019-09-23 11:07:45 +02:00
}
2021-02-23 17:55:26 +01:00
void BitcoreWebRequest::aborted(const Blockchain::ServiceUnavailableException &e)
2020-09-02 14:03:01 +02:00
{
2021-02-23 17:55:26 +01:00
QString error("could not find upstream service: %1");
switch (e.service()) {
case Blockchain::TheHub:
error = error.arg("The Hub");
break;
case Blockchain::IndexerTxIdDb:
error = error.arg("TxID indexer");
break;
case Blockchain::IndexerAddressDb:
error = error.arg("Addresses indexer");
break;
case Blockchain::IndexerSpentDb:
error = error.arg("Spent-db indexer");
break;
}
const bool temp = e.temporarily();
QTimer::singleShot(0, this, [=]() {
returnTemplatePath(socket(), temp ? "error.json" : "setup.html", error);
});
2020-09-02 14:03:01 +02:00
}
2019-09-23 11:07:45 +02:00
void BitcoreWebRequest::addDefaults(QJsonObject &node)
{
node.insert("network", m_map.value("network"));
node.insert("chain", m_map.value("chain"));
addRequestId(node);
}
void BitcoreWebRequest::threadSafeFinished()
{
switch (answerType) {
case TxForTxId: {
QJsonObject root;
if (answer.size() == 1) {
root = toJson(answer.front(), m_map);
auto header = blockHeaders.find(answer.front().blockHeight);
if (header != blockHeaders.end())
root = toJson(header->second, root);
}
addDefaults(root);
socket()->writeJson(QJsonDocument(root), s_JsonFormat);
break;
}
case TxForHeight:
case TxForBlockHash: {
QJsonArray root;
2020-12-25 23:51:06 +01:00
for (auto &tx : answer) {
2019-09-23 11:07:45 +02:00
QJsonObject o = toJson(tx, m_map);
auto header = blockHeaders.find(tx.blockHeight);
if (header != blockHeaders.end())
o = toJson(header->second, o);
addDefaults(o);
root.append(o);
}
socket()->writeJson(QJsonDocument(root), s_JsonFormat);
break;
}
case TxForTxIdCoins: {
QJsonObject root;
if (!answer.front().fullTxData.isEmpty()) {
const Blockchain::Transaction &transaction = answer.front();
Tx tx(transaction.fullTxData);
uint256 hash = tx.createHash();
QString myHash = uint256ToString(hash);
Tx::Iterator iter(tx);
2019-10-05 16:01:52 +02:00
assert(txRefs.size() == 1);
auto txRef = txRefs.begin();
2019-09-23 11:07:45 +02:00
QJsonArray inputs, outputs;
QJsonObject cur;
uint256 prevTx;
2019-10-05 16:01:52 +02:00
int outIndex = 0;
2019-09-23 11:07:45 +02:00
while (iter.next() != Tx::End) {
if (!transaction.isCoinbase()) {
if (iter.tag() == Tx::PrevTxHash) {
cur = QJsonObject();
prevTx = iter.uint256Data();
cur.insert("coinbase", transaction.isCoinbase());
cur.insert("spentTxid", myHash);
cur.insert("mintTxid", uint256ToString(prevTx));
cur.insert("spentHeight", transaction.blockHeight);
cur.insert("confirmations", -1); // copied from an actual bitcore server, looks wrong though!
auto bhIter = blockHeights.find(prevTx);
if (bhIter != blockHeights.end())
cur.insert("mintHeight", bhIter->second);
}
else if (iter.tag() == Tx::PrevTxIndex) {
cur.insert("mintIndex", iter.intData());
// Find the previous output, we should have fetched it.
for (const Blockchain::Transaction &t : answer) {
if (t.txid.size() == 32 && memcmp(t.txid.begin(), prevTx.begin(), 32) == 0) {
// we fetched this tx with outputs only, lets dig out what we need.
2020-12-25 23:51:06 +01:00
if (int(t.outputs.size()) > iter.intData()) {
2019-09-23 11:07:45 +02:00
auto output = t.outputs.at(iter.intData());
cur.insert("value", (qint64) output.amount);
parseScriptAndAddress(cur, output.outScript);
}
break;
}
}
} else if (iter.tag() == Tx::TxInScript) {
addDefaults(cur);
inputs.append(cur);
}
}
if (iter.tag() == Tx::OutputValue) {
cur = QJsonObject();
cur.insert("coinbase", transaction.isCoinbase());
cur.insert("confirmations", -1); // copied from an actual bitcore server, looks wrong though!
cur.insert("value", static_cast<qint64>(iter.longData()));
cur.insert("mintHeight", transaction.blockHeight);
cur.insert("mintIndex", outputs.size());
cur.insert("mintTxid", myHash);
2019-10-05 16:01:52 +02:00
auto out = txRef->second.find(outIndex++);
assert(out != txRef->second.end());
cur.insert("spentHeight", out->second.first);
cur.insert("spentTxid", QString());
for (const Blockchain::Transaction &t : answer) {
if (t.blockHeight == out->second.first && t.offsetInBlock == out->second.second) {
cur.insert("spentTxid", uint256ToString(t.txid));
break;
2019-09-23 11:07:45 +02:00
}
}
} else if (iter.tag() == Tx::OutputScript) {
parseScriptAndAddress(cur, iter.byteData());
addDefaults(cur);
outputs.append(cur);
}
}
root.insert("inputs", inputs);
root.insert("outputs", outputs);
}
socket()->writeJson(QJsonDocument(root), s_JsonFormat);
break;
}
2019-10-06 13:07:28 +02:00
case AddressUnspentOutputs:
2019-10-05 16:01:52 +02:00
case AddressTxs: {
QJsonArray root;
QString script;
for (auto tx : answer) {
auto refs = txRefs.find(std::make_pair(tx.blockHeight, tx.offsetInBlock));
if (refs == txRefs.end()) // not one of main transactions
continue;
const QString txid = uint256ToString(tx.txid);
Tx fullTx(tx.fullTxData);
2019-10-06 13:07:28 +02:00
for (auto out : refs->second) {
2020-01-17 19:44:29 +01:00
if (answerType == AddressUnspentOutputs && out.second.first != -2) // skip unspent
continue;
// txRef is described in header file.
2019-10-06 13:07:28 +02:00
// about 'out': outputIndex is 'first' and who spent it is 'second' (a pair)
2019-10-05 16:01:52 +02:00
QJsonObject o;
o.insert("coinbase", tx.offsetInBlock > 0 && tx.offsetInBlock < 90);
o.insert("mintHeight", tx.blockHeight);
o.insert("address", map().value("address"));
o.insert("mintTxid", txid);
2019-10-06 13:07:28 +02:00
o.insert("mintIndex", out.first);
2019-10-05 16:01:52 +02:00
o.insert("confirmations", -1); // not sure why this is -1 in Bitcore.
2019-10-06 13:07:28 +02:00
auto outData = fullTx.output(out.first);
2019-10-05 16:01:52 +02:00
o.insert("value", static_cast<qint64>(outData.outputValue));
if (script.isEmpty()) // stays the same for this entire call
2026-05-13 16:43:23 +02:00
script = QString::fromLatin1(QByteArray(outData.outputScript().begin(), outData.outputScript().size()).toHex());
2019-10-05 16:01:52 +02:00
o.insert("script", script);
2019-10-06 13:07:28 +02:00
o.insert("spentHeight", out.second.first);
2019-10-05 16:01:52 +02:00
o.insert("spentTxid", QString());
for (const Blockchain::Transaction &t : answer) {
2019-10-06 13:07:28 +02:00
if (t.blockHeight == out.second.first && t.offsetInBlock == out.second.second) {
2019-10-05 16:01:52 +02:00
o.insert("spentTxid", uint256ToString(t.txid));
break;
}
}
addDefaults(o);
root.append(o);
}
}
socket()->writeJson(QJsonDocument(root), s_JsonFormat);
break;
}
case AddressBalance: {
qint64 balance = 0;
for (auto tx : answer) {
auto refs = txRefs.find(std::make_pair(tx.blockHeight, tx.offsetInBlock));
2020-01-17 19:44:29 +01:00
for (auto out = refs->second.begin(); out != refs->second.end(); ++out) {
if (out->second.first == -2) { // unspent output
assert (int(tx.outputs.size()) > out->first);
balance += tx.outputs.at(out->first).amount;
}
}
}
QJsonObject root;
root["confirmed"] = balance;
root["balance"] = balance;
root["unconfirmed"] = 0;
socket()->writeJson(QJsonDocument(root), s_JsonFormat);
break;
}
2019-09-23 11:07:45 +02:00
default:
// TODO
break;
}
socket()->close();
}