wowlet: pull the live remote-node list from wownero.org/nodes

The feather websocket is disabled (no WOW backend); replace it with a Tor-routed
HTTP poll of https://wownero.org/nodes/api/nodes/wallet (the live, health-checked
node-map feed). Builds the managed node list from the feed's plain-http nodes
(skips SSL-only nodes feather can't reach), falls back to the bundled nodes.json
if the feed is unreachable, refreshes hourly. So users who don't run a local node
get a fresh, working remote-node list instead of a stale hardcoded one.
This commit is contained in:
jwinterm
2026-06-19 22:02:53 -04:00
parent 4d5805a75c
commit ea22d5dbe4
2 changed files with 57 additions and 0 deletions
+53
View File
@@ -12,6 +12,10 @@
#include "constants.h"
#include "utils/WebsocketNotifier.h"
#include "utils/TorManager.h"
#include "utils/Networking.h"
#include <QJsonArray>
#include <QNetworkReply>
#include <QTimer>
bool NodeList::addNode(const QString &node, NetworkType::Type networkType, NodeList::Type source) {
// We can't obtain references to QJsonObjects...
@@ -108,6 +112,16 @@ Nodes::Nodes(QObject *parent, Wallet *wallet)
this->loadConfig();
connect(websocketNotifier(), &WebsocketNotifier::NodesReceived, this, &Nodes::onWSNodesReceived);
// wowlet: the feather websocket is disabled (no WOW backend), so pull the live remote-node list from
// wownero.org/nodes instead — routed over Tor when Tor is active, with the bundled list (already loaded
// by loadConfig above) as the fallback. Refreshed hourly.
if (conf()->get(Config::disableWebsocket).toBool()) {
QTimer::singleShot(4000, this, &Nodes::fetchRemoteNodeList);
auto *refresh = new QTimer(this);
connect(refresh, &QTimer::timeout, this, &Nodes::fetchRemoteNodeList);
refresh->start(60 * 60 * 1000);
}
if (m_wallet) {
connect(m_wallet, &Wallet::walletRefreshed, this, &Nodes::onWalletRefreshed);
}
@@ -407,6 +421,45 @@ void Nodes::onWSNodesReceived(QList<FeatherNode> &nodes) {
this->updateModels();
}
void Nodes::fetchRemoteNodeList() {
if (conf()->get(Config::offlineMode).toBool())
return;
if (!m_wowNet)
m_wowNet = new Networking(this);
QNetworkReply *reply = m_wowNet->getJson(this, "https://wownero.org/nodes/api/nodes/wallet");
if (!reply)
return;
connect(reply, &QNetworkReply::finished, this, [this, reply] {
reply->deleteLater();
if (reply->error() != QNetworkReply::NoError) {
qWarning() << "wownero node feed unreachable, keeping bundled list:" << reply->errorString();
return;
}
QJsonObject root = QJsonDocument::fromJson(reply->readAll()).object();
const int best = root.value("height_best").toInt();
QList<FeatherNode> nodes;
for (const auto &v : root.value("nodes").toArray()) {
QJsonObject o = v.toObject();
// wowlet talks plain HTTP to daemons (over Tor); skip SSL-only nodes it can't reach.
if (o.value("scheme").toString() == "https")
continue;
const QString host = o.value("host").toString();
if (host.isEmpty())
continue;
const QString addr = QString("%1:%2").arg(host, QString::number(o.value("port").toInt()));
nodes.append(FeatherNode(addr, o.value("height").toInt(), best, true));
}
if (nodes.isEmpty()) {
qWarning() << "wownero node feed returned no usable (http) nodes; keeping bundled list";
return;
}
qInfo() << "wownero node feed: loaded" << nodes.count() << "remote nodes";
this->onWSNodesReceived(nodes);
});
}
void Nodes::onNodeSourceChanged(NodeSource nodeSource) {
this->resetLocalState();
this->updateModels();
+4
View File
@@ -143,6 +143,8 @@ struct FeatherNode {
}
};
class Networking;
class Nodes : public QObject {
Q_OBJECT
@@ -185,6 +187,7 @@ private:
QList<FeatherNode> m_customNodes;
QList<FeatherNode> m_websocketNodes;
Networking *m_wowNet = nullptr; // wowlet: live remote-node feed from wownero.org/nodes
FeatherNode m_connection; // current active connection, if any
@@ -194,6 +197,7 @@ private:
bool m_allowConnection = false;
FeatherNode pickEligibleNode();
void fetchRemoteNodeList(); // wowlet: pull /nodes/api/nodes/wallet, replacing the disabled websocket
bool useOnionNodes();
bool useI2PNodes();