@clawlabz/clawnetwork 0.1.2 → 0.1.4

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.
Files changed (2) hide show
  1. package/index.ts +194 -12
  2. package/package.json +1 -1
package/index.ts CHANGED
@@ -15,6 +15,7 @@ const DEFAULT_P2P_PORT = 9711
15
15
  const DEFAULT_NETWORK = 'mainnet'
16
16
  const DEFAULT_SYNC_MODE = 'light'
17
17
  const DEFAULT_HEALTH_CHECK_SECONDS = 30
18
+ const MIN_NODE_VERSION = '0.4.19'
18
19
  const DEFAULT_UI_PORT = 19877
19
20
  const MAX_RESTART_ATTEMPTS = 3
20
21
 
@@ -227,6 +228,16 @@ function getBinaryVersion(binaryPath: string): string | null {
227
228
  } catch { return null }
228
229
  }
229
230
 
231
+ function isVersionOlder(current: string, required: string): boolean {
232
+ const c = current.split('.').map(Number)
233
+ const r = required.split('.').map(Number)
234
+ for (let i = 0; i < 3; i++) {
235
+ if ((c[i] || 0) < (r[i] || 0)) return true
236
+ if ((c[i] || 0) > (r[i] || 0)) return false
237
+ }
238
+ return false
239
+ }
240
+
230
241
  function detectPlatformTarget(): string {
231
242
  const platform = process.platform === 'darwin' ? 'macos' : process.platform === 'win32' ? 'windows' : 'linux'
232
243
  const arch = process.arch === 'arm64' ? 'aarch64' : 'x86_64'
@@ -908,6 +919,21 @@ function buildUiHtml(cfg: PluginConfig): string {
908
919
  .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
920
  .modal-input:focus { border-color: var(--accent); }
910
921
  .modal-hint { font-size: 12px; color: var(--text-dim); margin-top: 6px; line-height: 1.5; }
922
+
923
+ .upgrade-banner { padding: 14px 16px; border-radius: var(--radius); margin-bottom: 16px; font-size: 13px; line-height: 1.6; display: flex; justify-content: space-between; align-items: center; gap: 12px; }
924
+ .upgrade-banner.recommended { background: rgba(255, 170, 0, 0.1); border: 1px solid rgba(255, 170, 0, 0.3); color: #ffaa00; }
925
+ .upgrade-banner.recommended .upgrade-text { flex: 1; }
926
+ .upgrade-banner.recommended .upgrade-actions { display: flex; gap: 8px; }
927
+ .upgrade-banner.required { background: rgba(255, 140, 0, 0.1); border: 1px solid rgba(255, 140, 0, 0.3); color: #ff8c3a; }
928
+ .upgrade-banner.required .upgrade-text { flex: 1; }
929
+ .upgrade-banner.required .upgrade-actions { display: flex; gap: 8px; }
930
+ .upgrade-banner.critical { background: rgba(239, 68, 68, 0.15); border: 1px solid rgba(239, 68, 68, 0.4); color: var(--danger); width: 100%; margin-left: calc(-20px - 1px); margin-right: calc(-20px - 1px); padding: 16px calc(20px + 1px); border-radius: 0; font-weight: 600; }
931
+ .upgrade-banner.critical .upgrade-text { flex: 1; }
932
+ .upgrade-banner.critical .upgrade-actions { display: flex; gap: 8px; }
933
+ .upgrade-btn { background: var(--accent); color: var(--bg); border: none; padding: 6px 12px; border-radius: 4px; font-size: 12px; cursor: pointer; font-weight: 600; transition: 0.2s; }
934
+ .upgrade-btn:hover { opacity: 0.85; }
935
+ .upgrade-dismiss { background: none; border: 1px solid currentColor; color: currentColor; padding: 4px 10px; border-radius: 4px; font-size: 12px; cursor: pointer; transition: 0.2s; }
936
+ .upgrade-dismiss:hover { opacity: 0.7; }
911
937
  </style>
912
938
  </head>
913
939
  <body>
@@ -923,6 +949,8 @@ function buildUiHtml(cfg: PluginConfig): string {
923
949
 
924
950
  <main class="container" style="padding-top:16px;padding-bottom:40px">
925
951
 
952
+ <div id="upgradeBanner" style="display:none" class="upgrade-banner"></div>
953
+
926
954
  <div class="panel">
927
955
  <div class="panel-title">Node</div>
928
956
  <div class="stats-grid" style="margin:0 0 4px">
@@ -944,8 +972,8 @@ function buildUiHtml(cfg: PluginConfig): string {
944
972
  </div>
945
973
  </div>
946
974
  <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>
975
+ <button class="btn primary" id="startBtn" onclick="doAction('start')">&#x25B6; Start Node</button>
976
+ <button class="btn danger" id="stopBtn" onclick="doAction('stop')">&#x25A0; Stop Node</button>
949
977
  </div>
950
978
  </div>
951
979
 
@@ -1267,10 +1295,11 @@ function buildUiHtml(cfg: PluginConfig): string {
1267
1295
  function renderStatus(s) {
1268
1296
  const statusEl = document.getElementById('statusValue');
1269
1297
  if (s.running) {
1270
- const dotClass = s.syncing ? 'syncing' : 'online';
1271
- const label = s.syncing ? 'Syncing' : 'Online';
1298
+ let dotClass = 'online', label = 'Online';
1299
+ if (s.syncing && s.peerless) { dotClass = 'syncing'; label = 'No Peers'; }
1300
+ else if (s.syncing) { dotClass = 'syncing'; label = 'Syncing'; }
1272
1301
  statusEl.innerHTML = '<span class="status-dot ' + dotClass + '"></span>' + label;
1273
- statusEl.className = 'stat-value green';
1302
+ statusEl.className = 'stat-value' + (s.syncing ? '' : ' green');
1274
1303
  } else {
1275
1304
  statusEl.innerHTML = '<span class="status-dot offline"></span>Offline';
1276
1305
  statusEl.className = 'stat-value danger';
@@ -1279,6 +1308,37 @@ function buildUiHtml(cfg: PluginConfig): string {
1279
1308
  document.getElementById('heightValue').textContent = s.blockHeight !== null ? s.blockHeight.toLocaleString() : '—';
1280
1309
  document.getElementById('peersValue').textContent = s.peerCount !== null ? s.peerCount : '—';
1281
1310
  document.getElementById('uptimeValue').textContent = s.uptimeFormatted || '—';
1311
+ document.getElementById('startBtn').disabled = s.running;
1312
+ document.getElementById('stopBtn').disabled = !s.running;
1313
+ document.getElementById('startBtn').style.opacity = s.running ? '0.4' : '1';
1314
+ document.getElementById('stopBtn').style.opacity = !s.running ? '0.4' : '1';
1315
+
1316
+ // Handle upgrade banner
1317
+ const bannerEl = document.getElementById('upgradeBanner');
1318
+ if (s.upgradeLevel && s.upgradeLevel !== 'up_to_date' && s.upgradeLevel !== 'unknown') {
1319
+ bannerEl.style.display = '';
1320
+ const recommended = s.upgradeLevel === 'recommended';
1321
+ const required = s.upgradeLevel === 'required';
1322
+ const critical = s.upgradeLevel === 'critical';
1323
+ bannerEl.className = 'upgrade-banner ' + s.upgradeLevel;
1324
+ let bannerHtml = '<div class="upgrade-text">';
1325
+ if (critical) {
1326
+ bannerHtml += '⚠ CRITICAL UPDATE REQUIRED — ' + (s.changelog || 'Security update required') + '. Node stopped for security.';
1327
+ } else if (required) {
1328
+ bannerHtml += 'Update recommended: v' + (s.latestVersion || '') + ' — ' + (s.changelog || 'Update available');
1329
+ } else if (recommended) {
1330
+ bannerHtml += 'Update available: v' + (s.latestVersion || '') + ' — ' + (s.changelog || 'New version available');
1331
+ }
1332
+ bannerHtml += '</div><div class="upgrade-actions">';
1333
+ bannerHtml += '<button class="upgrade-btn" onclick="doAction(\'upgrade\')">Update Now</button>';
1334
+ if (recommended) {
1335
+ bannerHtml += '<button class="upgrade-dismiss" onclick="document.getElementById(\'upgradeBanner\').style.display=\'none\'">Dismiss</button>';
1336
+ }
1337
+ bannerHtml += '</div>';
1338
+ bannerEl.innerHTML = bannerHtml;
1339
+ } else {
1340
+ bannerEl.style.display = 'none';
1341
+ }
1282
1342
 
1283
1343
  // Wallet
1284
1344
  cachedAddress = s.walletAddress || '';
@@ -1313,11 +1373,21 @@ function buildUiHtml(cfg: PluginConfig): string {
1313
1373
  }
1314
1374
 
1315
1375
  // Node info
1376
+ let versionStatusHtml = s.binaryVersion || '—';
1377
+ if (s.upgradeLevel === 'up_to_date') {
1378
+ versionStatusHtml = (s.binaryVersion || '—') + ' <span style="color:var(--green)">✓</span>';
1379
+ } else if (s.upgradeLevel === 'recommended') {
1380
+ versionStatusHtml = (s.binaryVersion || '—') + ' <span style="color:#ffaa00">→ ' + (s.latestVersion || '') + '</span>';
1381
+ } else if (s.upgradeLevel === 'required') {
1382
+ versionStatusHtml = (s.binaryVersion || '—') + ' <span style="color:#ff8c3a">⚠ Update recommended</span>';
1383
+ } else if (s.upgradeLevel === 'critical') {
1384
+ versionStatusHtml = (s.binaryVersion || '—') + ' <span style="color:var(--danger)">🔴 CRITICAL</span>';
1385
+ }
1316
1386
  const rows = [
1317
1387
  ['Network', s.network],
1318
1388
  ['Sync Mode', s.syncMode],
1319
1389
  ['RPC URL', s.rpcUrl],
1320
- ['Binary Version', s.binaryVersion || '—'],
1390
+ ['Binary Version', versionStatusHtml],
1321
1391
  ['Plugin Version', s.pluginVersion],
1322
1392
  ['PID', s.pid || '—'],
1323
1393
  ['Restart Count', s.restartCount],
@@ -1436,6 +1506,22 @@ async function handle(req, res) {
1436
1506
  let balance = '';
1437
1507
  let walletAddress = '';
1438
1508
  let agentName = '';
1509
+ let upgradeLevel = 'unknown';
1510
+ let latestVersion = '';
1511
+ let releaseUrl = '';
1512
+ let changelog = '';
1513
+ let announcement = null;
1514
+ // Fetch version info if available (Phase 1 endpoint)
1515
+ try {
1516
+ const v = await fetchJson('http://localhost:' + RPC_PORT + '/version');
1517
+ if (v && v.upgrade_level) {
1518
+ upgradeLevel = v.upgrade_level;
1519
+ latestVersion = v.latest_version || '';
1520
+ releaseUrl = v.release_url || '';
1521
+ changelog = v.changelog || '';
1522
+ announcement = v.announcement || null;
1523
+ }
1524
+ } catch {}
1439
1525
  try {
1440
1526
  const walletPath = path.join(os.homedir(), '.openclaw/workspace/clawnetwork/wallet.json');
1441
1527
  const w = JSON.parse(fs.readFileSync(walletPath, 'utf8'));
@@ -1446,7 +1532,7 @@ async function handle(req, res) {
1446
1532
  }
1447
1533
  } catch {}
1448
1534
  json(200, {
1449
- running: h.status === 'ok',
1535
+ running: h.status === 'ok' || h.status === 'degraded',
1450
1536
  blockHeight: h.height,
1451
1537
  peerCount: h.peer_count,
1452
1538
  network: h.chain_id,
@@ -1457,11 +1543,12 @@ async function handle(req, res) {
1457
1543
  pluginVersion: '0.1.1',
1458
1544
  uptime: h.uptime_secs,
1459
1545
  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',
1546
+ restartCount: 0, dataDir: path.join(os.homedir(), '.clawnetwork'), balance, agentName, syncing: h.status === 'degraded', peerless: h.peer_count === 0, lastBlockAgeSecs: h.last_block_age_secs,
1547
+ upgradeLevel, latestVersion, releaseUrl, changelog, announcement,
1461
1548
  });
1462
1549
  } catch {
1463
1550
  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 });
1551
+ 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, upgradeLevel: 'unknown', latestVersion: '', releaseUrl: '', changelog: '', announcement: null });
1465
1552
  }
1466
1553
  return;
1467
1554
  }
@@ -1583,8 +1670,21 @@ async function handle(req, res) {
1583
1670
  }
1584
1671
  if (a === 'start') {
1585
1672
  try {
1586
- // Check if already running
1673
+ // Check if already running — try RPC health first (covers stale PID file)
1587
1674
  const pidFile = path.join(os.homedir(), '.openclaw/workspace/clawnetwork/node.pid');
1675
+ try {
1676
+ const h = await fetchJson('http://localhost:' + RPC_PORT + '/health');
1677
+ if (h && (h.status === 'ok' || h.status === 'degraded')) {
1678
+ try {
1679
+ const { execSync } = require('child_process');
1680
+ const pgrep = execSync("pgrep -f 'claw-node start'", { encoding: 'utf8', timeout: 3000 }).trim();
1681
+ const livePid = parseInt(pgrep.split('\\n')[0], 10);
1682
+ if (livePid > 0) fs.writeFileSync(pidFile, String(livePid));
1683
+ } catch {}
1684
+ json(200, { message: 'Node already running' }); return;
1685
+ }
1686
+ } catch {}
1687
+ // Fallback: check PID file
1588
1688
  try {
1589
1689
  const pid = parseInt(fs.readFileSync(pidFile, 'utf8').trim(), 10);
1590
1690
  if (pid > 0) { try { process.kill(pid, 0); json(200, { message: 'Node already running', pid }); return; } catch {} }
@@ -1651,6 +1751,64 @@ async function handle(req, res) {
1651
1751
  json(200, { message: 'Use Stop then Start to restart the node' });
1652
1752
  return;
1653
1753
  }
1754
+ if (a === 'upgrade') {
1755
+ try {
1756
+ // 1. Stop running node
1757
+ const pidFile = path.join(os.homedir(), '.openclaw/workspace/clawnetwork/node.pid');
1758
+ try {
1759
+ const pid = parseInt(fs.readFileSync(pidFile, 'utf8').trim(), 10);
1760
+ if (pid > 0) try { process.kill(pid, 'SIGTERM'); } catch {}
1761
+ } catch {}
1762
+ try { require('child_process').execFileSync('pkill', ['-f', 'claw-node start'], { timeout: 5000 }); } catch {}
1763
+
1764
+ // 2. Download latest binary
1765
+ const binDir = path.join(os.homedir(), '.openclaw/bin');
1766
+ const binName = process.platform === 'win32' ? 'claw-node.exe' : 'claw-node';
1767
+ const target = process.platform === 'darwin'
1768
+ ? (process.arch === 'arm64' ? 'macos-aarch64' : 'macos-x86_64')
1769
+ : process.platform === 'win32' ? 'windows-x86_64' : 'linux-x86_64';
1770
+ const ext = process.platform === 'win32' ? 'zip' : 'tar.gz';
1771
+
1772
+ // Fetch latest release tag
1773
+ let version = 'latest';
1774
+ try {
1775
+ const res = await fetch('https://api.github.com/repos/clawlabz/claw-network/releases/latest');
1776
+ if (res.ok) { const d = await res.json(); if (d.tag_name) version = d.tag_name; }
1777
+ } catch {}
1778
+
1779
+ const baseUrl = version === 'latest'
1780
+ ? 'https://github.com/clawlabz/claw-network/releases/latest/download'
1781
+ : 'https://github.com/clawlabz/claw-network/releases/download/' + version;
1782
+ const downloadUrl = baseUrl + '/claw-node-' + target + '.' + ext;
1783
+
1784
+ const tmpFile = path.join(os.tmpdir(), 'claw-node-upgrade-' + Date.now() + '.' + ext);
1785
+ require('child_process').execFileSync('curl', ['-sSfL', '-o', tmpFile, downloadUrl], { timeout: 120000 });
1786
+
1787
+ // Ensure bin directory exists
1788
+ if (!fs.existsSync(binDir)) { fs.mkdirSync(binDir, { recursive: true }); }
1789
+
1790
+ // Extract binary
1791
+ if (ext === 'tar.gz') {
1792
+ require('child_process').execFileSync('tar', ['xzf', tmpFile, '-C', binDir], { timeout: 30000 });
1793
+ } else {
1794
+ // Windows zip handling
1795
+ const AdmZip = require('adm-zip');
1796
+ const zip = new AdmZip(tmpFile);
1797
+ zip.extractAllTo(binDir, true);
1798
+ }
1799
+ fs.chmodSync(path.join(binDir, binName), 0o755);
1800
+ try { fs.unlinkSync(tmpFile); } catch {}
1801
+
1802
+ // 3. Get new version
1803
+ let newVersion = 'unknown';
1804
+ try {
1805
+ newVersion = require('child_process').execFileSync(path.join(binDir, binName), ['--version'], { encoding: 'utf8', timeout: 5000 }).trim();
1806
+ } catch {}
1807
+
1808
+ json(200, { message: 'Upgraded to ' + newVersion + '. Restart the node from Dashboard.', newVersion });
1809
+ } catch (e) { json(500, { error: e.message }); }
1810
+ return;
1811
+ }
1654
1812
  json(400, { error: 'Unknown action: ' + a });
1655
1813
  return;
1656
1814
  }
@@ -1918,6 +2076,18 @@ export default function register(api: OpenClawApi) {
1918
2076
  out({ error: 'claw-node not found. Run: curl -sSf https://raw.githubusercontent.com/clawlabz/claw-network/main/claw-node/scripts/install.sh | bash' })
1919
2077
  return
1920
2078
  }
2079
+ } else {
2080
+ // Auto-upgrade if binary is older than required minimum
2081
+ const currentVersion = getBinaryVersion(binary)
2082
+ if (currentVersion && isVersionOlder(currentVersion, MIN_NODE_VERSION)) {
2083
+ api.logger?.info?.(`[clawnetwork] claw-node ${currentVersion} is outdated (need >=${MIN_NODE_VERSION}), upgrading...`)
2084
+ process.stdout.write(`Upgrading claw-node ${currentVersion} → ${MIN_NODE_VERSION}+...\n`)
2085
+ try {
2086
+ binary = await downloadBinary(api)
2087
+ } catch (e: unknown) {
2088
+ api.logger?.warn?.(`[clawnetwork] auto-upgrade failed: ${(e as Error).message}, continuing with ${currentVersion}`)
2089
+ }
2090
+ }
1921
2091
  }
1922
2092
  initNode(binary, cfg.network, api)
1923
2093
  startNodeProcess(binary, cfg, api)
@@ -2157,12 +2327,16 @@ export default function register(api: OpenClawApi) {
2157
2327
  // Check if already running (e.g. from a previous detached start)
2158
2328
  const state = isNodeRunning()
2159
2329
  if (state.running) {
2160
- api.logger?.info?.(`[clawnetwork] node already running (pid=${state.pid}), skipping auto-start`)
2330
+ api.logger?.info?.(`[clawnetwork] node already running (pid=${state.pid}), skipping node start`)
2161
2331
  startHealthCheck(cfg, api)
2332
+ // Still need to ensure heartbeat loop is running (may have been lost on gateway restart)
2333
+ const wallet = ensureWallet(cfg.network, api)
2334
+ await sleep(5_000)
2335
+ await autoRegisterMiner(cfg, wallet, api)
2162
2336
  return
2163
2337
  }
2164
2338
 
2165
- // Step 1: Ensure binary
2339
+ // Step 1: Ensure binary (auto-upgrade if outdated)
2166
2340
  let binary = findBinary()
2167
2341
  if (!binary) {
2168
2342
  if (cfg.autoDownload) {
@@ -2172,6 +2346,14 @@ export default function register(api: OpenClawApi) {
2172
2346
  api.logger?.error?.('[clawnetwork] claw-node not found and autoDownload is disabled')
2173
2347
  return
2174
2348
  }
2349
+ } else if (cfg.autoDownload) {
2350
+ const cv = getBinaryVersion(binary)
2351
+ if (cv && isVersionOlder(cv, MIN_NODE_VERSION)) {
2352
+ api.logger?.info?.(`[clawnetwork] claw-node ${cv} outdated (need >=${MIN_NODE_VERSION}), upgrading...`)
2353
+ try { binary = await downloadBinary(api) } catch (e: unknown) {
2354
+ api.logger?.warn?.(`[clawnetwork] auto-upgrade failed: ${(e as Error).message}`)
2355
+ }
2356
+ }
2175
2357
  }
2176
2358
 
2177
2359
  // Step 2: Init
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clawlabz/clawnetwork",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Run a ClawNetwork blockchain node inside OpenClaw. Every agent is a blockchain node.",
5
5
  "type": "module",
6
6
  "license": "MIT",