@clawlabz/clawnetwork 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.ts CHANGED
@@ -13,10 +13,21 @@ const GITHUB_REPO = 'clawlabz/claw-network'
13
13
  const DEFAULT_RPC_PORT = 9710
14
14
  const DEFAULT_P2P_PORT = 9711
15
15
  const DEFAULT_NETWORK = 'mainnet'
16
- const DEFAULT_SYNC_MODE = 'full'
16
+ const DEFAULT_SYNC_MODE = 'light'
17
17
  const DEFAULT_HEALTH_CHECK_SECONDS = 30
18
18
  const DEFAULT_UI_PORT = 19877
19
19
  const MAX_RESTART_ATTEMPTS = 3
20
+
21
+ // Built-in bootstrap peers for each network
22
+ const BOOTSTRAP_PEERS: Record<string, string[]> = {
23
+ mainnet: [
24
+ '/ip4/178.156.162.162/tcp/9711',
25
+ ],
26
+ testnet: [
27
+ '/ip4/178.156.162.162/tcp/9721',
28
+ ],
29
+ devnet: [], // local dev, no bootstrap
30
+ }
20
31
  const RESTART_BACKOFF_BASE_MS = 5_000
21
32
  const DECIMALS = 9
22
33
  const ONE_CLAW = BigInt(10 ** DECIMALS)
@@ -81,6 +92,7 @@ interface PluginConfig {
81
92
  syncMode: string
82
93
  healthCheckSeconds: number
83
94
  uiPort: number
95
+ extraBootstrapPeers: string[]
84
96
  }
85
97
 
86
98
  function getConfig(api: OpenClawApi): PluginConfig {
@@ -95,6 +107,7 @@ function getConfig(api: OpenClawApi): PluginConfig {
95
107
  syncMode: typeof c.syncMode === 'string' ? c.syncMode : DEFAULT_SYNC_MODE,
96
108
  healthCheckSeconds: typeof c.healthCheckSeconds === 'number' ? c.healthCheckSeconds : DEFAULT_HEALTH_CHECK_SECONDS,
97
109
  uiPort: typeof c.uiPort === 'number' ? c.uiPort : DEFAULT_UI_PORT,
110
+ extraBootstrapPeers: Array.isArray(c.extraBootstrapPeers) ? c.extraBootstrapPeers.filter((p: unknown) => typeof p === 'string') : [],
98
111
  }
99
112
  }
100
113
 
@@ -542,6 +555,12 @@ function startNodeProcess(binaryPath: string, cfg: PluginConfig, api: OpenClawAp
542
555
 
543
556
  const args = ['start', '--network', cfg.network, '--rpc-port', String(cfg.rpcPort), '--p2p-port', String(cfg.p2pPort), '--sync-mode', cfg.syncMode, '--allow-genesis']
544
557
 
558
+ // Add bootstrap peers: built-in for the network + user-configured extra peers
559
+ const peers = [...(BOOTSTRAP_PEERS[cfg.network] ?? []), ...cfg.extraBootstrapPeers]
560
+ for (const peer of peers) {
561
+ args.push('--bootstrap', peer)
562
+ }
563
+
545
564
  api.logger?.info?.(`[clawnetwork] starting node: ${binaryPath} ${args.join(' ')}`)
546
565
 
547
566
  rotateLogIfNeeded()
@@ -699,6 +718,84 @@ async function autoRegisterAgent(cfg: PluginConfig, wallet: WalletData, api: Ope
699
718
  }
700
719
  }
701
720
 
721
+ // ============================================================
722
+ // Mining: Auto Miner Registration + Heartbeat Loop
723
+ // ============================================================
724
+
725
+ // Heartbeat interval: MINER_GRACE_BLOCKS is 2000, at 3s/block = ~6000s.
726
+ // Send heartbeat every ~1000 blocks (~50 min) to stay well within grace period.
727
+ const MINER_HEARTBEAT_INTERVAL_MS = 50 * 60 * 1000 // 50 minutes
728
+ let minerHeartbeatTimer: unknown = null
729
+
730
+ async function autoRegisterMiner(cfg: PluginConfig, wallet: WalletData, api: OpenClawApi): Promise<void> {
731
+ if (!wallet.address) return
732
+
733
+ const binary = findBinary()
734
+ if (!binary) return
735
+
736
+ // Register as miner
737
+ const minerName = sanitizeAgentName(`openclaw-miner-${wallet.address.slice(0, 8)}`)
738
+ try {
739
+ const output = execFileSync(binary, [
740
+ 'register-miner', '--name', minerName,
741
+ '--rpc', `http://localhost:${cfg.rpcPort}`,
742
+ ], {
743
+ encoding: 'utf8',
744
+ timeout: 30_000,
745
+ env: { HOME: os.homedir(), PATH: process.env.PATH || '' },
746
+ })
747
+ api.logger?.info?.(`[clawnetwork] miner registered: ${minerName} — ${output.trim().slice(0, 200)}`)
748
+ } catch (e: unknown) {
749
+ // "already registered" is fine
750
+ const msg = (e as Error).message
751
+ if (msg.includes('already') || msg.includes('exists')) {
752
+ api.logger?.info?.(`[clawnetwork] miner already registered: ${wallet.address.slice(0, 12)}...`)
753
+ } else {
754
+ api.logger?.warn?.(`[clawnetwork] miner registration failed: ${msg.slice(0, 200)}`)
755
+ }
756
+ }
757
+
758
+ // Send first heartbeat immediately
759
+ await sendMinerHeartbeat(cfg, api)
760
+
761
+ // Start periodic heartbeat loop
762
+ startMinerHeartbeatLoop(cfg, api)
763
+ }
764
+
765
+ async function sendMinerHeartbeat(cfg: PluginConfig, api: OpenClawApi): Promise<void> {
766
+ const binary = findBinary()
767
+ if (!binary) return
768
+
769
+ try {
770
+ const output = execFileSync(binary, [
771
+ 'miner-heartbeat',
772
+ '--rpc', `http://localhost:${cfg.rpcPort}`,
773
+ ], {
774
+ encoding: 'utf8',
775
+ timeout: 30_000,
776
+ env: { HOME: os.homedir(), PATH: process.env.PATH || '' },
777
+ })
778
+ api.logger?.info?.(`[clawnetwork] ${output.trim()}`)
779
+ } catch (e: unknown) {
780
+ api.logger?.warn?.(`[clawnetwork] heartbeat failed: ${(e as Error).message.slice(0, 200)}`)
781
+ }
782
+ }
783
+
784
+ function startMinerHeartbeatLoop(cfg: PluginConfig, api: OpenClawApi): void {
785
+ if (minerHeartbeatTimer) clearInterval(minerHeartbeatTimer)
786
+ minerHeartbeatTimer = setInterval(() => {
787
+ sendMinerHeartbeat(cfg, api).catch(() => {})
788
+ }, MINER_HEARTBEAT_INTERVAL_MS)
789
+ api.logger?.info?.(`[clawnetwork] miner heartbeat loop started (every ${Math.round(MINER_HEARTBEAT_INTERVAL_MS / 60000)}min)`)
790
+ }
791
+
792
+ function stopMinerHeartbeatLoop(): void {
793
+ if (minerHeartbeatTimer) {
794
+ clearInterval(minerHeartbeatTimer)
795
+ minerHeartbeatTimer = null
796
+ }
797
+ }
798
+
702
799
  // ============================================================
703
800
  // WebUI Server
704
801
  // ============================================================
@@ -710,24 +807,28 @@ function buildUiHtml(cfg: PluginConfig): string {
710
807
  <meta charset="UTF-8">
711
808
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
712
809
  <title>ClawNetwork Node Dashboard</title>
713
- <link rel="icon" href="https://cdn.clawlabz.xyz/brand/favicon.png">
810
+ <link rel="icon" href="https://explorer.clawlabz.xyz/favicon.png">
811
+ <link rel="preconnect" href="https://fonts.googleapis.com">
812
+ <link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
714
813
  <style>
715
814
  :root {
716
- --bg: #0a0a12;
717
- --bg-panel: #12121f;
718
- --border: #1e1e3a;
719
- --accent: #00ccff;
720
- --accent-dim: rgba(0, 204, 255, 0.15);
721
- --green: #00ff88;
722
- --green-dim: rgba(0, 255, 136, 0.15);
723
- --purple: #8b5cf6;
724
- --text: #e0e0f0;
725
- --text-dim: #666688;
726
- --danger: #ff4455;
727
- --font: system-ui, -apple-system, sans-serif;
728
- --font-mono: 'SF Mono', 'Fira Code', Consolas, monospace;
815
+ --bg: #0a0705;
816
+ --bg-panel: #140e0a;
817
+ --border: #2a1c14;
818
+ --accent: #F96706;
819
+ --accent-dim: rgba(249, 103, 6, 0.15);
820
+ --accent-light: #FF8C3A;
821
+ --purple: #a855f7;
822
+ --purple-dim: rgba(168, 85, 247, 0.15);
823
+ --green: #22c55e;
824
+ --green-dim: rgba(34, 197, 94, 0.15);
825
+ --text: #fffaf5;
826
+ --text-dim: #8892a0;
827
+ --danger: #ef4444;
828
+ --font: 'Space Grotesk', system-ui, -apple-system, sans-serif;
829
+ --font-mono: 'JetBrains Mono', 'SF Mono', Consolas, monospace;
729
830
  --radius: 10px;
730
- --shadow: 0 4px 24px rgba(0, 0, 0, 0.4);
831
+ --shadow: 0 4px 24px rgba(0, 0, 0, 0.5);
731
832
  }
732
833
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
733
834
  body { background: var(--bg); color: var(--text); font-family: var(--font); line-height: 1.6; min-height: 100vh; }
@@ -737,16 +838,17 @@ function buildUiHtml(cfg: PluginConfig): string {
737
838
  .header { background: var(--bg-panel); border-bottom: 1px solid var(--border); padding: 16px 0; position: sticky; top: 0; z-index: 100; }
738
839
  .header .container { display: flex; align-items: center; justify-content: space-between; }
739
840
  .logo { font-size: 22px; font-weight: 800; letter-spacing: -0.5px; }
740
- .logo-claw { color: var(--accent); }
741
- .logo-net { color: var(--green); }
841
+ .logo-claw { color: #ffffff; }
842
+ .logo-net { color: var(--accent); }
742
843
  .header-badge { font-size: 11px; background: var(--accent-dim); color: var(--accent); padding: 2px 8px; border-radius: 4px; }
743
844
 
744
- .stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin: 24px 0; }
845
+ .stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 12px; }
745
846
  .stat-card { background: var(--bg-panel); border: 1px solid var(--border); border-radius: var(--radius); padding: 20px; }
746
847
  .stat-label { font-size: 12px; color: var(--text-dim); text-transform: uppercase; letter-spacing: 1px; }
747
848
  .stat-value { font-size: 28px; font-weight: 700; font-family: var(--font-mono); margin-top: 4px; }
748
849
  .stat-value.green { color: var(--green); }
749
850
  .stat-value.accent { color: var(--accent); }
851
+ .stat-value.purple { color: var(--purple); }
750
852
  .stat-value.danger { color: var(--danger); }
751
853
 
752
854
  .panel { background: var(--bg-panel); border: 1px solid var(--border); border-radius: var(--radius); padding: 20px; margin: 16px 0; }
@@ -761,13 +863,23 @@ function buildUiHtml(cfg: PluginConfig): string {
761
863
  .status-dot.offline { background: var(--danger); }
762
864
  .status-dot.syncing { background: #ffaa00; animation: pulse 1.5s infinite; }
763
865
 
764
- .btn { display: inline-flex; align-items: center; gap: 6px; padding: 8px 16px; border-radius: 6px; border: 1px solid var(--border); background: var(--bg-panel); color: var(--text); font-size: 13px; cursor: pointer; transition: 0.2s; }
866
+ .btn { display: inline-flex; align-items: center; gap: 6px; padding: 8px 16px; border-radius: 6px; border: 1px solid var(--border); background: var(--bg-panel); color: var(--text); font-size: 13px; cursor: pointer; transition: 0.2s; font-family: var(--font); }
765
867
  .btn:hover { border-color: var(--accent); color: var(--accent); }
766
868
  .btn.danger:hover { border-color: var(--danger); color: var(--danger); }
767
869
  .btn.primary { background: var(--accent-dim); border-color: var(--accent); color: var(--accent); }
768
- .btn-group { display: flex; gap: 8px; margin: 16px 0; flex-wrap: wrap; }
870
+ .node-controls { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; padding-top: 16px; margin-top: 16px; border-top: 1px solid var(--border); }
871
+ .node-controls .spacer { flex: 1; }
872
+
873
+ .wallet-hero { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; margin-bottom: 16px; flex-wrap: wrap; }
874
+ .wallet-balance { font-size: 36px; font-weight: 800; font-family: var(--font-mono); color: var(--accent); letter-spacing: -1px; line-height: 1; }
875
+ .wallet-balance-label { font-size: 11px; color: var(--text-dim); text-transform: uppercase; letter-spacing: 1px; margin-bottom: 6px; }
876
+ .wallet-addr-wrap { flex: 1; min-width: 0; }
877
+ .wallet-addr-label { font-size: 11px; color: var(--text-dim); text-transform: uppercase; letter-spacing: 1px; margin-bottom: 6px; }
878
+ .wallet-addr { font-family: var(--font-mono); font-size: 12px; background: var(--bg); padding: 8px 12px; border-radius: 6px; border: 1px solid var(--border); word-break: break-all; display: flex; align-items: center; gap: 8px; }
879
+ .copy-btn { background: none; border: none; color: var(--accent); cursor: pointer; font-size: 12px; padding: 2px 8px; border-radius: 4px; border: 1px solid var(--accent); white-space: nowrap; font-family: var(--font); transition: 0.2s; }
880
+ .copy-btn:hover { background: var(--accent-dim); }
769
881
 
770
- .logs-box { background: #080810; border: 1px solid var(--border); border-radius: var(--radius); padding: 16px; font-family: var(--font-mono); font-size: 12px; max-height: 300px; overflow-y: auto; white-space: pre-wrap; color: var(--text-dim); line-height: 1.8; }
882
+ .logs-box { background: #060402; border: 1px solid var(--border); border-radius: var(--radius); padding: 16px; font-family: var(--font-mono); font-size: 12px; max-height: 300px; overflow-y: auto; white-space: pre-wrap; color: var(--text-dim); line-height: 1.8; }
771
883
 
772
884
  .wallet-addr { font-family: var(--font-mono); font-size: 13px; background: var(--bg); padding: 8px 12px; border-radius: 6px; border: 1px solid var(--border); word-break: break-all; display: flex; align-items: center; gap: 8px; }
773
885
  .copy-btn { background: none; border: none; color: var(--accent); cursor: pointer; font-size: 14px; padding: 2px 6px; }
@@ -775,49 +887,101 @@ function buildUiHtml(cfg: PluginConfig): string {
775
887
 
776
888
  .toast { position: fixed; bottom: 24px; right: 24px; background: var(--bg-panel); border: 1px solid var(--accent); color: var(--accent); padding: 12px 20px; border-radius: 8px; font-size: 13px; opacity: 0; transition: 0.3s; z-index: 1000; }
777
889
  .toast.show { opacity: 1; }
890
+
891
+ .quick-actions { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; margin: 16px 0 0; }
892
+ .quick-action { background: var(--bg-panel); border: 1px solid var(--border); border-radius: var(--radius); padding: 14px 16px; cursor: pointer; transition: 0.2s; display: flex; align-items: center; gap: 10px; font-size: 13px; color: var(--text); }
893
+ .quick-action:hover { border-color: var(--accent); color: var(--accent); transform: translateY(-1px); }
894
+ .quick-action .qa-icon { font-size: 18px; width: 28px; text-align: center; }
895
+ .quick-action .qa-label { font-weight: 500; }
896
+ .quick-action .qa-hint { font-size: 11px; color: var(--text-dim); margin-top: 2px; }
897
+ .quick-action.warn:hover { border-color: var(--danger); color: var(--danger); }
898
+
899
+ .modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.7); display: none; align-items: center; justify-content: center; z-index: 200; }
900
+ .modal-overlay.open { display: flex; }
901
+ .modal { background: var(--bg-panel); border: 1px solid var(--border); border-radius: var(--radius); padding: 28px; max-width: 520px; width: 90%; box-shadow: var(--shadow); }
902
+ .modal-title { font-size: 16px; font-weight: 700; margin-bottom: 12px; }
903
+ .modal-warn { background: rgba(255,85,85,0.1); border: 1px solid var(--danger); border-radius: 6px; padding: 10px 14px; font-size: 12px; color: var(--danger); margin-bottom: 14px; line-height: 1.5; }
904
+ .modal-key { font-family: var(--font-mono); font-size: 13px; background: var(--bg); padding: 12px; border-radius: 6px; border: 1px solid var(--border); word-break: break-all; line-height: 1.6; user-select: all; }
905
+ .modal-actions { display: flex; gap: 8px; margin-top: 16px; justify-content: flex-end; }
906
+ .modal-close { background: none; border: 1px solid var(--border); color: var(--text-dim); padding: 8px 16px; border-radius: 6px; cursor: pointer; font-size: 13px; }
907
+ .modal-close:hover { border-color: var(--text); color: var(--text); }
908
+ .modal-input { width: 100%; box-sizing: border-box; background: var(--bg); border: 1px solid var(--border); border-radius: 6px; padding: 10px 12px; font-size: 14px; color: var(--text); font-family: var(--font-mono); outline: none; margin-top: 4px; }
909
+ .modal-input:focus { border-color: var(--accent); }
910
+ .modal-hint { font-size: 12px; color: var(--text-dim); margin-top: 6px; line-height: 1.5; }
778
911
  </style>
779
912
  </head>
780
913
  <body>
781
914
  <header class="header">
782
915
  <div class="container">
783
916
  <div style="display:flex;align-items:center;gap:14px">
784
- <div class="logo"><span class="logo-claw">Claw</span><span class="logo-net">Network</span></div>
917
+ <div class="logo"><img src="https://explorer.clawlabz.xyz/favicon.png" style="width:28px;height:28px;border-radius:6px;vertical-align:middle;margin-right:8px"><span class="logo-claw">Claw</span><span class="logo-net">Network</span></div>
785
918
  <span class="header-badge">Node Dashboard</span>
786
919
  </div>
787
920
  <span id="lastUpdate" style="font-size:12px;color:var(--text-dim)"></span>
788
921
  </div>
789
922
  </header>
790
923
 
791
- <main class="container" style="padding-top:8px;padding-bottom:40px">
792
- <div class="stats-grid">
793
- <div class="stat-card">
794
- <div class="stat-label">Status</div>
795
- <div class="stat-value" id="statusValue"><span class="status-dot offline"></span>Offline</div>
796
- </div>
797
- <div class="stat-card">
798
- <div class="stat-label">Block Height</div>
799
- <div class="stat-value accent" id="heightValue">—</div>
800
- </div>
801
- <div class="stat-card">
802
- <div class="stat-label">Peers</div>
803
- <div class="stat-value" id="peersValue">—</div>
924
+ <main class="container" style="padding-top:16px;padding-bottom:40px">
925
+
926
+ <div class="panel">
927
+ <div class="panel-title">Node</div>
928
+ <div class="stats-grid" style="margin:0 0 4px">
929
+ <div class="stat-card">
930
+ <div class="stat-label">Status</div>
931
+ <div class="stat-value" id="statusValue"><span class="status-dot offline"></span>Offline</div>
932
+ </div>
933
+ <div class="stat-card">
934
+ <div class="stat-label">Block Height</div>
935
+ <div class="stat-value accent" id="heightValue">—</div>
936
+ </div>
937
+ <div class="stat-card">
938
+ <div class="stat-label">Peers</div>
939
+ <div class="stat-value" id="peersValue">—</div>
940
+ </div>
941
+ <div class="stat-card">
942
+ <div class="stat-label">Uptime</div>
943
+ <div class="stat-value" id="uptimeValue">—</div>
944
+ </div>
804
945
  </div>
805
- <div class="stat-card">
806
- <div class="stat-label">Uptime</div>
807
- <div class="stat-value" id="uptimeValue">—</div>
946
+ <div class="node-controls">
947
+ <button class="btn primary" onclick="doAction('start')">&#x25B6; Start Node</button>
948
+ <button class="btn danger" onclick="doAction('stop')">&#x25A0; Stop Node</button>
808
949
  </div>
809
950
  </div>
810
951
 
811
- <div class="btn-group">
812
- <button class="btn primary" onclick="doAction('start')">Start Node</button>
813
- <button class="btn danger" onclick="doAction('stop')">Stop Node</button>
814
- <button class="btn" onclick="doAction('faucet')">Faucet (testnet)</button>
815
- <button class="btn" onclick="refreshLogs()">Refresh Logs</button>
816
- </div>
817
-
818
- <div class="panel">
952
+ <div class="panel" id="walletPanel">
819
953
  <div class="panel-title">Wallet</div>
820
- <div id="walletInfo">Loading...</div>
954
+ <div id="walletEmpty" style="color:var(--text-dim);font-size:13px">No wallet yet — start the node to generate one</div>
955
+ <div id="walletLoaded" style="display:none">
956
+ <div class="wallet-hero">
957
+ <div>
958
+ <div class="wallet-balance-label">Balance</div>
959
+ <div class="wallet-balance" id="walletBalance">—</div>
960
+ </div>
961
+ <div class="wallet-addr-wrap">
962
+ <div class="wallet-addr-label">Address</div>
963
+ <div class="wallet-addr"><span id="walletAddrText" style="flex:1;min-width:0;word-break:break-all"></span><button class="copy-btn" onclick="copyText(cachedAddress)">Copy</button></div>
964
+ </div>
965
+ </div>
966
+ <div class="quick-actions" id="walletActions">
967
+ <div class="quick-action" onclick="importToExtension()" id="qaImportExt">
968
+ <span class="qa-icon"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg></span>
969
+ <div><div class="qa-label">Import to Extension</div><div class="qa-hint" id="qaImportHint">One-click import to browser wallet</div></div>
970
+ </div>
971
+ <div class="quick-action warn" onclick="showExportKey()">
972
+ <span class="qa-icon"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="7.5" cy="15.5" r="5.5"/><path d="m21 2-9.6 9.6"/><path d="m15.5 7.5 3 3L22 7l-3-3"/></svg></span>
973
+ <div><div class="qa-label">Export Private Key</div><div class="qa-hint">Manual copy for backup</div></div>
974
+ </div>
975
+ <div class="quick-action" onclick="openExplorer()">
976
+ <span class="qa-icon"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg></span>
977
+ <div><div class="qa-label">View on Explorer</div><div class="qa-hint">Transaction history</div></div>
978
+ </div>
979
+ <div class="quick-action" id="qaRegister" onclick="handleRegisterAgent()">
980
+ <span class="qa-icon"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="4" width="16" height="16" rx="2"/><rect x="9" y="9" width="6" height="6"/><path d="M15 2v2M9 2v2M15 20v2M9 20v2M2 15h2M2 9h2M20 15h2M20 9h2"/></svg></span>
981
+ <div><div class="qa-label" id="qaRegisterLabel">Register Agent</div><div class="qa-hint" id="qaRegisterHint">On-chain identity</div></div>
982
+ </div>
983
+ </div>
984
+ </div>
821
985
  </div>
822
986
 
823
987
  <div class="panel">
@@ -826,13 +990,71 @@ function buildUiHtml(cfg: PluginConfig): string {
826
990
  </div>
827
991
 
828
992
  <div class="panel">
829
- <div class="panel-title">Recent Logs</div>
993
+ <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px">
994
+ <div class="panel-title" style="margin-bottom:0">Recent Logs</div>
995
+ <button class="btn" style="font-size:12px;padding:5px 12px" onclick="refreshLogs()">&#x21BB; Refresh</button>
996
+ </div>
830
997
  <div class="logs-box" id="logsBox">Loading...</div>
831
998
  </div>
832
999
  </main>
833
1000
 
1001
+ <footer style="border-top:1px solid var(--border);padding:24px 0;margin-top:16px">
1002
+ <div class="container" style="display:flex;flex-wrap:wrap;gap:20px;align-items:center;justify-content:space-between">
1003
+ <div style="display:flex;align-items:center;gap:8px">
1004
+ <img src="https://explorer.clawlabz.xyz/favicon.png" style="width:18px;height:18px;border-radius:4px;opacity:0.7">
1005
+ <span style="font-size:12px;color:var(--text-dim)">© 2026 ClawLabz</span>
1006
+ </div>
1007
+ <div style="display:flex;gap:20px;flex-wrap:wrap">
1008
+ <a href="https://chain.clawlabz.xyz" target="_blank" style="font-size:12px;color:var(--text-dim);text-decoration:none;transition:0.2s" onmouseover="this.style.color='var(--accent)'" onmouseout="this.style.color='var(--text-dim)'">Chain</a>
1009
+ <a href="https://explorer.clawlabz.xyz" target="_blank" style="font-size:12px;color:var(--text-dim);text-decoration:none;transition:0.2s" onmouseover="this.style.color='var(--accent)'" onmouseout="this.style.color='var(--text-dim)'">Explorer</a>
1010
+ <a href="https://chrome.google.com/webstore/search/ClawNetwork" target="_blank" style="font-size:12px;color:var(--text-dim);text-decoration:none;transition:0.2s" onmouseover="this.style.color='var(--accent)'" onmouseout="this.style.color='var(--text-dim)'">Wallet Extension</a>
1011
+ </div>
1012
+ </div>
1013
+ </footer>
1014
+
834
1015
  <div class="toast" id="toast"></div>
835
1016
 
1017
+ <div class="modal-overlay" id="registerModal" onclick="if(event.target===this)closeRegisterModal()">
1018
+ <div class="modal">
1019
+ <div class="modal-title">Register Agent</div>
1020
+ <p style="font-size:13px;color:var(--text-dim);margin:0 0 12px">Register your wallet as an AI Agent on ClawNetwork. The name is your on-chain identity — it does not need to be unique globally (the wallet address is what's unique). Registration is gas-free on mainnet.</p>
1021
+ <input id="registerNameInput" class="modal-input" type="text" placeholder="my-agent-name" maxlength="32" onkeydown="if(event.key==='Enter')submitRegisterAgent()" />
1022
+ <div class="modal-hint">Allowed: letters, numbers, hyphens, underscores. Max 32 chars.</div>
1023
+ <div class="modal-actions">
1024
+ <button class="modal-close" onclick="closeRegisterModal()">Cancel</button>
1025
+ <button class="btn primary" onclick="submitRegisterAgent()">Register</button>
1026
+ </div>
1027
+ </div>
1028
+ </div>
1029
+
1030
+ <div class="modal-overlay" id="installModal" onclick="if(event.target===this)closeInstallModal()">
1031
+ <div class="modal">
1032
+ <div class="modal-title">Install ClawNetwork Wallet</div>
1033
+ <p style="font-size:13px;color:var(--text-dim);margin:0 0 16px;line-height:1.6">The ClawNetwork browser extension is not detected. Install it first, then click Import to Extension to import your node wallet.</p>
1034
+ <div style="display:flex;gap:10px;flex-direction:column">
1035
+ <a href="https://chrome.google.com/webstore/search/ClawNetwork" target="_blank" class="btn primary" style="text-decoration:none;justify-content:center;padding:10px 16px">Open Chrome Web Store</a>
1036
+ <a href="https://chain.clawlabz.xyz" target="_blank" style="font-size:12px;color:var(--text-dim);text-decoration:none;text-align:center" onmouseover="this.style.color='var(--accent)'" onmouseout="this.style.color='var(--text-dim)'">Learn more at chain.clawlabz.xyz →</a>
1037
+ </div>
1038
+ <div class="modal-actions" style="margin-top:16px">
1039
+ <button class="modal-close" onclick="closeInstallModal()">Close</button>
1040
+ </div>
1041
+ </div>
1042
+ </div>
1043
+
1044
+ <div class="modal-overlay" id="exportModal" onclick="if(event.target===this)closeExportModal()">
1045
+ <div class="modal">
1046
+ <div class="modal-title">Export Private Key</div>
1047
+ <div class="modal-warn">
1048
+ &#x26A0;&#xFE0F; <strong>Never share your private key.</strong> Anyone with this key has full control of your wallet and funds. Only use this to import into your own browser extension or backup.
1049
+ </div>
1050
+ <div class="modal-key" id="exportKeyDisplay">Loading...</div>
1051
+ <div class="modal-actions">
1052
+ <button class="btn primary" onclick="copyExportKey()">Copy Private Key</button>
1053
+ <button class="modal-close" onclick="closeExportModal()">Close</button>
1054
+ </div>
1055
+ </div>
1056
+ </div>
1057
+
836
1058
  <script>
837
1059
  const API = '';
838
1060
  let autoRefresh = null;
@@ -844,17 +1066,202 @@ function buildUiHtml(cfg: PluginConfig): string {
844
1066
  setTimeout(() => el.classList.remove('show'), 3000);
845
1067
  }
846
1068
 
1069
+ let cachedAddress = '';
1070
+ let cachedNetwork = '';
1071
+ let cachedKey = '';
1072
+ let cachedAgentName = ''; // '' = not registered, string = registered name
1073
+
847
1074
  function copyText(text) {
848
1075
  navigator.clipboard.writeText(text).then(() => toast('Copied!')).catch(() => {});
849
1076
  }
850
1077
 
1078
+ function copyAddress() {
1079
+ if (!cachedAddress) { toast('No wallet address'); return; }
1080
+ copyText(cachedAddress);
1081
+ toast('Address copied!');
1082
+ }
1083
+
1084
+ async function showExportKey() {
1085
+ document.getElementById('exportKeyDisplay').textContent = 'Loading...';
1086
+ document.getElementById('exportModal').classList.add('open');
1087
+ try {
1088
+ const res = await fetch(API + '/api/wallet/export');
1089
+ const data = await res.json();
1090
+ if (data.error) { document.getElementById('exportKeyDisplay').textContent = data.error; return; }
1091
+ cachedKey = data.secretKey;
1092
+ document.getElementById('exportKeyDisplay').textContent = data.secretKey;
1093
+ } catch (e) { document.getElementById('exportKeyDisplay').textContent = 'Failed to load'; }
1094
+ }
1095
+
1096
+ function closeExportModal() {
1097
+ document.getElementById('exportModal').classList.remove('open');
1098
+ cachedKey = '';
1099
+ document.getElementById('exportKeyDisplay').textContent = '';
1100
+ }
1101
+
1102
+ function copyExportKey() {
1103
+ if (!cachedKey) return;
1104
+ copyText(cachedKey);
1105
+ toast('Private key copied! Paste into browser extension to import.');
1106
+ }
1107
+
1108
+ function openExplorer() {
1109
+ if (!cachedAddress) { toast('No wallet address'); return; }
1110
+ window.open('https://explorer.clawlabz.xyz/address/' + cachedAddress, '_blank');
1111
+ }
1112
+
1113
+ function openFaucet() {
1114
+ window.open('https://chain.clawlabz.xyz/faucet', '_blank');
1115
+ }
1116
+
1117
+ // Detect ClawNetwork extension provider (for enhanced flow when available)
1118
+ let hasExtension = false;
1119
+ function checkExtension() {
1120
+ if (window.clawNetwork && window.clawNetwork.isClawNetwork) {
1121
+ hasExtension = true;
1122
+ }
1123
+ }
1124
+ checkExtension();
1125
+ setTimeout(checkExtension, 1000);
1126
+ setTimeout(checkExtension, 3000);
1127
+
1128
+ async function importToExtension() {
1129
+ // Try externally_connectable direct channel first (bypasses page JS context)
1130
+ const extIds = await detectExtensionIds();
1131
+ if (extIds.length > 0) {
1132
+ toast('Connecting to extension (secure channel)...');
1133
+ try {
1134
+ const res = await fetch(API + '/api/wallet/export');
1135
+ const data = await res.json();
1136
+ if (!data.secretKey) { toast('No private key found'); return; }
1137
+ // Direct to background — private key never in page JS event loop
1138
+ const extId = extIds[0];
1139
+ await chromeExtSend(extId, { method: 'claw_requestAccounts' });
1140
+ toast('Approve the import in your extension popup...');
1141
+ await chromeExtSend(extId, { method: 'claw_importAccountKey', params: [data.secretKey, 'ClawNetwork Node'] });
1142
+ toast('Account imported to extension!');
1143
+ return;
1144
+ } catch (e) { /* fall through to provider method */ }
1145
+ }
1146
+ // Fallback: use window.clawNetwork provider
1147
+ if (!window.clawNetwork) {
1148
+ document.getElementById('installModal').classList.add('open');
1149
+ return;
1150
+ }
1151
+ toast('Connecting to extension...');
1152
+ try {
1153
+ await window.clawNetwork.request({ method: 'claw_requestAccounts' });
1154
+ const res = await fetch(API + '/api/wallet/export');
1155
+ const data = await res.json();
1156
+ if (!data.secretKey) { toast('No private key found'); return; }
1157
+ toast('Approve the import in your extension popup...');
1158
+ await window.clawNetwork.request({ method: 'claw_importAccountKey', params: [data.secretKey, 'ClawNetwork Node'] });
1159
+ toast('Account imported to extension!');
1160
+ } catch (e) { toast('Import failed: ' + (e.message || e)); }
1161
+ }
1162
+
1163
+ function chromeExtSend(extId, msg) {
1164
+ return new Promise((resolve, reject) => {
1165
+ if (!chrome || !chrome.runtime || !chrome.runtime.sendMessage) { reject(new Error('No chrome.runtime')); return; }
1166
+ chrome.runtime.sendMessage(extId, msg, (response) => {
1167
+ if (chrome.runtime.lastError) { reject(new Error(chrome.runtime.lastError.message)); return; }
1168
+ if (response && response.success === false) { reject(new Error(response.error || 'Failed')); return; }
1169
+ resolve(response);
1170
+ });
1171
+ });
1172
+ }
1173
+
1174
+ async function detectExtensionIds() {
1175
+ // Try known extension IDs or probe for externally_connectable
1176
+ // In production, the extension ID is stable after Chrome Web Store publish
1177
+ // For dev, try to detect via management API or stored ID
1178
+ const ids = [];
1179
+ try {
1180
+ if (chrome && chrome.runtime && chrome.runtime.sendMessage) {
1181
+ // Try sending a ping to see if any extension responds
1182
+ // This requires knowing the extension ID. For now, check localStorage.
1183
+ const stored = localStorage.getItem('clawnetwork_extension_id');
1184
+ if (stored) ids.push(stored);
1185
+ }
1186
+ } catch {}
1187
+ return ids;
1188
+ }
1189
+
1190
+ async function transferFromDashboard() {
1191
+ const to = prompt('Recipient address (64 hex chars):');
1192
+ if (!to) return;
1193
+ const amount = prompt('Amount (CLAW):');
1194
+ if (!amount) return;
1195
+ if (window.clawNetwork) {
1196
+ try {
1197
+ toast('Approve transfer in extension...');
1198
+ await window.clawNetwork.request({ method: 'claw_requestAccounts' });
1199
+ const result = await window.clawNetwork.request({ method: 'claw_transfer', params: [to, amount] });
1200
+ toast('Transfer sent! Hash: ' + (result && result.txHash ? result.txHash.slice(0, 16) + '...' : 'submitted'));
1201
+ } catch (e) { toast('Transfer failed: ' + (e.message || e)); }
1202
+ } else {
1203
+ try {
1204
+ const res = await fetch(API + '/api/transfer', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({to, amount}) });
1205
+ const data = await res.json();
1206
+ toast(data.ok ? 'Transfer sent! Hash: ' + (data.txHash || '').slice(0, 16) + '...' : 'Error: ' + data.error);
1207
+ } catch (e) { toast('Transfer failed: ' + e.message); }
1208
+ }
1209
+ setTimeout(fetchStatus, 3000);
1210
+ }
1211
+
1212
+ function closeInstallModal() {
1213
+ document.getElementById('installModal').classList.remove('open');
1214
+ }
1215
+
1216
+ function handleRegisterAgent() {
1217
+ if (cachedAgentName) {
1218
+ toast('Already registered as "' + cachedAgentName + '"');
1219
+ return;
1220
+ }
1221
+ openRegisterModal();
1222
+ }
1223
+
1224
+ function openRegisterModal() {
1225
+ document.getElementById('registerNameInput').value = '';
1226
+ document.getElementById('registerModal').classList.add('open');
1227
+ setTimeout(() => document.getElementById('registerNameInput').focus(), 50);
1228
+ }
1229
+
1230
+ function closeRegisterModal() {
1231
+ document.getElementById('registerModal').classList.remove('open');
1232
+ }
1233
+
1234
+ async function submitRegisterAgent() {
1235
+ const raw = document.getElementById('registerNameInput').value.trim();
1236
+ const name = raw.replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 32);
1237
+ if (!name) { toast('Please enter an agent name'); return; }
1238
+ closeRegisterModal();
1239
+ if (window.clawNetwork) {
1240
+ try {
1241
+ toast('Approve registration in extension...');
1242
+ await window.clawNetwork.request({ method: 'claw_requestAccounts' });
1243
+ await window.clawNetwork.request({ method: 'claw_registerAgent', params: [name] });
1244
+ toast('Agent "' + name + '" registered!');
1245
+ } catch (e) { toast('Registration failed: ' + (e.message || e)); }
1246
+ } else {
1247
+ try {
1248
+ const res = await fetch(API + '/api/agent/register', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({name}) });
1249
+ const data = await res.json();
1250
+ toast(data.ok ? 'Agent "' + name + '" registered!' : 'Error: ' + data.error);
1251
+ } catch (e) { toast('Registration failed: ' + e.message); }
1252
+ }
1253
+ }
1254
+
851
1255
  async function fetchStatus() {
852
1256
  try {
853
1257
  const res = await fetch(API + '/api/status');
854
1258
  const data = await res.json();
855
1259
  renderStatus(data);
856
1260
  document.getElementById('lastUpdate').textContent = 'Updated: ' + new Date().toLocaleTimeString();
857
- } catch (e) { console.error(e); }
1261
+ } catch (e) {
1262
+ console.error(e);
1263
+ renderStatus({ running: false, blockHeight: null, peerCount: null, walletAddress: '', network: 'mainnet', syncMode: 'light', rpcUrl: 'http://localhost:19877', pluginVersion: '0.1.1', restartCount: 0, dataDir: '', balance: '', syncing: false, uptimeFormatted: '—', pid: null });
1264
+ }
858
1265
  }
859
1266
 
860
1267
  function renderStatus(s) {
@@ -874,11 +1281,36 @@ function buildUiHtml(cfg: PluginConfig): string {
874
1281
  document.getElementById('uptimeValue').textContent = s.uptimeFormatted || '—';
875
1282
 
876
1283
  // Wallet
877
- const wHtml = s.walletAddress
878
- ? '<div class="wallet-addr">' + s.walletAddress + ' <button class="copy-btn" onclick="copyText(\\''+s.walletAddress+'\\')">Copy</button></div>' +
879
- (s.balance ? '<div style="margin-top:8px;font-size:14px;color:var(--green)">' + s.balance + '</div>' : '')
880
- : '<div style="color:var(--text-dim)">No wallet yet — start the node to generate one</div>';
881
- document.getElementById('walletInfo').innerHTML = wHtml;
1284
+ cachedAddress = s.walletAddress || '';
1285
+ cachedNetwork = s.network || '';
1286
+ if (s.walletAddress) {
1287
+ document.getElementById('walletEmpty').style.display = 'none';
1288
+ document.getElementById('walletLoaded').style.display = '';
1289
+ document.getElementById('walletAddrText').textContent = s.walletAddress;
1290
+ document.getElementById('walletBalance').textContent = s.balance || '—';
1291
+ // Agent status
1292
+ cachedAgentName = s.agentName || '';
1293
+ const regCard = document.getElementById('qaRegister');
1294
+ const regLabel = document.getElementById('qaRegisterLabel');
1295
+ const regHint = document.getElementById('qaRegisterHint');
1296
+ if (cachedAgentName) {
1297
+ regLabel.textContent = 'Agent Registered';
1298
+ regHint.innerHTML = '<span style="color:var(--green)">&#x2713; ' + cachedAgentName + '</span>';
1299
+ regCard.style.borderColor = 'var(--green)';
1300
+ regCard.style.opacity = '0.85';
1301
+ } else {
1302
+ regLabel.textContent = 'Register Agent';
1303
+ regHint.textContent = 'On-chain identity';
1304
+ regCard.style.borderColor = '';
1305
+ regCard.style.opacity = '';
1306
+ }
1307
+ // Extension detection hint
1308
+ const hasExt = !!(window.clawNetwork && window.clawNetwork.isClawNetwork);
1309
+ document.getElementById('qaImportHint').textContent = hasExt ? 'Extension detected — click to import' : 'Install wallet extension first';
1310
+ } else {
1311
+ document.getElementById('walletEmpty').style.display = '';
1312
+ document.getElementById('walletLoaded').style.display = 'none';
1313
+ }
882
1314
 
883
1315
  // Node info
884
1316
  const rows = [
@@ -923,152 +1355,375 @@ function buildUiHtml(cfg: PluginConfig): string {
923
1355
  </html>`
924
1356
  }
925
1357
 
926
- let uiServer: unknown = null
927
-
928
- function startUiServer(cfg: PluginConfig, api: OpenClawApi): string | null {
929
- const http = require('http')
1358
+ // ── UI Server (standalone script, forked as background process) ──
930
1359
 
931
- if (uiServer) return null
1360
+ const UI_SERVER_SCRIPT = `
1361
+ const http = require('http');
1362
+ const fs = require('fs');
1363
+ const os = require('os');
1364
+ const path = require('path');
932
1365
 
933
- let actualPort = cfg.uiPort
934
- const maxRetries = 10
1366
+ const PORT = parseInt(process.argv[2] || '19877', 10);
1367
+ const RPC_PORT = parseInt(process.argv[3] || '9710', 10);
1368
+ const LOG_PATH = process.argv[4] || path.join(os.homedir(), '.openclaw/workspace/clawnetwork/node.log');
1369
+ const PORT_FILE = path.join(os.homedir(), '.openclaw/clawnetwork-ui-port');
1370
+ const MAX_RETRIES = 10;
935
1371
 
936
- const handleRequest = async (req: { method: string; url: string }, res: { writeHead: (s: number, h?: Record<string, string>) => void; end: (data?: string) => void; setHeader: (k: string, v: string) => void }) => {
937
- const url = new URL(req.url, `http://localhost:${actualPort}`)
938
- const pathname = url.pathname
1372
+ async function fetchJson(url) {
1373
+ const r = await fetch(url);
1374
+ return r.json();
1375
+ }
939
1376
 
940
- res.setHeader('Access-Control-Allow-Origin', '*')
941
- res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
942
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type')
943
- if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return }
1377
+ async function rpcCall(method, params) {
1378
+ const r = await fetch('http://localhost:' + RPC_PORT, {
1379
+ method: 'POST',
1380
+ headers: { 'Content-Type': 'application/json' },
1381
+ body: JSON.stringify({ jsonrpc: '2.0', method, params: params || [], id: Date.now() }),
1382
+ });
1383
+ const d = await r.json();
1384
+ if (d.error) throw new Error(d.error.message || JSON.stringify(d.error));
1385
+ return d.result;
1386
+ }
944
1387
 
945
- const json = (status: number, data: unknown) => {
946
- res.writeHead(status, { 'content-type': 'application/json' })
947
- res.end(JSON.stringify(data))
948
- }
1388
+ function formatClaw(raw) {
1389
+ const v = BigInt(raw);
1390
+ const ONE = BigInt(1e9);
1391
+ const w = v / ONE;
1392
+ const f = v % ONE;
1393
+ if (f === 0n) return w + ' CLAW';
1394
+ return w + '.' + f.toString().padStart(9, '0').replace(/0+$/, '') + ' CLAW';
1395
+ }
949
1396
 
950
- // Serve dashboard
951
- if (pathname === '/' || pathname === '/index.html') {
952
- res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' })
953
- res.end(buildUiHtml(cfg))
954
- return
955
- }
1397
+ function readBody(req) {
1398
+ return new Promise((resolve, reject) => {
1399
+ let data = '';
1400
+ req.on('data', (chunk) => { data += chunk; });
1401
+ req.on('end', () => { try { resolve(JSON.parse(data || '{}')); } catch { resolve({}); } });
1402
+ req.on('error', reject);
1403
+ setTimeout(() => reject(new Error('Body read timeout')), 10000);
1404
+ });
1405
+ }
956
1406
 
957
- // Status API
958
- if (pathname === '/api/status' && req.method === 'GET') {
959
- const health = await checkHealth(cfg.rpcPort)
960
- lastHealth = health
961
- const status = buildStatus(cfg)
962
- // Enrich with balance
963
- let balance = ''
964
- const wallet = loadWallet()
965
- if (wallet?.address) {
966
- try {
967
- const raw = await rpcCall(cfg.rpcPort, 'claw_getBalance', [wallet.address])
968
- balance = formatClaw(String(raw as string))
969
- } catch { /* ok */ }
970
- }
971
- json(200, { ...status, balance, syncing: health.syncing })
972
- return
973
- }
1407
+ function findNodeBinary() {
1408
+ const binDir = path.join(os.homedir(), '.openclaw/bin');
1409
+ const dataDir = path.join(os.homedir(), '.clawnetwork');
1410
+ const binName = process.platform === 'win32' ? 'claw-node.exe' : 'claw-node';
1411
+ let binary = path.join(binDir, binName);
1412
+ if (fs.existsSync(binary)) return binary;
1413
+ binary = path.join(dataDir, 'bin', 'claw-node');
1414
+ if (fs.existsSync(binary)) return binary;
1415
+ return null;
1416
+ }
974
1417
 
975
- // Logs API
976
- if (pathname === '/api/logs' && req.method === 'GET') {
977
- try {
978
- if (!fs.existsSync(LOG_PATH)) { json(200, { logs: 'No logs yet' }); return }
979
- const content = fs.readFileSync(LOG_PATH, 'utf8')
980
- const lines = content.split('\n')
981
- json(200, { logs: lines.slice(-80).join('\n') })
982
- } catch (e: unknown) {
983
- json(500, { error: (e as Error).message })
984
- }
985
- return
986
- }
1418
+ async function handle(req, res) {
1419
+ const url = new URL(req.url, 'http://localhost:' + PORT);
1420
+ const p = url.pathname;
1421
+ res.setHeader('Access-Control-Allow-Origin', '*');
1422
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
1423
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
1424
+ if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return; }
987
1425
 
988
- // Action API
989
- if (pathname.startsWith('/api/action/') && req.method === 'POST') {
990
- const action = pathname.split('/').pop()
1426
+ const json = (s, d) => { res.writeHead(s, { 'content-type': 'application/json' }); res.end(JSON.stringify(d)); };
991
1427
 
992
- if (action === 'start') {
993
- let binary = findBinary()
994
- if (!binary && cfg.autoDownload) {
995
- try { binary = await downloadBinary(api) } catch (e: unknown) {
996
- json(500, { error: (e as Error).message }); return
997
- }
1428
+ if (p === '/' || p === '/index.html') {
1429
+ res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
1430
+ res.end(HTML);
1431
+ return;
1432
+ }
1433
+ if (p === '/api/status') {
1434
+ try {
1435
+ const h = await fetchJson('http://localhost:' + RPC_PORT + '/health');
1436
+ let balance = '';
1437
+ let walletAddress = '';
1438
+ let agentName = '';
1439
+ try {
1440
+ const walletPath = path.join(os.homedir(), '.openclaw/workspace/clawnetwork/wallet.json');
1441
+ const w = JSON.parse(fs.readFileSync(walletPath, 'utf8'));
1442
+ walletAddress = w.address || '';
1443
+ if (w.address) {
1444
+ const b = await rpcCall('claw_getBalance', [w.address]); balance = formatClaw(b);
1445
+ try { const ag = await rpcCall('claw_getAgent', [w.address]); agentName = (ag && ag.name) ? ag.name : ''; } catch {}
998
1446
  }
999
- if (!binary) { json(400, { error: 'claw-node binary not found' }); return }
1000
- if (nodeProcess && !nodeProcess.killed) { json(200, { message: 'Node already running' }); return }
1001
- initNode(binary, cfg.network, api)
1002
- startNodeProcess(binary, cfg, api)
1003
- json(200, { message: 'Node starting...' })
1004
- return
1005
- }
1006
-
1007
- if (action === 'stop') {
1008
- stopNode(api)
1009
- json(200, { message: 'Node stopped' })
1010
- return
1447
+ } catch {}
1448
+ json(200, {
1449
+ running: h.status === 'ok',
1450
+ blockHeight: h.height,
1451
+ peerCount: h.peer_count,
1452
+ network: h.chain_id,
1453
+ syncMode: 'light',
1454
+ rpcUrl: 'http://localhost:' + RPC_PORT,
1455
+ walletAddress,
1456
+ binaryVersion: h.version,
1457
+ pluginVersion: '0.1.1',
1458
+ uptime: h.uptime_secs,
1459
+ uptimeFormatted: h.uptime_secs < 60 ? h.uptime_secs + 's' : h.uptime_secs < 3600 ? Math.floor(h.uptime_secs/60) + 'm' : Math.floor(h.uptime_secs/3600) + 'h ' + Math.floor((h.uptime_secs%3600)/60) + 'm',
1460
+ restartCount: 0, dataDir: path.join(os.homedir(), '.clawnetwork'), balance, agentName, syncing: h.status === 'degraded',
1461
+ });
1462
+ } catch {
1463
+ const walletAddr = (() => { try { return JSON.parse(fs.readFileSync(path.join(os.homedir(), '.openclaw/workspace/clawnetwork/wallet.json'), 'utf8')).address; } catch { return ''; } })();
1464
+ json(200, { running: false, blockHeight: null, peerCount: null, walletAddress: walletAddr, network: 'mainnet', syncMode: 'light', rpcUrl: 'http://localhost:' + RPC_PORT, pluginVersion: '0.1.1', restartCount: 0, dataDir: path.join(os.homedir(), '.clawnetwork'), balance: '', agentName: '', syncing: false, uptimeFormatted: '—', pid: null });
1011
1465
  }
1012
-
1013
- if (action === 'faucet') {
1014
- const wallet = loadWallet()
1015
- if (!wallet?.address) { json(400, { error: 'No wallet' }); return }
1466
+ return;
1467
+ }
1468
+ if (p === '/api/logs') {
1469
+ try {
1470
+ if (!fs.existsSync(LOG_PATH)) { json(200, { logs: 'No logs yet' }); return; }
1471
+ const c = fs.readFileSync(LOG_PATH, 'utf8').split('\\n');
1472
+ json(200, { logs: c.slice(-80).join('\\n') });
1473
+ } catch (e) { json(500, { error: e.message }); }
1474
+ return;
1475
+ }
1476
+ if (p === '/api/wallet/export') {
1477
+ // Only allow from localhost (127.0.0.1) — never expose to network
1478
+ const host = req.headers.host || '';
1479
+ if (!host.startsWith('127.0.0.1') && !host.startsWith('localhost')) {
1480
+ json(403, { error: 'Wallet export only available from localhost' });
1481
+ return;
1482
+ }
1483
+ try {
1484
+ const walletPath = path.join(os.homedir(), '.openclaw/workspace/clawnetwork/wallet.json');
1485
+ const w = JSON.parse(fs.readFileSync(walletPath, 'utf8'));
1486
+ json(200, { address: w.address, secretKey: w.secret_key || w.secretKey || w.private_key || '' });
1487
+ } catch (e) { json(400, { error: 'No wallet found' }); }
1488
+ return;
1489
+ }
1490
+ // ── Business API endpoints (mirrors Gateway methods) ──
1491
+ if (p === '/api/wallet/balance') {
1492
+ try {
1493
+ const walletPath = path.join(os.homedir(), '.openclaw/workspace/clawnetwork/wallet.json');
1494
+ const w = JSON.parse(fs.readFileSync(walletPath, 'utf8'));
1495
+ const address = new URL(req.url, 'http://localhost').searchParams.get('address') || w.address;
1496
+ const b = await rpcCall('claw_getBalance', [address]);
1497
+ json(200, { address, balance: String(b), formatted: formatClaw(b) });
1498
+ } catch (e) { json(400, { error: e.message }); }
1499
+ return;
1500
+ }
1501
+ if (p === '/api/transfer' && req.method === 'POST') {
1502
+ try {
1503
+ const body = await readBody(req);
1504
+ const { to, amount } = body;
1505
+ if (!to || !amount) { json(400, { error: 'Missing params: to, amount' }); return; }
1506
+ if (!/^[0-9a-f]{64}$/i.test(to)) { json(400, { error: 'Invalid address (64 hex chars)' }); return; }
1507
+ if (!/^\\d+(\\.\\d+)?$/.test(amount) || parseFloat(amount) <= 0) { json(400, { error: 'Invalid amount' }); return; }
1508
+ const bin = findNodeBinary();
1509
+ if (!bin) { json(400, { error: 'claw-node binary not found' }); return; }
1510
+ const { execFileSync } = require('child_process');
1511
+ const out = execFileSync(bin, ['transfer', to, amount, '--rpc', 'http://localhost:' + RPC_PORT], { encoding: 'utf8', timeout: 30000, env: { HOME: os.homedir(), PATH: process.env.PATH || '' } });
1512
+ const h = out.match(/[0-9a-f]{64}/i);
1513
+ json(200, { ok: true, txHash: h ? h[0] : '', to, amount });
1514
+ } catch (e) { json(500, { error: e.message }); }
1515
+ return;
1516
+ }
1517
+ if (p === '/api/stake' && req.method === 'POST') {
1518
+ try {
1519
+ const body = await readBody(req);
1520
+ const { amount, action } = body;
1521
+ if (!amount && action !== 'claim') { json(400, { error: 'Missing amount' }); return; }
1522
+ const bin = findNodeBinary();
1523
+ if (!bin) { json(400, { error: 'claw-node binary not found' }); return; }
1524
+ const { execFileSync } = require('child_process');
1525
+ const cmd = action === 'withdraw' ? 'stake withdraw' : action === 'claim' ? 'stake claim' : 'stake deposit';
1526
+ const args = cmd.split(' ').concat(amount ? [amount] : []).concat(['--rpc', 'http://localhost:' + RPC_PORT]);
1527
+ const out = execFileSync(bin, args, { encoding: 'utf8', timeout: 30000, env: { HOME: os.homedir(), PATH: process.env.PATH || '' } });
1528
+ json(200, { ok: true, raw: out.trim() });
1529
+ } catch (e) { json(500, { error: e.message }); }
1530
+ return;
1531
+ }
1532
+ if (p === '/api/agent/register' && req.method === 'POST') {
1533
+ try {
1534
+ const body = await readBody(req);
1535
+ const name = (body.name || 'openclaw-agent-' + Date.now().toString(36)).replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 32);
1536
+ const bin = findNodeBinary();
1537
+ if (!bin) { json(400, { error: 'claw-node binary not found' }); return; }
1538
+ const { execFileSync } = require('child_process');
1539
+ const out = execFileSync(bin, ['agent', 'register', '--name', name, '--rpc', 'http://localhost:' + RPC_PORT], { encoding: 'utf8', timeout: 30000, env: { HOME: os.homedir(), PATH: process.env.PATH || '' } });
1540
+ const h = out.match(/[0-9a-f]{64}/i);
1541
+ json(200, { ok: true, txHash: h ? h[0] : '', name });
1542
+ } catch (e) { json(500, { error: e.message }); }
1543
+ return;
1544
+ }
1545
+ if (p === '/api/service/register' && req.method === 'POST') {
1546
+ try {
1547
+ const body = await readBody(req);
1548
+ const { serviceType, endpoint, description, priceAmount } = body;
1549
+ if (!serviceType || !endpoint) { json(400, { error: 'Missing: serviceType, endpoint' }); return; }
1550
+ const bin = findNodeBinary();
1551
+ if (!bin) { json(400, { error: 'claw-node binary not found' }); return; }
1552
+ const { execFileSync } = require('child_process');
1553
+ const out = execFileSync(bin, ['service', 'register', '--type', serviceType, '--endpoint', endpoint, '--description', description || '', '--price', priceAmount || '0', '--rpc', 'http://localhost:' + RPC_PORT], { encoding: 'utf8', timeout: 30000, env: { HOME: os.homedir(), PATH: process.env.PATH || '' } });
1554
+ json(200, { ok: true, raw: out.trim() });
1555
+ } catch (e) { json(500, { error: e.message }); }
1556
+ return;
1557
+ }
1558
+ if (p === '/api/service/search') {
1559
+ try {
1560
+ const t = new URL(req.url, 'http://localhost').searchParams.get('type');
1561
+ const result = await rpcCall('claw_getServices', t ? [t] : []);
1562
+ json(200, { services: result });
1563
+ } catch (e) { json(500, { error: e.message }); }
1564
+ return;
1565
+ }
1566
+ if (p === '/api/node/config') {
1567
+ try {
1568
+ const cfgPath = path.join(os.homedir(), '.openclaw/workspace/clawnetwork/config.json');
1569
+ const cfg = fs.existsSync(cfgPath) ? JSON.parse(fs.readFileSync(cfgPath, 'utf8')) : {};
1570
+ json(200, { ...cfg, rpcPort: RPC_PORT, uiPort: PORT });
1571
+ } catch (e) { json(200, { rpcPort: RPC_PORT, uiPort: PORT }); }
1572
+ return;
1573
+ }
1574
+ if (p.startsWith('/api/action/') && req.method === 'POST') {
1575
+ const a = p.split('/').pop();
1576
+ if (a === 'faucet') {
1577
+ try {
1578
+ const w = JSON.parse(fs.readFileSync(path.join(os.homedir(), '.openclaw/workspace/clawnetwork/wallet.json'), 'utf8'));
1579
+ const r = await rpcCall('claw_faucet', [w.address]);
1580
+ json(200, { message: 'Faucet success', ...r });
1581
+ } catch (e) { json(400, { error: e.message }); }
1582
+ return;
1583
+ }
1584
+ if (a === 'start') {
1585
+ try {
1586
+ // Check if already running
1587
+ const pidFile = path.join(os.homedir(), '.openclaw/workspace/clawnetwork/node.pid');
1016
1588
  try {
1017
- const result = await rpcCall(cfg.rpcPort, 'claw_faucet', [wallet.address])
1018
- json(200, { message: 'Faucet success', ...(result as Record<string, unknown>) })
1019
- } catch (e: unknown) {
1020
- json(400, { error: (e as Error).message, message: 'Faucet only works on testnet/devnet' })
1589
+ const pid = parseInt(fs.readFileSync(pidFile, 'utf8').trim(), 10);
1590
+ if (pid > 0) { try { process.kill(pid, 0); json(200, { message: 'Node already running', pid }); return; } catch {} }
1591
+ } catch {}
1592
+ // Find binary
1593
+ const binDir = path.join(os.homedir(), '.openclaw/bin');
1594
+ const dataDir = path.join(os.homedir(), '.clawnetwork');
1595
+ const binName = process.platform === 'win32' ? 'claw-node.exe' : 'claw-node';
1596
+ let binary = path.join(binDir, binName);
1597
+ if (!fs.existsSync(binary)) { binary = path.join(dataDir, 'bin', 'claw-node'); }
1598
+ if (!fs.existsSync(binary)) { json(400, { error: 'claw-node binary not found. Run: openclaw clawnetwork:download' }); return; }
1599
+ // Read config for network/ports
1600
+ const cfgPath = path.join(os.homedir(), '.openclaw/workspace/clawnetwork/config.json');
1601
+ let network = 'mainnet', p2pPort = 9711, syncMode = 'light', extraPeers = [];
1602
+ try {
1603
+ const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
1604
+ if (cfg.network) network = cfg.network;
1605
+ if (cfg.p2pPort) p2pPort = cfg.p2pPort;
1606
+ if (cfg.syncMode) syncMode = cfg.syncMode;
1607
+ if (cfg.extraBootstrapPeers) extraPeers = cfg.extraBootstrapPeers;
1608
+ } catch {}
1609
+ const bootstrapPeers = { mainnet: ['/ip4/178.156.162.162/tcp/9711'], testnet: ['/ip4/178.156.162.162/tcp/9721'], devnet: [] };
1610
+ const peers = [...(bootstrapPeers[network] || []), ...extraPeers];
1611
+ const args = ['start', '--network', network, '--rpc-port', String(RPC_PORT), '--p2p-port', String(p2pPort), '--sync-mode', syncMode, '--allow-genesis'];
1612
+ for (const peer of peers) { args.push('--bootstrap', peer); }
1613
+ // Spawn detached
1614
+ const logPath = path.join(os.homedir(), '.openclaw/workspace/clawnetwork/node.log');
1615
+ const logFd = fs.openSync(logPath, 'a');
1616
+ const { spawn: nodeSpawn } = require('child_process');
1617
+ const child = nodeSpawn(binary, args, {
1618
+ stdio: ['ignore', logFd, logFd],
1619
+ detached: true,
1620
+ env: { HOME: os.homedir(), PATH: process.env.PATH || '/usr/local/bin:/usr/bin:/bin', RUST_LOG: process.env.RUST_LOG || 'claw=info' },
1621
+ });
1622
+ child.unref();
1623
+ fs.closeSync(logFd);
1624
+ fs.writeFileSync(pidFile, String(child.pid));
1625
+ // Remove stop signal if exists
1626
+ const stopFile = path.join(os.homedir(), '.openclaw/workspace/clawnetwork/stop.signal');
1627
+ try { fs.unlinkSync(stopFile); } catch {}
1628
+ json(200, { message: 'Node started', pid: child.pid });
1629
+ } catch (e) { json(500, { error: e.message }); }
1630
+ return;
1631
+ }
1632
+ if (a === 'stop') {
1633
+ try {
1634
+ const pidFile = path.join(os.homedir(), '.openclaw/workspace/clawnetwork/node.pid');
1635
+ let pid = null;
1636
+ try { pid = parseInt(fs.readFileSync(pidFile, 'utf8').trim(), 10); } catch {}
1637
+ if (pid && pid > 0) {
1638
+ try { process.kill(pid, 'SIGTERM'); } catch {}
1021
1639
  }
1022
- return
1023
- }
1024
-
1025
- json(404, { error: 'Unknown action' })
1026
- return
1640
+ // Write stop signal for restart loop
1641
+ const stopFile = path.join(os.homedir(), '.openclaw/workspace/clawnetwork/stop.signal');
1642
+ try { fs.writeFileSync(stopFile, String(Date.now())); } catch {}
1643
+ // Also kill by name (covers orphans)
1644
+ try { require('child_process').execFileSync('pkill', ['-f', 'claw-node start'], { timeout: 3000 }); } catch {}
1645
+ try { fs.unlinkSync(pidFile); } catch {}
1646
+ json(200, { message: 'Node stopped' });
1647
+ } catch (e) { json(500, { error: e.message }); }
1648
+ return;
1027
1649
  }
1028
-
1029
- json(404, { error: 'Not found' })
1650
+ if (a === 'restart') {
1651
+ json(200, { message: 'Use Stop then Start to restart the node' });
1652
+ return;
1653
+ }
1654
+ json(400, { error: 'Unknown action: ' + a });
1655
+ return;
1030
1656
  }
1657
+ json(404, { error: 'Not found' });
1658
+ }
1031
1659
 
1032
- // Try ports with fallback
1033
- for (let attempt = 0; attempt < maxRetries; attempt++) {
1034
- const tryPort = cfg.uiPort + attempt
1035
- try {
1036
- const server = http.createServer((req: unknown, res: unknown) => {
1037
- handleRequest(req as any, res as any).catch((err: Error) => {
1038
- try { (res as any).writeHead(500); (res as any).end(JSON.stringify({ error: err.message })) } catch { /* ok */ }
1039
- })
1040
- })
1660
+ function tryListen(attempt) {
1661
+ if (attempt >= MAX_RETRIES) { console.error('Failed to bind UI server'); process.exit(1); }
1662
+ const port = PORT + attempt;
1663
+ const srv = http.createServer((req, res) => handle(req, res).catch(e => { try { res.writeHead(500); res.end(e.message); } catch {} }));
1664
+ srv.on('error', () => tryListen(attempt + 1));
1665
+ srv.listen(port, '127.0.0.1', () => {
1666
+ fs.mkdirSync(path.dirname(PORT_FILE), { recursive: true });
1667
+ fs.writeFileSync(PORT_FILE, JSON.stringify({ port, pid: process.pid, startedAt: new Date().toISOString() }));
1668
+ console.log('ClawNetwork Dashboard: http://127.0.0.1:' + port);
1669
+ process.on('SIGINT', () => { try { fs.unlinkSync(PORT_FILE); } catch {} process.exit(0); });
1670
+ process.on('SIGTERM', () => { try { fs.unlinkSync(PORT_FILE); } catch {} process.exit(0); });
1671
+ });
1672
+ }
1673
+ tryListen(0);
1674
+ `
1041
1675
 
1042
- // Synchronous-ish listen check
1043
- let bound = false
1044
- server.listen(tryPort, '127.0.0.1')
1045
- server.on('listening', () => { bound = true })
1046
- server.on('error', () => { /* handled below */ })
1676
+ function startUiServer(cfg: PluginConfig, api: OpenClawApi): string | null {
1677
+ // Check if already running
1678
+ const existing = getDashboardUrl()
1679
+ if (existing) {
1680
+ api.logger?.info?.(`[clawnetwork] dashboard already running: ${existing}`)
1681
+ return existing
1682
+ }
1047
1683
 
1048
- // Give it a moment
1049
- actualPort = tryPort
1050
- uiServer = server
1684
+ // Write the standalone UI server script to a temp file and fork it
1685
+ const scriptPath = path.join(WORKSPACE_DIR, 'ui-server.js')
1686
+ ensureDir(WORKSPACE_DIR)
1051
1687
 
1052
- // Write port file for discovery
1053
- ensureDir(path.dirname(UI_PORT_FILE))
1054
- fs.writeFileSync(UI_PORT_FILE, JSON.stringify({ port: tryPort, pid: process.pid, startedAt: new Date().toISOString() }))
1688
+ // Write HTML to a separate file, script reads it at startup
1689
+ const htmlPath = path.join(WORKSPACE_DIR, 'ui-dashboard.html')
1690
+ fs.writeFileSync(htmlPath, buildUiHtml(cfg))
1055
1691
 
1056
- api.logger?.info?.(`[clawnetwork] dashboard: http://127.0.0.1:${tryPort}`)
1057
- return `http://127.0.0.1:${tryPort}`
1058
- } catch {
1059
- continue
1692
+ // Inject HTML path into script (read from file, no template escaping issues)
1693
+ const fullScript = `const HTML_PATH = ${JSON.stringify(htmlPath)};\nconst HTML = require('fs').readFileSync(HTML_PATH, 'utf8');\n${UI_SERVER_SCRIPT}`
1694
+ fs.writeFileSync(scriptPath, fullScript)
1695
+
1696
+ try {
1697
+ const child = fork(scriptPath, [String(cfg.uiPort), String(cfg.rpcPort), LOG_PATH], {
1698
+ detached: true,
1699
+ stdio: 'ignore',
1700
+ })
1701
+ child.unref()
1702
+ api.logger?.info?.(`[clawnetwork] dashboard starting on http://127.0.0.1:${cfg.uiPort}`)
1703
+
1704
+ // Wait briefly for port file
1705
+ for (let i = 0; i < 10; i++) {
1706
+ const url = getDashboardUrl()
1707
+ if (url) return url
1708
+ // Busy-wait 200ms (can't use async sleep here)
1709
+ const start = Date.now()
1710
+ while (Date.now() - start < 200) { /* spin */ }
1060
1711
  }
1712
+ return `http://127.0.0.1:${cfg.uiPort}`
1713
+ } catch (e: unknown) {
1714
+ api.logger?.warn?.(`[clawnetwork] failed to start dashboard: ${(e as Error).message}`)
1715
+ return null
1061
1716
  }
1062
-
1063
- api.logger?.warn?.('[clawnetwork] failed to start dashboard UI server')
1064
- return null
1065
1717
  }
1066
1718
 
1067
1719
  function stopUiServer(): void {
1068
- if (uiServer) {
1069
- try { uiServer.close() } catch { /* ok */ }
1070
- uiServer = null
1071
- }
1720
+ try {
1721
+ const raw = fs.readFileSync(UI_PORT_FILE, 'utf8')
1722
+ const info = JSON.parse(raw)
1723
+ if (info.pid) {
1724
+ try { process.kill(info.pid, 'SIGTERM') } catch { /* ok */ }
1725
+ }
1726
+ } catch { /* no file */ }
1072
1727
  try { fs.unlinkSync(UI_PORT_FILE) } catch { /* ok */ }
1073
1728
  }
1074
1729
 
@@ -1266,11 +1921,15 @@ export default function register(api: OpenClawApi) {
1266
1921
  }
1267
1922
  initNode(binary, cfg.network, api)
1268
1923
  startNodeProcess(binary, cfg, api)
1269
- out({ message: 'Node started', pid: nodeProcess?.pid, network: cfg.network, rpc: `http://localhost:${cfg.rpcPort}` })
1924
+ // Start UI dashboard
1925
+ const dashUrl = startUiServer(cfg, api)
1926
+ out({ message: 'Node started', pid: nodeProcess?.pid, network: cfg.network, rpc: `http://localhost:${cfg.rpcPort}`, dashboard: dashUrl || `http://127.0.0.1:${cfg.uiPort}` })
1270
1927
  }
1271
1928
 
1272
1929
  const handleStop = () => {
1930
+ stopMinerHeartbeatLoop()
1273
1931
  stopNode(api)
1932
+ stopUiServer()
1274
1933
  out({ message: 'Node stopped' })
1275
1934
  }
1276
1935
 
@@ -1521,16 +2180,23 @@ export default function register(api: OpenClawApi) {
1521
2180
  // Step 3: Wallet
1522
2181
  const wallet = ensureWallet(cfg.network, api)
1523
2182
 
1524
- // Step 4: Start node
2183
+ // Step 4: Save config for UI server to read
2184
+ const cfgPath = path.join(WORKSPACE_DIR, 'config.json')
2185
+ fs.writeFileSync(cfgPath, JSON.stringify({ network: cfg.network, rpcPort: cfg.rpcPort, p2pPort: cfg.p2pPort, syncMode: cfg.syncMode, extraBootstrapPeers: cfg.extraBootstrapPeers }))
2186
+
2187
+ // Step 5: Start node
1525
2188
  startNodeProcess(binary, cfg, api)
1526
2189
 
1527
- // Step 5: Start UI dashboard
2190
+ // Step 6: Start UI dashboard
1528
2191
  startUiServer(cfg, api)
1529
2192
 
1530
- // Step 6: Auto-register agent (wait for node to sync)
2193
+ // Step 7: Wait for node to sync, then auto-register
1531
2194
  await sleep(15_000)
1532
2195
  await autoRegisterAgent(cfg, wallet, api)
1533
2196
 
2197
+ // Step 8: Auto-register as miner + start heartbeat loop
2198
+ await autoRegisterMiner(cfg, wallet, api)
2199
+
1534
2200
  } catch (err: unknown) {
1535
2201
  api.logger?.error?.(`[clawnetwork] startup failed: ${(err as Error).message}`)
1536
2202
  }
@@ -1538,6 +2204,7 @@ export default function register(api: OpenClawApi) {
1538
2204
  },
1539
2205
  stop: () => {
1540
2206
  api.logger?.info?.('[clawnetwork] shutting down...')
2207
+ stopMinerHeartbeatLoop()
1541
2208
  stopNode(api)
1542
2209
  stopUiServer()
1543
2210
  },
@@ -35,7 +35,7 @@
35
35
  "syncMode": {
36
36
  "type": "string",
37
37
  "enum": ["full", "fast", "light"],
38
- "default": "full"
38
+ "default": "light"
39
39
  },
40
40
  "healthCheckSeconds": {
41
41
  "type": "number",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clawlabz/clawnetwork",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Run a ClawNetwork blockchain node inside OpenClaw. Every agent is a blockchain node.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -42,6 +42,12 @@
42
42
  "install": {
43
43
  "npmSpec": "@clawlabz/clawnetwork",
44
44
  "defaultChoice": "npm"
45
+ },
46
+ "compat": {
47
+ "pluginApi": ">=2026.3.24"
48
+ },
49
+ "build": {
50
+ "openclawVersion": "2026.3.28"
45
51
  }
46
52
  },
47
53
  "publishConfig": {