44
55#include " ConsoleDashboard.h"
66#include " AppMain.h"
7+ #include " Config.h"
78#include " CurlHandler.h"
89#include " Log.h"
910#include " Version.h"
@@ -158,6 +159,15 @@ void ConsoleDashboard::processInput() {
158159 }
159160 }
160161 break ;
162+ case ' f' :
163+ case ' F' :
164+ // Fix IPFS Addresses.Announce when we've detected the fixable condition
165+ {
166+ Log* log = Log::GetInstance ();
167+ log->addMessage (" Attempting to fix IPFS announce list..." );
168+ std::thread ([this ]() { applyIpfsAnnounceFix (); }).detach ();
169+ }
170+ break ;
161171 case ' h' :
162172 case ' H' :
163173 case ' ?' :
@@ -183,7 +193,7 @@ void ConsoleDashboard::processInput() {
183193 log->addMessage (" External IP: " + ip);
184194 }
185195 }
186- log->addMessage (" Keys: Q=Quit A=Assets P=Port check L=Log level H=This info" );
196+ log->addMessage (" Keys: Q=Quit A=Assets P=Port check L=Log level F=Fix IPFS announce H=This info" );
187197 }
188198 break ;
189199 default :
@@ -232,6 +242,18 @@ void ConsoleDashboard::render() {
232242 std::thread ([this ]() { checkPspRegistration (); }).detach ();
233243 }
234244 }
245+ // Run IPFS announce diagnosis in background (at startup + every 10 min)
246+ {
247+ auto now = std::chrono::steady_clock::now ();
248+ auto elapsed = std::chrono::duration<double >(now - _lastIpfsAnnounceCheck).count ();
249+ if (elapsed >= 600.0 || !_ipfsAnnounceChecked) {
250+ // Only run once WebServer has had time to resolve external IP
251+ WebServer* ws = app->getWebServerIfSet ();
252+ if (ws && !ws->getExternalIP ().empty () && ws->getExternalIP () != " unknown" ) {
253+ std::thread ([this ]() { checkIpfsAnnounce (); }).detach ();
254+ }
255+ }
256+ }
235257
236258 ChainAnalyzer* analyzer = app->getChainAnalyzerIfSet ();
237259 if (analyzer) {
@@ -390,6 +412,15 @@ void ConsoleDashboard::render() {
390412 out << " \n " ; totalRows++;
391413 }
392414
415+ // IPFS announce hint — shown only when there's actionable advice
416+ {
417+ std::lock_guard<std::mutex> lock (_ipfsAnnounceMutex);
418+ if (!_ipfsAnnounceHint.empty ()) {
419+ out << ERASE_LINE << " IPFS: " << FG_YELLOW << _ipfsAnnounceHint << RESET << " \n " ;
420+ totalRows++;
421+ }
422+ }
423+
393424 // Row 5: separator
394425 out << ERASE_LINE << std::string (w, ' -' ) << " \n " ; totalRows++;
395426
@@ -482,7 +513,17 @@ void ConsoleDashboard::render() {
482513 }
483514
484515 // Help bar (cursor is already on the right line after the \n above)
485- out << ERASE_LINE << DIM << " [Q] Quit [A] Assets [P] Ports [L] Log Level [H] Info" << RESET;
516+ // Include [F] only when the fix is applicable, to avoid cluttering the UI.
517+ bool showFixKey = false ;
518+ {
519+ std::lock_guard<std::mutex> lock (_ipfsAnnounceMutex);
520+ showFixKey = !_ipfsAnnouncedDirectly && _ipfsPort4001Open && _ipfsAnnounceChecked;
521+ }
522+ out << ERASE_LINE << DIM << " [Q] Quit [A] Assets [P] Ports [L] Log Level" ;
523+ if (showFixKey) {
524+ out << " " << RESET << FG_YELLOW << " [F] Fix IPFS" << RESET << DIM;
525+ }
526+ out << " [H] Info" << RESET;
486527
487528 // Write everything in one shot to minimize flicker
488529 std::cout << out.str () << std::flush;
@@ -574,6 +615,166 @@ void ConsoleDashboard::loadPayoutInfo() {
574615 }
575616}
576617
618+ // ---- IPFS announce detection & repair --------------------------------------
619+
620+ namespace {
621+ std::string urlEncodeComponent (const std::string& s) {
622+ std::string out;
623+ out.reserve (s.size () * 3 );
624+ for (unsigned char c : s) {
625+ if ((c >= ' A' && c <= ' Z' ) || (c >= ' a' && c <= ' z' ) ||
626+ (c >= ' 0' && c <= ' 9' ) || c == ' -' || c == ' _' || c == ' .' || c == ' ~' ) {
627+ out += (char )c;
628+ } else {
629+ char buf[4 ];
630+ snprintf (buf, sizeof (buf), " %%%02X" , c);
631+ out += buf;
632+ }
633+ }
634+ return out;
635+ }
636+
637+ // Read ipfspath from config.cfg, default to Kubo's standard HTTP API.
638+ std::string getIpfsApiBase () {
639+ try {
640+ Config config (" config.cfg" );
641+ std::string p = config.getString (" ipfspath" , " http://localhost:5001/api/v0/" );
642+ if (!p.empty () && p.back () != ' /' ) p += ' /' ;
643+ return p;
644+ } catch (...) {
645+ return " http://localhost:5001/api/v0/" ;
646+ }
647+ }
648+ }
649+
650+ void ConsoleDashboard::checkIpfsAnnounce () {
651+ // Snapshot the WAN IP from the web server (already resolved via ipify at startup)
652+ AppMain* app = AppMain::GetInstance ();
653+ WebServer* ws = app->getWebServerIfSet ();
654+ std::string wanIp;
655+ if (ws) wanIp = ws->getExternalIP ();
656+ if (wanIp.empty () || wanIp == " unknown" ) {
657+ std::lock_guard<std::mutex> lock (_ipfsAnnounceMutex);
658+ _ipfsAnnounceHint.clear ();
659+ return ;
660+ }
661+
662+ std::string apiBase = getIpfsApiBase ();
663+
664+ // Step 1: does IPFS /id already list an address containing our WAN IP?
665+ bool announced = false ;
666+ try {
667+ std::string idJson = CurlHandler::post (apiBase + " id" , {}, 5000 );
668+ // Cheap substring check — we don't need full JSON parsing for this.
669+ if (idJson.find (wanIp) != std::string::npos) {
670+ announced = true ;
671+ }
672+ } catch (...) {
673+ // IPFS API unreachable — can't diagnose, bail out quietly
674+ std::lock_guard<std::mutex> lock (_ipfsAnnounceMutex);
675+ _ipfsAnnounceHint.clear ();
676+ _ipfsAnnounceChecked = false ;
677+ return ;
678+ }
679+
680+ // Step 2: if not announced, probe port 4001 externally to see whether
681+ // fixing is even possible. If the port isn't reachable, setting
682+ // Addresses.Announce would advertise a dead address.
683+ bool portOpen = false ;
684+ if (!announced) {
685+ try {
686+ std::string resp = CurlHandler::get (" http://ifconfig.co/port/4001" , 10000 );
687+ if (resp.find (" \" reachable\" : true" ) != std::string::npos ||
688+ resp.find (" \" reachable\" :true" ) != std::string::npos) {
689+ portOpen = true ;
690+ }
691+ } catch (...) {
692+ // leave portOpen = false
693+ }
694+ }
695+
696+ std::lock_guard<std::mutex> lock (_ipfsAnnounceMutex);
697+ _ipfsAnnouncedDirectly = announced;
698+ _ipfsPort4001Open = portOpen;
699+ _ipfsAnnounceChecked = true ;
700+ _lastIpfsAnnounceCheck = std::chrono::steady_clock::now ();
701+
702+ if (announced) {
703+ _ipfsAnnounceHint.clear (); // nothing to fix
704+ } else if (portOpen) {
705+ _ipfsAnnounceHint = " Port 4001 is open but IPFS isn't announcing it — press [F] to fix" ;
706+ } else {
707+ _ipfsAnnounceHint.clear (); // NAT'd with no port forward — relay path is the right answer
708+ }
709+ }
710+
711+ bool ConsoleDashboard::applyIpfsAnnounceFix () {
712+ Log* log = Log::GetInstance ();
713+ AppMain* app = AppMain::GetInstance ();
714+
715+ // Must have a known WAN IP to announce
716+ WebServer* ws = app->getWebServerIfSet ();
717+ std::string wanIp;
718+ if (ws) wanIp = ws->getExternalIP ();
719+ if (wanIp.empty () || wanIp == " unknown" ) {
720+ log->addMessage (" Fix aborted: external IP not known yet" , Log::WARNING);
721+ return false ;
722+ }
723+
724+ // Sanity check: only run if our diagnosis said the port is open. This
725+ // prevents setting Announce to a dead address on a NAT'd box.
726+ {
727+ std::lock_guard<std::mutex> lock (_ipfsAnnounceMutex);
728+ if (!_ipfsAnnounceChecked) {
729+ log->addMessage (" Fix aborted: diagnosis hasn't run yet, please wait a moment" , Log::WARNING);
730+ return false ;
731+ }
732+ if (_ipfsAnnouncedDirectly) {
733+ log->addMessage (" Nothing to fix: IPFS is already announcing a direct address" );
734+ return false ;
735+ }
736+ if (!_ipfsPort4001Open) {
737+ log->addMessage (" Fix aborted: port 4001 is NOT reachable from the internet. "
738+ " Forward TCP 4001 on your router first (press [P] to recheck)." ,
739+ Log::WARNING);
740+ return false ;
741+ }
742+ }
743+
744+ // Build the JSON array argument: ["/ip4/<wan>/tcp/4001","/ip4/<wan>/udp/4001/quic-v1"]
745+ std::string jsonArray = " [\" /ip4/" + wanIp + " /tcp/4001\" ,\" /ip4/" + wanIp + " /udp/4001/quic-v1\" ]" ;
746+
747+ // Build the IPFS config URL: POST .../config?arg=Addresses.Announce&arg=<encoded>&json=true
748+ std::string apiBase = getIpfsApiBase ();
749+ std::string url = apiBase + " config?arg=Addresses.Announce&arg="
750+ + urlEncodeComponent (jsonArray) + " &json=true" ;
751+
752+ log->addMessage (" Setting Kubo Addresses.Announce = " + jsonArray);
753+ try {
754+ std::string response = CurlHandler::post (url, {}, 10000 );
755+ // Kubo echoes the new config on success; look for our WAN IP in the response.
756+ if (response.find (wanIp) == std::string::npos) {
757+ log->addMessage (" IPFS config set returned unexpected response: " + response.substr (0 , 200 ),
758+ Log::WARNING);
759+ return false ;
760+ }
761+ } catch (const std::exception& e) {
762+ log->addMessage (std::string (" IPFS config set failed: " ) + e.what (), Log::WARNING);
763+ return false ;
764+ }
765+
766+ log->addMessage (" Addresses.Announce updated successfully." );
767+ log->addMessage (" IMPORTANT: restart IPFS Desktop (tray icon -> Quit, relaunch) "
768+ " to activate the new announce list." , Log::WARNING);
769+ log->addMessage (" After IPFS restart, DigiAssetCore will automatically pick up "
770+ " the direct address on the next keepalive cycle." );
771+
772+ // Mark as applied so the hint goes away until next check
773+ std::lock_guard<std::mutex> lock (_ipfsAnnounceMutex);
774+ _ipfsAnnounceHint = " Addresses.Announce set — restart IPFS Desktop to activate" ;
775+ return true ;
776+ }
777+
577778// ---- Port checking ----------------------------------------------------------
578779
579780void ConsoleDashboard::checkPorts () {
0 commit comments