@clawlabz/clawnetwork 0.1.0 → 0.1.1

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
  // ============================================================
@@ -775,6 +872,24 @@ function buildUiHtml(cfg: PluginConfig): string {
775
872
 
776
873
  .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
874
  .toast.show { opacity: 1; }
875
+
876
+ .quick-actions { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 10px; margin: 16px 0; }
877
+ .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); }
878
+ .quick-action:hover { border-color: var(--accent); color: var(--accent); transform: translateY(-1px); }
879
+ .quick-action .qa-icon { font-size: 18px; width: 28px; text-align: center; }
880
+ .quick-action .qa-label { font-weight: 500; }
881
+ .quick-action .qa-hint { font-size: 11px; color: var(--text-dim); margin-top: 2px; }
882
+ .quick-action.warn:hover { border-color: var(--danger); color: var(--danger); }
883
+
884
+ .modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.7); display: none; align-items: center; justify-content: center; z-index: 200; }
885
+ .modal-overlay.open { display: flex; }
886
+ .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); }
887
+ .modal-title { font-size: 16px; font-weight: 700; margin-bottom: 12px; }
888
+ .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; }
889
+ .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; }
890
+ .modal-actions { display: flex; gap: 8px; margin-top: 16px; justify-content: flex-end; }
891
+ .modal-close { background: none; border: 1px solid var(--border); color: var(--text-dim); padding: 8px 16px; border-radius: 6px; cursor: pointer; font-size: 13px; }
892
+ .modal-close:hover { border-color: var(--text); color: var(--text); }
778
893
  </style>
779
894
  </head>
780
895
  <body>
@@ -818,6 +933,36 @@ function buildUiHtml(cfg: PluginConfig): string {
818
933
  <div class="panel">
819
934
  <div class="panel-title">Wallet</div>
820
935
  <div id="walletInfo">Loading...</div>
936
+ <div class="quick-actions" id="walletActions" style="display:none">
937
+ <div class="quick-action" onclick="copyAddress()">
938
+ <span class="qa-icon">&#x1F4CB;</span>
939
+ <div><div class="qa-label">Copy Address</div><div class="qa-hint">Share to receive CLAW</div></div>
940
+ </div>
941
+ <div class="quick-action" onclick="importToExtension()" id="qaImportExt" style="display:none">
942
+ <span class="qa-icon">&#x1F517;</span>
943
+ <div><div class="qa-label">Import to Extension</div><div class="qa-hint">One-click import to browser wallet</div></div>
944
+ </div>
945
+ <div class="quick-action warn" onclick="showExportKey()">
946
+ <span class="qa-icon">&#x1F511;</span>
947
+ <div><div class="qa-label">Export Private Key</div><div class="qa-hint">Manual copy for backup</div></div>
948
+ </div>
949
+ <div class="quick-action" onclick="openExplorer()">
950
+ <span class="qa-icon">&#x1F50D;</span>
951
+ <div><div class="qa-label">View on Explorer</div><div class="qa-hint">Transaction history</div></div>
952
+ </div>
953
+ <div class="quick-action" onclick="transferFromDashboard()">
954
+ <span class="qa-icon">&#x1F4B8;</span>
955
+ <div><div class="qa-label">Transfer CLAW</div><div class="qa-hint">Send to any address</div></div>
956
+ </div>
957
+ <div class="quick-action" onclick="registerAgentFromDashboard()">
958
+ <span class="qa-icon">&#x1F916;</span>
959
+ <div><div class="qa-label">Register Agent</div><div class="qa-hint">On-chain identity</div></div>
960
+ </div>
961
+ <div class="quick-action" onclick="openFaucet()">
962
+ <span class="qa-icon">&#x1F6B0;</span>
963
+ <div><div class="qa-label">Open Faucet</div><div class="qa-hint">Get testnet CLAW</div></div>
964
+ </div>
965
+ </div>
821
966
  </div>
822
967
 
823
968
  <div class="panel">
@@ -833,6 +978,20 @@ function buildUiHtml(cfg: PluginConfig): string {
833
978
 
834
979
  <div class="toast" id="toast"></div>
835
980
 
981
+ <div class="modal-overlay" id="exportModal" onclick="if(event.target===this)closeExportModal()">
982
+ <div class="modal">
983
+ <div class="modal-title">Export Private Key</div>
984
+ <div class="modal-warn">
985
+ &#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.
986
+ </div>
987
+ <div class="modal-key" id="exportKeyDisplay">Loading...</div>
988
+ <div class="modal-actions">
989
+ <button class="btn primary" onclick="copyExportKey()">Copy Private Key</button>
990
+ <button class="modal-close" onclick="closeExportModal()">Close</button>
991
+ </div>
992
+ </div>
993
+ </div>
994
+
836
995
  <script>
837
996
  const API = '';
838
997
  let autoRefresh = null;
@@ -844,10 +1003,167 @@ function buildUiHtml(cfg: PluginConfig): string {
844
1003
  setTimeout(() => el.classList.remove('show'), 3000);
845
1004
  }
846
1005
 
1006
+ let cachedAddress = '';
1007
+ let cachedNetwork = '';
1008
+ let cachedKey = '';
1009
+
847
1010
  function copyText(text) {
848
1011
  navigator.clipboard.writeText(text).then(() => toast('Copied!')).catch(() => {});
849
1012
  }
850
1013
 
1014
+ function copyAddress() {
1015
+ if (!cachedAddress) { toast('No wallet address'); return; }
1016
+ copyText(cachedAddress);
1017
+ toast('Address copied!');
1018
+ }
1019
+
1020
+ async function showExportKey() {
1021
+ document.getElementById('exportKeyDisplay').textContent = 'Loading...';
1022
+ document.getElementById('exportModal').classList.add('open');
1023
+ try {
1024
+ const res = await fetch(API + '/api/wallet/export');
1025
+ const data = await res.json();
1026
+ if (data.error) { document.getElementById('exportKeyDisplay').textContent = data.error; return; }
1027
+ cachedKey = data.secretKey;
1028
+ document.getElementById('exportKeyDisplay').textContent = data.secretKey;
1029
+ } catch (e) { document.getElementById('exportKeyDisplay').textContent = 'Failed to load'; }
1030
+ }
1031
+
1032
+ function closeExportModal() {
1033
+ document.getElementById('exportModal').classList.remove('open');
1034
+ cachedKey = '';
1035
+ document.getElementById('exportKeyDisplay').textContent = '';
1036
+ }
1037
+
1038
+ function copyExportKey() {
1039
+ if (!cachedKey) return;
1040
+ copyText(cachedKey);
1041
+ toast('Private key copied! Paste into browser extension to import.');
1042
+ }
1043
+
1044
+ function openExplorer() {
1045
+ if (!cachedAddress) { toast('No wallet address'); return; }
1046
+ window.open('https://explorer.clawlabz.xyz/address/' + cachedAddress, '_blank');
1047
+ }
1048
+
1049
+ function openFaucet() {
1050
+ window.open('https://chain.clawlabz.xyz/faucet', '_blank');
1051
+ }
1052
+
1053
+ // Detect ClawNetwork extension provider
1054
+ let hasExtension = false;
1055
+ function checkExtension() {
1056
+ if (window.clawNetwork && window.clawNetwork.isClawNetwork) {
1057
+ hasExtension = true;
1058
+ const el = document.getElementById('qaImportExt');
1059
+ if (el) el.style.display = '';
1060
+ }
1061
+ }
1062
+ // Check immediately and after a short delay (extension injects at document_start)
1063
+ checkExtension();
1064
+ setTimeout(checkExtension, 1000);
1065
+ setTimeout(checkExtension, 3000);
1066
+
1067
+ async function importToExtension() {
1068
+ // Try externally_connectable direct channel first (bypasses page JS context)
1069
+ const extIds = await detectExtensionIds();
1070
+ if (extIds.length > 0) {
1071
+ toast('Connecting to extension (secure channel)...');
1072
+ try {
1073
+ const res = await fetch(API + '/api/wallet/export');
1074
+ const data = await res.json();
1075
+ if (!data.secretKey) { toast('No private key found'); return; }
1076
+ // Direct to background — private key never in page JS event loop
1077
+ const extId = extIds[0];
1078
+ await chromeExtSend(extId, { method: 'claw_requestAccounts' });
1079
+ toast('Approve the import in your extension popup...');
1080
+ await chromeExtSend(extId, { method: 'claw_importAccountKey', params: [data.secretKey, 'ClawNetwork Node'] });
1081
+ toast('Account imported to extension!');
1082
+ return;
1083
+ } catch (e) { /* fall through to provider method */ }
1084
+ }
1085
+ // Fallback: use window.clawNetwork provider
1086
+ if (!window.clawNetwork) { toast('ClawNetwork extension not detected. Install it first.'); return; }
1087
+ toast('Connecting to extension...');
1088
+ try {
1089
+ await window.clawNetwork.request({ method: 'claw_requestAccounts' });
1090
+ const res = await fetch(API + '/api/wallet/export');
1091
+ const data = await res.json();
1092
+ if (!data.secretKey) { toast('No private key found'); return; }
1093
+ toast('Approve the import in your extension popup...');
1094
+ await window.clawNetwork.request({ method: 'claw_importAccountKey', params: [data.secretKey, 'ClawNetwork Node'] });
1095
+ toast('Account imported to extension!');
1096
+ } catch (e) { toast('Import failed: ' + (e.message || e)); }
1097
+ }
1098
+
1099
+ function chromeExtSend(extId, msg) {
1100
+ return new Promise((resolve, reject) => {
1101
+ if (!chrome || !chrome.runtime || !chrome.runtime.sendMessage) { reject(new Error('No chrome.runtime')); return; }
1102
+ chrome.runtime.sendMessage(extId, msg, (response) => {
1103
+ if (chrome.runtime.lastError) { reject(new Error(chrome.runtime.lastError.message)); return; }
1104
+ if (response && response.success === false) { reject(new Error(response.error || 'Failed')); return; }
1105
+ resolve(response);
1106
+ });
1107
+ });
1108
+ }
1109
+
1110
+ async function detectExtensionIds() {
1111
+ // Try known extension IDs or probe for externally_connectable
1112
+ // In production, the extension ID is stable after Chrome Web Store publish
1113
+ // For dev, try to detect via management API or stored ID
1114
+ const ids = [];
1115
+ try {
1116
+ if (chrome && chrome.runtime && chrome.runtime.sendMessage) {
1117
+ // Try sending a ping to see if any extension responds
1118
+ // This requires knowing the extension ID. For now, check localStorage.
1119
+ const stored = localStorage.getItem('clawnetwork_extension_id');
1120
+ if (stored) ids.push(stored);
1121
+ }
1122
+ } catch {}
1123
+ return ids;
1124
+ }
1125
+
1126
+ async function transferFromDashboard() {
1127
+ const to = prompt('Recipient address (64 hex chars):');
1128
+ if (!to) return;
1129
+ const amount = prompt('Amount (CLAW):');
1130
+ if (!amount) return;
1131
+ if (window.clawNetwork) {
1132
+ try {
1133
+ toast('Approve transfer in extension...');
1134
+ await window.clawNetwork.request({ method: 'claw_requestAccounts' });
1135
+ const result = await window.clawNetwork.request({ method: 'claw_transfer', params: [to, amount] });
1136
+ toast('Transfer sent! Hash: ' + (result && result.txHash ? result.txHash.slice(0, 16) + '...' : 'submitted'));
1137
+ } catch (e) { toast('Transfer failed: ' + (e.message || e)); }
1138
+ } else {
1139
+ try {
1140
+ const res = await fetch(API + '/api/transfer', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({to, amount}) });
1141
+ const data = await res.json();
1142
+ toast(data.ok ? 'Transfer sent! Hash: ' + (data.txHash || '').slice(0, 16) + '...' : 'Error: ' + data.error);
1143
+ } catch (e) { toast('Transfer failed: ' + e.message); }
1144
+ }
1145
+ setTimeout(fetchStatus, 3000);
1146
+ }
1147
+
1148
+ async function registerAgentFromDashboard() {
1149
+ const name = prompt('Agent name (alphanumeric, max 32 chars):', 'openclaw-agent');
1150
+ if (!name) return;
1151
+ if (window.clawNetwork) {
1152
+ try {
1153
+ toast('Approve registration in extension...');
1154
+ await window.clawNetwork.request({ method: 'claw_requestAccounts' });
1155
+ await window.clawNetwork.request({ method: 'claw_registerAgent', params: [name] });
1156
+ toast('Agent registered!');
1157
+ } catch (e) { toast('Registration failed: ' + (e.message || e)); }
1158
+ } else {
1159
+ try {
1160
+ const res = await fetch(API + '/api/agent/register', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({name}) });
1161
+ const data = await res.json();
1162
+ toast(data.ok ? 'Agent registered!' : 'Error: ' + data.error);
1163
+ } catch (e) { toast('Registration failed: ' + e.message); }
1164
+ }
1165
+ }
1166
+
851
1167
  async function fetchStatus() {
852
1168
  try {
853
1169
  const res = await fetch(API + '/api/status');
@@ -874,11 +1190,14 @@ function buildUiHtml(cfg: PluginConfig): string {
874
1190
  document.getElementById('uptimeValue').textContent = s.uptimeFormatted || '—';
875
1191
 
876
1192
  // Wallet
1193
+ cachedAddress = s.walletAddress || '';
1194
+ cachedNetwork = s.network || '';
877
1195
  const wHtml = s.walletAddress
878
1196
  ? '<div class="wallet-addr">' + s.walletAddress + ' <button class="copy-btn" onclick="copyText(\\''+s.walletAddress+'\\')">Copy</button></div>' +
879
1197
  (s.balance ? '<div style="margin-top:8px;font-size:14px;color:var(--green)">' + s.balance + '</div>' : '')
880
1198
  : '<div style="color:var(--text-dim)">No wallet yet — start the node to generate one</div>';
881
1199
  document.getElementById('walletInfo').innerHTML = wHtml;
1200
+ document.getElementById('walletActions').style.display = s.walletAddress ? '' : 'none';
882
1201
 
883
1202
  // Node info
884
1203
  const rows = [
@@ -923,152 +1242,366 @@ function buildUiHtml(cfg: PluginConfig): string {
923
1242
  </html>`
924
1243
  }
925
1244
 
926
- let uiServer: unknown = null
1245
+ // ── UI Server (standalone script, forked as background process) ──
927
1246
 
928
- function startUiServer(cfg: PluginConfig, api: OpenClawApi): string | null {
929
- const http = require('http')
1247
+ const UI_SERVER_SCRIPT = `
1248
+ const http = require('http');
1249
+ const fs = require('fs');
1250
+ const os = require('os');
1251
+ const path = require('path');
930
1252
 
931
- if (uiServer) return null
1253
+ const PORT = parseInt(process.argv[2] || '19877', 10);
1254
+ const RPC_PORT = parseInt(process.argv[3] || '9710', 10);
1255
+ const LOG_PATH = process.argv[4] || path.join(os.homedir(), '.openclaw/workspace/clawnetwork/node.log');
1256
+ const PORT_FILE = path.join(os.homedir(), '.openclaw/clawnetwork-ui-port');
1257
+ const MAX_RETRIES = 10;
932
1258
 
933
- let actualPort = cfg.uiPort
934
- const maxRetries = 10
1259
+ async function fetchJson(url) {
1260
+ const r = await fetch(url);
1261
+ return r.json();
1262
+ }
935
1263
 
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
1264
+ async function rpcCall(method, params) {
1265
+ const r = await fetch('http://localhost:' + RPC_PORT, {
1266
+ method: 'POST',
1267
+ headers: { 'Content-Type': 'application/json' },
1268
+ body: JSON.stringify({ jsonrpc: '2.0', method, params: params || [], id: Date.now() }),
1269
+ });
1270
+ const d = await r.json();
1271
+ if (d.error) throw new Error(d.error.message || JSON.stringify(d.error));
1272
+ return d.result;
1273
+ }
939
1274
 
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 }
1275
+ function formatClaw(raw) {
1276
+ const v = BigInt(raw);
1277
+ const ONE = BigInt(1e9);
1278
+ const w = v / ONE;
1279
+ const f = v % ONE;
1280
+ if (f === 0n) return w + ' CLAW';
1281
+ return w + '.' + f.toString().padStart(9, '0').replace(/0+$/, '') + ' CLAW';
1282
+ }
944
1283
 
945
- const json = (status: number, data: unknown) => {
946
- res.writeHead(status, { 'content-type': 'application/json' })
947
- res.end(JSON.stringify(data))
948
- }
1284
+ function readBody(req) {
1285
+ return new Promise((resolve, reject) => {
1286
+ let data = '';
1287
+ req.on('data', (chunk) => { data += chunk; });
1288
+ req.on('end', () => { try { resolve(JSON.parse(data || '{}')); } catch { resolve({}); } });
1289
+ req.on('error', reject);
1290
+ setTimeout(() => reject(new Error('Body read timeout')), 10000);
1291
+ });
1292
+ }
949
1293
 
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
- }
1294
+ function findNodeBinary() {
1295
+ const binDir = path.join(os.homedir(), '.openclaw/bin');
1296
+ const dataDir = path.join(os.homedir(), '.clawnetwork');
1297
+ const binName = process.platform === 'win32' ? 'claw-node.exe' : 'claw-node';
1298
+ let binary = path.join(binDir, binName);
1299
+ if (fs.existsSync(binary)) return binary;
1300
+ binary = path.join(dataDir, 'bin', 'claw-node');
1301
+ if (fs.existsSync(binary)) return binary;
1302
+ return null;
1303
+ }
956
1304
 
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
- }
1305
+ async function handle(req, res) {
1306
+ const url = new URL(req.url, 'http://localhost:' + PORT);
1307
+ const p = url.pathname;
1308
+ res.setHeader('Access-Control-Allow-Origin', '*');
1309
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
1310
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
1311
+ if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return; }
1312
+
1313
+ const json = (s, d) => { res.writeHead(s, { 'content-type': 'application/json' }); res.end(JSON.stringify(d)); };
974
1314
 
975
- // Logs API
976
- if (pathname === '/api/logs' && req.method === 'GET') {
1315
+ if (p === '/' || p === '/index.html') {
1316
+ res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
1317
+ res.end(HTML);
1318
+ return;
1319
+ }
1320
+ if (p === '/api/status') {
1321
+ try {
1322
+ const h = await fetchJson('http://localhost:' + RPC_PORT + '/health');
1323
+ let balance = '';
977
1324
  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
1325
+ const walletPath = path.join(os.homedir(), '.openclaw/workspace/clawnetwork/wallet.json');
1326
+ const w = JSON.parse(fs.readFileSync(walletPath, 'utf8'));
1327
+ if (w.address) { const b = await rpcCall('claw_getBalance', [w.address]); balance = formatClaw(b); }
1328
+ } catch {}
1329
+ json(200, {
1330
+ running: h.status === 'ok',
1331
+ blockHeight: h.height,
1332
+ peerCount: h.peer_count,
1333
+ network: h.chain_id,
1334
+ syncMode: 'light',
1335
+ rpcUrl: 'http://localhost:' + RPC_PORT,
1336
+ walletAddress: (() => { try { return JSON.parse(fs.readFileSync(path.join(os.homedir(), '.openclaw/workspace/clawnetwork/wallet.json'), 'utf8')).address; } catch { return ''; } })(),
1337
+ binaryVersion: h.version,
1338
+ pluginVersion: '0.1.0',
1339
+ uptime: h.uptime_secs,
1340
+ 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',
1341
+ restartCount: 0, dataDir: path.join(os.homedir(), '.clawnetwork'), balance, syncing: h.status === 'degraded',
1342
+ });
1343
+ } catch { json(200, { running: false, blockHeight: null, peerCount: null }); }
1344
+ return;
1345
+ }
1346
+ if (p === '/api/logs') {
1347
+ try {
1348
+ if (!fs.existsSync(LOG_PATH)) { json(200, { logs: 'No logs yet' }); return; }
1349
+ const c = fs.readFileSync(LOG_PATH, 'utf8').split('\\n');
1350
+ json(200, { logs: c.slice(-80).join('\\n') });
1351
+ } catch (e) { json(500, { error: e.message }); }
1352
+ return;
1353
+ }
1354
+ if (p === '/api/wallet/export') {
1355
+ // Only allow from localhost (127.0.0.1) — never expose to network
1356
+ const host = req.headers.host || '';
1357
+ if (!host.startsWith('127.0.0.1') && !host.startsWith('localhost')) {
1358
+ json(403, { error: 'Wallet export only available from localhost' });
1359
+ return;
986
1360
  }
987
-
988
- // Action API
989
- if (pathname.startsWith('/api/action/') && req.method === 'POST') {
990
- const action = pathname.split('/').pop()
991
-
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
- }
998
- }
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
1011
- }
1012
-
1013
- if (action === 'faucet') {
1014
- const wallet = loadWallet()
1015
- if (!wallet?.address) { json(400, { error: 'No wallet' }); return }
1361
+ try {
1362
+ const walletPath = path.join(os.homedir(), '.openclaw/workspace/clawnetwork/wallet.json');
1363
+ const w = JSON.parse(fs.readFileSync(walletPath, 'utf8'));
1364
+ json(200, { address: w.address, secretKey: w.secret_key || w.secretKey || w.private_key || '' });
1365
+ } catch (e) { json(400, { error: 'No wallet found' }); }
1366
+ return;
1367
+ }
1368
+ // ── Business API endpoints (mirrors Gateway methods) ──
1369
+ if (p === '/api/wallet/balance') {
1370
+ try {
1371
+ const walletPath = path.join(os.homedir(), '.openclaw/workspace/clawnetwork/wallet.json');
1372
+ const w = JSON.parse(fs.readFileSync(walletPath, 'utf8'));
1373
+ const address = new URL(req.url, 'http://localhost').searchParams.get('address') || w.address;
1374
+ const b = await rpcCall('claw_getBalance', [address]);
1375
+ json(200, { address, balance: String(b), formatted: formatClaw(b) });
1376
+ } catch (e) { json(400, { error: e.message }); }
1377
+ return;
1378
+ }
1379
+ if (p === '/api/transfer' && req.method === 'POST') {
1380
+ try {
1381
+ const body = await readBody(req);
1382
+ const { to, amount } = body;
1383
+ if (!to || !amount) { json(400, { error: 'Missing params: to, amount' }); return; }
1384
+ if (!/^[0-9a-f]{64}$/i.test(to)) { json(400, { error: 'Invalid address (64 hex chars)' }); return; }
1385
+ if (!/^\\d+(\\.\\d+)?$/.test(amount) || parseFloat(amount) <= 0) { json(400, { error: 'Invalid amount' }); return; }
1386
+ const bin = findNodeBinary();
1387
+ if (!bin) { json(400, { error: 'claw-node binary not found' }); return; }
1388
+ const { execFileSync } = require('child_process');
1389
+ const out = execFileSync(bin, ['transfer', to, amount, '--rpc', 'http://localhost:' + RPC_PORT], { encoding: 'utf8', timeout: 30000, env: { HOME: os.homedir(), PATH: process.env.PATH || '' } });
1390
+ const h = out.match(/[0-9a-f]{64}/i);
1391
+ json(200, { ok: true, txHash: h ? h[0] : '', to, amount });
1392
+ } catch (e) { json(500, { error: e.message }); }
1393
+ return;
1394
+ }
1395
+ if (p === '/api/stake' && req.method === 'POST') {
1396
+ try {
1397
+ const body = await readBody(req);
1398
+ const { amount, action } = body;
1399
+ if (!amount && action !== 'claim') { json(400, { error: 'Missing amount' }); return; }
1400
+ const bin = findNodeBinary();
1401
+ if (!bin) { json(400, { error: 'claw-node binary not found' }); return; }
1402
+ const { execFileSync } = require('child_process');
1403
+ const cmd = action === 'withdraw' ? 'stake withdraw' : action === 'claim' ? 'stake claim' : 'stake deposit';
1404
+ const args = cmd.split(' ').concat(amount ? [amount] : []).concat(['--rpc', 'http://localhost:' + RPC_PORT]);
1405
+ const out = execFileSync(bin, args, { encoding: 'utf8', timeout: 30000, env: { HOME: os.homedir(), PATH: process.env.PATH || '' } });
1406
+ json(200, { ok: true, raw: out.trim() });
1407
+ } catch (e) { json(500, { error: e.message }); }
1408
+ return;
1409
+ }
1410
+ if (p === '/api/agent/register' && req.method === 'POST') {
1411
+ try {
1412
+ const body = await readBody(req);
1413
+ const name = (body.name || 'openclaw-agent-' + Date.now().toString(36)).replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 32);
1414
+ const bin = findNodeBinary();
1415
+ if (!bin) { json(400, { error: 'claw-node binary not found' }); return; }
1416
+ const { execFileSync } = require('child_process');
1417
+ 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 || '' } });
1418
+ const h = out.match(/[0-9a-f]{64}/i);
1419
+ json(200, { ok: true, txHash: h ? h[0] : '', name });
1420
+ } catch (e) { json(500, { error: e.message }); }
1421
+ return;
1422
+ }
1423
+ if (p === '/api/service/register' && req.method === 'POST') {
1424
+ try {
1425
+ const body = await readBody(req);
1426
+ const { serviceType, endpoint, description, priceAmount } = body;
1427
+ if (!serviceType || !endpoint) { json(400, { error: 'Missing: serviceType, endpoint' }); return; }
1428
+ const bin = findNodeBinary();
1429
+ if (!bin) { json(400, { error: 'claw-node binary not found' }); return; }
1430
+ const { execFileSync } = require('child_process');
1431
+ 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 || '' } });
1432
+ json(200, { ok: true, raw: out.trim() });
1433
+ } catch (e) { json(500, { error: e.message }); }
1434
+ return;
1435
+ }
1436
+ if (p === '/api/service/search') {
1437
+ try {
1438
+ const t = new URL(req.url, 'http://localhost').searchParams.get('type');
1439
+ const result = await rpcCall('claw_getServices', t ? [t] : []);
1440
+ json(200, { services: result });
1441
+ } catch (e) { json(500, { error: e.message }); }
1442
+ return;
1443
+ }
1444
+ if (p === '/api/node/config') {
1445
+ try {
1446
+ const cfgPath = path.join(os.homedir(), '.openclaw/workspace/clawnetwork/config.json');
1447
+ const cfg = fs.existsSync(cfgPath) ? JSON.parse(fs.readFileSync(cfgPath, 'utf8')) : {};
1448
+ json(200, { ...cfg, rpcPort: RPC_PORT, uiPort: PORT });
1449
+ } catch (e) { json(200, { rpcPort: RPC_PORT, uiPort: PORT }); }
1450
+ return;
1451
+ }
1452
+ if (p.startsWith('/api/action/') && req.method === 'POST') {
1453
+ const a = p.split('/').pop();
1454
+ if (a === 'faucet') {
1455
+ try {
1456
+ const w = JSON.parse(fs.readFileSync(path.join(os.homedir(), '.openclaw/workspace/clawnetwork/wallet.json'), 'utf8'));
1457
+ const r = await rpcCall('claw_faucet', [w.address]);
1458
+ json(200, { message: 'Faucet success', ...r });
1459
+ } catch (e) { json(400, { error: e.message }); }
1460
+ return;
1461
+ }
1462
+ if (a === 'start') {
1463
+ try {
1464
+ // Check if already running
1465
+ const pidFile = path.join(os.homedir(), '.openclaw/workspace/clawnetwork/node.pid');
1016
1466
  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' })
1467
+ const pid = parseInt(fs.readFileSync(pidFile, 'utf8').trim(), 10);
1468
+ if (pid > 0) { try { process.kill(pid, 0); json(200, { message: 'Node already running', pid }); return; } catch {} }
1469
+ } catch {}
1470
+ // Find binary
1471
+ const binDir = path.join(os.homedir(), '.openclaw/bin');
1472
+ const dataDir = path.join(os.homedir(), '.clawnetwork');
1473
+ const binName = process.platform === 'win32' ? 'claw-node.exe' : 'claw-node';
1474
+ let binary = path.join(binDir, binName);
1475
+ if (!fs.existsSync(binary)) { binary = path.join(dataDir, 'bin', 'claw-node'); }
1476
+ if (!fs.existsSync(binary)) { json(400, { error: 'claw-node binary not found. Run: openclaw clawnetwork:download' }); return; }
1477
+ // Read config for network/ports
1478
+ const cfgPath = path.join(os.homedir(), '.openclaw/workspace/clawnetwork/config.json');
1479
+ let network = 'mainnet', p2pPort = 9711, syncMode = 'light', extraPeers = [];
1480
+ try {
1481
+ const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
1482
+ if (cfg.network) network = cfg.network;
1483
+ if (cfg.p2pPort) p2pPort = cfg.p2pPort;
1484
+ if (cfg.syncMode) syncMode = cfg.syncMode;
1485
+ if (cfg.extraBootstrapPeers) extraPeers = cfg.extraBootstrapPeers;
1486
+ } catch {}
1487
+ const bootstrapPeers = { mainnet: ['/ip4/178.156.162.162/tcp/9711'], testnet: ['/ip4/178.156.162.162/tcp/9721'], devnet: [] };
1488
+ const peers = [...(bootstrapPeers[network] || []), ...extraPeers];
1489
+ const args = ['start', '--network', network, '--rpc-port', String(RPC_PORT), '--p2p-port', String(p2pPort), '--sync-mode', syncMode, '--allow-genesis'];
1490
+ for (const peer of peers) { args.push('--bootstrap', peer); }
1491
+ // Spawn detached
1492
+ const logPath = path.join(os.homedir(), '.openclaw/workspace/clawnetwork/node.log');
1493
+ const logFd = fs.openSync(logPath, 'a');
1494
+ const { spawn: nodeSpawn } = require('child_process');
1495
+ const child = nodeSpawn(binary, args, {
1496
+ stdio: ['ignore', logFd, logFd],
1497
+ detached: true,
1498
+ env: { HOME: os.homedir(), PATH: process.env.PATH || '/usr/local/bin:/usr/bin:/bin', RUST_LOG: process.env.RUST_LOG || 'claw=info' },
1499
+ });
1500
+ child.unref();
1501
+ fs.closeSync(logFd);
1502
+ fs.writeFileSync(pidFile, String(child.pid));
1503
+ // Remove stop signal if exists
1504
+ const stopFile = path.join(os.homedir(), '.openclaw/workspace/clawnetwork/stop.signal');
1505
+ try { fs.unlinkSync(stopFile); } catch {}
1506
+ json(200, { message: 'Node started', pid: child.pid });
1507
+ } catch (e) { json(500, { error: e.message }); }
1508
+ return;
1509
+ }
1510
+ if (a === 'stop') {
1511
+ try {
1512
+ const pidFile = path.join(os.homedir(), '.openclaw/workspace/clawnetwork/node.pid');
1513
+ let pid = null;
1514
+ try { pid = parseInt(fs.readFileSync(pidFile, 'utf8').trim(), 10); } catch {}
1515
+ if (pid && pid > 0) {
1516
+ try { process.kill(pid, 'SIGTERM'); } catch {}
1021
1517
  }
1022
- return
1023
- }
1024
-
1025
- json(404, { error: 'Unknown action' })
1026
- return
1518
+ // Write stop signal for restart loop
1519
+ const stopFile = path.join(os.homedir(), '.openclaw/workspace/clawnetwork/stop.signal');
1520
+ try { fs.writeFileSync(stopFile, String(Date.now())); } catch {}
1521
+ // Also kill by name (covers orphans)
1522
+ try { require('child_process').execFileSync('pkill', ['-f', 'claw-node start'], { timeout: 3000 }); } catch {}
1523
+ try { fs.unlinkSync(pidFile); } catch {}
1524
+ json(200, { message: 'Node stopped' });
1525
+ } catch (e) { json(500, { error: e.message }); }
1526
+ return;
1027
1527
  }
1028
-
1029
- json(404, { error: 'Not found' })
1528
+ if (a === 'restart') {
1529
+ json(200, { message: 'Use Stop then Start to restart the node' });
1530
+ return;
1531
+ }
1532
+ json(400, { error: 'Unknown action: ' + a });
1533
+ return;
1030
1534
  }
1535
+ json(404, { error: 'Not found' });
1536
+ }
1031
1537
 
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
- })
1538
+ function tryListen(attempt) {
1539
+ if (attempt >= MAX_RETRIES) { console.error('Failed to bind UI server'); process.exit(1); }
1540
+ const port = PORT + attempt;
1541
+ const srv = http.createServer((req, res) => handle(req, res).catch(e => { try { res.writeHead(500); res.end(e.message); } catch {} }));
1542
+ srv.on('error', () => tryListen(attempt + 1));
1543
+ srv.listen(port, '127.0.0.1', () => {
1544
+ fs.mkdirSync(path.dirname(PORT_FILE), { recursive: true });
1545
+ fs.writeFileSync(PORT_FILE, JSON.stringify({ port, pid: process.pid, startedAt: new Date().toISOString() }));
1546
+ console.log('ClawNetwork Dashboard: http://127.0.0.1:' + port);
1547
+ process.on('SIGINT', () => { try { fs.unlinkSync(PORT_FILE); } catch {} process.exit(0); });
1548
+ process.on('SIGTERM', () => { try { fs.unlinkSync(PORT_FILE); } catch {} process.exit(0); });
1549
+ });
1550
+ }
1551
+ tryListen(0);
1552
+ `
1553
+
1554
+ function startUiServer(cfg: PluginConfig, api: OpenClawApi): string | null {
1555
+ // Check if already running
1556
+ const existing = getDashboardUrl()
1557
+ if (existing) {
1558
+ api.logger?.info?.(`[clawnetwork] dashboard already running: ${existing}`)
1559
+ return existing
1560
+ }
1041
1561
 
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 */ })
1562
+ // Write the standalone UI server script to a temp file and fork it
1563
+ const scriptPath = path.join(WORKSPACE_DIR, 'ui-server.js')
1564
+ ensureDir(WORKSPACE_DIR)
1047
1565
 
1048
- // Give it a moment
1049
- actualPort = tryPort
1050
- uiServer = server
1566
+ // Write HTML to a separate file, script reads it at startup
1567
+ const htmlPath = path.join(WORKSPACE_DIR, 'ui-dashboard.html')
1568
+ fs.writeFileSync(htmlPath, buildUiHtml(cfg))
1051
1569
 
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() }))
1570
+ // Inject HTML path into script (read from file, no template escaping issues)
1571
+ const fullScript = `const HTML_PATH = ${JSON.stringify(htmlPath)};\nconst HTML = require('fs').readFileSync(HTML_PATH, 'utf8');\n${UI_SERVER_SCRIPT}`
1572
+ fs.writeFileSync(scriptPath, fullScript)
1055
1573
 
1056
- api.logger?.info?.(`[clawnetwork] dashboard: http://127.0.0.1:${tryPort}`)
1057
- return `http://127.0.0.1:${tryPort}`
1058
- } catch {
1059
- continue
1574
+ try {
1575
+ const child = fork(scriptPath, [String(cfg.uiPort), String(cfg.rpcPort), LOG_PATH], {
1576
+ detached: true,
1577
+ stdio: 'ignore',
1578
+ })
1579
+ child.unref()
1580
+ api.logger?.info?.(`[clawnetwork] dashboard starting on http://127.0.0.1:${cfg.uiPort}`)
1581
+
1582
+ // Wait briefly for port file
1583
+ for (let i = 0; i < 10; i++) {
1584
+ const url = getDashboardUrl()
1585
+ if (url) return url
1586
+ // Busy-wait 200ms (can't use async sleep here)
1587
+ const start = Date.now()
1588
+ while (Date.now() - start < 200) { /* spin */ }
1060
1589
  }
1590
+ return `http://127.0.0.1:${cfg.uiPort}`
1591
+ } catch (e: unknown) {
1592
+ api.logger?.warn?.(`[clawnetwork] failed to start dashboard: ${(e as Error).message}`)
1593
+ return null
1061
1594
  }
1062
-
1063
- api.logger?.warn?.('[clawnetwork] failed to start dashboard UI server')
1064
- return null
1065
1595
  }
1066
1596
 
1067
1597
  function stopUiServer(): void {
1068
- if (uiServer) {
1069
- try { uiServer.close() } catch { /* ok */ }
1070
- uiServer = null
1071
- }
1598
+ try {
1599
+ const raw = fs.readFileSync(UI_PORT_FILE, 'utf8')
1600
+ const info = JSON.parse(raw)
1601
+ if (info.pid) {
1602
+ try { process.kill(info.pid, 'SIGTERM') } catch { /* ok */ }
1603
+ }
1604
+ } catch { /* no file */ }
1072
1605
  try { fs.unlinkSync(UI_PORT_FILE) } catch { /* ok */ }
1073
1606
  }
1074
1607
 
@@ -1266,11 +1799,15 @@ export default function register(api: OpenClawApi) {
1266
1799
  }
1267
1800
  initNode(binary, cfg.network, api)
1268
1801
  startNodeProcess(binary, cfg, api)
1269
- out({ message: 'Node started', pid: nodeProcess?.pid, network: cfg.network, rpc: `http://localhost:${cfg.rpcPort}` })
1802
+ // Start UI dashboard
1803
+ const dashUrl = startUiServer(cfg, api)
1804
+ 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
1805
  }
1271
1806
 
1272
1807
  const handleStop = () => {
1808
+ stopMinerHeartbeatLoop()
1273
1809
  stopNode(api)
1810
+ stopUiServer()
1274
1811
  out({ message: 'Node stopped' })
1275
1812
  }
1276
1813
 
@@ -1521,16 +2058,23 @@ export default function register(api: OpenClawApi) {
1521
2058
  // Step 3: Wallet
1522
2059
  const wallet = ensureWallet(cfg.network, api)
1523
2060
 
1524
- // Step 4: Start node
2061
+ // Step 4: Save config for UI server to read
2062
+ const cfgPath = path.join(WORKSPACE_DIR, 'config.json')
2063
+ fs.writeFileSync(cfgPath, JSON.stringify({ network: cfg.network, rpcPort: cfg.rpcPort, p2pPort: cfg.p2pPort, syncMode: cfg.syncMode, extraBootstrapPeers: cfg.extraBootstrapPeers }))
2064
+
2065
+ // Step 5: Start node
1525
2066
  startNodeProcess(binary, cfg, api)
1526
2067
 
1527
- // Step 5: Start UI dashboard
2068
+ // Step 6: Start UI dashboard
1528
2069
  startUiServer(cfg, api)
1529
2070
 
1530
- // Step 6: Auto-register agent (wait for node to sync)
2071
+ // Step 7: Wait for node to sync, then auto-register
1531
2072
  await sleep(15_000)
1532
2073
  await autoRegisterAgent(cfg, wallet, api)
1533
2074
 
2075
+ // Step 8: Auto-register as miner + start heartbeat loop
2076
+ await autoRegisterMiner(cfg, wallet, api)
2077
+
1534
2078
  } catch (err: unknown) {
1535
2079
  api.logger?.error?.(`[clawnetwork] startup failed: ${(err as Error).message}`)
1536
2080
  }
@@ -1538,6 +2082,7 @@ export default function register(api: OpenClawApi) {
1538
2082
  },
1539
2083
  stop: () => {
1540
2084
  api.logger?.info?.('[clawnetwork] shutting down...')
2085
+ stopMinerHeartbeatLoop()
1541
2086
  stopNode(api)
1542
2087
  stopUiServer()
1543
2088
  },
@@ -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.1",
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": {