Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 14ce8cc2a7 | |||
| d180b5870c | |||
| 596d4ab55c | |||
| 5af26f186e | |||
| 5530920b25 | |||
| 3ed9a42b3c |
@@ -9,7 +9,7 @@ on:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
VERSION: "5.1.5"
|
||||
VERSION: "5.1.8"
|
||||
|
||||
jobs:
|
||||
build-windows-qt:
|
||||
|
||||
@@ -261,7 +261,7 @@ distclean: release-distclean debug-distclean FORCE
|
||||
-$(DEL_FILE) .qmake.stash
|
||||
|
||||
E:/repos/triangles/src/leveldb/libleveldb.a: FORCE
|
||||
cd E:/repos/triangles/src/leveldb && CC=gcc CXX=g++ TARGET_OS=OS_WINDOWS_CROSSCOMPILE $(MAKE) OPT="-fpermissive -O2" libleveldb.a libmemenv.a && ranlib E:/repos/triangles/src/leveldb/libleveldb.a && ranlib E:/repos/triangles/src/leveldb/libmemenv.a
|
||||
cd E:/repos/triangles/src/leveldb && CC=gcc CXX=g++ TARGET_OS=OS_WINDOWS_CROSSCOMPILE $(MAKE) OPT="-fno-keep-inline-dllexport -march=nocona -msahf -mtune=generic -Wa,-mbig-obj -O2" libleveldb.a libmemenv.a && ranlib E:/repos/triangles/src/leveldb/libleveldb.a && ranlib E:/repos/triangles/src/leveldb/libmemenv.a
|
||||
|
||||
release-mocclean:
|
||||
$(MAKE) -f $(MAKEFILE).Release mocclean
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@
|
||||
// These need to be macros, as version.cpp's and triangles-qt.rc's voodoo requires it
|
||||
#define CLIENT_VERSION_MAJOR 5
|
||||
#define CLIENT_VERSION_MINOR 1
|
||||
#define CLIENT_VERSION_REVISION 5
|
||||
#define CLIENT_VERSION_REVISION 8
|
||||
#define CLIENT_VERSION_BUILD 0
|
||||
|
||||
// Converts the parameter X to a string after macro replacement on X has been performed.
|
||||
|
||||
+59
-18
@@ -12,6 +12,7 @@
|
||||
#include "checkpoints.h"
|
||||
#include "smessage.h"
|
||||
#include "tor/onion_v3.h"
|
||||
#include "tor/tor_process.h"
|
||||
#ifdef ENABLE_ZMQ
|
||||
#include "zmqpublishnotifier.h"
|
||||
#endif
|
||||
@@ -154,6 +155,7 @@ void Shutdown(void* parg)
|
||||
|
||||
SecureMsgShutdown();
|
||||
ShutdownTorV3();
|
||||
StopTorProcess();
|
||||
|
||||
#ifdef ENABLE_ZMQ
|
||||
if (pzmqNotifier)
|
||||
@@ -403,6 +405,12 @@ std::string HelpMessage()
|
||||
" -rpcsslprivatekeyfile=<file.pem> " + _("Server private key (default: server.pem)") + "\n" +
|
||||
" -rpcsslciphers=<ciphers> " + _("Acceptable ciphers (default: TLSv1+HIGH:!SSLv2:!aNULL:!eNULL:!AH:!3DES:@STRENGTH)") + "\n" +
|
||||
|
||||
"\n" + _("REST API options:") + "\n" +
|
||||
" -rest " + _("Enable public REST API on RPC port (default: 0)") + "\n" +
|
||||
" -restcorsorigin=<origin> " + _("CORS Access-Control-Allow-Origin header (default: *)") + "\n" +
|
||||
" -restapikey=<key> " + _("Bearer token for authenticated wallet endpoints") + "\n" +
|
||||
" -restratelimit=<n> " + _("Max requests/sec per IP for public endpoints (default: 30, 0=disabled)") + "\n" +
|
||||
|
||||
"\n" + _("Secure messaging options:") + "\n" +
|
||||
" -nosmsg " + _("Disable secure messaging.") + "\n" +
|
||||
" -debugsmsg " + _("Log extra debug messages.") + "\n" +
|
||||
@@ -694,24 +702,26 @@ bool AppInit2()
|
||||
//if (nSocksVersion != 4 && nSocksVersion != 5)
|
||||
// return InitError(strprintf(_("Unknown -socks proxy version requested: %i"), nSocksVersion));
|
||||
|
||||
do {
|
||||
// Network selection: enable all networks (IPv4, IPv6, Tor)
|
||||
// Tor is always enabled; clearnet is also allowed for seed node discovery
|
||||
// Users can restrict to Tor-only with -onlynet=tor
|
||||
if (mapArgs.count("-onlynet")) {
|
||||
std::set<enum Network> nets;
|
||||
|
||||
|
||||
|
||||
|
||||
nets.insert(NET_TOR);
|
||||
|
||||
BOOST_FOREACH(std::string snet, mapMultiArgs["-onlynet"]) {
|
||||
enum Network net = ParseNetwork(snet);
|
||||
if (net == NET_UNROUTABLE)
|
||||
return InitError(strprintf(_("Unknown network specified in -onlynet: '%s'"), snet.c_str()));
|
||||
nets.insert(net);
|
||||
}
|
||||
for (int n = 0; n < NET_MAX; n++) {
|
||||
enum Network net = (enum Network)n;
|
||||
if (!nets.count(net))
|
||||
SetLimited(net);
|
||||
}
|
||||
} while (false);
|
||||
}
|
||||
|
||||
|
||||
CService addrOnion;
|
||||
// need to move onion_port to a header
|
||||
// Tor proxy: always configured for .onion connectivity
|
||||
CService addrOnion;
|
||||
unsigned short const onion_port = 19099;
|
||||
|
||||
if (mapArgs.count("-tor") && mapArgs["-tor"] != "0") {
|
||||
@@ -722,10 +732,8 @@ bool AppInit2()
|
||||
addrOnion = CService("127.0.0.1", onion_port);
|
||||
}
|
||||
|
||||
if (true) {
|
||||
SetProxy(NET_TOR, addrOnion, 5);
|
||||
SetReachable(NET_TOR);
|
||||
}
|
||||
SetProxy(NET_TOR, addrOnion, 5);
|
||||
SetReachable(NET_TOR);
|
||||
|
||||
// see Step 2: parameter interactions for more information about these
|
||||
fNoListen = !GetBoolArg("-listen", true);
|
||||
@@ -956,18 +964,33 @@ bool AppInit2()
|
||||
printf(" rescan %15"PRId64"ms\n", GetTimeMillis() - nStart);
|
||||
}
|
||||
|
||||
// ********************************************************* Step 8.5: initialize Tor V3 identity
|
||||
// ********************************************************* Step 8.5: start Tor and initialize V3 identity
|
||||
{
|
||||
uiInterface.InitMessage(_("Starting Tor..."));
|
||||
printf("Starting Tor process...\n");
|
||||
|
||||
// Start the Tor process (finds/launches tor binary, provides SOCKS proxy)
|
||||
std::string torDataPath = (GetDataDir() / "tor_data").string();
|
||||
bool torStarted = StartTorProcess(torDataPath);
|
||||
|
||||
if (torStarted) {
|
||||
printf("Tor process running, SOCKS proxy at %s\n",
|
||||
CTorProcess::GetInstance()->GetSocksProxy().c_str());
|
||||
} else {
|
||||
printf("WARNING: Tor not available. .onion peers will not be reachable.\n");
|
||||
printf(" Clearnet connections will still work normally.\n");
|
||||
}
|
||||
|
||||
// Initialize Tor V3 identity (Ed25519 keys, onion address)
|
||||
uiInterface.InitMessage(_("Initializing Tor V3 identity..."));
|
||||
printf("Initializing Tor V3 onion identity...\n");
|
||||
|
||||
// Tor V3 identity is innate to Triangles — always enabled
|
||||
LoadTorV3Config();
|
||||
TorV3Config& torConfig = GetTorV3Config();
|
||||
torConfig.enableTor = true;
|
||||
torConfig.enableHiddenService = true;
|
||||
torConfig.hiddenServicePort = GetListenPort();
|
||||
torConfig.torDataDirectory = (GetDataDir() / "tor_data").string();
|
||||
torConfig.torDataDirectory = torDataPath;
|
||||
|
||||
if (InitTorV3()) {
|
||||
string onionAddr = CTorV3Manager::GetInstance()->GetWalletOnionAddress();
|
||||
@@ -990,6 +1013,24 @@ bool AppInit2()
|
||||
} else {
|
||||
printf("WARNING: Failed to initialize Tor V3 identity\n");
|
||||
}
|
||||
|
||||
// Also check if Tor gave us a hidden service hostname
|
||||
if (torStarted) {
|
||||
fs::path torHsHostname = fs::path(torDataPath) / "hidden_service" / "hostname";
|
||||
if (fs::exists(torHsHostname)) {
|
||||
ifstream f(torHsHostname.string().c_str());
|
||||
string torOnion;
|
||||
if (f.is_open() && getline(f, torOnion)) {
|
||||
// Trim whitespace
|
||||
while (!torOnion.empty() && (torOnion.back() == '\n' || torOnion.back() == '\r' || torOnion.back() == ' '))
|
||||
torOnion.pop_back();
|
||||
if (!torOnion.empty()) {
|
||||
AddLocal(CService(torOnion, GetListenPort(), fNameLookup), LOCAL_MANUAL);
|
||||
printf("Tor hidden service (from Tor process): %s\n", torOnion.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ********************************************************* Step 9: import blocks
|
||||
|
||||
+14
-15
@@ -1835,12 +1835,8 @@ bool CBlock::ConnectBlock(CTxDB& txdb, CBlockIndex* pindex, bool fJustCheck)
|
||||
}
|
||||
|
||||
// Watch for transactions paying to me
|
||||
// Skip during initial block download - wallet will rescan on next normal startup
|
||||
if (!IsInitialBlockDownload())
|
||||
{
|
||||
BOOST_FOREACH(CTransaction& tx, vtx)
|
||||
SyncWithWallets(tx, this, true);
|
||||
}
|
||||
BOOST_FOREACH(CTransaction& tx, vtx)
|
||||
SyncWithWallets(tx, this, true);
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -2040,11 +2036,8 @@ bool CBlock::SetBestChain(CTxDB& txdb, CBlockIndex* pindexNew)
|
||||
|
||||
// Update best block in wallet (so we can detect restored wallets)
|
||||
bool fIsInitialDownload = IsInitialBlockDownload();
|
||||
if (!fIsInitialDownload)
|
||||
{
|
||||
const CBlockLocator locator(pindexNew);
|
||||
::SetBestChain(locator);
|
||||
}
|
||||
const CBlockLocator locator(pindexNew);
|
||||
::SetBestChain(locator);
|
||||
|
||||
// New best block
|
||||
hashBestChain = hash;
|
||||
@@ -2057,7 +2050,8 @@ bool CBlock::SetBestChain(CTxDB& txdb, CBlockIndex* pindexNew)
|
||||
|
||||
uint256 nBestBlockTrust = pindexBest->nHeight != 0 ? (pindexBest->nChainTrust - pindexBest->pprev->nChainTrust) : pindexBest->nChainTrust;
|
||||
|
||||
if (nBestHeight % 10000 == 0 || nBestHeight > 2186900)
|
||||
// Log every 500 blocks during sync, every block once caught up
|
||||
if (nBestHeight % 500 == 0 || !IsInitialBlockDownload())
|
||||
printf("SetBestChain: new best=%s height=%d trust=%s blocktrust=%"PRId64" date=%s\n",
|
||||
hashBestChain.ToString().substr(0,20).c_str(), nBestHeight,
|
||||
CBigNum(nBestChainTrust).ToString().c_str(),
|
||||
@@ -2604,8 +2598,8 @@ bool ProcessBlock(CNode* pfrom, CBlock* pblock)
|
||||
mapOrphanBlocksByPrev.erase(hashPrev);
|
||||
}
|
||||
|
||||
if (nBestHeight % 10000 == 0 || nBestHeight > 2186900)
|
||||
printf("ProcessBlock: ACCEPTED\n");
|
||||
if (nBestHeight % 500 == 0 || !IsInitialBlockDownload())
|
||||
printf("ProcessBlock: ACCEPTED block %d\n", nBestHeight);
|
||||
|
||||
// triangles: if responsible for sync-checkpoint send it
|
||||
if (pfrom && !CSyncCheckpoint::strMasterPrivKey.empty())
|
||||
@@ -3226,7 +3220,7 @@ bool static ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv)
|
||||
(nAskedForBlocks < 1 || vNodes.size() <= 1))
|
||||
{
|
||||
nAskedForBlocks++;
|
||||
pfrom->PushGetHeaders(pindexBest, uint256(0));
|
||||
pfrom->PushGetBlocks(pindexBest, uint256(0));
|
||||
}
|
||||
|
||||
// Relay alerts
|
||||
@@ -3599,6 +3593,11 @@ bool static ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv)
|
||||
|
||||
if (nRequested > 0 && fDebug)
|
||||
printf("requested %d blocks from headers announcement\n", nRequested);
|
||||
|
||||
// If we received a full batch, continue sync via getblocks
|
||||
// (the getblocks/inv/orphan cycle handles chain continuation)
|
||||
if (vHeaders.size() >= 2000)
|
||||
pfrom->PushGetBlocks(pindexBest, uint256(0));
|
||||
}
|
||||
|
||||
|
||||
|
||||
+10
-1
@@ -114,6 +114,7 @@ OBJS= \
|
||||
obj/net_bootstrap.o \
|
||||
obj/protocol.o \
|
||||
obj/trianglesrpc.o \
|
||||
obj/rest.o \
|
||||
obj/rpcdump.o \
|
||||
obj/rpcnet.o \
|
||||
obj/rpcmining.o \
|
||||
@@ -134,7 +135,8 @@ OBJS= \
|
||||
obj/scrypt-x86.o \
|
||||
obj/scrypt-x86_64.o \
|
||||
obj/smessage.o \
|
||||
obj/onion_v3.o
|
||||
obj/onion_v3.o \
|
||||
obj/tor_process.o
|
||||
|
||||
all: trianglesd.exe
|
||||
|
||||
@@ -199,6 +201,13 @@ obj/onion_v3.o: tor/onion_v3.cpp
|
||||
-e '/^$$/ d' -e 's/$$/ :/' < $(@:%.o=%.d) >> $(@:%.o=%.P); \
|
||||
rm -f $(@:%.o=%.d)
|
||||
|
||||
obj/tor_process.o: tor/tor_process.cpp
|
||||
$(CXX) -c $(xCXXFLAGS) -MMD -MF $(@:%.o=%.d) -o $@ $<
|
||||
@cp $(@:%.o=%.d) $(@:%.o=%.P); \
|
||||
sed -e 's/#.*//' -e 's/^[^:]*: *//' -e 's/ *\\$$//' \
|
||||
-e '/^$$/ d' -e 's/$$/ :/' < $(@:%.o=%.d) >> $(@:%.o=%.P); \
|
||||
rm -f $(@:%.o=%.d)
|
||||
|
||||
obj/net_bootstrap.o: net_bootstrap.cpp
|
||||
$(CXX) -c $(xCXXFLAGS) -MMD -MF $(@:%.o=%.d) -o $@ $<
|
||||
@cp $(@:%.o=%.d) $(@:%.o=%.P); \
|
||||
|
||||
+10
-1
@@ -149,6 +149,7 @@ OBJS= \
|
||||
obj/net_bootstrap.o \
|
||||
obj/protocol.o \
|
||||
obj/trianglesrpc.o \
|
||||
obj/rest.o \
|
||||
obj/rpcdump.o \
|
||||
obj/rpcnet.o \
|
||||
obj/rpcmining.o \
|
||||
@@ -169,7 +170,8 @@ OBJS= \
|
||||
obj/scrypt-x86.o \
|
||||
obj/scrypt-x86_64.o \
|
||||
obj/smessage.o \
|
||||
obj/onion_v3.o
|
||||
obj/onion_v3.o \
|
||||
obj/tor_process.o
|
||||
|
||||
# ZMQ support (optional)
|
||||
# Build with: make -f makefile.unix USE_ZMQ=1
|
||||
@@ -245,6 +247,13 @@ obj/onion_v3.o: tor/onion_v3.cpp
|
||||
-e '/^$$/ d' -e 's/$$/ :/' < $(@:%.o=%.d) >> $(@:%.o=%.P); \
|
||||
rm -f $(@:%.o=%.d)
|
||||
|
||||
obj/tor_process.o: tor/tor_process.cpp
|
||||
$(CXX) -c $(xCXXFLAGS) -MMD -MF $(@:%.o=%.d) -o $@ $<
|
||||
@cp $(@:%.o=%.d) $(@:%.o=%.P); \
|
||||
sed -e 's/#.*//' -e 's/^[^:]*: *//' -e 's/ *\\$$//' \
|
||||
-e '/^$$/ d' -e 's/$$/ :/' < $(@:%.o=%.d) >> $(@:%.o=%.P); \
|
||||
rm -f $(@:%.o=%.d)
|
||||
|
||||
obj/net_bootstrap.o: net_bootstrap.cpp
|
||||
$(CXX) -c $(xCXXFLAGS) -MMD -MF $(@:%.o=%.d) -o $@ $<
|
||||
@cp $(@:%.o=%.d) $(@:%.o=%.P); \
|
||||
|
||||
+61
-6
@@ -40,6 +40,7 @@ void ThreadOpenAddedConnections2(void* parg);
|
||||
#ifdef USE_UPNP
|
||||
void ThreadMapPort2(void* parg);
|
||||
#endif
|
||||
void ThreadDNSAddressSeed(void* parg);
|
||||
void ThreadDNSAddressSeed2(void* parg);
|
||||
bool OpenNetworkConnection(const CAddress& addrConnect, CSemaphoreGrant *grantOutbound = NULL, const char *strDest = NULL, bool fOneShot = false);
|
||||
|
||||
@@ -1375,6 +1376,7 @@ void ThreadOnionSeed(void* parg)
|
||||
|
||||
unsigned int pnSeed[] = {
|
||||
0xCE58E9C2, // DNS2-OpenClaw: 194.233.88.206
|
||||
0x13A7D04A, // DNS3-Sami: 74.208.167.19
|
||||
};
|
||||
|
||||
void DumpAddresses()
|
||||
@@ -1416,6 +1418,58 @@ void ThreadDumpAddress(void* parg)
|
||||
printf("ThreadDumpAddress exited\n");
|
||||
}
|
||||
|
||||
void ThreadDNSAddressSeed2(void* parg)
|
||||
{
|
||||
static const char* strDNSSeed[] = {
|
||||
"seed1.cryptographic-triangles.org",
|
||||
"seed2.cryptographic-triangles.org",
|
||||
"seed3.cryptographic-triangles.org",
|
||||
"backup-seed.cryptographic-triangles.org",
|
||||
};
|
||||
|
||||
printf("Loading addresses from DNS seeds...\n");
|
||||
int found = 0;
|
||||
|
||||
for (unsigned int seed_idx = 0; seed_idx < ARRAYLEN(strDNSSeed); seed_idx++)
|
||||
{
|
||||
if (fShutdown)
|
||||
return;
|
||||
|
||||
vector<CNetAddr> vaddr;
|
||||
if (LookupHost(strDNSSeed[seed_idx], vaddr))
|
||||
{
|
||||
BOOST_FOREACH(CNetAddr& ip, vaddr)
|
||||
{
|
||||
CAddress addr(CService(ip, GetDefaultPort()));
|
||||
addr.nTime = GetTime() - 3*24*60*60; // 3 days ago
|
||||
addrman.Add(addr, CNetAddr(strDNSSeed[seed_idx], true));
|
||||
found++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
printf("%d addresses found from DNS seeds\n", found);
|
||||
}
|
||||
|
||||
void ThreadDNSAddressSeed(void* parg)
|
||||
{
|
||||
RenameThread("Triangles-dnsseed");
|
||||
try
|
||||
{
|
||||
vnThreadsRunning[THREAD_DNSSEED]++;
|
||||
ThreadDNSAddressSeed2(parg);
|
||||
vnThreadsRunning[THREAD_DNSSEED]--;
|
||||
}
|
||||
catch (std::exception& e) {
|
||||
vnThreadsRunning[THREAD_DNSSEED]--;
|
||||
PrintException(&e, "ThreadDNSAddressSeed()");
|
||||
} catch (...) {
|
||||
vnThreadsRunning[THREAD_DNSSEED]--;
|
||||
PrintException(NULL, "ThreadDNSAddressSeed()");
|
||||
}
|
||||
printf("ThreadDNSAddressSeed exited\n");
|
||||
}
|
||||
|
||||
void ThreadOpenConnections(void* parg)
|
||||
{
|
||||
// Make this thread recognisable as the connection opening thread
|
||||
@@ -1952,9 +2006,10 @@ void static Discover()
|
||||
}
|
||||
|
||||
static void run_tor() {
|
||||
// Old embedded Tor v2 client removed - incompatible with OpenSSL 3.x.
|
||||
// Tor v3 onion services are handled by onion_v3.cpp via external Tor/SOCKS5.
|
||||
printf("Tor v3 mode: using external Tor process via SOCKS5 proxy.\n");
|
||||
// Tor process is now managed by CTorProcess (tor_process.cpp)
|
||||
// which starts an external Tor binary with SOCKS5 proxy and v3 hidden service.
|
||||
// The old embedded Tor v2 code was removed (incompatible with OpenSSL 3.x).
|
||||
printf("Tor v3 mode: using managed Tor process via SOCKS5 proxy.\n");
|
||||
set_initialized();
|
||||
}
|
||||
|
||||
@@ -2030,9 +2085,9 @@ void StartNode(void* parg)
|
||||
if (fUseUPnP)
|
||||
MapPort();
|
||||
|
||||
// Get addresses from IRC and advertise ours
|
||||
//if (!NewThread(ThreadIRCSeed, NULL))
|
||||
// printf("Error: NewThread(ThreadIRCSeed) failed\n");
|
||||
// DNS seed lookup
|
||||
if (!NewThread(ThreadDNSAddressSeed, NULL))
|
||||
printf("Error: NewThread(ThreadDNSAddressSeed) failed\n");
|
||||
|
||||
// Send and receive from sockets, accept connections
|
||||
if (!NewThread(ThreadSocketHandler, NULL))
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
#include "introdialog.h"
|
||||
#include "util.h"
|
||||
|
||||
#include <QSettings>
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QFileDialog>
|
||||
#include <QDir>
|
||||
#include <QMessageBox>
|
||||
#include <QDialogButtonBox>
|
||||
|
||||
#include <boost/filesystem.hpp>
|
||||
|
||||
IntroDialog::IntroDialog(QWidget *parent) :
|
||||
QDialog(parent)
|
||||
{
|
||||
setWindowTitle("Triangles");
|
||||
setMinimumWidth(520);
|
||||
|
||||
// Match existing Triangles dark theme
|
||||
setStyleSheet(
|
||||
"QDialog { background-color: #000; color: #f26522; }"
|
||||
"QLabel { color: #f26522; }"
|
||||
"QRadioButton { color: #f26522; }"
|
||||
"QRadioButton::indicator { border: 1px solid #f26522; background-color: #000; width: 12px; height: 12px; border-radius: 7px; }"
|
||||
"QRadioButton::indicator:checked { background-color: #f26522; }"
|
||||
"QLineEdit { background-color: #1c1c1c; border: 1px solid #f26522; color: #f26522; padding: 4px; }"
|
||||
"QPushButton { background-color: #000; color: #f26522; border: 1px solid #f26522; padding: 4px 16px; min-height: 20px; }"
|
||||
"QPushButton:hover { background-color: #61280E; }"
|
||||
);
|
||||
|
||||
defaultDataDir = QString::fromStdString(GetDefaultDataDir().string());
|
||||
|
||||
QVBoxLayout *mainLayout = new QVBoxLayout(this);
|
||||
mainLayout->setContentsMargins(20, 20, 20, 20);
|
||||
mainLayout->setSpacing(12);
|
||||
|
||||
// Welcome header
|
||||
QLabel *welcomeLabel = new QLabel(tr("Welcome to Triangles!"));
|
||||
welcomeLabel->setStyleSheet("font-size: 16px; font-weight: bold; color: #f26522;");
|
||||
mainLayout->addWidget(welcomeLabel);
|
||||
|
||||
// Description
|
||||
QLabel *descLabel = new QLabel(tr(
|
||||
"Triangles will store its blockchain data, wallet, and configuration in a data directory. "
|
||||
"You can use the default directory or choose a custom location. "
|
||||
"The data directory requires several hundred MB of free space."
|
||||
));
|
||||
descLabel->setWordWrap(true);
|
||||
mainLayout->addWidget(descLabel);
|
||||
|
||||
mainLayout->addSpacing(8);
|
||||
|
||||
// Default directory radio
|
||||
defaultRadio = new QRadioButton(tr("Use the default data directory"));
|
||||
defaultRadio->setChecked(true);
|
||||
mainLayout->addWidget(defaultRadio);
|
||||
|
||||
// Show default path
|
||||
QLabel *defaultPathLabel = new QLabel(defaultDataDir);
|
||||
defaultPathLabel->setStyleSheet("color: #999; margin-left: 24px; font-size: 11px;");
|
||||
mainLayout->addWidget(defaultPathLabel);
|
||||
|
||||
mainLayout->addSpacing(4);
|
||||
|
||||
// Custom directory radio
|
||||
customRadio = new QRadioButton(tr("Use a custom data directory:"));
|
||||
mainLayout->addWidget(customRadio);
|
||||
|
||||
// Path input + browse button
|
||||
QHBoxLayout *pathLayout = new QHBoxLayout();
|
||||
pathLayout->setContentsMargins(24, 0, 0, 0);
|
||||
|
||||
pathEdit = new QLineEdit(defaultDataDir);
|
||||
pathEdit->setEnabled(false);
|
||||
pathLayout->addWidget(pathEdit);
|
||||
|
||||
browseButton = new QPushButton(tr("Browse..."));
|
||||
browseButton->setEnabled(false);
|
||||
pathLayout->addWidget(browseButton);
|
||||
|
||||
mainLayout->addLayout(pathLayout);
|
||||
|
||||
// Free space label
|
||||
freeSpaceLabel = new QLabel();
|
||||
freeSpaceLabel->setStyleSheet("color: #999; margin-left: 24px; font-size: 11px;");
|
||||
mainLayout->addWidget(freeSpaceLabel);
|
||||
|
||||
mainLayout->addStretch(1);
|
||||
|
||||
// OK / Cancel buttons
|
||||
QHBoxLayout *buttonLayout = new QHBoxLayout();
|
||||
buttonLayout->addStretch(1);
|
||||
|
||||
QPushButton *okButton = new QPushButton(tr("OK"));
|
||||
QPushButton *cancelButton = new QPushButton(tr("Cancel"));
|
||||
|
||||
buttonLayout->addWidget(okButton);
|
||||
buttonLayout->addWidget(cancelButton);
|
||||
mainLayout->addLayout(buttonLayout);
|
||||
|
||||
// Connections
|
||||
connect(defaultRadio, SIGNAL(toggled(bool)), this, SLOT(on_defaultRadio_toggled(bool)));
|
||||
connect(browseButton, SIGNAL(clicked()), this, SLOT(on_browseButton_clicked()));
|
||||
connect(pathEdit, SIGNAL(textChanged(QString)), this, SLOT(updateFreeSpace()));
|
||||
connect(okButton, SIGNAL(clicked()), this, SLOT(accept()));
|
||||
connect(cancelButton, SIGNAL(clicked()), this, SLOT(reject()));
|
||||
|
||||
updateFreeSpace();
|
||||
}
|
||||
|
||||
QString IntroDialog::getDataDirectory() const
|
||||
{
|
||||
if (defaultRadio->isChecked())
|
||||
return defaultDataDir;
|
||||
return pathEdit->text();
|
||||
}
|
||||
|
||||
void IntroDialog::setDataDirectory(const QString &dir)
|
||||
{
|
||||
pathEdit->setText(dir);
|
||||
if (dir == defaultDataDir) {
|
||||
defaultRadio->setChecked(true);
|
||||
} else {
|
||||
customRadio->setChecked(true);
|
||||
}
|
||||
}
|
||||
|
||||
void IntroDialog::on_browseButton_clicked()
|
||||
{
|
||||
QString dir = QFileDialog::getExistingDirectory(this, tr("Choose data directory"), pathEdit->text());
|
||||
if (!dir.isEmpty())
|
||||
pathEdit->setText(dir);
|
||||
}
|
||||
|
||||
void IntroDialog::on_defaultRadio_toggled(bool checked)
|
||||
{
|
||||
pathEdit->setEnabled(!checked);
|
||||
browseButton->setEnabled(!checked);
|
||||
if (checked)
|
||||
pathEdit->setText(defaultDataDir);
|
||||
updateFreeSpace();
|
||||
}
|
||||
|
||||
void IntroDialog::updateFreeSpace()
|
||||
{
|
||||
QString path = getDataDirectory();
|
||||
boost::filesystem::path fsPath(path.toStdString());
|
||||
|
||||
// Walk up to find an existing parent
|
||||
try {
|
||||
while (!fsPath.empty() && !boost::filesystem::exists(fsPath))
|
||||
fsPath = fsPath.parent_path();
|
||||
|
||||
if (!fsPath.empty()) {
|
||||
boost::filesystem::space_info si = boost::filesystem::space(fsPath);
|
||||
double freeGB = (double)si.available / (1024.0 * 1024.0 * 1024.0);
|
||||
freeSpaceLabel->setText(tr("Free space: %1 GB").arg(QString::number(freeGB, 'f', 2)));
|
||||
} else {
|
||||
freeSpaceLabel->setText(tr("Cannot determine free space"));
|
||||
}
|
||||
} catch (const boost::filesystem::filesystem_error &) {
|
||||
freeSpaceLabel->setText(tr("Cannot determine free space"));
|
||||
}
|
||||
}
|
||||
|
||||
bool IntroDialog::pickDataDirectory()
|
||||
{
|
||||
namespace fs = boost::filesystem;
|
||||
|
||||
QSettings settings;
|
||||
// If -datadir was passed on the command line, skip the dialog entirely
|
||||
if (mapArgs.count("-datadir"))
|
||||
return true;
|
||||
|
||||
QString dataDir = settings.value("strDataDir", "").toString();
|
||||
|
||||
if (dataDir.isEmpty()) {
|
||||
// First run - show the dialog
|
||||
IntroDialog dlg;
|
||||
if (dlg.exec() != QDialog::Accepted)
|
||||
return false;
|
||||
|
||||
dataDir = dlg.getDataDirectory();
|
||||
settings.setValue("strDataDir", dataDir);
|
||||
}
|
||||
|
||||
// If the saved path is the default, don't set -datadir (let normal defaults work)
|
||||
QString defaultDir = QString::fromStdString(GetDefaultDataDir().string());
|
||||
if (dataDir != defaultDir) {
|
||||
mapArgs["-datadir"] = dataDir.toStdString();
|
||||
}
|
||||
|
||||
// Ensure the directory exists
|
||||
try {
|
||||
fs::create_directories(fs::path(dataDir.toStdString()));
|
||||
} catch (const fs::filesystem_error &) {
|
||||
QMessageBox::critical(0, "Triangles",
|
||||
QString("Error: Could not create data directory \"%1\".").arg(dataDir));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
#ifndef INTRODIALOG_H
|
||||
#define INTRODIALOG_H
|
||||
|
||||
#include <QDialog>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QRadioButton>
|
||||
#include <QPushButton>
|
||||
|
||||
/** Data directory selection dialog shown on first run. */
|
||||
class IntroDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit IntroDialog(QWidget *parent = 0);
|
||||
|
||||
QString getDataDirectory() const;
|
||||
void setDataDirectory(const QString &dir);
|
||||
|
||||
/**
|
||||
* Check settings or show the dialog to choose data directory.
|
||||
* Returns true if a directory was selected, false if the user cancelled.
|
||||
* Sets mapArgs["-datadir"] if a non-default directory was chosen.
|
||||
*/
|
||||
static bool pickDataDirectory();
|
||||
|
||||
private slots:
|
||||
void on_browseButton_clicked();
|
||||
void on_defaultRadio_toggled(bool checked);
|
||||
void updateFreeSpace();
|
||||
|
||||
private:
|
||||
QRadioButton *defaultRadio;
|
||||
QRadioButton *customRadio;
|
||||
QLineEdit *pathEdit;
|
||||
QPushButton *browseButton;
|
||||
QLabel *freeSpaceLabel;
|
||||
QString defaultDataDir;
|
||||
};
|
||||
|
||||
#endif // INTRODIALOG_H
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "guiutil.h"
|
||||
#include "guiconstants.h"
|
||||
|
||||
#include "introdialog.h"
|
||||
#include "init.h"
|
||||
#include "ui_interface.h"
|
||||
#include "qtipcserver.h"
|
||||
@@ -131,6 +132,10 @@ int main(int argc, char *argv[])
|
||||
// Command-line options take precedence:
|
||||
ParseParameters(argc, argv);
|
||||
|
||||
// Show data directory selection dialog on first run (unless -datadir was passed)
|
||||
if (!IntroDialog::pickDataDirectory())
|
||||
return 0;
|
||||
|
||||
// ... then triangles.conf:
|
||||
if (!boost::filesystem::is_directory(GetDataDir(false)))
|
||||
{
|
||||
|
||||
+925
@@ -0,0 +1,925 @@
|
||||
// Copyright (c) 2024 The Triangles developers
|
||||
// Distributed under the MIT/X11 software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#include "rest.h"
|
||||
#include "trianglesrpc.h"
|
||||
#include "main.h"
|
||||
#include "sync.h"
|
||||
#include "util.h"
|
||||
#include "base58.h"
|
||||
#include "addressindex.h"
|
||||
#include "wallet.h"
|
||||
#include "init.h"
|
||||
|
||||
#include <boost/algorithm/string.hpp>
|
||||
#include <boost/lexical_cast.hpp>
|
||||
|
||||
using namespace std;
|
||||
using namespace json_spirit;
|
||||
|
||||
// Forward declarations from rpcblockchain.cpp
|
||||
extern Object blockToJSON(const CBlock& block, const CBlockIndex* blockindex, bool fPrintTransactionDetail);
|
||||
extern void TxToJSON(const CTransaction& tx, const uint256 hashBlock, json_spirit::Object& entry);
|
||||
|
||||
// Forward declaration from trianglesrpc.cpp
|
||||
extern bool HTTPAuthorized(map<string, string>& mapHeaders);
|
||||
extern string rfc1123Time();
|
||||
|
||||
// ============================================================================
|
||||
// Rate limiter
|
||||
// ============================================================================
|
||||
|
||||
struct IPRateLimiter
|
||||
{
|
||||
int64_t nTokens;
|
||||
int64_t nLastRefill;
|
||||
|
||||
IPRateLimiter() : nTokens(0), nLastRefill(0) {}
|
||||
|
||||
bool Allow(int64_t nMaxTokens, int64_t nRefillRate)
|
||||
{
|
||||
int64_t nNow = GetTime();
|
||||
if (nLastRefill == 0) {
|
||||
nLastRefill = nNow;
|
||||
nTokens = nMaxTokens;
|
||||
}
|
||||
// Refill tokens
|
||||
int64_t nElapsed = nNow - nLastRefill;
|
||||
if (nElapsed > 0) {
|
||||
nTokens += nElapsed * nRefillRate;
|
||||
if (nTokens > nMaxTokens)
|
||||
nTokens = nMaxTokens;
|
||||
nLastRefill = nNow;
|
||||
}
|
||||
if (nTokens > 0) {
|
||||
nTokens--;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
static CCriticalSection cs_rateLimiter;
|
||||
static map<string, IPRateLimiter> mapRateLimiters;
|
||||
static int64_t nLastCleanup = 0;
|
||||
|
||||
bool CheckRESTRateLimit(const string& strIP)
|
||||
{
|
||||
int64_t nLimit = GetArg("-restratelimit", 30);
|
||||
int64_t nBurst = nLimit * 2; // burst = 2x sustained rate
|
||||
|
||||
if (nLimit <= 0)
|
||||
return true; // rate limiting disabled
|
||||
|
||||
LOCK(cs_rateLimiter);
|
||||
|
||||
// Periodic cleanup of stale entries (every 60s)
|
||||
int64_t nNow = GetTime();
|
||||
if (nNow - nLastCleanup > 60) {
|
||||
map<string, IPRateLimiter>::iterator it = mapRateLimiters.begin();
|
||||
while (it != mapRateLimiters.end()) {
|
||||
if (nNow - it->second.nLastRefill > 300) // 5 min stale
|
||||
mapRateLimiters.erase(it++);
|
||||
else
|
||||
++it;
|
||||
}
|
||||
nLastCleanup = nNow;
|
||||
}
|
||||
|
||||
return mapRateLimiters[strIP].Allow(nBurst, nLimit);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// HTTP response helpers
|
||||
// ============================================================================
|
||||
|
||||
string HTTPReplyREST(int nStatus, const string& strMsg, const string& contentType)
|
||||
{
|
||||
string strCorsOrigin = GetArg("-restcorsorigin", "*");
|
||||
|
||||
const char *cStatus;
|
||||
if (nStatus == 200) cStatus = "OK";
|
||||
else if (nStatus == 204) cStatus = "No Content";
|
||||
else if (nStatus == 400) cStatus = "Bad Request";
|
||||
else if (nStatus == 401) cStatus = "Unauthorized";
|
||||
else if (nStatus == 403) cStatus = "Forbidden";
|
||||
else if (nStatus == 404) cStatus = "Not Found";
|
||||
else if (nStatus == 429) cStatus = "Too Many Requests";
|
||||
else if (nStatus == 500) cStatus = "Internal Server Error";
|
||||
else if (nStatus == 503) cStatus = "Service Unavailable";
|
||||
else cStatus = "";
|
||||
|
||||
return strprintf(
|
||||
"HTTP/1.1 %d %s\r\n"
|
||||
"Date: %s\r\n"
|
||||
"Connection: close\r\n"
|
||||
"Content-Length: %" PRIszu "\r\n"
|
||||
"Content-Type: %s\r\n"
|
||||
"Access-Control-Allow-Origin: %s\r\n"
|
||||
"Access-Control-Allow-Methods: GET, POST, OPTIONS\r\n"
|
||||
"Access-Control-Allow-Headers: Content-Type, Authorization\r\n"
|
||||
"Access-Control-Max-Age: 86400\r\n"
|
||||
"Server: Triangles/%s\r\n"
|
||||
"\r\n"
|
||||
"%s",
|
||||
nStatus,
|
||||
cStatus,
|
||||
rfc1123Time().c_str(),
|
||||
strMsg.size(),
|
||||
contentType.c_str(),
|
||||
strCorsOrigin.c_str(),
|
||||
FormatFullVersion().c_str(),
|
||||
strMsg.c_str());
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// URL parsing
|
||||
// ============================================================================
|
||||
|
||||
static void ParseRESTPath(const string& strURI, vector<string>& parts, map<string, string>& queryParams)
|
||||
{
|
||||
string path = strURI;
|
||||
|
||||
// Split query string
|
||||
size_t qpos = path.find('?');
|
||||
string queryString;
|
||||
if (qpos != string::npos) {
|
||||
queryString = path.substr(qpos + 1);
|
||||
path = path.substr(0, qpos);
|
||||
}
|
||||
|
||||
// Split path into parts
|
||||
boost::split(parts, path, boost::is_any_of("/"));
|
||||
|
||||
// Parse query parameters
|
||||
if (!queryString.empty()) {
|
||||
vector<string> pairs;
|
||||
boost::split(pairs, queryString, boost::is_any_of("&"));
|
||||
for (size_t i = 0; i < pairs.size(); i++) {
|
||||
size_t eq = pairs[i].find('=');
|
||||
if (eq != string::npos)
|
||||
queryParams[pairs[i].substr(0, eq)] = pairs[i].substr(eq + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool IsRESTPath(const string& strURI)
|
||||
{
|
||||
return strURI.size() >= 6 && strURI.substr(0, 6) == "/rest/";
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Auth helpers
|
||||
// ============================================================================
|
||||
|
||||
static bool RESTAuthorized(map<string, string>& mapHeaders)
|
||||
{
|
||||
// Check Bearer token first (if -restapikey is set)
|
||||
string strApiKey = GetArg("-restapikey", "");
|
||||
if (!strApiKey.empty()) {
|
||||
string strAuth = mapHeaders.count("authorization") ? mapHeaders["authorization"] : "";
|
||||
if (strAuth.substr(0, 7) == "Bearer ") {
|
||||
string strToken = strAuth.substr(7);
|
||||
boost::trim(strToken);
|
||||
if (TimingResistantEqual(strToken, strApiKey))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to Basic Auth (same as RPC)
|
||||
if (mapHeaders.count("authorization"))
|
||||
return HTTPAuthorized(mapHeaders);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// JSON error helper
|
||||
// ============================================================================
|
||||
|
||||
static string RESTError(const string& message, int code = -1)
|
||||
{
|
||||
Object obj;
|
||||
obj.push_back(Pair("error", message));
|
||||
if (code != -1)
|
||||
obj.push_back(Pair("code", code));
|
||||
return write_string(Value(obj), false) + "\n";
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Helper: call an RPC method and return JSON string
|
||||
// ============================================================================
|
||||
|
||||
static bool CallRPCMethod(const string& method, const Array& params,
|
||||
string& strReply, int& nStatus)
|
||||
{
|
||||
try {
|
||||
Value result = tableRPC.execute(method, params);
|
||||
strReply = write_string(result, false) + "\n";
|
||||
nStatus = HTTP_OK;
|
||||
return true;
|
||||
}
|
||||
catch (Object& objError) {
|
||||
int code = find_value(objError, "code").get_int();
|
||||
string msg = find_value(objError, "message").get_str();
|
||||
if (code == RPC_METHOD_NOT_FOUND) nStatus = HTTP_NOT_FOUND;
|
||||
else if (code == RPC_WALLET_UNLOCK_NEEDED) nStatus = HTTP_FORBIDDEN;
|
||||
else if (code == RPC_INVALID_PARAMETER || code == RPC_INVALID_ADDRESS_OR_KEY) nStatus = HTTP_BAD_REQUEST;
|
||||
else nStatus = HTTP_INTERNAL_SERVER_ERROR;
|
||||
strReply = RESTError(msg, code);
|
||||
return true;
|
||||
}
|
||||
catch (std::exception& e) {
|
||||
nStatus = HTTP_INTERNAL_SERVER_ERROR;
|
||||
strReply = RESTError(e.what());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Public endpoint handlers
|
||||
// ============================================================================
|
||||
|
||||
// GET /rest/chaininfo
|
||||
static bool HandleChainInfo(string& strReply, int& nStatus)
|
||||
{
|
||||
LOCK(cs_main);
|
||||
Object obj, diff;
|
||||
obj.push_back(Pair("chain", fTestNet ? string("test") : string("main")));
|
||||
obj.push_back(Pair("blocks", (int)nBestHeight));
|
||||
obj.push_back(Pair("bestblockhash", hashBestChain.GetHex()));
|
||||
diff.push_back(Pair("proof-of-work", GetDifficulty()));
|
||||
diff.push_back(Pair("proof-of-stake", GetDifficulty(GetLastBlockIndex(pindexBest, true))));
|
||||
obj.push_back(Pair("difficulty", diff));
|
||||
obj.push_back(Pair("moneysupply", ValueFromAmount(pindexBest->nMoneySupply)));
|
||||
strReply = write_string(Value(obj), false) + "\n";
|
||||
nStatus = HTTP_OK;
|
||||
return true;
|
||||
}
|
||||
|
||||
// GET /rest/block/{hash_or_param}[.hex]
|
||||
static bool HandleBlock(const string& param, const string& format, string& strReply, int& nStatus)
|
||||
{
|
||||
LOCK(cs_main);
|
||||
uint256 hash(param);
|
||||
if (mapBlockIndex.count(hash) == 0) {
|
||||
nStatus = HTTP_NOT_FOUND;
|
||||
strReply = RESTError("Block not found");
|
||||
return true;
|
||||
}
|
||||
CBlock block;
|
||||
CBlockIndex* pblockindex = mapBlockIndex[hash];
|
||||
block.ReadFromDisk(pblockindex, true);
|
||||
|
||||
if (format == "hex") {
|
||||
CDataStream ssBlock(SER_NETWORK, PROTOCOL_VERSION);
|
||||
ssBlock << block;
|
||||
strReply = HexStr(ssBlock.begin(), ssBlock.end()) + "\n";
|
||||
} else {
|
||||
Object obj = blockToJSON(block, pblockindex, false);
|
||||
strReply = write_string(Value(obj), false) + "\n";
|
||||
}
|
||||
nStatus = HTTP_OK;
|
||||
return true;
|
||||
}
|
||||
|
||||
// GET /rest/blockheader/{hash}
|
||||
static bool HandleBlockHeader(const string& param, string& strReply, int& nStatus)
|
||||
{
|
||||
LOCK(cs_main);
|
||||
uint256 hash(param);
|
||||
if (mapBlockIndex.count(hash) == 0) {
|
||||
nStatus = HTTP_NOT_FOUND;
|
||||
strReply = RESTError("Block not found");
|
||||
return true;
|
||||
}
|
||||
CBlockIndex* pblockindex = mapBlockIndex[hash];
|
||||
Object result;
|
||||
result.push_back(Pair("hash", pblockindex->GetBlockHash().GetHex()));
|
||||
result.push_back(Pair("confirmations", pindexBest->nHeight - pblockindex->nHeight + 1));
|
||||
result.push_back(Pair("height", pblockindex->nHeight));
|
||||
result.push_back(Pair("version", pblockindex->nVersion));
|
||||
result.push_back(Pair("merkleroot", pblockindex->hashMerkleRoot.GetHex()));
|
||||
result.push_back(Pair("time", (boost::int64_t)pblockindex->GetBlockTime()));
|
||||
result.push_back(Pair("nonce", (boost::uint64_t)pblockindex->nNonce));
|
||||
result.push_back(Pair("bits", HexBits(pblockindex->nBits)));
|
||||
result.push_back(Pair("difficulty", GetDifficulty(pblockindex)));
|
||||
result.push_back(Pair("flags", strprintf("%s%s",
|
||||
pblockindex->IsProofOfStake() ? "proof-of-stake" : "proof-of-work",
|
||||
pblockindex->GeneratedStakeModifier() ? " stake-modifier" : "")));
|
||||
if (pblockindex->pprev)
|
||||
result.push_back(Pair("previousblockhash", pblockindex->pprev->GetBlockHash().GetHex()));
|
||||
if (pblockindex->pnext)
|
||||
result.push_back(Pair("nextblockhash", pblockindex->pnext->GetBlockHash().GetHex()));
|
||||
strReply = write_string(Value(result), false) + "\n";
|
||||
nStatus = HTTP_OK;
|
||||
return true;
|
||||
}
|
||||
|
||||
// GET /rest/tx/{txid}[.hex]
|
||||
static bool HandleTx(const string& param, const string& format, string& strReply, int& nStatus)
|
||||
{
|
||||
LOCK(cs_main);
|
||||
uint256 hash(param);
|
||||
CTransaction tx;
|
||||
uint256 hashBlock = 0;
|
||||
if (!GetTransaction(hash, tx, hashBlock)) {
|
||||
nStatus = HTTP_NOT_FOUND;
|
||||
strReply = RESTError("Transaction not found");
|
||||
return true;
|
||||
}
|
||||
if (format == "hex") {
|
||||
CDataStream ssTx(SER_NETWORK, PROTOCOL_VERSION);
|
||||
ssTx << tx;
|
||||
strReply = HexStr(ssTx.begin(), ssTx.end()) + "\n";
|
||||
} else {
|
||||
Object obj;
|
||||
obj.push_back(Pair("txid", tx.GetHash().GetHex()));
|
||||
TxToJSON(tx, hashBlock, obj);
|
||||
strReply = write_string(Value(obj), false) + "\n";
|
||||
}
|
||||
nStatus = HTTP_OK;
|
||||
return true;
|
||||
}
|
||||
|
||||
// GET /rest/blockhashbyheight/{n}
|
||||
static bool HandleBlockHashByHeight(const string& param, string& strReply, int& nStatus)
|
||||
{
|
||||
LOCK(cs_main);
|
||||
int nHeight = atoi(param.c_str());
|
||||
if (nHeight < 0 || nHeight > nBestHeight) {
|
||||
nStatus = HTTP_NOT_FOUND;
|
||||
strReply = RESTError("Block height out of range");
|
||||
return true;
|
||||
}
|
||||
CBlockIndex* pblockindex = FindBlockByHeight(nHeight);
|
||||
Object obj;
|
||||
obj.push_back(Pair("blockhash", pblockindex->phashBlock->GetHex()));
|
||||
strReply = write_string(Value(obj), false) + "\n";
|
||||
nStatus = HTTP_OK;
|
||||
return true;
|
||||
}
|
||||
|
||||
// GET /rest/blockbyheight/{n}
|
||||
static bool HandleBlockByHeight(const string& param, const string& format, string& strReply, int& nStatus)
|
||||
{
|
||||
LOCK(cs_main);
|
||||
int nHeight = atoi(param.c_str());
|
||||
if (nHeight < 0 || nHeight > nBestHeight) {
|
||||
nStatus = HTTP_NOT_FOUND;
|
||||
strReply = RESTError("Block height out of range");
|
||||
return true;
|
||||
}
|
||||
CBlockIndex* pblockindex = FindBlockByHeight(nHeight);
|
||||
CBlock block;
|
||||
block.ReadFromDisk(pblockindex, true);
|
||||
|
||||
if (format == "hex") {
|
||||
CDataStream ssBlock(SER_NETWORK, PROTOCOL_VERSION);
|
||||
ssBlock << block;
|
||||
strReply = HexStr(ssBlock.begin(), ssBlock.end()) + "\n";
|
||||
} else {
|
||||
Object obj = blockToJSON(block, pblockindex, false);
|
||||
strReply = write_string(Value(obj), false) + "\n";
|
||||
}
|
||||
nStatus = HTTP_OK;
|
||||
return true;
|
||||
}
|
||||
|
||||
// GET /rest/mempool
|
||||
static bool HandleMempool(string& strReply, int& nStatus)
|
||||
{
|
||||
vector<uint256> vtxid;
|
||||
mempool.queryHashes(vtxid);
|
||||
Array a;
|
||||
BOOST_FOREACH(const uint256& hash, vtxid)
|
||||
a.push_back(hash.ToString());
|
||||
strReply = write_string(Value(a), false) + "\n";
|
||||
nStatus = HTTP_OK;
|
||||
return true;
|
||||
}
|
||||
|
||||
// GET /rest/difficulty
|
||||
static bool HandleDifficulty(string& strReply, int& nStatus)
|
||||
{
|
||||
Array params;
|
||||
return CallRPCMethod("getdifficulty", params, strReply, nStatus);
|
||||
}
|
||||
|
||||
// GET /rest/supply
|
||||
static bool HandleSupply(string& strReply, int& nStatus)
|
||||
{
|
||||
Array params;
|
||||
return CallRPCMethod("gettxoutsetinfo", params, strReply, nStatus);
|
||||
}
|
||||
|
||||
// GET /rest/staking
|
||||
static bool HandleStaking(string& strReply, int& nStatus)
|
||||
{
|
||||
Array params;
|
||||
return CallRPCMethod("getstakinginfo", params, strReply, nStatus);
|
||||
}
|
||||
|
||||
// GET /rest/mining
|
||||
static bool HandleMining(string& strReply, int& nStatus)
|
||||
{
|
||||
Array params;
|
||||
return CallRPCMethod("getmininginfo", params, strReply, nStatus);
|
||||
}
|
||||
|
||||
// GET /rest/subsidy
|
||||
static bool HandleSubsidy(string& strReply, int& nStatus)
|
||||
{
|
||||
Array params;
|
||||
return CallRPCMethod("getsubsidy", params, strReply, nStatus);
|
||||
}
|
||||
|
||||
// GET /rest/estimatefee
|
||||
static bool HandleEstimateFee(string& strReply, int& nStatus)
|
||||
{
|
||||
Array params;
|
||||
params.push_back(6); // default 6 blocks
|
||||
return CallRPCMethod("estimatefee", params, strReply, nStatus);
|
||||
}
|
||||
|
||||
// GET /rest/checkpoint
|
||||
static bool HandleCheckpoint(string& strReply, int& nStatus)
|
||||
{
|
||||
Array params;
|
||||
return CallRPCMethod("getcheckpoint", params, strReply, nStatus);
|
||||
}
|
||||
|
||||
// GET /rest/network
|
||||
static bool HandleNetwork(string& strReply, int& nStatus)
|
||||
{
|
||||
Array params;
|
||||
return CallRPCMethod("getnetworkinfo", params, strReply, nStatus);
|
||||
}
|
||||
|
||||
// GET /rest/peers
|
||||
static bool HandlePeers(string& strReply, int& nStatus)
|
||||
{
|
||||
Array params;
|
||||
return CallRPCMethod("getpeerinfo", params, strReply, nStatus);
|
||||
}
|
||||
|
||||
// GET /rest/validate/{address}
|
||||
static bool HandleValidate(const string& addr, string& strReply, int& nStatus)
|
||||
{
|
||||
Array params;
|
||||
params.push_back(addr);
|
||||
return CallRPCMethod("validateaddress", params, strReply, nStatus);
|
||||
}
|
||||
|
||||
// GET /rest/address/{addr}/balance
|
||||
static bool HandleAddressBalance(const string& addr, string& strReply, int& nStatus)
|
||||
{
|
||||
if (!fAddressIndex) {
|
||||
nStatus = 503;
|
||||
strReply = RESTError("Address index not enabled. Start daemon with -addressindex=1");
|
||||
return true;
|
||||
}
|
||||
Object addrObj;
|
||||
Array addrArray;
|
||||
addrArray.push_back(addr);
|
||||
addrObj.push_back(Pair("addresses", addrArray));
|
||||
Array params;
|
||||
params.push_back(addrObj);
|
||||
return CallRPCMethod("getaddressbalance", params, strReply, nStatus);
|
||||
}
|
||||
|
||||
// GET /rest/address/{addr}/utxos
|
||||
static bool HandleAddressUtxos(const string& addr, string& strReply, int& nStatus)
|
||||
{
|
||||
if (!fAddressIndex) {
|
||||
nStatus = 503;
|
||||
strReply = RESTError("Address index not enabled. Start daemon with -addressindex=1");
|
||||
return true;
|
||||
}
|
||||
Object addrObj;
|
||||
Array addrArray;
|
||||
addrArray.push_back(addr);
|
||||
addrObj.push_back(Pair("addresses", addrArray));
|
||||
Array params;
|
||||
params.push_back(addrObj);
|
||||
return CallRPCMethod("getaddressutxos", params, strReply, nStatus);
|
||||
}
|
||||
|
||||
// GET /rest/address/{addr}/txids[?start=N&end=N]
|
||||
static bool HandleAddressTxids(const string& addr, const map<string, string>& queryParams,
|
||||
string& strReply, int& nStatus)
|
||||
{
|
||||
if (!fAddressIndex) {
|
||||
nStatus = 503;
|
||||
strReply = RESTError("Address index not enabled. Start daemon with -addressindex=1");
|
||||
return true;
|
||||
}
|
||||
Object addrObj;
|
||||
Array addrArray;
|
||||
addrArray.push_back(addr);
|
||||
addrObj.push_back(Pair("addresses", addrArray));
|
||||
|
||||
map<string, string>::const_iterator itStart = queryParams.find("start");
|
||||
map<string, string>::const_iterator itEnd = queryParams.find("end");
|
||||
if (itStart != queryParams.end())
|
||||
addrObj.push_back(Pair("start", atoi(itStart->second.c_str())));
|
||||
if (itEnd != queryParams.end())
|
||||
addrObj.push_back(Pair("end", atoi(itEnd->second.c_str())));
|
||||
|
||||
Array params;
|
||||
params.push_back(addrObj);
|
||||
return CallRPCMethod("getaddresstxids", params, strReply, nStatus);
|
||||
}
|
||||
|
||||
// POST /rest/tx/decode body: {"hex":"..."}
|
||||
static bool HandleTxDecode(const string& strBody, string& strReply, int& nStatus)
|
||||
{
|
||||
Value valBody;
|
||||
if (!read_string(strBody, valBody) || valBody.type() != obj_type) {
|
||||
nStatus = HTTP_BAD_REQUEST;
|
||||
strReply = RESTError("Invalid JSON body. Expected: {\"hex\":\"...\"}");
|
||||
return true;
|
||||
}
|
||||
Object bodyObj = valBody.get_obj();
|
||||
Value hexVal = find_value(bodyObj, "hex");
|
||||
if (hexVal.type() != str_type) {
|
||||
nStatus = HTTP_BAD_REQUEST;
|
||||
strReply = RESTError("Missing 'hex' field in request body");
|
||||
return true;
|
||||
}
|
||||
Array params;
|
||||
params.push_back(hexVal.get_str());
|
||||
return CallRPCMethod("decoderawtransaction", params, strReply, nStatus);
|
||||
}
|
||||
|
||||
// POST /rest/tx/send body: {"hex":"..."}
|
||||
static bool HandleTxSend(const string& strBody, string& strReply, int& nStatus)
|
||||
{
|
||||
Value valBody;
|
||||
if (!read_string(strBody, valBody) || valBody.type() != obj_type) {
|
||||
nStatus = HTTP_BAD_REQUEST;
|
||||
strReply = RESTError("Invalid JSON body. Expected: {\"hex\":\"...\"}");
|
||||
return true;
|
||||
}
|
||||
Object bodyObj = valBody.get_obj();
|
||||
Value hexVal = find_value(bodyObj, "hex");
|
||||
if (hexVal.type() != str_type) {
|
||||
nStatus = HTTP_BAD_REQUEST;
|
||||
strReply = RESTError("Missing 'hex' field in request body");
|
||||
return true;
|
||||
}
|
||||
Array params;
|
||||
params.push_back(hexVal.get_str());
|
||||
return CallRPCMethod("sendrawtransaction", params, strReply, nStatus);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Wallet endpoint handlers (authenticated)
|
||||
// ============================================================================
|
||||
|
||||
// GET /rest/wallet/info
|
||||
static bool HandleWalletInfo(string& strReply, int& nStatus)
|
||||
{
|
||||
Array params;
|
||||
return CallRPCMethod("getwalletinfo", params, strReply, nStatus);
|
||||
}
|
||||
|
||||
// GET /rest/wallet/balance
|
||||
static bool HandleWalletBalance(string& strReply, int& nStatus)
|
||||
{
|
||||
Array params;
|
||||
return CallRPCMethod("getbalance", params, strReply, nStatus);
|
||||
}
|
||||
|
||||
// GET /rest/wallet/transactions[?count=N&skip=N]
|
||||
static bool HandleWalletTransactions(const map<string, string>& queryParams,
|
||||
string& strReply, int& nStatus)
|
||||
{
|
||||
Array params;
|
||||
params.push_back("*"); // all accounts
|
||||
|
||||
map<string, string>::const_iterator itCount = queryParams.find("count");
|
||||
map<string, string>::const_iterator itSkip = queryParams.find("skip");
|
||||
|
||||
int nCount = 10;
|
||||
int nSkip = 0;
|
||||
if (itCount != queryParams.end())
|
||||
nCount = atoi(itCount->second.c_str());
|
||||
if (itSkip != queryParams.end())
|
||||
nSkip = atoi(itSkip->second.c_str());
|
||||
|
||||
params.push_back(nCount);
|
||||
params.push_back(nSkip);
|
||||
return CallRPCMethod("listtransactions", params, strReply, nStatus);
|
||||
}
|
||||
|
||||
// GET /rest/wallet/transaction/{txid}
|
||||
static bool HandleWalletTransaction(const string& txid, string& strReply, int& nStatus)
|
||||
{
|
||||
Array params;
|
||||
params.push_back(txid);
|
||||
return CallRPCMethod("gettransaction", params, strReply, nStatus);
|
||||
}
|
||||
|
||||
// GET /rest/wallet/unspent[?minconf=N&maxconf=N]
|
||||
static bool HandleWalletUnspent(const map<string, string>& queryParams,
|
||||
string& strReply, int& nStatus)
|
||||
{
|
||||
Array params;
|
||||
map<string, string>::const_iterator itMin = queryParams.find("minconf");
|
||||
map<string, string>::const_iterator itMax = queryParams.find("maxconf");
|
||||
|
||||
params.push_back(itMin != queryParams.end() ? atoi(itMin->second.c_str()) : 1);
|
||||
params.push_back(itMax != queryParams.end() ? atoi(itMax->second.c_str()) : 9999999);
|
||||
return CallRPCMethod("listunspent", params, strReply, nStatus);
|
||||
}
|
||||
|
||||
// GET /rest/wallet/addresses
|
||||
static bool HandleWalletAddresses(string& strReply, int& nStatus)
|
||||
{
|
||||
Array params;
|
||||
return CallRPCMethod("listaddressgroupings", params, strReply, nStatus);
|
||||
}
|
||||
|
||||
// GET /rest/wallet/staking
|
||||
static bool HandleWalletStaking(string& strReply, int& nStatus)
|
||||
{
|
||||
Array params;
|
||||
return CallRPCMethod("getstakinginfo", params, strReply, nStatus);
|
||||
}
|
||||
|
||||
// POST /rest/wallet/address/new body: {} or {"account":"..."}
|
||||
static bool HandleWalletNewAddress(const string& strBody, string& strReply, int& nStatus)
|
||||
{
|
||||
Array params;
|
||||
if (!strBody.empty()) {
|
||||
Value valBody;
|
||||
if (read_string(strBody, valBody) && valBody.type() == obj_type) {
|
||||
Value acctVal = find_value(valBody.get_obj(), "account");
|
||||
if (acctVal.type() == str_type)
|
||||
params.push_back(acctVal.get_str());
|
||||
}
|
||||
}
|
||||
return CallRPCMethod("getnewaddress", params, strReply, nStatus);
|
||||
}
|
||||
|
||||
// POST /rest/wallet/send body: {"address":"...", "amount":N}
|
||||
static bool HandleWalletSend(const string& strBody, string& strReply, int& nStatus)
|
||||
{
|
||||
Value valBody;
|
||||
if (!read_string(strBody, valBody) || valBody.type() != obj_type) {
|
||||
nStatus = HTTP_BAD_REQUEST;
|
||||
strReply = RESTError("Invalid JSON body. Expected: {\"address\":\"...\", \"amount\":N}");
|
||||
return true;
|
||||
}
|
||||
Object bodyObj = valBody.get_obj();
|
||||
Value addrVal = find_value(bodyObj, "address");
|
||||
Value amtVal = find_value(bodyObj, "amount");
|
||||
if (addrVal.type() != str_type || (amtVal.type() != real_type && amtVal.type() != int_type)) {
|
||||
nStatus = HTTP_BAD_REQUEST;
|
||||
strReply = RESTError("Missing 'address' (string) or 'amount' (number) in request body");
|
||||
return true;
|
||||
}
|
||||
Array params;
|
||||
params.push_back(addrVal.get_str());
|
||||
params.push_back(amtVal);
|
||||
|
||||
// Optional comment fields
|
||||
Value commentVal = find_value(bodyObj, "comment");
|
||||
Value commentToVal = find_value(bodyObj, "comment_to");
|
||||
if (commentVal.type() == str_type)
|
||||
params.push_back(commentVal.get_str());
|
||||
else
|
||||
params.push_back("");
|
||||
if (commentToVal.type() == str_type)
|
||||
params.push_back(commentToVal.get_str());
|
||||
|
||||
return CallRPCMethod("sendtoaddress", params, strReply, nStatus);
|
||||
}
|
||||
|
||||
// POST /rest/wallet/sendmany body: {"recipients":{"addr":amount,...}}
|
||||
static bool HandleWalletSendMany(const string& strBody, string& strReply, int& nStatus)
|
||||
{
|
||||
Value valBody;
|
||||
if (!read_string(strBody, valBody) || valBody.type() != obj_type) {
|
||||
nStatus = HTTP_BAD_REQUEST;
|
||||
strReply = RESTError("Invalid JSON body. Expected: {\"recipients\":{\"addr\":amount,...}}");
|
||||
return true;
|
||||
}
|
||||
Object bodyObj = valBody.get_obj();
|
||||
Value recipVal = find_value(bodyObj, "recipients");
|
||||
if (recipVal.type() != obj_type) {
|
||||
nStatus = HTTP_BAD_REQUEST;
|
||||
strReply = RESTError("Missing 'recipients' object in request body");
|
||||
return true;
|
||||
}
|
||||
Array params;
|
||||
params.push_back(""); // fromaccount (default)
|
||||
params.push_back(recipVal); // {addr: amount, ...}
|
||||
return CallRPCMethod("sendmany", params, strReply, nStatus);
|
||||
}
|
||||
|
||||
// POST /rest/wallet/unlock body: {"passphrase":"...", "timeout":N, "staking_only":bool}
|
||||
static bool HandleWalletUnlock(const string& strBody, string& strReply, int& nStatus)
|
||||
{
|
||||
Value valBody;
|
||||
if (!read_string(strBody, valBody) || valBody.type() != obj_type) {
|
||||
nStatus = HTTP_BAD_REQUEST;
|
||||
strReply = RESTError("Invalid JSON body. Expected: {\"passphrase\":\"...\", \"timeout\":N}");
|
||||
return true;
|
||||
}
|
||||
Object bodyObj = valBody.get_obj();
|
||||
Value passVal = find_value(bodyObj, "passphrase");
|
||||
Value timeVal = find_value(bodyObj, "timeout");
|
||||
if (passVal.type() != str_type || timeVal.type() != int_type) {
|
||||
nStatus = HTTP_BAD_REQUEST;
|
||||
strReply = RESTError("Missing 'passphrase' (string) or 'timeout' (integer) in request body");
|
||||
return true;
|
||||
}
|
||||
Array params;
|
||||
params.push_back(passVal.get_str());
|
||||
params.push_back(timeVal.get_int());
|
||||
|
||||
Value stakingVal = find_value(bodyObj, "staking_only");
|
||||
if (stakingVal.type() == bool_type)
|
||||
params.push_back(stakingVal.get_bool());
|
||||
|
||||
return CallRPCMethod("walletpassphrase", params, strReply, nStatus);
|
||||
}
|
||||
|
||||
// POST /rest/wallet/lock
|
||||
static bool HandleWalletLock(string& strReply, int& nStatus)
|
||||
{
|
||||
Array params;
|
||||
return CallRPCMethod("walletlock", params, strReply, nStatus);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Main router
|
||||
// ============================================================================
|
||||
|
||||
bool HandleRESTRequest(const string& strMethod,
|
||||
const string& strURI,
|
||||
const string& strBody,
|
||||
map<string, string>& mapHeaders,
|
||||
string& strReply,
|
||||
string& strContentType,
|
||||
int& nStatus)
|
||||
{
|
||||
strContentType = "application/json";
|
||||
|
||||
// OPTIONS: CORS preflight
|
||||
if (strMethod == "OPTIONS") {
|
||||
nStatus = 204;
|
||||
strReply = "";
|
||||
return true;
|
||||
}
|
||||
|
||||
// Parse path
|
||||
vector<string> parts;
|
||||
map<string, string> queryParams;
|
||||
ParseRESTPath(strURI, parts, queryParams);
|
||||
// parts: ["", "rest", "resource", "param", ...]
|
||||
|
||||
if (parts.size() < 3) {
|
||||
nStatus = HTTP_NOT_FOUND;
|
||||
strReply = RESTError("Not found");
|
||||
return true;
|
||||
}
|
||||
|
||||
string resource = parts[2];
|
||||
|
||||
// Detect format suffix (.hex, .json)
|
||||
string format = "json";
|
||||
string lastPart = parts.size() > 3 ? parts[parts.size() - 1] : "";
|
||||
size_t dotPos = lastPart.rfind('.');
|
||||
string param;
|
||||
if (dotPos != string::npos) {
|
||||
param = lastPart.substr(0, dotPos);
|
||||
format = lastPart.substr(dotPos + 1);
|
||||
} else {
|
||||
param = lastPart;
|
||||
}
|
||||
|
||||
if (format == "hex")
|
||||
strContentType = "text/plain";
|
||||
|
||||
// ---- Wallet endpoints (authenticated) ----
|
||||
if (resource == "wallet") {
|
||||
if (!RESTAuthorized(mapHeaders)) {
|
||||
nStatus = HTTP_UNAUTHORIZED;
|
||||
strReply = RESTError("Authentication required for wallet endpoints");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (parts.size() < 4) {
|
||||
nStatus = HTTP_NOT_FOUND;
|
||||
strReply = RESTError("Unknown wallet endpoint");
|
||||
return true;
|
||||
}
|
||||
|
||||
string walletResource = parts[3];
|
||||
|
||||
if (strMethod == "GET") {
|
||||
if (walletResource == "info")
|
||||
return HandleWalletInfo(strReply, nStatus);
|
||||
if (walletResource == "balance")
|
||||
return HandleWalletBalance(strReply, nStatus);
|
||||
if (walletResource == "transactions")
|
||||
return HandleWalletTransactions(queryParams, strReply, nStatus);
|
||||
if (walletResource == "transaction" && parts.size() > 4)
|
||||
return HandleWalletTransaction(parts[4], strReply, nStatus);
|
||||
if (walletResource == "unspent")
|
||||
return HandleWalletUnspent(queryParams, strReply, nStatus);
|
||||
if (walletResource == "addresses")
|
||||
return HandleWalletAddresses(strReply, nStatus);
|
||||
if (walletResource == "staking")
|
||||
return HandleWalletStaking(strReply, nStatus);
|
||||
}
|
||||
else if (strMethod == "POST") {
|
||||
if (walletResource == "address" && parts.size() > 4 && parts[4] == "new")
|
||||
return HandleWalletNewAddress(strBody, strReply, nStatus);
|
||||
if (walletResource == "send")
|
||||
return HandleWalletSend(strBody, strReply, nStatus);
|
||||
if (walletResource == "sendmany")
|
||||
return HandleWalletSendMany(strBody, strReply, nStatus);
|
||||
if (walletResource == "unlock")
|
||||
return HandleWalletUnlock(strBody, strReply, nStatus);
|
||||
if (walletResource == "lock")
|
||||
return HandleWalletLock(strReply, nStatus);
|
||||
}
|
||||
|
||||
nStatus = HTTP_NOT_FOUND;
|
||||
strReply = RESTError("Unknown wallet endpoint");
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---- Public GET endpoints ----
|
||||
if (strMethod == "GET") {
|
||||
if (resource == "chaininfo")
|
||||
return HandleChainInfo(strReply, nStatus);
|
||||
if (resource == "block" && !param.empty())
|
||||
return HandleBlock(param, format, strReply, nStatus);
|
||||
if (resource == "blockheader" && !param.empty())
|
||||
return HandleBlockHeader(param, strReply, nStatus);
|
||||
if (resource == "tx") {
|
||||
// /rest/tx/decode and /rest/tx/send are POST-only
|
||||
if (!param.empty())
|
||||
return HandleTx(param, format, strReply, nStatus);
|
||||
}
|
||||
if (resource == "blockhashbyheight" && !param.empty())
|
||||
return HandleBlockHashByHeight(param, strReply, nStatus);
|
||||
if (resource == "blockbyheight" && !param.empty())
|
||||
return HandleBlockByHeight(param, format, strReply, nStatus);
|
||||
if (resource == "mempool")
|
||||
return HandleMempool(strReply, nStatus);
|
||||
if (resource == "difficulty")
|
||||
return HandleDifficulty(strReply, nStatus);
|
||||
if (resource == "supply")
|
||||
return HandleSupply(strReply, nStatus);
|
||||
if (resource == "staking")
|
||||
return HandleStaking(strReply, nStatus);
|
||||
if (resource == "mining")
|
||||
return HandleMining(strReply, nStatus);
|
||||
if (resource == "subsidy")
|
||||
return HandleSubsidy(strReply, nStatus);
|
||||
if (resource == "estimatefee")
|
||||
return HandleEstimateFee(strReply, nStatus);
|
||||
if (resource == "checkpoint")
|
||||
return HandleCheckpoint(strReply, nStatus);
|
||||
if (resource == "network")
|
||||
return HandleNetwork(strReply, nStatus);
|
||||
if (resource == "peers")
|
||||
return HandlePeers(strReply, nStatus);
|
||||
if (resource == "validate" && !param.empty())
|
||||
return HandleValidate(param, strReply, nStatus);
|
||||
|
||||
// Address endpoints: /rest/address/{addr}/balance etc.
|
||||
if (resource == "address" && parts.size() >= 5) {
|
||||
string addr = parts[3];
|
||||
string addrAction = parts[4];
|
||||
if (addrAction == "balance")
|
||||
return HandleAddressBalance(addr, strReply, nStatus);
|
||||
if (addrAction == "utxos")
|
||||
return HandleAddressUtxos(addr, strReply, nStatus);
|
||||
if (addrAction == "txids")
|
||||
return HandleAddressTxids(addr, queryParams, strReply, nStatus);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Public POST endpoints ----
|
||||
if (strMethod == "POST") {
|
||||
if (resource == "tx" && parts.size() >= 4) {
|
||||
string txAction = parts[3];
|
||||
if (txAction == "decode")
|
||||
return HandleTxDecode(strBody, strReply, nStatus);
|
||||
if (txAction == "send")
|
||||
return HandleTxSend(strBody, strReply, nStatus);
|
||||
}
|
||||
}
|
||||
|
||||
nStatus = HTTP_NOT_FOUND;
|
||||
strReply = RESTError("Unknown REST endpoint");
|
||||
return true;
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
// Copyright (c) 2024 The Triangles developers
|
||||
// Distributed under the MIT/X11 software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#ifndef TRIANGLES_REST_H
|
||||
#define TRIANGLES_REST_H
|
||||
|
||||
#include <string>
|
||||
#include <map>
|
||||
|
||||
class CBlock;
|
||||
class CBlockIndex;
|
||||
class CTransaction;
|
||||
|
||||
// REST API response builder with CORS headers
|
||||
std::string HTTPReplyREST(int nStatus, const std::string& strMsg,
|
||||
const std::string& contentType = "application/json");
|
||||
|
||||
// Main REST request router
|
||||
// Returns true if the URI was handled as a REST request, false if it should fall through to RPC
|
||||
bool HandleRESTRequest(const std::string& strMethod,
|
||||
const std::string& strURI,
|
||||
const std::string& strBody,
|
||||
std::map<std::string, std::string>& mapHeaders,
|
||||
std::string& strReply,
|
||||
std::string& strContentType,
|
||||
int& nStatus);
|
||||
|
||||
// Check if a URI is a REST API path
|
||||
bool IsRESTPath(const std::string& strURI);
|
||||
|
||||
// Check rate limit for an IP address. Returns true if allowed.
|
||||
bool CheckRESTRateLimit(const std::string& strIP);
|
||||
|
||||
#endif // TRIANGLES_REST_H
|
||||
@@ -0,0 +1,413 @@
|
||||
// Copyright (c) 2024-2025 Triangles developers
|
||||
// Tor Process Manager - launches and manages an external Tor binary
|
||||
// Distributed under the MIT/X11 software license
|
||||
|
||||
#ifdef WIN32
|
||||
#define NOMINMAX
|
||||
#ifndef WIN32_LEAN_AND_MEAN
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#endif
|
||||
#ifndef _WIN32_WINNT
|
||||
#define _WIN32_WINNT 0x0600
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#include "tor_process.h"
|
||||
#include "../util.h"
|
||||
#include "../net.h"
|
||||
|
||||
#include <boost/filesystem.hpp>
|
||||
#include <fstream>
|
||||
#include <cstdio>
|
||||
|
||||
#ifdef WIN32
|
||||
#include <winsock2.h>
|
||||
#include <ws2tcpip.h>
|
||||
#include <tlhelp32.h>
|
||||
#else
|
||||
#include <sys/types.h>
|
||||
#include <sys/wait.h>
|
||||
#include <sys/socket.h>
|
||||
#include <netinet/in.h>
|
||||
#include <signal.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
namespace fs = boost::filesystem;
|
||||
|
||||
static CTorProcess* torProcessInstance = nullptr;
|
||||
|
||||
CTorProcess* CTorProcess::GetInstance()
|
||||
{
|
||||
if (!torProcessInstance) {
|
||||
torProcessInstance = new CTorProcess();
|
||||
}
|
||||
return torProcessInstance;
|
||||
}
|
||||
|
||||
CTorProcess::CTorProcess()
|
||||
: socksPort(19099)
|
||||
, hiddenServicePort(24112)
|
||||
, running(false)
|
||||
#ifdef WIN32
|
||||
, hProcess(NULL)
|
||||
, processId(0)
|
||||
#else
|
||||
, processId(0)
|
||||
#endif
|
||||
{
|
||||
}
|
||||
|
||||
CTorProcess::~CTorProcess()
|
||||
{
|
||||
Stop();
|
||||
}
|
||||
|
||||
std::string CTorProcess::FindTorBinary()
|
||||
{
|
||||
// List of candidate paths to search for the Tor binary
|
||||
std::vector<std::string> candidates;
|
||||
|
||||
#ifdef WIN32
|
||||
// Same directory as the wallet executable
|
||||
char exePath[MAX_PATH];
|
||||
if (GetModuleFileNameA(NULL, exePath, MAX_PATH)) {
|
||||
fs::path exeDir = fs::path(exePath).parent_path();
|
||||
candidates.push_back((exeDir / "tor.exe").string());
|
||||
candidates.push_back((exeDir / "tor" / "tor.exe").string());
|
||||
candidates.push_back((exeDir / "Tor" / "tor.exe").string());
|
||||
}
|
||||
|
||||
// Data directory
|
||||
candidates.push_back((GetDataDir() / "tor.exe").string());
|
||||
candidates.push_back((GetDataDir() / "tor" / "tor.exe").string());
|
||||
|
||||
// Common Windows install locations
|
||||
const char* programFiles = getenv("ProgramFiles");
|
||||
if (programFiles) {
|
||||
candidates.push_back(std::string(programFiles) + "\\Tor\\tor.exe");
|
||||
candidates.push_back(std::string(programFiles) + "\\Tor Browser\\Browser\\TorBrowser\\Tor\\tor.exe");
|
||||
}
|
||||
const char* programFilesX86 = getenv("ProgramFiles(x86)");
|
||||
if (programFilesX86) {
|
||||
candidates.push_back(std::string(programFilesX86) + "\\Tor\\tor.exe");
|
||||
candidates.push_back(std::string(programFilesX86) + "\\Tor Browser\\Browser\\TorBrowser\\Tor\\tor.exe");
|
||||
}
|
||||
const char* localAppData = getenv("LOCALAPPDATA");
|
||||
if (localAppData) {
|
||||
candidates.push_back(std::string(localAppData) + "\\Tor Browser\\Browser\\TorBrowser\\Tor\\tor.exe");
|
||||
}
|
||||
|
||||
// Tor Expert Bundle (common install)
|
||||
candidates.push_back("C:\\Tor\\tor.exe");
|
||||
|
||||
#else
|
||||
// Same directory as the wallet executable
|
||||
char exePath[4096];
|
||||
ssize_t len = readlink("/proc/self/exe", exePath, sizeof(exePath) - 1);
|
||||
if (len > 0) {
|
||||
exePath[len] = '\0';
|
||||
fs::path exeDir = fs::path(exePath).parent_path();
|
||||
candidates.push_back((exeDir / "tor").string());
|
||||
candidates.push_back((exeDir / "tor" / "tor").string());
|
||||
}
|
||||
|
||||
// Data directory
|
||||
candidates.push_back((GetDataDir() / "tor").string());
|
||||
candidates.push_back((GetDataDir() / "tor" / "tor").string());
|
||||
|
||||
// Standard Linux/macOS locations
|
||||
candidates.push_back("/usr/bin/tor");
|
||||
candidates.push_back("/usr/local/bin/tor");
|
||||
candidates.push_back("/usr/sbin/tor");
|
||||
candidates.push_back("/opt/tor/bin/tor");
|
||||
candidates.push_back("/snap/bin/tor");
|
||||
|
||||
// Homebrew (macOS)
|
||||
candidates.push_back("/opt/homebrew/bin/tor");
|
||||
candidates.push_back("/usr/local/opt/tor/bin/tor");
|
||||
#endif
|
||||
|
||||
// Check each candidate
|
||||
for (const std::string& path : candidates) {
|
||||
if (fs::exists(path)) {
|
||||
printf("Found Tor binary at: %s\n", path.c_str());
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
bool CTorProcess::IsPortInUse(int port)
|
||||
{
|
||||
#ifdef WIN32
|
||||
SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
|
||||
if (sock == INVALID_SOCKET) return false;
|
||||
|
||||
struct sockaddr_in addr;
|
||||
memset(&addr, 0, sizeof(addr));
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_addr.s_addr = inet_addr("127.0.0.1");
|
||||
addr.sin_port = htons(port);
|
||||
|
||||
int result = connect(sock, (struct sockaddr*)&addr, sizeof(addr));
|
||||
closesocket(sock);
|
||||
return (result == 0);
|
||||
#else
|
||||
int sock = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (sock < 0) return false;
|
||||
|
||||
struct sockaddr_in addr;
|
||||
memset(&addr, 0, sizeof(addr));
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_addr.s_addr = inet_addr("127.0.0.1");
|
||||
addr.sin_port = htons(port);
|
||||
|
||||
int result = connect(sock, (struct sockaddr*)&addr, sizeof(addr));
|
||||
close(sock);
|
||||
return (result == 0);
|
||||
#endif
|
||||
}
|
||||
|
||||
bool CTorProcess::WriteTorrc()
|
||||
{
|
||||
fs::path dataPath(torDataDir);
|
||||
fs::create_directories(dataPath);
|
||||
|
||||
torrcPath = (dataPath / "torrc").string();
|
||||
|
||||
// Hidden service directory
|
||||
fs::path hsDir = dataPath / "hidden_service";
|
||||
fs::create_directories(hsDir);
|
||||
|
||||
std::ofstream torrc(torrcPath.c_str());
|
||||
if (!torrc.is_open()) {
|
||||
printf("ERROR: Cannot write torrc to %s\n", torrcPath.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
torrc << "# Triangles Wallet Tor Configuration (auto-generated)\n";
|
||||
torrc << "# Do not edit - this file is overwritten on startup\n\n";
|
||||
|
||||
// SOCKS proxy for wallet connections
|
||||
torrc << "SocksPort " << socksPort << "\n";
|
||||
|
||||
// Data directory for Tor state
|
||||
fs::path torStateDir = dataPath / "state";
|
||||
fs::create_directories(torStateDir);
|
||||
torrc << "DataDirectory " << torStateDir.string() << "\n";
|
||||
|
||||
// V3 hidden service so this node is reachable via .onion
|
||||
torrc << "HiddenServiceDir " << hsDir.string() << "\n";
|
||||
torrc << "HiddenServiceVersion 3\n";
|
||||
torrc << "HiddenServicePort " << hiddenServicePort
|
||||
<< " 127.0.0.1:" << hiddenServicePort << "\n";
|
||||
|
||||
// Reduce bandwidth/resource usage for wallet use
|
||||
torrc << "ClientOnly 1\n";
|
||||
|
||||
// Disable unused features
|
||||
torrc << "AvoidDiskWrites 1\n";
|
||||
torrc << "Log notice stderr\n";
|
||||
|
||||
torrc.close();
|
||||
|
||||
printf("Wrote torrc to %s (SOCKS %d, HS port %d)\n",
|
||||
torrcPath.c_str(), socksPort, hiddenServicePort);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CTorProcess::Start(const std::string& dataDir, int socks, int hsPort)
|
||||
{
|
||||
socksPort = socks;
|
||||
hiddenServicePort = hsPort;
|
||||
torDataDir = dataDir;
|
||||
|
||||
// Check if something is already listening on our SOCKS port
|
||||
if (IsPortInUse(socksPort)) {
|
||||
printf("Tor SOCKS port %d already in use - assuming Tor is running\n", socksPort);
|
||||
running = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Find Tor binary
|
||||
torBinaryPath = FindTorBinary();
|
||||
if (torBinaryPath.empty()) {
|
||||
printf("WARNING: Tor binary not found. Install Tor for .onion connectivity.\n");
|
||||
printf(" Windows: Download from https://www.torproject.org/download/tor/\n");
|
||||
printf(" Linux: apt install tor or yum install tor\n");
|
||||
printf(" Place tor executable next to the wallet binary for auto-detection.\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Write configuration
|
||||
if (!WriteTorrc()) {
|
||||
printf("ERROR: Failed to write Tor configuration\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
printf("Starting Tor process: %s -f %s\n", torBinaryPath.c_str(), torrcPath.c_str());
|
||||
|
||||
#ifdef WIN32
|
||||
STARTUPINFOA si;
|
||||
PROCESS_INFORMATION pi;
|
||||
ZeroMemory(&si, sizeof(si));
|
||||
si.cb = sizeof(si);
|
||||
si.dwFlags = STARTF_USESHOWWINDOW;
|
||||
si.wShowWindow = SW_HIDE; // Run hidden
|
||||
ZeroMemory(&pi, sizeof(pi));
|
||||
|
||||
std::string cmdLine = "\"" + torBinaryPath + "\" -f \"" + torrcPath + "\"";
|
||||
|
||||
if (!CreateProcessA(
|
||||
NULL,
|
||||
(LPSTR)cmdLine.c_str(),
|
||||
NULL, NULL,
|
||||
FALSE,
|
||||
CREATE_NO_WINDOW,
|
||||
NULL, NULL,
|
||||
&si, &pi))
|
||||
{
|
||||
printf("ERROR: Failed to start Tor process (error %lu)\n", GetLastError());
|
||||
return false;
|
||||
}
|
||||
|
||||
hProcess = pi.hProcess;
|
||||
processId = pi.dwProcessId;
|
||||
CloseHandle(pi.hThread);
|
||||
|
||||
printf("Tor process started (PID %lu)\n", processId);
|
||||
#else
|
||||
pid_t pid = fork();
|
||||
if (pid < 0) {
|
||||
printf("ERROR: Failed to fork for Tor process\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (pid == 0) {
|
||||
// Child process - exec Tor
|
||||
// Redirect stdout/stderr to /dev/null to avoid cluttering wallet output
|
||||
freopen("/dev/null", "w", stdout);
|
||||
freopen("/dev/null", "w", stderr);
|
||||
execl(torBinaryPath.c_str(), torBinaryPath.c_str(),
|
||||
"-f", torrcPath.c_str(), (char*)NULL);
|
||||
// If exec fails, exit child
|
||||
_exit(1);
|
||||
}
|
||||
|
||||
processId = pid;
|
||||
printf("Tor process started (PID %d)\n", processId);
|
||||
#endif
|
||||
|
||||
running = true;
|
||||
|
||||
// Wait a few seconds for Tor to bootstrap, then check if SOCKS is up
|
||||
printf("Waiting for Tor to bootstrap...\n");
|
||||
for (int i = 0; i < 30; i++) {
|
||||
MilliSleep(1000);
|
||||
if (fShutdown) {
|
||||
Stop();
|
||||
return false;
|
||||
}
|
||||
if (IsPortInUse(socksPort)) {
|
||||
printf("Tor SOCKS proxy ready on port %d (took %ds)\n", socksPort, i + 1);
|
||||
|
||||
// Read and display the hidden service hostname if available
|
||||
fs::path hsHostname = fs::path(torDataDir) / "hidden_service" / "hostname";
|
||||
if (fs::exists(hsHostname)) {
|
||||
std::ifstream f(hsHostname.string().c_str());
|
||||
std::string hostname;
|
||||
if (f.is_open() && std::getline(f, hostname)) {
|
||||
printf("Tor hidden service: %s\n", hostname.c_str());
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if Tor process is still alive
|
||||
if (!IsRunning()) {
|
||||
printf("ERROR: Tor process exited prematurely\n");
|
||||
running = false;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
printf("WARNING: Tor started but SOCKS proxy not yet ready after 30s\n");
|
||||
printf(" Tor may still be bootstrapping. .onion connections will work once ready.\n");
|
||||
return true;
|
||||
}
|
||||
|
||||
void CTorProcess::Stop()
|
||||
{
|
||||
if (!running) return;
|
||||
|
||||
#ifdef WIN32
|
||||
if (hProcess != NULL) {
|
||||
printf("Stopping Tor process (PID %lu)...\n", processId);
|
||||
TerminateProcess(hProcess, 0);
|
||||
WaitForSingleObject(hProcess, 5000);
|
||||
CloseHandle(hProcess);
|
||||
hProcess = NULL;
|
||||
}
|
||||
#else
|
||||
if (processId > 0) {
|
||||
printf("Stopping Tor process (PID %d)...\n", processId);
|
||||
kill(processId, SIGTERM);
|
||||
// Wait up to 5 seconds for graceful shutdown
|
||||
for (int i = 0; i < 50; i++) {
|
||||
int status;
|
||||
pid_t result = waitpid(processId, &status, WNOHANG);
|
||||
if (result != 0) break;
|
||||
MilliSleep(100);
|
||||
}
|
||||
// Force kill if still running
|
||||
kill(processId, SIGKILL);
|
||||
waitpid(processId, NULL, 0);
|
||||
}
|
||||
#endif
|
||||
|
||||
processId = 0;
|
||||
running = false;
|
||||
printf("Tor process stopped\n");
|
||||
}
|
||||
|
||||
bool CTorProcess::IsRunning()
|
||||
{
|
||||
if (!running) return false;
|
||||
|
||||
#ifdef WIN32
|
||||
if (hProcess == NULL) return false;
|
||||
DWORD exitCode;
|
||||
if (GetExitCodeProcess(hProcess, &exitCode)) {
|
||||
return (exitCode == STILL_ACTIVE);
|
||||
}
|
||||
return false;
|
||||
#else
|
||||
if (processId <= 0) return false;
|
||||
int status;
|
||||
pid_t result = waitpid(processId, &status, WNOHANG);
|
||||
if (result == 0) return true; // Still running
|
||||
if (result == processId) {
|
||||
running = false;
|
||||
return false; // Exited
|
||||
}
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
std::string CTorProcess::GetSocksProxy() const
|
||||
{
|
||||
return "127.0.0.1:" + std::to_string(socksPort);
|
||||
}
|
||||
|
||||
// Global convenience functions
|
||||
bool StartTorProcess(const std::string& dataDir)
|
||||
{
|
||||
return CTorProcess::GetInstance()->Start(dataDir);
|
||||
}
|
||||
|
||||
void StopTorProcess()
|
||||
{
|
||||
CTorProcess::GetInstance()->Stop();
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
// Copyright (c) 2024-2025 Triangles developers
|
||||
// Tor Process Manager - launches and manages an external Tor binary
|
||||
// Distributed under the MIT/X11 software license
|
||||
|
||||
#ifndef TRIANGLES_TOR_PROCESS_H
|
||||
#define TRIANGLES_TOR_PROCESS_H
|
||||
|
||||
#include <string>
|
||||
|
||||
#ifdef WIN32
|
||||
#include <windows.h>
|
||||
#endif
|
||||
|
||||
// Manages starting/stopping an external Tor process
|
||||
// Provides SOCKS5 proxy for .onion connectivity and v3 hidden service
|
||||
class CTorProcess
|
||||
{
|
||||
private:
|
||||
std::string torBinaryPath;
|
||||
std::string torDataDir;
|
||||
std::string torrcPath;
|
||||
int socksPort;
|
||||
int hiddenServicePort;
|
||||
bool running;
|
||||
|
||||
#ifdef WIN32
|
||||
HANDLE hProcess;
|
||||
DWORD processId;
|
||||
#else
|
||||
pid_t processId;
|
||||
#endif
|
||||
|
||||
// Find the Tor binary in standard locations
|
||||
std::string FindTorBinary();
|
||||
|
||||
// Generate torrc configuration file
|
||||
bool WriteTorrc();
|
||||
|
||||
// Check if SOCKS port is already in use (another Tor running)
|
||||
bool IsPortInUse(int port);
|
||||
|
||||
public:
|
||||
CTorProcess();
|
||||
~CTorProcess();
|
||||
|
||||
// Start the Tor process
|
||||
// Returns true if Tor was started or is already running
|
||||
bool Start(const std::string& dataDir, int socksPort = 19099, int hsPort = 24112);
|
||||
|
||||
// Stop the Tor process
|
||||
void Stop();
|
||||
|
||||
// Check if Tor is running
|
||||
bool IsRunning();
|
||||
|
||||
// Get the SOCKS proxy address
|
||||
std::string GetSocksProxy() const;
|
||||
|
||||
// Get the Tor binary path (for diagnostics)
|
||||
std::string GetBinaryPath() const { return torBinaryPath; }
|
||||
|
||||
// Singleton access
|
||||
static CTorProcess* GetInstance();
|
||||
};
|
||||
|
||||
// Global convenience functions
|
||||
bool StartTorProcess(const std::string& dataDir);
|
||||
void StopTorProcess();
|
||||
|
||||
#endif // TRIANGLES_TOR_PROCESS_H
|
||||
+18
-183
@@ -1020,187 +1020,10 @@ static string JSONRPCExecBatch(const Array& vReq)
|
||||
return write_string(Value(ret), false) + "\n";
|
||||
}
|
||||
|
||||
// REST API support
|
||||
extern Object blockToJSON(const CBlock& block, const CBlockIndex* blockindex, bool fPrintTransactionDetail);
|
||||
extern void TxToJSON(const CTransaction& tx, const uint256 hashBlock, json_spirit::Object& entry);
|
||||
// REST API support (implementation in rest.cpp)
|
||||
#include "rest.h"
|
||||
|
||||
static string HTTPReplyREST(int nStatus, const string& strMsg, const string& contentType = "application/json")
|
||||
{
|
||||
const char *cStatus;
|
||||
if (nStatus == HTTP_OK) cStatus = "OK";
|
||||
else if (nStatus == HTTP_BAD_REQUEST) cStatus = "Bad Request";
|
||||
else if (nStatus == HTTP_NOT_FOUND) cStatus = "Not Found";
|
||||
else if (nStatus == HTTP_INTERNAL_SERVER_ERROR) cStatus = "Internal Server Error";
|
||||
else cStatus = "";
|
||||
return strprintf(
|
||||
"HTTP/1.1 %d %s\r\n"
|
||||
"Date: %s\r\n"
|
||||
"Connection: close\r\n"
|
||||
"Content-Length: %"PRIszu"\r\n"
|
||||
"Content-Type: %s\r\n"
|
||||
"Access-Control-Allow-Origin: *\r\n"
|
||||
"Server: Triangles-json-rpc/%s\r\n"
|
||||
"\r\n"
|
||||
"%s",
|
||||
nStatus,
|
||||
cStatus,
|
||||
rfc1123Time().c_str(),
|
||||
strMsg.size(),
|
||||
contentType.c_str(),
|
||||
FormatFullVersion().c_str(),
|
||||
strMsg.c_str());
|
||||
}
|
||||
|
||||
static bool HandleRESTRequest(const string& strURI, string& strReply, string& strContentType, int& nStatus)
|
||||
{
|
||||
// Parse: /rest/<resource>[/<param>][.format]
|
||||
vector<string> parts;
|
||||
string uri = strURI;
|
||||
// Remove query string if present
|
||||
size_t qpos = uri.find('?');
|
||||
if (qpos != string::npos) uri = uri.substr(0, qpos);
|
||||
|
||||
boost::split(parts, uri, boost::is_any_of("/"));
|
||||
// parts[0]="" parts[1]="rest" parts[2]="resource" parts[3]="param.format"
|
||||
if (parts.size() < 3) {
|
||||
nStatus = HTTP_NOT_FOUND;
|
||||
strReply = "{\"error\":\"Not found\"}";
|
||||
return true;
|
||||
}
|
||||
|
||||
string resource = parts[2];
|
||||
|
||||
// Get param and format from last path component
|
||||
string lastPart = parts.size() > 3 ? parts[parts.size()-1] : "";
|
||||
string param, format = "json";
|
||||
size_t dotPos = lastPart.rfind('.');
|
||||
if (dotPos != string::npos) {
|
||||
param = lastPart.substr(0, dotPos);
|
||||
format = lastPart.substr(dotPos+1);
|
||||
} else {
|
||||
param = lastPart;
|
||||
}
|
||||
|
||||
strContentType = (format == "hex") ? "text/plain" : "application/json";
|
||||
nStatus = HTTP_OK;
|
||||
|
||||
try {
|
||||
LOCK(cs_main);
|
||||
|
||||
if (resource == "chaininfo") {
|
||||
Object obj, diff;
|
||||
obj.push_back(Pair("chain", fTestNet ? string("test") : string("main")));
|
||||
obj.push_back(Pair("blocks", (int)nBestHeight));
|
||||
obj.push_back(Pair("bestblockhash", hashBestChain.GetHex()));
|
||||
diff.push_back(Pair("proof-of-work", GetDifficulty()));
|
||||
diff.push_back(Pair("proof-of-stake", GetDifficulty(GetLastBlockIndex(pindexBest, true))));
|
||||
obj.push_back(Pair("difficulty", diff));
|
||||
obj.push_back(Pair("moneysupply", ValueFromAmount(pindexBest->nMoneySupply)));
|
||||
strReply = write_string(Value(obj), false) + "\n";
|
||||
}
|
||||
else if (resource == "block" && !param.empty()) {
|
||||
uint256 hash(param);
|
||||
if (mapBlockIndex.count(hash) == 0) {
|
||||
nStatus = HTTP_NOT_FOUND;
|
||||
strReply = "{\"error\":\"Block not found\"}";
|
||||
return true;
|
||||
}
|
||||
CBlock block;
|
||||
CBlockIndex* pblockindex = mapBlockIndex[hash];
|
||||
block.ReadFromDisk(pblockindex, true);
|
||||
|
||||
if (format == "hex") {
|
||||
CDataStream ssBlock(SER_NETWORK, PROTOCOL_VERSION);
|
||||
ssBlock << block;
|
||||
strReply = HexStr(ssBlock.begin(), ssBlock.end()) + "\n";
|
||||
} else {
|
||||
Object obj = blockToJSON(block, pblockindex, false);
|
||||
strReply = write_string(Value(obj), false) + "\n";
|
||||
}
|
||||
}
|
||||
else if (resource == "blockheader" && !param.empty()) {
|
||||
uint256 hash(param);
|
||||
if (mapBlockIndex.count(hash) == 0) {
|
||||
nStatus = HTTP_NOT_FOUND;
|
||||
strReply = "{\"error\":\"Block not found\"}";
|
||||
return true;
|
||||
}
|
||||
CBlockIndex* pblockindex = mapBlockIndex[hash];
|
||||
Object result;
|
||||
result.push_back(Pair("hash", pblockindex->GetBlockHash().GetHex()));
|
||||
result.push_back(Pair("confirmations", pindexBest->nHeight - pblockindex->nHeight + 1));
|
||||
result.push_back(Pair("height", pblockindex->nHeight));
|
||||
result.push_back(Pair("version", pblockindex->nVersion));
|
||||
result.push_back(Pair("merkleroot", pblockindex->hashMerkleRoot.GetHex()));
|
||||
result.push_back(Pair("time", (boost::int64_t)pblockindex->GetBlockTime()));
|
||||
result.push_back(Pair("nonce", (boost::uint64_t)pblockindex->nNonce));
|
||||
result.push_back(Pair("bits", HexBits(pblockindex->nBits)));
|
||||
result.push_back(Pair("difficulty", GetDifficulty(pblockindex)));
|
||||
result.push_back(Pair("flags", strprintf("%s%s",
|
||||
pblockindex->IsProofOfStake() ? "proof-of-stake" : "proof-of-work",
|
||||
pblockindex->GeneratedStakeModifier() ? " stake-modifier" : "")));
|
||||
if (pblockindex->pprev)
|
||||
result.push_back(Pair("previousblockhash", pblockindex->pprev->GetBlockHash().GetHex()));
|
||||
if (pblockindex->pnext)
|
||||
result.push_back(Pair("nextblockhash", pblockindex->pnext->GetBlockHash().GetHex()));
|
||||
strReply = write_string(Value(result), false) + "\n";
|
||||
}
|
||||
else if (resource == "tx" && !param.empty()) {
|
||||
uint256 hash(param);
|
||||
CTransaction tx;
|
||||
uint256 hashBlock = 0;
|
||||
if (!GetTransaction(hash, tx, hashBlock))
|
||||
{
|
||||
nStatus = HTTP_NOT_FOUND;
|
||||
strReply = "{\"error\":\"Transaction not found\"}";
|
||||
return true;
|
||||
}
|
||||
if (format == "hex") {
|
||||
CDataStream ssTx(SER_NETWORK, PROTOCOL_VERSION);
|
||||
ssTx << tx;
|
||||
strReply = HexStr(ssTx.begin(), ssTx.end()) + "\n";
|
||||
} else {
|
||||
Object obj;
|
||||
obj.push_back(Pair("txid", tx.GetHash().GetHex()));
|
||||
TxToJSON(tx, hashBlock, obj);
|
||||
strReply = write_string(Value(obj), false) + "\n";
|
||||
}
|
||||
}
|
||||
else if (resource == "blockhashbyheight" && !param.empty()) {
|
||||
int nHeight = atoi(param.c_str());
|
||||
if (nHeight < 0 || nHeight > nBestHeight) {
|
||||
nStatus = HTTP_NOT_FOUND;
|
||||
strReply = "{\"error\":\"Block height out of range\"}";
|
||||
return true;
|
||||
}
|
||||
CBlockIndex* pblockindex = FindBlockByHeight(nHeight);
|
||||
Object obj;
|
||||
obj.push_back(Pair("blockhash", pblockindex->phashBlock->GetHex()));
|
||||
strReply = write_string(Value(obj), false) + "\n";
|
||||
}
|
||||
else if (resource == "mempool") {
|
||||
vector<uint256> vtxid;
|
||||
mempool.queryHashes(vtxid);
|
||||
Array a;
|
||||
BOOST_FOREACH(const uint256& hash, vtxid)
|
||||
a.push_back(hash.ToString());
|
||||
strReply = write_string(Value(a), false) + "\n";
|
||||
}
|
||||
else {
|
||||
nStatus = HTTP_NOT_FOUND;
|
||||
strReply = "{\"error\":\"Unknown REST endpoint\"}";
|
||||
}
|
||||
}
|
||||
catch (std::exception& e) {
|
||||
nStatus = HTTP_INTERNAL_SERVER_ERROR;
|
||||
strReply = strprintf("{\"error\":\"%s\"}", e.what());
|
||||
}
|
||||
catch (...) {
|
||||
nStatus = HTTP_INTERNAL_SERVER_ERROR;
|
||||
strReply = "{\"error\":\"Internal server error\"}";
|
||||
}
|
||||
return true;
|
||||
}
|
||||
// Old HandleRESTRequest removed - now in rest.cpp
|
||||
|
||||
/**
|
||||
* Handle SSE (Server-Sent Events) stream connection.
|
||||
@@ -1294,20 +1117,32 @@ void ThreadRPCServer3(void* parg)
|
||||
|
||||
ReadHTTP(conn->stream(), mapHeaders, strRequest);
|
||||
|
||||
// Handle REST API requests (unauthenticated, read-only)
|
||||
// Handle REST API requests
|
||||
string strHTTPMethod = mapHeaders.count("_method") ? mapHeaders["_method"] : "POST";
|
||||
string strURI = mapHeaders.count("_uri") ? mapHeaders["_uri"] : "/";
|
||||
|
||||
if (strHTTPMethod == "GET" && strURI.substr(0, 6) == "/rest/")
|
||||
if (IsRESTPath(strURI) || (strHTTPMethod == "OPTIONS" && IsRESTPath(strURI)))
|
||||
{
|
||||
if (!GetBoolArg("-rest", false))
|
||||
{
|
||||
conn->stream() << HTTPReplyREST(HTTP_FORBIDDEN, "{\"error\":\"REST API not enabled. Start with -rest=1\"}") << std::flush;
|
||||
break;
|
||||
}
|
||||
|
||||
// Rate limit public (non-wallet) endpoints
|
||||
if (strURI.find("/rest/wallet/") == string::npos)
|
||||
{
|
||||
string strPeerIP = conn->peer_address_to_string();
|
||||
if (!CheckRESTRateLimit(strPeerIP))
|
||||
{
|
||||
conn->stream() << HTTPReplyREST(429, "{\"error\":\"Rate limit exceeded. Try again later.\"}") << std::flush;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
string strReply, strContentType;
|
||||
int nRESTStatus;
|
||||
HandleRESTRequest(strURI, strReply, strContentType, nRESTStatus);
|
||||
HandleRESTRequest(strHTTPMethod, strURI, strRequest, mapHeaders, strReply, strContentType, nRESTStatus);
|
||||
conn->stream() << HTTPReplyREST(nRESTStatus, strReply, strContentType) << std::flush;
|
||||
break;
|
||||
}
|
||||
|
||||
+2
-2
@@ -30,7 +30,7 @@ static const int DATABASE_VERSION = 70509;
|
||||
// network protocol versioning
|
||||
//
|
||||
|
||||
static const int PROTOCOL_VERSION = 70205;
|
||||
static const int PROTOCOL_VERSION = 70206;
|
||||
|
||||
// v5 hard fork: require new protocol version (disconnects old nodes)
|
||||
static const int MIN_PROTO_VERSION = 70205;
|
||||
@@ -53,7 +53,7 @@ static const int MEMPOOL_GD_VERSION = 60002;
|
||||
|
||||
#define DISPLAY_VERSION_MAJOR 5
|
||||
#define DISPLAY_VERSION_MINOR 1
|
||||
#define DISPLAY_VERSION_REVISION 5
|
||||
#define DISPLAY_VERSION_REVISION 7
|
||||
#define DISPLAY_VERSION_BUILD 0
|
||||
|
||||
#endif
|
||||
|
||||
@@ -202,6 +202,7 @@ HEADERS += src/qt/trianglesgui.h \
|
||||
src/qt/sendcoinsdialog.h \
|
||||
src/qt/addressbookpage.h \
|
||||
src/qt/aboutdialog.h \
|
||||
src/qt/introdialog.h \
|
||||
src/qt/editaddressdialog.h \
|
||||
src/qt/trianglesaddressvalidator.h \
|
||||
src/alert.h \
|
||||
@@ -217,6 +218,7 @@ HEADERS += src/qt/trianglesgui.h \
|
||||
src/kernel.h \
|
||||
src/net_bootstrap.h \
|
||||
src/tor/onion_v3.h \
|
||||
src/tor/tor_process.h \
|
||||
src/tor/tor_crypto_compat.h \
|
||||
src/scrypt.h \
|
||||
src/pbkdf2.h \
|
||||
@@ -261,6 +263,7 @@ HEADERS += src/qt/trianglesgui.h \
|
||||
src/qt/transactionview.h \
|
||||
src/qt/walletmodel.h \
|
||||
src/trianglesrpc.h \
|
||||
src/rest.h \
|
||||
src/qt/overviewpage.h \
|
||||
src/qt/csvmodelwriter.h \
|
||||
src/crypter.h \
|
||||
@@ -314,6 +317,7 @@ SOURCES += src/qt/triangles.cpp src/qt/trianglesgui.cpp \
|
||||
src/qt/coincontroltreewidget.cpp \
|
||||
src/qt/addressbookpage.cpp \
|
||||
src/qt/aboutdialog.cpp \
|
||||
src/qt/introdialog.cpp \
|
||||
src/qt/editaddressdialog.cpp \
|
||||
src/qt/trianglesaddressvalidator.cpp \
|
||||
# Old embedded Tor v2 client removed - incompatible with OpenSSL 3.x
|
||||
@@ -351,6 +355,7 @@ SOURCES += src/qt/triangles.cpp src/qt/trianglesgui.cpp \
|
||||
src/qt/transactionview.cpp \
|
||||
src/qt/walletmodel.cpp \
|
||||
src/trianglesrpc.cpp \
|
||||
src/rest.cpp \
|
||||
src/rpcdump.cpp \
|
||||
src/rpcnet.cpp \
|
||||
src/rpcmining.cpp \
|
||||
@@ -384,6 +389,7 @@ SOURCES += src/qt/triangles.cpp src/qt/trianglesgui.cpp \
|
||||
src/kernel.cpp \
|
||||
src/net_bootstrap.cpp \
|
||||
src/tor/onion_v3.cpp \
|
||||
src/tor/tor_process.cpp \
|
||||
src/scrypt-arm.S \
|
||||
src/scrypt-x86.S \
|
||||
src/scrypt-x86_64.S \
|
||||
|
||||
Reference in New Issue
Block a user