@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 +846 -179
- package/openclaw.plugin.json +1 -1
- package/package.json +7 -1
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 = '
|
|
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://
|
|
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: #
|
|
717
|
-
--bg-panel: #
|
|
718
|
-
--border: #
|
|
719
|
-
--accent: #
|
|
720
|
-
--accent-dim: rgba(
|
|
721
|
-
--
|
|
722
|
-
--
|
|
723
|
-
--purple:
|
|
724
|
-
--
|
|
725
|
-
--
|
|
726
|
-
--
|
|
727
|
-
--
|
|
728
|
-
--
|
|
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.
|
|
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:
|
|
741
|
-
.logo-net { color: var(--
|
|
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(
|
|
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
|
-
.
|
|
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: #
|
|
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:
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
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="
|
|
806
|
-
<
|
|
807
|
-
<
|
|
946
|
+
<div class="node-controls">
|
|
947
|
+
<button class="btn primary" onclick="doAction('start')">▶ Start Node</button>
|
|
948
|
+
<button class="btn danger" onclick="doAction('stop')">■ Stop Node</button>
|
|
808
949
|
</div>
|
|
809
950
|
</div>
|
|
810
951
|
|
|
811
|
-
<div class="
|
|
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="
|
|
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
|
|
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()">↻ 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
|
+
⚠️ <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) {
|
|
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
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
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)">✓ ' + 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
934
|
-
|
|
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
|
-
|
|
937
|
-
|
|
938
|
-
|
|
1372
|
+
async function fetchJson(url) {
|
|
1373
|
+
const r = await fetch(url);
|
|
1374
|
+
return r.json();
|
|
1375
|
+
}
|
|
939
1376
|
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
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
|
-
|
|
946
|
-
|
|
947
|
-
|
|
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
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
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
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
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
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
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
|
-
|
|
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
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
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
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
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
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
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
|
|
1018
|
-
json(200, { message: '
|
|
1019
|
-
} catch
|
|
1020
|
-
|
|
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
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
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
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
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
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
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
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
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
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
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
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
2190
|
+
// Step 6: Start UI dashboard
|
|
1528
2191
|
startUiServer(cfg, api)
|
|
1529
2192
|
|
|
1530
|
-
// Step
|
|
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
|
},
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clawlabz/clawnetwork",
|
|
3
|
-
"version": "0.1.
|
|
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": {
|