Compare commits

...

6 Commits

Author SHA1 Message Date
sami7777 14ce8cc2a7 Update CI version to 5.1.8
Build All Platforms / build-windows-qt (push) Waiting to run
Build All Platforms / build-windows-daemon (push) Waiting to run
Build All Platforms / build-linux-qt (push) Waiting to run
Build All Platforms / build-linux-daemon (push) Waiting to run
Build All Platforms / build-macos (push) Waiting to run
Build All Platforms / release (push) Blocked by required conditions
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 03:51:12 -07:00
sami7777 d180b5870c Bump version to v5.1.8
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 03:45:55 -07:00
sami7777 596d4ab55c Enable real-time wallet sync and verbose progress during block download
Remove IBD guards on SyncWithWallets and SetBestChain so wallet
transactions appear as blocks are connected. Log every 500 blocks
during sync instead of every 10,000.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 02:58:45 -07:00
sami7777 5af26f186e v5.1.7: Add Tor process manager for .onion connectivity
Build All Platforms / build-windows-qt (push) Waiting to run
Build All Platforms / build-windows-daemon (push) Waiting to run
Build All Platforms / build-linux-qt (push) Waiting to run
Build All Platforms / build-linux-daemon (push) Waiting to run
Build All Platforms / build-macos (push) Waiting to run
Build All Platforms / release (push) Blocked by required conditions
Adds CTorProcess which finds and launches an external Tor binary as a
subprocess, providing a SOCKS5 proxy on port 19099 and a v3 hidden
service on port 24112. The wallet auto-detects Tor from common install
locations or the app directory. Falls back gracefully to clearnet-only
if Tor is not found. Also fixes Tor-only network restriction that
blocked IPv4/IPv6, and bumps version to 5.1.7.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 02:31:55 -07:00
sami7777 5530920b25 Add data directory selection dialog on first run
Shows an intro dialog on first launch letting users choose where to store
blockchain data. Saves the choice in QSettings so it only appears once.
Styled to match the existing Triangles dark theme.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 02:25:26 -07:00
sami7777 3ed9a42b3c v5.1.6: Fix block sync stall for fresh nodes, REST API refactor
Build All Platforms / build-windows-qt (push) Waiting to run
Build All Platforms / build-windows-daemon (push) Waiting to run
Build All Platforms / build-linux-qt (push) Waiting to run
Build All Platforms / build-macos (push) Waiting to run
Build All Platforms / release (push) Blocked by required conditions
Build All Platforms / build-linux-daemon (push) Failing after 25m20s
Critical fix: revert initial sync from getheaders back to getblocks.
The headers-first change (05b1fd1) broke chain continuation — after
downloading the first 2000 blocks, fresh nodes would stall because
the getheaders path has no orphan-based continuation mechanism.
The getblocks/inv/orphan cycle is required for full chain sync.

Also adds getblocks fallback to the headers handler so if headers
are used via other paths, sync still continues.

Other changes:
- Extract REST API into separate rest.cpp/rest.h
- Add REST rate limiting and CORS support

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 01:57:50 -07:00
18 changed files with 1878 additions and 229 deletions
+1 -1
View File
@@ -9,7 +9,7 @@ on:
workflow_dispatch:
env:
VERSION: "5.1.5"
VERSION: "5.1.8"
jobs:
build-windows-qt:
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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))
+205
View File
@@ -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;
}
+42
View File
@@ -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
+5
View File
@@ -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
View File
@@ -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
View File
@@ -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
+413
View File
@@ -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();
}
+70
View File
@@ -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
View File
@@ -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
View File
@@ -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
+6
View File
@@ -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 \