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>
|
2019-11-12 17:43:13 +01:00
|
|
|
#include <networkmanager/NetworkManager.h>
|
2019-11-13 15:11:39 +01:00
|
|
|
#include <primitives/script.h>
|
2021-11-02 11:04:59 +01:00
|
|
|
#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;
|
|
|
|
|
}
|
2019-11-07 16:08:53 +01:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2019-11-12 17:43:13 +01:00
|
|
|
void BitcoreProxy::parseConfig(const std::string &confFile)
|
2019-09-23 11:07:45 +02:00
|
|
|
{
|
2019-11-12 17:43:13 +01: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();
|
|
|
|
|
}
|
|
|
|
|
|
2019-11-11 19:32:33 +01:00
|
|
|
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));
|
|
|
|
|
}
|
|
|
|
|
|
2019-11-12 17:43:13 +01:00
|
|
|
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)
|
|
|
|
|
{
|
2019-11-12 17:43:13 +01:00
|
|
|
// 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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-10-06 13:27:19 +02:00
|
|
|
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;
|
2019-10-06 13:27:19 +02:00
|
|
|
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;
|
|
|
|
|
}
|
2019-10-06 13:27:19 +02:00
|
|
|
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;
|
2019-10-06 13:27:19 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
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();
|
|
|
|
|
}
|